{"openapi":"3.1.0","info":{"title":"TrustBill Partner API","version":"1.0.0","description":"Public API for ERP integrators pushing invoices, managing customers, and receiving webhooks under a connected SME. Every mutation is Idempotency-Key protected; every list is cursor-paginated; every webhook is HMAC-signed and retried with backoff.","contact":{"email":"info@trustbill.ae"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://trustbill.ae/api","description":"Production (use test API keys `erp_test_...` for a safe sandbox against the same host)"}],"security":[{"apiKeyAuth":[],"apiSecretAuth":[],"connectionSecret":[]}],"tags":[{"name":"invoices"},{"name":"customers"},{"name":"validation"},{"name":"webhooks"},{"name":"meta"}],"paths":{"/v1/partner/ping":{"get":{"tags":["meta"],"summary":"Round-trip health check","description":"Confirms API key + secret work. Does NOT require x-erp-partner-secret.","security":[{"apiKeyAuth":[],"apiSecretAuth":[]}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/partner/whoami":{"get":{"tags":["meta"],"summary":"Introspect the API key","description":"Returns which ERP partner + which mode (test/live) the current API credentials belong to.","security":[{"apiKeyAuth":[],"apiSecretAuth":[]}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoamiResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/partner/validate-trn":{"get":{"tags":["validation"],"summary":"Pre-flight TRN validation against the FTA registry","description":"Verifies a 15-digit TRN against the FTA registry before you build the invoice payload. Results are cached for 24h. Not scoped to an SME — no x-erp-partner-secret required.","security":[{"apiKeyAuth":[],"apiSecretAuth":[]}],"parameters":[{"in":"query","name":"trn","required":true,"schema":{"type":"string","pattern":"^\\d{15}$"},"example":"100000000000000"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrnValidateResponse"}}}},"400":{"$ref":"#/components/responses/InvalidQuery"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/partner/customers":{"post":{"tags":["customers"],"summary":"Upsert a customer under the connected SME","description":"Idempotent on (tenant_id, externalRef). Requires Idempotency-Key.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpsertBody"},"example":{"externalRef":"acme-2024-001","name":"ACME Trading LLC","kind":"buyer","country":"AE","trn":"100000000000003","email":"billing@acme.example","phone":"+971 4 000 0000","address":{"line1":"Office 501, Building 5","city":"Dubai","emirate":"Dubai","country":"AE"}}}}},"responses":{"200":{"description":"created or updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/partner/customers/{externalRef}":{"get":{"tags":["customers"],"summary":"Read a customer by externalRef","parameters":[{"in":"path","name":"externalRef","required":true,"schema":{"type":"string"},"example":"acme-2024-001"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/partner/invoices":{"post":{"tags":["invoices"],"summary":"Submit one invoice and auto-queue it for FTA delivery","description":"Server-side POST → transform → validate → deliver runs automatically; you get status via webhook or by polling GET :id.\n\n**Response `status` (create-only vocabulary):**\n- `queued` — invoice entered the delivery pipeline.\n- `held` — pre-submit validation refused it. `heldReason` + `heldErrors[]` tell you which fields to fix. To retry: DELETE the held invoice and POST the corrected body.\n- `draft` — auto-submit crashed. Recover via `POST /v1/partner/invoices/{id}/retry`.\n\n**Reserved test TRNs (test / sandbox modes only):**\n- `seller.trn = \"100000000000000\"` → **always accepts** end-to-end. Skips validation + delivery, moves `draft → delivered` synchronously, fires the HMAC webhook within ~5s. Use this to smoke-test your integration including the webhook handler.\n- `seller.trn = \"100000000000666\"` → **always rejects** with `403 test_trn_reject`. Use to exercise your rejection code path.\nLive mode ignores both — TRN cross-check applies as normal.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceCreateBody"},"example":{"trn":"100000000000000","number":"INV-2026-0001","issueDate":"2026-09-16","dueDate":"2026-10-16","documentType":"tax_invoice","currency":"AED","seller":{"name":"Your SME Legal Name","trn":"100000000000000","country":"AE","address":{"line1":"Office 501, Al Fahim Tower","city":"Dubai","emirate":"Dubai","country":"AE"}},"buyer":{"name":"ACME Trading LLC","trn":"100000000000003","country":"AE","address":{"line1":"Marina Plaza","city":"Dubai","emirate":"Dubai","country":"AE"}},"lines":[{"description":"Consulting hours","quantity":10,"unitPrice":500,"vatRate":0.05,"taxCategory":"standard_5","unitOfMeasureCode":"HUR","itemType":"services"}]}}}},"responses":{"201":{"description":"queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceQueued"}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"tags":["invoices"],"summary":"List invoices you have submitted for the connected SME","parameters":[{"in":"query","name":"status","schema":{"type":"array","items":{"type":"string"}},"example":["delivered","rejected"]},{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"number","schema":{"type":"string"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/partner/invoices/{id}":{"get":{"tags":["invoices"],"summary":"Fetch one invoice's current status","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["invoices"],"summary":"Cancel a pre-delivery invoice","description":"Deletes draft / queued / blocked_no_credit / rejected. Post-delivery invoices must be reversed with a credit note.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/v1/partner/invoices/{id}/retry":{"post":{"tags":["invoices"],"summary":"Retry a rejected or blocked-no-credit invoice","description":"Same rate-limit the SME UI uses (3/hour/invoice).","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"202":{"description":"requeued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceQueued"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/partner/invoices/{id}/replay-webhook":{"post":{"tags":["webhooks"],"summary":"Force-resend the last webhook payload for an invoice","description":"Creates a fresh delivery row with a refreshed timestamp so the HMAC signature stays inside your anti-replay window. Original history is preserved.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"replay queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/partner/invoices/{id}/webhook-deliveries":{"get":{"tags":["webhooks"],"summary":"Audit trail of outbound webhook attempts for one invoice","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/partner/invoices/bulk":{"post":{"tags":["invoices"],"summary":"Submit up to 100 invoices in one call","description":"Per-item idempotency keys are derived from the batch's Idempotency-Key as `${batchKey}:${index}`. To recover from a partial failure, RESEND THE ORIGINAL BODY (same items, same order, same batch key) — the API will replay each item's stored result deterministically. Do NOT retry a subset with a fresh index range: the derived per-item keys would collide with completed items and hash-mismatch to 409.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkInvoiceBody"},"example":{"invoices":[{"externalRef":"acme-batch-1","trn":"100000000000000","number":"INV-2026-1001","issueDate":"2026-09-16","currency":"AED","seller":{"name":"Your SME Legal Name","trn":"100000000000000","country":"AE","address":{"line1":"Office 501, Al Fahim Tower","city":"Dubai","emirate":"Dubai","country":"AE"}},"buyer":{"name":"ACME","trn":"100000000000003","country":"AE","address":{"line1":"Marina Plaza","city":"Dubai","emirate":"Dubai","country":"AE"}},"lines":[{"description":"Item","quantity":1,"unitPrice":100,"vatRate":0.05,"unitOfMeasureCode":"EA","itemType":"goods"}]}]}}}},"responses":{"207":{"description":"per-item results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkResponse"}}}},"400":{"$ref":"#/components/responses/InvalidBody"},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"webhooks":{"invoiceStatusChanged":{"post":{"tags":["webhooks"],"summary":"invoice.status_changed — TrustBill → your ERP","description":"When an invoice you pushed reaches a terminal state (`delivered` or `rejected`), TrustBill POSTs this payload to your registered webhook URL. Deliveries are durable — persisted in `erp_partner_webhook_deliveries` and retried on the schedule below until 2xx or exhaustion.\n\n**Verification (required):**\n1. Read `X-Webhook-Timestamp` and `X-Webhook-Signature: t=<unix>,v1=<hex>`.\n2. Reject if `|now - t| > 300` seconds.\n3. Compute `hmac_sha256(webhook_secret, `${t}.${raw_body}`)` and constant-time-compare to `v1=` value.\n4. Only after both checks pass, parse the JSON and update your record.\n\n**Retry schedule** (on non-2xx or transport failure): 30 s → 2 m → 10 m → 30 m → 1 h → 3 h. After 6 attempts the delivery is marked `exhausted`; use `POST /v1/partner/invoices/{id}/replay-webhook` to fire a fresh attempt manually.\n\n**Headers you should trust:**\n- `X-Webhook-Signature: t=<unix>,v1=<hex>` — HMAC-SHA256 over `${t}.${raw_body}`.\n- `X-Webhook-Timestamp: <unix>` — the same `t` (convenience for logging).\n\nThe legacy `x-webhook-secret` header is no longer emitted; if you see it in old logs, it was retired.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceStatusChangedPayload"},"example":{"event":"invoice.status_changed","erpPartnerId":"3e7f2b6e-9c4a-4c8f-9d1e-8a6b5c4d3e2f","invoiceId":"5a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d","status":"delivered","reason":null,"timestamp":"2026-09-16T09:30:00.000Z"}}}},"responses":{"200":{"description":"ACK. Any 2xx body is accepted."},"4XX":{"description":"TrustBill treats any non-2xx as a delivery failure and re-tries per the backoff schedule above."},"5XX":{"description":"Same as 4xx — retryable."}}}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"},"apiSecretAuth":{"type":"apiKey","in":"header","name":"X-API-Secret"},"connectionSecret":{"type":"apiKey","in":"header","name":"x-erp-partner-secret"}},"parameters":{"IdempotencyKey":{"in":"header","name":"Idempotency-Key","required":true,"schema":{"type":"string","minLength":1,"maxLength":128},"description":"Deterministic key (UUID or hash). Same key + same body replays the stored result."}},"responses":{"InvalidBody":{"description":"Body failed schema validation","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"InvalidQuery":{"description":"Query parameters failed validation","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"Unauthorized":{"description":"Missing/invalid credentials","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"Forbidden":{"description":"Credentials valid but not allowed for this scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"NotFound":{"description":"Resource missing or not owned by caller","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"Conflict":{"description":"State transition not allowed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"IdempotencyConflict":{"description":"The same `Idempotency-Key` was used with a DIFFERENT request body. Use a fresh key (recommended: a UUID) for every new request; the same key is only safe for RETRY of the identical request.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}},"RateLimited":{"description":"Per-partner budget exceeded","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemError"}}}}},"schemas":{"PingResponse":{"type":"object","required":["ok","mode","timestamp"],"properties":{"ok":{"type":"boolean","example":true},"mode":{"type":"string","enum":["test","live"]},"timestamp":{"type":"string","format":"date-time"}}},"WhoamiResponse":{"type":"object","required":["partnerId","erpName","status","mode","connectedSmes","lastTestAt","timestamp"],"properties":{"partnerId":{"type":"string","format":"uuid"},"erpName":{"type":"string"},"status":{"type":"string","enum":["test","live_pending","live_active","live_rejected","revoked","unknown"]},"mode":{"type":"string","enum":["test","live"]},"connectedSmes":{"type":"integer","description":"Count of SMEs currently connected to this ERP."},"lastTestAt":{"type":"string","format":"date-time","nullable":true},"timestamp":{"type":"string","format":"date-time"}}},"TrnValidateResponse":{"type":"object","required":["trn","status","cachedAt"],"properties":{"trn":{"type":"string"},"status":{"type":"string","enum":["verified","rejected","pending"]},"legalName":{"type":"string","description":"Present when status=verified"},"reason":{"type":"string","description":"Present when status=rejected"},"cachedAt":{"type":"string","format":"date-time"}}},"Address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"emirate":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2,"default":"AE"},"postalCode":{"type":"string"}}},"CustomerUpsertBody":{"type":"object","required":["externalRef","name"],"properties":{"externalRef":{"type":"string","maxLength":128},"name":{"type":"string","maxLength":300},"kind":{"type":"string","enum":["buyer","seller","both"],"default":"buyer"},"country":{"type":"string","minLength":2,"maxLength":2,"default":"AE"},"trn":{"type":"string","pattern":"^\\d{15}$","nullable":true},"email":{"type":"string","format":"email","nullable":true},"phone":{"type":"string","nullable":true},"address":{"$ref":"#/components/schemas/Address"}}},"Customer":{"type":"object","required":["customerId","externalRef","name","kind","country","createdAt","updatedAt"],"properties":{"customerId":{"type":"string","format":"uuid"},"externalRef":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string"},"country":{"type":"string"},"trn":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"address":{"$ref":"#/components/schemas/Address"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PartyAddress":{"type":"object","required":["line1","city"],"description":"Physical address. `line1` + `city` are required for both seller and buyer. Pre-submit validator refuses invoices whose party has no address; docs previously hid this requirement.","properties":{"line1":{"type":"string","maxLength":200},"line2":{"type":"string","maxLength":200},"city":{"type":"string","maxLength":100},"emirate":{"type":"string","maxLength":50},"country":{"type":"string","minLength":2,"maxLength":2,"default":"AE"},"postalCode":{"type":"string","maxLength":20}}},"InvoiceParty":{"type":"object","required":["name","country","address"],"description":"Seller or buyer block. `trn` is required whenever the party is VAT-registered (any UAE B2B counterparty above the mandatory threshold). `legalRegistrationType: PAS` unlocks passport-based non-registered buyers, in which case `passportNumber` + `passportCountry` are required and `trn` is not.","properties":{"name":{"type":"string","maxLength":300},"trn":{"type":"string","pattern":"^\\d{15}$","description":"15-digit UAE TRN. Required for VAT-registered parties. Omit when `legalRegistrationType = PAS`."},"country":{"type":"string","minLength":2,"maxLength":2,"default":"AE"},"address":{"$ref":"#/components/schemas/PartyAddress"},"email":{"type":"string","format":"email","nullable":true},"phone":{"type":"string","maxLength":30,"nullable":true},"legalRegistrationType":{"type":"string","enum":["TIN","PAS"],"default":"TIN","description":"TIN = standard tax-registered party (uses `trn`). PAS = passport-based non-registered buyer (uses `passportNumber` + `passportCountry`)."},"passportNumber":{"type":"string","description":"Required when `legalRegistrationType = PAS`."},"passportCountry":{"type":"string","minLength":2,"maxLength":2,"description":"ISO-2 country of the passport issuer. Required when `legalRegistrationType = PAS`."},"tradeLicenseNumber":{"type":"string","description":"UAE trade licence number. Some downstream ASPs surface this on the FTA-facing UBL."}}},"DeliveryAddress":{"type":"object","description":"Physical delivery / ship-to address. MANDATORY for export invoices and any invoice whose delivery differs from the buyer address (e-commerce, drop-shipping, DDP shipments).","properties":{"line1":{"type":"string"},"line2":{"type":"string"},"city":{"type":"string"},"emirate":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2},"postalCode":{"type":"string"},"actualDeliveryDate":{"type":"string","format":"date","description":"When the goods were delivered / service rendered."},"latestDeliveryDate":{"type":"string","format":"date","description":"Contractual delivery deadline. Common on export invoices."}}},"InvoiceLine":{"type":"object","required":["description","quantity","unitPrice","vatRate"],"properties":{"description":{"type":"string"},"quantity":{"type":"number"},"unitPrice":{"type":"number","description":"Line unit price in AED (decimal, not fils)."},"vatRate":{"type":"number","minimum":0,"maximum":1,"description":"VAT rate as a DECIMAL fraction, not a percent. Use 0.05 for 5% UAE VAT, 0 for zero-rated. Passing 5 will fail schema validation."},"taxCategory":{"type":"string","enum":["standard_5","zero_rated_export","zero_rated_healthcare","zero_rated_education","exempt_financial","exempt_residential_rent","reverse_charge_designated_zone","reverse_charge_electronic_devices","reverse_charge_gold_diamonds","reverse_charge_crude_or_refined_oil"],"default":"standard_5","description":"FTA-aligned tax-category. Most B2B invoices are `standard_5` (5% VAT). Use `zero_rated_*` for the four FTA zero-rate categories, `exempt_*` for exempt supplies, `reverse_charge_*` for reverse-charge transactions. Do NOT pass UBL short codes like S/Z/E/O; those are rejected."},"taxCategoryReason":{"type":"string","description":"Optional free-text elaboration on the tax category. Kept in the ERP audit trail; not surfaced to FTA."},"classificationCode":{"type":"string","description":"HS commodity classification (e.g. `85287200` for LCD TVs). Optional; strongly recommended for exports."},"unitOfMeasureCode":{"type":"string","default":"EA","description":"UN/CEFACT rec20 code. Common: `EA` (each), `HUR` (hour), `KGM` (kilogram), `LTR` (litre), `MTR` (metre), `SET`. Defaults to `EA` when omitted."},"itemType":{"type":"string","enum":["goods","services","both"],"description":"Line kind. `goods` = physical delivery (triggers export / delivery-address requirements). `services` = intangible / labour lines. `both` = mixed line. Optional; downstream ASP falls back to a heuristic when omitted."},"discountAmount":{"type":"number","description":"Absolute AED discount deducted from `quantity * unitPrice` before tax."},"chargeAmount":{"type":"number","description":"Absolute AED surcharge added before tax (freight, handling, etc.)."},"classificationCodeScheme":{"type":"string","enum":["HS"],"default":"HS","description":"Scheme for `classificationCode`. Only `HS` (Harmonised System) is currently supported."}}},"InvoiceCreateBody":{"type":"object","required":["trn","number","issueDate","currency","seller","buyer","lines"],"description":"Full partner-facing invoice envelope. The MINIMUM viable body needs `trn` + `number` + `issueDate` + `currency` + `seller{name,country,address}` + `buyer{name,country,address}` + `lines[{description,quantity,unitPrice,vatRate}]`. Every additional field on this schema becomes MANDATORY under specific document types or business scenarios (credit notes, exports, bank-transfer payment, etc.); those constraints are noted per-field.","properties":{"trn":{"type":"string","pattern":"^\\d{15}$","description":"SUPPLIER TRN. Cross-checked against the SME resolved by `x-erp-partner-secret`; mismatch = 403 `trn_mismatch`. In test/sandbox mode, `100000000000000` bypasses this check AND completes the whole pipeline synchronously (fires the webhook) so partners can smoke-test the receive-side."},"number":{"type":"string","description":"Your ERP's invoice number. Unique per SME per year. Not resettable."},"issueDate":{"type":"string","format":"date","description":"ISO-8601 date the invoice was raised. FTA rules apply about how far in the past this can be."},"dueDate":{"type":"string","format":"date","nullable":true,"description":"Payment due date. Optional for immediate-payment invoices; recommended for anything on credit terms."},"invoiceType":{"type":"string","enum":["sale","purchase"],"default":"sale","description":"Standard sale (SME is seller) vs. purchase (SME is buyer, recording an incoming invoice). Most B2B integrations only ever send `sale`."},"documentType":{"type":"string","enum":["tax_invoice","commercial_invoice","simplified_tax_invoice","credit_note","debit_note","self_billed_invoice"],"default":"tax_invoice","description":"UBL document class. `tax_invoice` is the B2B default. `credit_note` / `debit_note` REQUIRE `precedingInvoiceNumber` + `precedingInvoiceIssueDate` + `creditNoteReasonCode`."},"precedingInvoiceReference":{"type":"string","description":"DEPRECATED. Use `precedingInvoiceNumber`. Kept for pre-v1.5 callers."},"precedingInvoiceNumber":{"type":"string","description":"Required for `credit_note` / `debit_note`. The invoice number this document adjusts."},"precedingInvoiceIssueDate":{"type":"string","format":"date","description":"Required for `credit_note` / `debit_note`. Issue-date of the invoice this document adjusts."},"creditNoteReasonCode":{"type":"string","enum":["01","02","03","04","05"],"description":"UBL 5189 reason code. Required for `credit_note`. 01 = cancellation, 02 = correction, 03 = discount, 04 = return of goods, 05 = other."},"currency":{"type":"string","enum":["AED"],"description":"Only AED is supported today. Multi-currency support is on the roadmap."},"purchaseOrderReference":{"type":"string","description":"Optional PO number your buyer wants echoed onto their AP record."},"salesOrderReference":{"type":"string","description":"Optional SO number your ERP uses to link back to the fulfillment record."},"contractReference":{"type":"string","description":"Optional master-contract identifier — for framework agreements / MSAs."},"seller":{"allOf":[{"$ref":"#/components/schemas/InvoiceParty"}],"description":"Your SME's own party block. Both `seller` AND `buyer` are required; omitting either returns 400."},"buyer":{"allOf":[{"$ref":"#/components/schemas/InvoiceParty"}],"description":"The invoice recipient. `trn` is required for VAT-registered buyers (most UAE B2B). For non-registered end-consumers with cross-border passports, set `legalRegistrationType: \"PAS\"` and provide `passportNumber` + `passportCountry`."},"deliveryAddress":{"allOf":[{"$ref":"#/components/schemas/DeliveryAddress"}],"description":"REQUIRED for export invoices (buyer.country !== AE) and any invoice whose delivery differs from the buyer's address. Optional for domestic same-address B2B."},"paymentMeansCode":{"type":"string","enum":["10","20","30","42","48","49","57","58"],"description":"UBL 4461 payment-means code. 10 = cash, 20 = cheque, 30 = credit transfer (bank), 42 = direct debit, 48 = card, 49 = ACH, 57 = standing order, 58 = SEPA. Required when `paymentAccountIdentifier` is set."},"paymentAccountIdentifier":{"type":"string","description":"IBAN or bank account number for `paymentMeansCode = 30` (bank transfer). Required for bank-transfer invoices — pre-submit validator refuses without it."},"paymentAccountName":{"type":"string","description":"Beneficiary name on the bank account. Sits alongside `paymentAccountIdentifier`."},"paymentTermsNote":{"type":"string","description":"Free-text terms shown on the buyer's invoice (\"Net 30\", \"COD\", etc.)."},"lines":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/InvoiceLine"}}}},"InvoiceQueued":{"type":"object","required":["invoiceId","number","status"],"description":"Response from POST /invoices and each item in POST /invoices/bulk. `status` reflects the REAL post-submit state — `queued` (entered the delivery pipeline), `held` (pre-submit validation refused it; see `heldReason` + `heldErrors`), or `draft` (auto-submit crashed; recover via POST /invoices/:id/retry).","properties":{"invoiceId":{"type":"string","format":"uuid"},"number":{"type":"string"},"status":{"type":"string","enum":["queued","held","draft"]},"heldReason":{"type":"string","description":"Present only when status=held. Short machine-readable reason (e.g. `pint_ae_failures`)."},"heldErrors":{"type":"array","description":"Present only when status=held. Per-field validation errors.","items":{"type":"object","properties":{"code":{"type":"string"},"path":{"type":"string"},"message":{"type":"string"}}}}}},"InvoiceStatus":{"type":"object","required":["invoiceId","number","status","createdAt","updatedAt"],"properties":{"invoiceId":{"type":"string","format":"uuid"},"number":{"type":"string"},"status":{"type":"string","description":"Every invoice starts in `draft` and transitions forward. `queued`/`transformed`/`validated` are in-pipeline states; `delivered` + `acknowledged` are the success terminals; `rejected` and `blocked_no_credit` are the failure terminals.","enum":["draft","queued","transformed","validated","blocked_no_credit","delivered","acknowledged","rejected"]},"reason":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"InvoiceListResponse":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceStatus"}},"nextCursor":{"type":"string","nullable":true}}},"BulkInvoiceBody":{"type":"object","required":["invoices"],"properties":{"invoices":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/InvoiceCreateBody"}}}},"BulkResponse":{"type":"object","required":["results","summary"],"properties":{"results":{"type":"array","items":{"oneOf":[{"type":"object","required":["index","ok","invoiceId","number","status"],"properties":{"index":{"type":"integer"},"externalRef":{"type":"string"},"ok":{"type":"boolean","enum":[true]},"invoiceId":{"type":"string","format":"uuid"},"number":{"type":"string"},"status":{"type":"string"}}},{"type":"object","required":["index","ok","error"],"properties":{"index":{"type":"integer"},"externalRef":{"type":"string"},"ok":{"type":"boolean","enum":[false]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}]}},"summary":{"type":"object","required":["total","ok","failed"],"properties":{"total":{"type":"integer"},"ok":{"type":"integer"},"failed":{"type":"integer"}}}}},"ReplayResponse":{"type":"object","required":["deliveryId","status"],"properties":{"deliveryId":{"type":"string","format":"uuid"},"status":{"type":"string","example":"pending"}}},"WebhookDelivery":{"type":"object","required":["deliveryId","status","event","mode","attempts","maxAttempts","nextAttemptAt","createdAt","updatedAt"],"properties":{"deliveryId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","delivered","failed","exhausted"]},"event":{"type":"string"},"mode":{"type":"string","enum":["test","live"]},"attempts":{"type":"integer"},"maxAttempts":{"type":"integer"},"lastResponseStatus":{"type":"integer","nullable":true},"lastError":{"type":"string","nullable":true},"nextAttemptAt":{"type":"string","format":"date-time"},"deliveredAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"WebhookDeliveryList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"ProblemError":{"type":"object","required":["type","title","status"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"code":{"type":"string","example":"validation_failed"},"meta":{"type":"object","additionalProperties":true}}},"InvoiceStatusChangedPayload":{"type":"object","required":["event","erpPartnerId","invoiceId","status","reason","timestamp"],"properties":{"event":{"type":"string","enum":["invoice.status_changed"],"description":"Only event type emitted today. Future events will be additive; treat unknown values as no-op."},"erpPartnerId":{"type":"string","format":"uuid","description":"Your ERP-partner id — same value `whoami` returns. Useful when one endpoint receives webhooks for multiple TrustBill environments."},"invoiceId":{"type":"string","format":"uuid","description":"The TrustBill invoice id you got back from `POST /v1/partner/invoices`."},"status":{"type":"string","enum":["delivered","rejected"],"description":"Only terminal states fire a webhook. `acknowledged` collapses into `delivered` for the partner surface."},"reason":{"type":"string","nullable":true,"description":"Human-readable failure reason when status=rejected; null otherwise."},"timestamp":{"type":"string","format":"date-time","description":"ISO-8601 timestamp of the delivery attempt. Same seconds-precision value the HMAC signature commits to via `X-Webhook-Timestamp`."}}}}}}