نظرة عامة
ثلاثة بيانات اعتماد، ثلاثة مالكين
- API Key + API Secret — one set per ERP install, مفتاح API + سر API — مجموعة واحدة لكل تثبيت ERP، يحتفظ بها شركة الاستشارات أو بائع ERP. جميع الشركات الصغيرة والمتوسطة على هذا ERP تشاركها.
- Webhook Secret — one per ERP install. سر خطاف الويب — واحد لكل تثبيت ERP. يرسله TrustBill كـ x-webhook-secret على كل خطاف حالة ويب؛ يتحقق منه ERP الخاص بك.
- Connection Secret (
tb_secret_…) — one per SME. سر الاتصال (tb_secret_…) — واحد لكل شركة صغيرة ومتوسطة. تنقر كل شركة صغيرة ومتوسطة على Connect في بوابة TrustBill الخاصة بها، تحصل على سر فريد، تلصقه في إعدادات شركتها داخل ERP الخاص بك. يتم إرساله كـ x-erp-partner-secret على كل دفعة.
إذا قمت بتصميم سر "لكل مستلم فاتورة" أو "لكل عميل"، فأنت تقوم بذلك بشكل خاطئ — يحدد سر الاتصال البائع (الشركة الصغيرة والمتوسطة التي تصدر الفاتورة)، وليس المشتري أبدًا.
يصف هذا الدليل كيفية دمج نظام ERP الخاص بك (Odoo، NetSuite، SAP، مخصص، إلخ) مع TrustBill بحيث يمكن لعملاء الشركات الصغيرة والمتوسطة لديك إرسال فواتير ضريبة الفاتورة الإلكترونية لهيئة الإمارات للضرائب مباشرة من ERP الخاص بك — دون مغادرة سير العمل الحالي.
التكامل ثنائي الاتجاه. يدفع ERP الخاص بك الفواتير إلى TrustBill عبر HTTP POST. يرسلها TrustBill إلى هيئة الإمارات للضرائب/مزود الخدمة ويتصل مرة أخرى بخطاف الويب الخاص بك عندما تتغير الحالة (تم التسليم أو تم الرفض).
من يفعل ماذا
- Consultancy (you): شركة الاستشارات (أنت): تسجل تكامل ERP مرة واحدة، توفر عنوان URL لخطاف الويب + سر، تحصل على بيانات اعتماد API للاختبار، تطلب الموافقة المباشرة.
- SME (your client): الشركة الصغيرة والمتوسطة (عميلك): تنقر على "Connect" في صفحة مصادر TrustBill الخاصة بهم، تحصل على سر اتصال لكل شركة صغيرة ومتوسطة للصق في ERP الخاص بك.
- Your ERP: ERP الخاص بك: يدفع كل فاتورة إلى POST /v1/partner/invoices مع بيانات اعتماد API + سر اتصال لكل شركة صغيرة ومتوسطة.
- TrustBill: TrustBill: يصادق على الدفعة، يرسل إلى هيئة الإمارات للضرائب، ثم يرسل تحديثات الحالة إلى عنوان URL لخطاف الويب الخاص بك.
The four-step flow
Step 1 (once, per consultancy)
─────────────────────────────
Consultancy portal ──(register ERP + webhook URL + secret)──► TrustBill
│
└── admin approval → status: live_active
Step 2 (once, per SME)
──────────────────────
SME TrustBill portal ──(click "Connect")──► TrustBill
│
└── returns tb_secret_<64hex> (shown ONCE)
│
▼
SME pastes into your ERP settings
Step 3 (per invoice)
────────────────────
Your ERP ──POST /v1/partner/invoices──► TrustBill ──► FTA/ASP
headers:
X-API-Key: erp_live_...
X-API-Secret: ...
x-erp-partner-secret: tb_secret_...
Idempotency-Key: <unique>
→ 201 { invoiceId, number, status: "queued" }
Step 4 (async, after FTA responds)
──────────────────────────────────
TrustBill ──POST <your_webhook_url>──► Your ERP
header:
x-webhook-secret: <the secret you registered in Step 1>
body:
{ event, erpPartnerId, invoiceId, status, reason, timestamp }الميزات — مطلوبة، موصى بها، اختيارية
ليس كل ميزة يجب أن تُشحن في اليوم الأول. تخبر هذه المصفوفة فريق الهندسة الخاص بك كيف يبدو التكامل الحد الأدى القابل للحياة (شحن هذا وستكون متوافقًا مع هيئة الإمارات للضرائب)، وما يجعله جاهزًا للإنتاج، وما هو التلميع الاختياري. كما تسرد القدرة التي تفتحها كل ميزة بحيث تعرف ما تتنازل عنه إذا تخطيتها.
مطلوب — التكامل الحد الأدنى القابل للحياة
| Feature | What it does | Unlocks |
|---|---|---|
| Credential storage — API Key + Secret, per-SME connection secret, webhook secret | 4 encrypted-at-rest strings. API Key/Secret + Webhook Secret are one-per-ERP; connection secret is one-per-SME. | Auth for every single call. |
Submit invoice — POST /v1/partner/invoices | Map local invoice → TrustBill body; send with API creds + connection secret + a unique Idempotency-Key; persist the returned invoiceId. | Push invoice to the FTA. Nothing else in the integration matters if this doesn't work. |
| Status webhook receiver — public HTTPS endpoint | Accept POST from TrustBill, verify auth (see next row), update local invoice status + reason, return 2xx within 8 s. | Real-time status without polling. Without it, invoices stay Queued in your UI forever. |
Webhook auth — one of: HMAC (v1.5+) OR plain x-webhook-secret | Verify the caller is genuinely TrustBill. Constant-time compare or HMAC-SHA256; reject with 401 otherwise. | Security. Without this an attacker who guesses your URL can forge status updates into your ERP. |
| Local invoice status field — enum | Store queued / delivered / rejected on the local invoice row. Populate at Step 3 (queued) and Step 4 (delivered/rejected). | Accounting can see what's FTA-compliant. Filters & reports depend on this. |
| SME's own TRN — settings input | 15-digit field in company/SME setup. This is what goes in top-level body.trn — must exactly match the TRN registered on trustbill.ae for the same company. | Passes TrustBill's TRN-vs-SME cross-check. Any mismatch → 403. |
| Submit button on invoice detail | Manual trigger to push. Disable when local validation fails. | Operator control — some invoices need review before FTA. |
| Idempotency-Key generation | Stable per (invoice, attempt). Pattern: <erp-tenant-id>-<local-invoice-id>-<attempt>. Store the key you sent so a retry uses the same one. | Safe retries — TrustBill de-dupes; you never create duplicate invoices on network flakes. |
موصى به — مطلوب للبقاء على قيد الحياة في الإنتاج
| Feature | What it does | Unlocks |
|---|---|---|
Polling fallback — GET /v1/partner/invoices/{id} (v1.5) | Scheduled job every ~15 min that fetches status for anything stuck in queued for > 5 min. Also a manual "Sync now" button. | Survival when webhooks are lost — TrustBill fires once, no retry. Without polling, one dropped webhook = permanently stuck invoice. |
| HMAC signature verify (v1.5) | Verify x-webhook-signature header (HMAC-SHA256 over timestamp + body) with a 5-min replay window. Fall back to plain secret only when HMAC absent. | Replay-safe production security. Auditors will ask. |
Test connection button — GET /v1/partner/whoami | One-click check from Settings that shows mode (test/live) + connected SME count + partner name. | Saves 30-min "why doesn't it work?" debug sessions per install. |
| Retry-on-rejection button | Only visible when status = Rejected. Generates a NEW Idempotency-Key and pushes again. | Recovery path when a rejection was due to fixable data (wrong TRN, missing address). Without it, operator has to duplicate the invoice. |
| Rejection reason panel | Show the reason string from the webhook on the invoice detail screen so the SME can fix + retry without contacting support. | Faster time-to-fix; reduces support load. |
| Structured error handling | Switch on TrustBill's code field in 4xx responses: invalid_body → show validation errors; trn_mismatch → tell SME to fix TRN; sme_not_connected → tell them to reconnect on trustbill.ae. | Human-friendly errors instead of "500 Server Error". |
| Test vs Live mode isolation | Detect key prefix (erp_test_ vs erp_live_) and surface which mode is active in the ERP UI. Ideally allow separate test-mode credentials in a QA install. | Safe QA. Prevents "we thought we were on test but pushed to production" incidents. |
| Webhook audit log | Persist every inbound webhook (raw body, headers, signature-ok flag, timestamp). 90-day retention minimum. | Support triage. When someone reports "invoice X wasn't marked delivered", you can prove whether TrustBill ever called. |
| Invoice list status filter | Filter chip: TrustBill status Queued / Delivered / Rejected. | Accounting can find everything that needs attention in one click. |
| Dashboard KPI widget | N queued · N delivered today · N rejected today · N stuck > 15 min. | Operator noticies problems before customers do. |
اختياري — تلميع وحالات استخدام متقدمة
| Feature | What it does | Unlocks |
|---|---|---|
Document-type picker — documentType field (v1.5) | Dropdown on invoice detail: Tax Invoice / Simplified Tax Invoice / Credit Note / Debit Note / Commercial Invoice / Self-Billed. Sent as documentType in the body. | Full UBL taxonomy. Without this you can only push standard tax_invoice (388) — fine for most B2B, insufficient for refunds/simplified/self-billed. |
Credit / debit note support — precedingInvoiceReference | When user creates a credit or debit note, capture which original invoice it amends, send as precedingInvoiceReference. Required by FTA for amendments. | Legally-valid refunds and adjustments. Without it, the FTA rejects amendments. |
| Simplified tax invoice — B2C support | For sales < AED 10,000 to non-VAT-registered buyers. Set documentType: "simplified_tax_invoice". Buyer TRN not required. | B2C sales. Skip if your ERP only serves B2B. |
| Self-billed invoice | documentType: "self_billed_invoice" — buyer generates the invoice on the supplier's behalf. Rare but required in some industries (utilities, agriculture). | Industry-specific. Skip unless you serve utility/telecom/agri customers. |
| Bulk-submit action | Accounting-staff shortcut: multi-select 50 invoices → "Push all to TrustBill" → generates a job that submits with rate-limiting. | Month-end catch-up. Otherwise operators click Submit 50 times. |
| Auto-submit on invoice post | Toggle in settings: "Push to TrustBill immediately when invoice is posted." Off by default — some SMEs want manual review. | Zero-touch compliance for high-volume SMEs. |
| Analytics dashboard | Deeper view: 7/30/90-day delivery rate, top rejection reasons, avg time-to-delivered per SME. | Insight for consultancies managing many SMEs. |
| Multi-language settings labels | Arabic + English side-by-side on the TrustBill Integration settings screen (most UAE ERPs already do this). | Local-market fit. Optional if your ERP is English-only. |
| Slack/Teams webhook for rejections | Post to a channel when invoice status flips to rejected, so ops sees it without checking the dashboard. | Proactive incident response. |
| Reserved-TRN test suite (v1.5) | Automated tests using TRN 100000000000000 (always accepts) and 100000000000666 (always rejects) so CI can verify both paths without real SME data. | Regression-proof deploys. Highly recommended for any partner shipping updates weekly. |
مصفوفة القدرات — نظرة سريعة
| Business capability | Features required | Effort |
|---|---|---|
| Push a B2B tax invoice to FTA (the 80% use case) | All Required rows. | ~1 week for a Laravel/Node ERP dev. |
| Handle rejections gracefully | All Required + rejection panel + retry button + structured error switch. | +2 days. |
| Survive an incident (webhook down, network flake) | Above + polling fallback + webhook audit log + dashboard KPI. | +2 days. |
| Serve B2C customers | Above + document-type picker + simplified-invoice type. | +1 day. |
| Handle refunds & adjustments | Above + credit/debit note support + precedingInvoiceReference capture. | +1 day. |
| Month-end mass-catch-up | Above + bulk-submit action + rate-limiter. | +1 day. |
| Production-audit-ready | Everything Recommended + HMAC verification + reserved-TRN CI tests. | +2 days. |
ما يجب أن يبنيه فريق نظام تخطيط موارد المؤسسات الخاص بك
يوفر TrustBill واجهة برمجة التطبيقات + البوابة. يملك فريق ERP الخاص بك كل شيء على جانب ERP لإكمال المزامنة ثنائية الاتجاه. إليك قائمة التحقق — الشاشات والأزراب وعمال الخلفية وتغييرات نموذج البيانات — مجمعة حسب الاتجاه الذي تخدمه.
1. الشاشات والأزراب (داخل ERP الخاص بك)
| Screen | Element | What it does |
|---|---|---|
| Settings → TrustBill Integration (system-wide, one per ERP tenant) | Input: TrustBill API Key | Paste the erp_test_... / erp_live_... from Step 1. Store in ERP config. |
| Same screen | Input: TrustBill API Secret (password type) | Paste the one-time secret from Step 1. Store encrypted at rest. |
| Same screen | Button: Test connection | Calls GET https://trustbill.ae/api/v1/partner/ping with the credentials, shows OK/failed. |
| Company/Customer/Client setup (per SME the ERP serves) | Input: TrustBill Connection Secret (password type) | Paste the tb_secret_... the SME copied from their TrustBill portal in Step 2. Store encrypted, per-SME. |
| Invoice detail screen | Button: Submit to FTA | Triggers the outbound push (Step 3). Disable until the invoice validates against your existing rules. |
| Invoice detail screen | Status pill: Queued / Delivered / Rejected | Reflects the TrustBill state. Queued right after Step 3's 201; updated by the webhook (Step 4). |
| Invoice detail screen (when rejected) | Rejection reason panel | Show the reason string from the webhook so the SME can fix and retry. |
| Invoice detail screen | Button: Retry submission | Only visible when status is Rejected. Generates a NEW Idempotency-Key and pushes again. |
| Invoice list screen | Filter chip: TrustBill status | Lets accounting staff surface everything queued/rejected quickly. |
2. الاتجاه 1 — ERP → TrustBill (صادر، إرسال الفاتورة)
عميل HTTP في ERP الخاص بك يطلق عندما تنقر الشركة الصغيرة والمتوسطة على Submit to FTA:
- Look up the SME's stored
TrustBill Connection Secret(from company settings) and your globalAPI Key/Secret. - Generate a stable
Idempotency-Key— recommended:<erp-tenant-id>-<local-invoice-id>-<attempt>. Same key = same result; a new attempt increments the suffix. - Map your local invoice schema to TrustBill's body shape (see Step 3 body example).
- POST to
/v1/partner/invoices. Handle 2xx → save the returnedinvoiceIdagainst your local invoice, set local status =Queued. - Handle 4xx with a clear UI message (see Error responses section). 5xx / network failures: retry with the SAME
Idempotency-Key(safe — TrustBill de-dupes). - Log every attempt (request id, HTTP status, response body) for support triage.
3. الاتجاه 2 — TrustBill → ERP (وارد، مزامنة الحالة)
نقطة نهاية HTTPS عامة على ERP الخاص بك التي يرسل TrustBill إليها عندما يستجيب هيئة الإمارات للضرائب/مزود الخدمة:
- Expose an HTTPS endpoint — the URL you registered as
testWebhookUrl(Step 1). No auth token; auth is via header. - On every call: constant-time compare
x-webhook-secretheader to the secret you registered. Reject with 401 otherwise. - Parse the JSON body → look up your local invoice by TrustBill's
invoiceId. If not found, log & 200 (don't reject; another region might replay). - Update local status (
delivered→ yourDelivered,rejected→ yourRejected) + storereason. - Return 2xx quickly (< 8 s). Slow work: enqueue an internal job.
- Make it idempotent — if TrustBill re-sends the same
invoiceId+status, don't double-apply (compare against your last stored status).
4. إضافات نموذج البيانات
| Table | Column | Type / purpose |
|---|---|---|
erp_config (single-row) | trustbill_api_key | string — one per ERP install. |
erp_config | trustbill_api_secret_enc | encrypted string — never log the plaintext. |
erp_config | trustbill_webhook_secret_enc | encrypted string — used to verify inbound webhooks. |
companies (or your SME/tenant table) | trustbill_connection_secret_enc | encrypted string, per-SME. Store separately from the global API secret. |
invoices | trustbill_invoice_id | nullable UUID — populated after 201 from Step 3. |
invoices | trustbill_status | enum: queued | delivered | rejected. |
invoices | trustbill_status_reason | nullable string — populated on rejection. |
invoices | trustbill_idempotency_key | string — the key you sent; needed for safe retry. |
trustbill_webhook_log (new) | received_at, invoice_id, status, raw_body | audit trail. Retention: 90 days min. |
5. الأمان والعمليات
- Never log secrets — API secret, connection secret, webhook secret. Scrub them from request/response logs.
- HTTPS everywhere — TrustBill rejects
http://webhook URLs and won't follow redirects. - Constant-time compare for
x-webhook-secret(e.g.crypto.timingSafeEqualin Node,hmac.compare_digestin Python). - Rate-limit your outbound POSTs to avoid tripping TrustBill's ingest limiter under a mass-send.
- Metric + alert on: 4xx rate on push, webhook signature mismatches, invoices stuck in
queued> 15 min. - Rotation — when an SME regenerates their connection secret, invalidate the old one in your ERP and prompt them to re-paste.
6. الأشياء الجيدة
- "Sync now" button on the invoice that manually re-fetches status via
GET /v1/invoices/:id(fallback for missed webhooks). - Dashboard widget: N invoices queued, N delivered today, N rejected today.
- Bulk-submit action for accounting staff to push a filtered list of invoices at once (respect rate limits + generate distinct idempotency keys).
- Test-mode toggle in ERP settings so QA can use
erp_test_...credentials without touching live customer data.
الخطوة 1 — تسجيل نظام تخطيط موارد المؤسسات الخاص بك
سجل ERP الخاص بك كشريك مع TrustBill. هذه عملية لمرة واحدة لكل شركة استشارات. ستحصل على بيانات اعتماد API وتكوين عنوان URL لخطاف الويب الخاص بك.
If you're used to integrating with an Accredited Service Provider (ASP) directly, you'd normally email the ASP, wait days for Sandbox keys, then set up your webhook after. TrustBill inverts that.
| What ERP teams call it | What TrustBill calls it |
|---|---|
| Sandbox API key | testApiKey (starts erp_test_...) |
| Sandbox API secret | revealedTestSecret |
| Production API key | liveApiKey (starts erp_live_...) |
| Production API secret | revealedLiveSecret |
| Sandbox / staging environment | "test mode" |
- You fill in the registration form (business info + your webhook URL + your webhook secret).
- The moment you submit, TrustBill mints
testApiKey+revealedTestSecretand returns them in the response. No approval wait, no email, no KYC step. You're building in the next 30 seconds. - You build against test creds — same endpoint as live (
/v1/partner/invoices). - When outbound + webhook are working end-to-end, click Request Live. A TrustBill admin reviews (only human step in the whole flow) and either approves or rejects with a reason. On approval,
liveApiKey+revealedLiveSecretare minted; test creds keep working alongside so nothing breaks mid-transition.
Yes. Webhook URL and Webhook Secret are optional on the registration form. You can register with them empty, get test creds instantly, build and validate outbound first, then come back to /erp-partner and add the webhook config later (“Webhook & connectivity test” card). Click Test integration to confirm TrustBill can reach your receiver.
Sign in to the consultancy portal and go to /erp-partner. Fill in the register form once. Test-mode credentials are minted instantly — no approval needed to start building.
Who provides the Webhook URL & Secret?
You do — the ERP partner. TrustBill doesn't hand these out. Your ERP owns the receiver endpoint (URL) and picks the shared secret. TrustBill just stores them and sends them back on every status callback so your ERP can verify the call actually came from TrustBill.
- Must be reachable from the public internet (no
localhost, no private IPs). - Format:
https://your-erp.com/<any-path>. Common patterns:/webhooks/trustbill,/api/trustbill/webhook. - For the Cloud ERP shipped by TrustBill, this is fixed:
https://erp.trustbill.ae/trustbill/webhook— the addon registers that route. - For a self-hosted ERP (Odoo, NetSuite, custom), you pick the path and wire it to your inbound webhook handler.
- Generate one now, e.g.
openssl rand -hex 32(256 bits of entropy). Any strong random string works — TrustBill doesn't care about format. - Save the same value in two places: paste it into this registration form AND into your ERP's webhook-verification setting (the value your ERP will constant-time compare against the
x-webhook-secretheader on every inbound call). - Store encrypted at rest inside your ERP. Never log the plaintext. Never expose it in client-side code.
- If it ever leaks, generate a new one and update both sides — TrustBill's Webhook Secret can be rotated any time from the dashboard.
Fields
| Field | Required | Notes |
|---|---|---|
erpName | Yes | e.g. "Acme ERP" |
description | Optional | Short description shown to SMEs. |
contactEmail | Yes | Where TrustBill support reaches you. |
website, logoUrl | Optional | Displayed on the SME Sources card. |
companyCount | Optional | Approximate number of SMEs you serve. |
testWebhookUrl | Optional | You provide. Public HTTPS URL of the endpoint YOUR ERP exposes to receive status updates. Cloud ERP by TrustBill: https://erp.trustbill.ae/trustbill/webhook. Others: your own path. Can be added later from the dashboard. |
testWebhookSecret | Optional | You invent. Generate with openssl rand -hex 32. TrustBill echoes it back on every webhook as the x-webhook-secret header. Save the same value in your ERP's webhook verifier. Envelope-encrypted at rest. |
What you get back
testApiKey— anerp_test_...string you send asX-API-Key.revealedTestSecret— shown exactly once in the response. Store it — you send it asX-API-Secret. TrustBill only stores its scrypt hash; regenerate if lost.status: "test"— you can start integration testing immediately.
Going live
When testing is done, click Request live access. A TrustBill admin reviews it. On approval you get a liveApiKey/liveWebhookSecret pair; your test credentials keep working the whole time so nothing breaks mid-transition.
الخطوة 2 — اتصال الشركات الصغيرة والمتوسطة
بمجرد تسجيلك (اختبار أو مباشر)، ترى كل شركة صغيرة ومتوسطة تابعة لشركة الاستشارات الخاصة بك ERP الخاص بك كبطاقة في صفحة مصادر TrustBill الخاصة بهم.
- SME opens their TrustBill portal → ERP integration.
- SME clicks Connect on your ERP card.
- TrustBill generates a per-SME secret
tb_secret_<64 hex chars>, shows it once. - SME copies it and pastes it into your ERP's "TrustBill Integration" settings — this is the secret your ERP will send on every invoice push.
- SME can rotate the secret at any time from the same page (invalidates the old one immediately).
Why per-SME: your API credential proves the caller is your ERP, but only tb_secret_... proves the push belongs to a specific SME. TrustBill uses it (not the TRN in the body) to resolve which SME the invoice belongs to; the TRN is cross-checked as defense-in-depth.
الخطوة 3 — إرسال الفواتير
عندما تنقر شركة صغيرة ومتوسطة على "Submit to FTA" داخل ERP الخاص بك، يرسل ERP الخاص بك الفاتورة إلى TrustBill. يتحقق TrustBill من المصادقة + الرقم الضريبي، يحفظ مسودة، يضيف إرسالًا تلقائيًا إلى هيئة الإمارات للضرائب/مزود الخدمة، ويعيد 201 مع معرف الفاتورة.
Endpoint
POST https://trustbill.ae/api/v1/partner/invoicesHeaders
| Header | Purpose |
|---|---|
X-API-Key | Your ERP's API key from Step 1 (erp_test_... or erp_live_...). |
X-API-Secret | Your ERP's API secret from Step 1. Verified against a scrypt hash. |
x-erp-partner-secret | The tb_secret_... the specific SME pasted in Step 2. This is what identifies the SME. |
Idempotency-Key | Required. Unique per invoice (1–128 chars). Retrying with the same key returns the original result, never a duplicate invoice. |
Content-Type | application/json |
Body
Invoice data plus the SME's 15-digit TRN (cross-checked against the connection secret). Real, working example (copy-paste against a test partner):
{
"trn": "100000000000042",
"number": "INV-2026-0001",
"issueDate": "2026-08-01",
"dueDate": "2026-08-15",
"currency": "AED",
"invoiceType": "sale",
"seller": {
"name": "TrustBill Test Co LLC",
"trn": "100000000000003",
"country": "AE",
"address": {
"line1": "Sheikh Zayed Road",
"city": "Dubai",
"emirate": "Dubai"
},
"kind": "seller"
},
"buyer": {
"name": "Acme Trading LLC",
"trn": "100000000000042",
"country": "AE",
"address": {
"line1": "Marina Plaza",
"city": "Dubai",
"emirate": "Dubai"
},
"kind": "buyer"
},
"lines": [
{
"description": "Consulting service, August",
"quantity": 1,
"unitPrice": 5000,
"vatRate": 0.05,
"taxCategory": "standard_5"
}
],
"documentType": "tax_invoice"
}Field reference
| Field | Type | Notes |
|---|---|---|
trn | 15-digit string | Top-level TRN — must match the SME resolved by x-erp-partner-secret. Cross-checked; mismatch → 403 trn_mismatch. |
number | string (1–64) | Unique per (SME, mode). Duplicates return 500 today (planned 409 in a future release). |
issueDate | date YYYY-MM-DD | Date string, not ISO-8601 datetime. Dubai timezone recommended. |
dueDate | date (optional) | Same format as issueDate. |
currency | "AED" | Only AED accepted in Phase 2a. |
invoiceType | "sale" | "purchase" | Defaults to "sale". |
seller.address.line1 | string | Required when address is present. Same for buyer.address.line1. |
seller.address.city | string | Required when address present. |
seller.country, buyer.country | ISO 2-letter | e.g. "AE". Required. |
lines[].unitPrice | decimal AED | Not fils. A price of AED 5,000 is 5000, not 500000. |
lines[].vatRate | fraction 0-1 | Not percent. 5% VAT is 0.05, not 5. |
lines[].taxCategory | enum | standard_5, zero_rated_export, zero_rated_healthcare, zero_rated_education, exempt_financial. |
lines[] | array | 1–200 items. Each requires description, quantity > 0, unitPrice ≥ 0. |
Common validation gotchas
- Never send
nullfor optional fields. Zod on the server accepts absent (undefined) or a valid value, but rejects explicitnull. If you don't have an email, omit the key — don't send"email": null. - Fils vs AED. Line prices are decimal AED. Sending
500000when you mean AED 5,000 will make a very expensive invoice. - Percent vs fraction. Standard VAT is
0.05, not5. A rate of5fails the schema (vatRate ≤ 1). - Top-level
trnis the SME's own TRN (the seller for sale invoices). TrustBill resolves yourx-erp-partner-secretto an SME tenant, then comparesbody.trnto that SME's registered TRN. ForinvoiceType: "sale"this equalsseller.trn; forinvoiceType: "purchase"it equalsbuyer.trn. Any other value → 403trn_mismatch. - Date format for
issueDate/dueDate. Send"2026-08-01", not"2026-08-01T00:00:00Z".
Success response
HTTP/1.1 201 Created
{ "invoiceId": "…", "number": "INV-2026-0001", "status": "queued" }Example (curl)
curl -X POST https://trustbill.ae/api/v1/partner/invoices \
-H "X-API-Key: erp_live_..." \
-H "X-API-Secret: ..." \
-H "x-erp-partner-secret: tb_secret_..." \
-H "Idempotency-Key: acme-inv-2026-0001" \
-H "Content-Type: application/json" \
-d @invoice.jsonTesting the body shape locally
Before wiring your ERP client, POST the body directly with curl against the test API. If it returns 201, your mapping is correct. If it returns 400 with meta.issues, each issue names the exact JSON path that failed — e.g. "buyer.email: invalid_type" means you sent null where the schema wants an omitted key. Fix the mapper before touching the queue/HTTP client code.
الخطوة 4 — استلام الحالة
عندما يستجيب هيئة الإمارات للضرائب/مزود الخدمة، يرسل TrustBill تغيير الحالة إلى عنوان URL لخطاف الويب الذي قمت بتكوينه في الخطوة 1. أطلق وانسَ: يرسل TrustBill مرة واحدة، لا يعيد المحاولة. إذا فاتك واحد، يمكنك دائمًا الاستقصاء عن الحالة عبر زر "Sync now" في ERP الخاص بك.
Request
POST <your webhook URL>
x-webhook-secret: <the secret you registered in Step 1>
Content-Type: application/json
{
"event": "invoice.status_changed",
"erpPartnerId": "…",
"invoiceId": "…",
"status": "delivered" | "rejected",
"reason": null | "asp_rejected: <fingerprint>" | "validation_failed: …",
"timestamp": "2026-08-01T12:34:56.789Z"
}Fields
| Field | Meaning |
|---|---|
event | Always "invoice.status_changed" for now. |
erpPartnerId | Your ERP Partner id — the same one GET /v1/consultancy/erp-partner returned. |
invoiceId | TrustBill's UUID for the invoice — matches the invoiceId from the Step 3 response. |
status | "delivered" = FTA/ASP accepted. "rejected" = failed validation or ASP rejection. |
reason | null when delivered. Short reason string when rejected. |
timestamp | ISO-8601 UTC. |
Handler checklist
- v1.5+: verify
x-webhook-signature(HMAC) with the recipe in the HMAC section. Fall back to a constant-time compare ofx-webhook-secretonly when the HMAC header is absent (pre-v1.5 sender). - Return HTTP
200(or any 2xx) quickly. TrustBill uses an 8s timeout; long processing should happen async. - Process idempotently — if TrustBill re-sends the same
invoiceId+status(e.g. after a reconfigure), don't double-apply. - Follow redirects yourself if needed — TrustBill sends with
redirect: errorto avoid SSRF, so it will not auto-follow 3xx. - Missed a webhook? Poll
GET /v1/partner/invoices/{id}as your fallback (see Polling section) — TrustBill fires webhooks once, no automatic retry.
الاحتياطي بالاستقصاء — v1.5+ v1.5+
عندما يكون خطاف الويب الخاص بك معطلاً، في منتصف الترحيل، أو إذا كنت تريد فقط فحصًا صحيًا، استقصي عن الحالة مباشرة. نفس المصادقة مثل POST — مفتاح API + سر بالإضافة إلى الرقم الضريبي للشركة الصغيرة والمتوسطة في الرأس.
Endpoint
GET https://trustbill.ae/api/v1/partner/invoices/{invoiceId}Headers
X-API-Key: erp_live_...
X-API-Secret: ...
x-erp-partner-secret: tb_secret_...Success response
HTTP/1.1 200 OK
{
"invoiceId": "88132d4d-56f0-4721-8a3a-079ac8d4a709",
"number": "INV-2026-0001",
"status": "delivered",
"reason": null,
"createdAt": "2026-08-04T05:19:21.403Z",
"updatedAt": "2026-08-04T05:19:23.078Z"
}Same status vocabulary as the webhook. Reuse your webhook handler's switch statement; only the delivery mechanism differs. Statuses you'll see:queued, transformed, validated, blocked_no_credit, delivered, rejected. Both reason and humanMessage are surfaced from the last status change event.
404 semantics
HTTP/1.1 404 Not Found
{
"type": "https://docs.trustbill.ae/errors/not_found",
"title": "Not Found",
"status": 404,
"code": "not_found",
"detail": "No invoice with that id exists under your connected SME.",
"request_id": "..."
}Same 404 response whether the id doesn't exist OR belongs to a different SME. This is intentional — we don't leak whether an ID exists under a partner you're not authorized for. Use detail for user-facing text.
whoami — التأمل الذاتي v1.5+ v1.5+
فحص صحي سريع لـ ERP الخاص بك: "هل أنا في وضع الاختبار أو المباشر، وكم عدد الشركات الصغيرة والمتوسطة المتصلة بنشاط الآن؟" المستخدم بواسطة زر "Test connection" في إضافة Cloud ERP.
Endpoint
GET https://trustbill.ae/api/v1/partner/whoamiResponse
HTTP/1.1 200 OK
{
"partnerId": "9c8f06bf-00f6-4df6-be58-343ce48a1a86",
"erpName": "Acme ERP",
"status": "live_active",
"mode": "live",
"connectedSmes": 17,
"lastTestAt": "2026-08-04T05:19:21.397Z",
"timestamp": "2026-08-04T10:00:00.000Z"
}No secrets are ever returned. Safe to expose the response to your internal ops dashboard.
x-webhook-secret: <the secret you registered in Step 1> ← legacy
x-webhook-timestamp: <unix seconds, e.g. 1725441234> ← v1.5
x-webhook-signature: t=<unix>,v1=<HMAC-SHA256(t + "." + body, secret) hex> ← v1.5
x-webhook-signature: t=<unix>,v1=<HMAC-SHA256(t + "." + body, secret) base64> ← v1.5التحقق في ERP الخاص بك (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_SKEW_SECONDS = 300; // 5 minutes
function verifyWebhook(req, storedSecret) {
const header = req.headers["x-webhook-signature"];
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map(s => s.trim().split("=", 2))
);
const t = Number(parts.t);
const sig = parts.v1;
if (!t || !sig) return false;
if (Math.abs(Date.now() / 1000 - t) > MAX_SKEW_SECONDS) return false;
const rawBody = req.rawBody.toString(); // exact bytes as received
const expected = createHmac("sha256", storedSecret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(sig, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}التحقق في PHP
$header = $request->header('x-webhook-signature');
$parts = [];
foreach (explode(',', $header) as $seg) {
[$k, $v] = explode('=', trim($seg), 2);
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
$sig = $parts['v1'] ?? '';
if (abs(time() - $t) > 300) return abort(401);
$expected = hash_hmac('sha256', $t . '.' . $request->getContent(), $storedSecret);
if (!hash_equals($expected, $sig)) return abort(401);Prefer HMAC over the legacy header. If HMAC verification fails, reject the request — do NOT silently fall back to x-webhook-secret. That would let an attacker who intercepted a plain-secret call replay it forever. Fall-back to plain-secret is only appropriate when the HMAC header is entirely absent (indicating a pre-v1.5 caller).
إضافة Cloud ERP (Workdo Dash)
أنواع المستندات v1.5+ v1.5+
حقل documentType الاختياري في نص POST يرسم إلى رموز نوع فاتورة FTA/UBL. الافتراضي هو tax_invoice (UBL 388) عند حذفه، لذا لا تحتاج إلى هذا لـ B2B الأساسي.
القيم المدعومة
| documentType | UBL code | Use when |
|---|---|---|
tax_invoice | 388 | Standard B2B tax invoice (the default). |
commercial_invoice | 380 | Non-VAT commercial invoice. |
simplified_tax_invoice | 751 | B2C sale < AED 10,000. |
credit_note | 381 | Refund / reversal — set precedingInvoiceReference. |
debit_note | 383 | Additional charge — set precedingInvoiceReference. |
self_billed_invoice | 389 | Buyer issues on behalf of the supplier. |
precedingInvoiceReference
أرقام التعريف الضريبية للاختبار المحجوزة v1.5+ v1.5+
يتم احترامها فقط عندما يكون مفتاح API الخاص بك في وضع الاختبار (erp_test_...). استخدم هذه لممارسة كود المسار السعيد ومسار الرفض في ERP الخاص بك دون الحاجة إلى بيانات حقيقية للشركات الصغيرة والمتوسطة.
| TRN | Behaviour |
|---|---|
100000000000000 | Always accepts. Skips the TRN cross-check against the SME row — useful for smoke tests where you don't want to set up an SME just to prove your HTTP client works. Still requires a valid x-erp-partner-secret. |
100000000000666 | Always rejects. Returns 403 test_trn_reject. Lets you verify your ERP handles the error path — retry logic, user-facing error message, dashboard flag. |
erp_live_... and they get the standard TRN-mismatch treatment. There is no way to short-circuit real invoice validation in production.الاختبار
- Register with test credentials (Step 1). Save
revealedTestSecret. - Stand up a public HTTPS webhook receiver (e.g. webhook.site for a first pass).
- On the ERP Partner dashboard, click Test integration. TrustBill will hit your webhook once with a sample payload (
test: true) and confirm the round-trip. - Sign in as an SME parented to your consultancy, go to ERP integration, click Connect, copy the
tb_secret_.... - From your test harness, do a real
POST /v1/partner/invoicespush with test API credentials + the SME's connection secret. - Watch your webhook for the status change once the (test) ASP responds.
- Check the Recent activity log on the ERP Partner dashboard — every ping, webhook test, and outcome is recorded.
ردود الأخطاء
تتبع الأخطاء RFC 7807 (problem+json). الرموز الشائعة على POST /v1/partner/invoices:
| HTTP | Code | When |
|---|---|---|
| 401 | missing_api_credentials | No X-API-Key / X-API-Secret headers. |
| 401 | invalid_api_credentials | API key/secret don't match, or your integration hasn't been approved for live mode. |
| 401 | missing_connection_credential | No x-erp-partner-secret header. |
| 401 | invalid_connection_credential | The tb_secret_... doesn't match any active SME connection. |
| 403 | connection_partner_mismatch | Secret belongs to a different ERP Partner (never mix credentials). |
| 403 | sme_not_connected | SME disconnected your ERP or the connection was revoked. |
| 403 | trn_mismatch | TRN in the body doesn't match the SME who owns the connection secret. |
| 403 | test_trn_reject | v1.5+. You sent the reserved test TRN 100000000000666. Meant to help you test the rejection path. |
| 404 | not_found | v1.5+ (on GET /v1/partner/invoices/{id}). Either the id doesn't exist, or it belongs to a different SME. detail field carries "No invoice with that id exists under your connected SME." — safe to display. Same response either way to avoid ID enumeration. |
| 400 | idempotency_required | Missing or malformed Idempotency-Key header. |
| 400 | invalid_body | Body failed schema validation. Details in meta.issues. |
الانتقال إلى الوضع المباشر
- Finish integration tests end-to-end in test mode.
- On
/erp-partner, click Request live access. Status moves tolive_pending. - A TrustBill admin reviews and either approves (mints
erp_live_...credentials, copies your test webhook config to live) or rejects with a reason. - Switch your ERP to use the live API key/secret. The
x-erp-partner-secretper SME does not change. - Test mode keeps working, so you can regression-test at any time without touching production.
الدعم
- WhatsApp: +971 52 260 9313
- Email: info@trustbill.ae
- Docs: docs.trustbill.ae