Overview
This guide explains how to connect ERPNext — the open-source Frappe ERP used by manufacturers, distributors and service companies across the UAE — to TrustBill, a UAE FTA-accredited service provider (ASP). The result is fully PINT-AE compliant e-invoicing submitted to the FTA without leaving ERPNext.
ERPNext already handles multi-currency, TRN, UAE VAT and bilingual item/customer masters. What it does not include out of the box is PINT-AE XML generation and transmission through an FTA-accredited ASP over the Peppol network. TrustBill closes that gap.
What this integration delivers
- Submit Sales Invoices and Credit Notes directly from ERPNext to the FTA
- Frappe Server Script webhook — no core-file changes, survives version upgrades
- PINT-AE XML generated, validated and delivered by TrustBill on your behalf
- FTA acknowledgement written back to ERPNext custom fields on the Sales Invoice
- Optional custom Frappe app for packaged install and version-managed updates
- Bulk migration path for historical invoices via CSV or API replay
What is ERPNext?
ERPNext is an open-source, modular ERP built on the Frappe framework. It is deployed either as Frappe Cloud (managed SaaS), Frappe Cloud Private, or self-hosted on-premise / VPS using the Frappe Bench stack.
- Core DocTypes: Sales Invoice, Customer, Item, Company, Address, Tax Template, Payment Terms Template
- Native REST API:
/api/resource/{DocType}and/api/method/{method.path} - Server Scripts: attach Python hooks to document events without modifying core code
- Scheduled Jobs: cron-style background workers inside Frappe's job queue
- Multi-currency, multi-company, multi-branch and GCC VAT ready
Supported Versions
| ERPNext Version | Frappe Version | Support Level |
|---|---|---|
| v14.x | v14.x | Fully supported |
| v15.x | v15.x | Recommended + fully supported |
| v16+ / develop | v16+ | Supported with minor schema checks |
Prerequisites
- ERPNext site running v14, v15 or v16 with Server Script permissions enabled
- UAE company set with TRN in Company master
- Customer TRNs populated for B2B clients (simplified invoices can omit customer TRN)
- Active TrustBill SME account with API credentials
- Network egress allowed from ERPNext host to TrustBill API
Integration Architecture
The standard flow keeps ERPNext as the system of record for invoices and pushes a PINT-AE-ready payload to TrustBill at submission time.
- Invoice created / submitted in ERPNext Sales Invoice DocType.
- Frappe Server Script (After Submit event) transforms the invoice into TrustBill's JSON format.
- POST to TrustBill
/api/v1/invoiceswith API key authentication. - TrustBill validates the payload, converts to PINT-AE XML and delivers over Peppol via the FTA accredited ASP.
- FTA acknowledgement is stored back in ERPNext custom fields on the same invoice.
Connection Methods
1. Server Script + Webhook (Recommended)
Use Frappe Server Script on the Sales Invoice on_submit hook. No custom app to install, upgrades-safe, and works on Frappe Cloud and self-hosted sites.
2. Custom Frappe App
Package the integration as a TrustBill Frappe app for multi-site deployments, version control and automated updates via bench update.
3. Scheduled Batch Sync
For high-volume sites, run a Scheduled Job that polls submitted invoices and pushes them to TrustBill in batches every 2 minutes.
4. CSV Export / Import
Fallback for one-off migrations: export Sales Invoice Register from ERPNext, transform columns, and upload via TrustBill bulk import.
Authentication
TrustBill API uses HTTP Bearer tokens. Generate an API key from TrustBill Settings → API Keys, then include it in every request header.
POST https://trustbill.ae/api/v1/invoices
Authorization: Bearer tb_your_api_key_here
Content-Type: application/jsonFor Frappe outbound calls, store the token in a TrustBill Settings single DocType or in the site config and read it via frappe.conf.get().
Server Script Setup
This is the fastest production-ready setup. It sends every submitted Sales Invoice to TrustBill automatically.
Step 1: Add custom fields to Sales Invoice
trustbill_status(Select): Pending / Submitted / Acknowledged / Rejectedtrustbill_uuid(Data): TrustBill invoice UUIDtrustbill_ack_ref(Data): FTA acknowledgement referencetrustbill_reject_reason(Text): Error / rejection message
Step 2: Create Server Script (After Submit)
import json
import requests
import frappe
def submit_to_trustbill(doc, method):
if doc.trustbill_status and doc.trustbill_status != "Pending":
return
token = frappe.conf.get("trustbill_api_token")
url = "https://trustbill.ae/api/v1/invoices"
payload = {
"invoiceNumber": doc.name,
"issueDate": str(doc.posting_date),
"dueDate": str(doc.due_date) if doc.due_date else str(doc.posting_date),
"currency": doc.currency,
"seller": {
"trn": frappe.get_value("Company", doc.company, "tax_id"),
"legalName": doc.company,
"address": get_company_address(doc.company)
},
"buyer": {
"trn": doc.tax_id or "",
"legalName": doc.customer_name,
"address": get_customer_address(doc.customer)
},
"items": [
{
"name": item.item_name,
"quantity": float(item.qty),
"unitPrice": float(item.rate),
"discount": float(item.discount_amount or 0),
"taxPercent": float(item.tax_rate or 0),
"lineTotal": float(item.amount)
}
for item in doc.items
],
"totals": {
"netAmount": float(doc.net_total),
"taxAmount": float(doc.total_taxes_and_charges or 0),
"grossAmount": float(doc.grand_total)
}
}
try:
resp = requests.post(
url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
json=payload,
timeout=30
)
data = resp.json()
if resp.ok:
doc.db_set("trustbill_status", "Submitted", commit=False)
doc.db_set("trustbill_uuid", data.get("uuid"), commit=False)
else:
doc.db_set("trustbill_status", "Rejected", commit=False)
doc.db_set("trustbill_reject_reason", json.dumps(data), commit=False)
except Exception as e:
doc.db_set("trustbill_status", "Rejected", commit=False)
doc.db_set("trustbill_reject_reason", str(e), commit=False)
def get_company_address(company):
addr = frappe.get_all("Address", filters={"is_your_company_address": 1, "link_name": company}, limit=1)
if addr:
return frappe.get_value("Address", addr[0].name, "address_line1")
return ""
def get_customer_address(customer):
addr = frappe.get_all("Dynamic Link", filters={"parenttype": "Address", "link_doctype": "Customer", "link_name": customer}, limit=1)
if addr:
return frappe.get_value("Address", addr[0].parent, "address_line1")
return ""Step 3: Wire the event
Create a new Server Script: DocType Event → Sales Invoice → After Submit → Paste submit_to_trustbill(doc, method). Enable and save.
ERPNext → TrustBill Field Mapping
| ERPNext Field | TrustBill JSON | Notes |
|---|---|---|
doc.name | invoiceNumber | Must be unique per TRN |
posting_date | issueDate | ISO-8601 date |
Company.tax_id | seller.trn | 15-digit UAE TRN |
Customer.tax_id | buyer.trn | Required for B2B |
items[].item_name | items[].name | Arabic + English if available |
items[].qty | items[].quantity | Numeric, positive |
items[].rate | items[].unitPrice | After line discount |
grand_total | totals.grossAmount | VAT inclusive |
UAE VAT / Tax Mapping
Map ERPNext Sales Taxes and Charges Template rows to FTA tax categories. TrustBill accepts the following UAE tax category codes.
| ERPNext Tax Row | TrustBill taxCategory | Rate |
|---|---|---|
| Standard VAT | STANDARD | 5% |
| Zero Rated | ZERO_RATE | 0% |
| Exempt | EXEMPT | — |
| Reverse Charge | REVERSE_CHARGE | 5% |
| Out of Scope | OUT_OF_SCOPE | — |
Credit Notes
ERPNext Sales Return / Credit Note should be mapped to TrustBill's credit-note endpoint. Include the original invoice number in originalInvoiceNumber and a negative gross amount.
POST https://trustbill.ae/api/v1/credit-notes
{
"originalInvoiceNumber": "SINV-00001",
"issueDate": "2026-09-08",
"creditNoteNumber": "CN-00001",
"reason": "Goods returned",
"totals": {
"grossAmount": -525.00
}
}Status Tracking
TrustBill can call a webhook back to your Frappe site whenever an invoice status changes. Add a custom API method in ERPNext to receive the callback.
# In a Server Script marked as API method
def handle_trustbill_webhook():
import frappe, json
data = frappe._dict(frappe.request.get_json())
frappe.db.set_value(
"Sales Invoice",
data.invoiceNumber,
{
"trustbill_status": data.status,
"trustbill_ack_ref": data.ackRef or None,
"trustbill_reject_reason": data.reason or None
}
)
return {"ok": True}Register the webhook URL in TrustBill Settings → Webhooks:/api/method/handle_trustbill_webhook.
Bulk Migration
For historical invoices, use a one-off Frappe Background Job or the Desk "Bulk Update" tool to replay submitted invoices through the TrustBill API.
def replay_invoices():
for name in frappe.get_all("Sales Invoice", filters={"docstatus": 1}, pluck="name"):
doc = frappe.get_doc("Sales Invoice", name)
submit_to_trustbill(doc, "on_submit")
frappe.db.commit()Sandbox & Live Modes
TrustBill provides a sandbox endpoint for end-to-end testing without touching the FTA. Use it until your FTA registration and ASP assignment are complete.
| Mode | Endpoint | What it does |
|---|---|---|
| Sandbox | https://trustbill.ae/api/v1/invoices?mode=sandbox | Validates PINT-AE XML, no FTA delivery |
| Live | https://trustbill.ae/api/v1/invoices?mode=live | Submits to FTA via ASP over Peppol |
Multi-Company & Multi-Currency
Frappe/ERPNext supports multiple Company doctypes in one site — common for UAE groups running a mainland trading company alongside a free-zone company. TrustBill treats each ERPNext company as an independent seller with its own TRN.
Company → TrustBill profile mapping
Add a custom field custom_trustbill_api_key on the Company doctype, or maintain a lookup in your Server Script, so each company's Sales Invoices resolve to the correct TrustBill API key and TRN. Never submit two companies' invoices under one shared TRN.
Multi-currency invoices
ERPNext's Sales Invoice stores currency and conversion_rate against the company's base currency. When the invoice currency is not AED, send both the trade currency amount and the AED-converted total (using base_grand_total) so the PINT-AE document always carries an AED value for FTA reporting.
Multiple branches / cost centers
If ERPNext branches map to different Emirates or physical locations, include the branch/cost center code in your internal audit log even though PINT-AE itself doesn't require a branch field — it helps reconcile FTA submissions during a VAT audit.
Error Handling & API Errors
Wrap every TrustBill call in your Server Script with proper error handling — log the response and update the Sales Invoice status field rather than letting the script fail silently.
| HTTP Status | Meaning | Action |
|---|---|---|
| 400 | Malformed JSON or missing required field | Fix the Server Script payload; do not retry unchanged |
| 401 | Invalid or expired API key | Rotate the key in ERPNext's site config / secrets |
| 403 | TRN on invoice does not match the API key's registered TRN | Fix company → TRN mapping |
| 409 | Duplicate invoice number for this seller | Check idempotency key / Sales Invoice name reuse |
| 422 | PINT-AE validation failure (tax, TRN format, totals mismatch) | Inspect errors[] array; fix mapping |
| 429 / 5xx | Rate limited or TrustBill/FTA temporarily unavailable | Retry with exponential backoff (max ~5 attempts) |
Setup Checklist
- UAE TRN configured for every ERPNext company
- Customer records populated with TRNs for B2B customers
- Server Script / REST API access enabled and API secret rotated out of source control
- Company → TrustBill API key mapping created (custom field or config)
- Tax template → PINT-AE tax category mapping reviewed with finance
- Credit Note doctype flow mapped to TrustBill credit notes
- Webhook or polling job configured for status write-back
- Sandbox invoices tested for standard, zero-rated, exempt and reverse-charge cases
- Bulk migration CSV template validated against historical Sales Invoice data
- Go-live date agreed and live mode switched only after sandbox sign-off
Troubleshooting
- 422 validation error: Check that all B2B customer TRNs are 15 digits and company TRN is set.
- Server Script not firing: Confirm the script is enabled and the DocType Event is "After Submit".
- Timeout: Increase Frappe HTTP timeout to 60s or switch to a background job for high-latency links.
- Missing Arabic names: Populate the "Arabic Name" custom field on Customer and Item; include it in the payload.
Support
Need help with the ERPNext integration? Our integrations team can review your Server Script, field mapping and VAT setup.