E-Invoice Integration Guide

staging Try it in Swagger UI →

E-Invoice Integration API — Integration Guide

This service sits between your application (ERP, POS, billing system) and Marmin, the UAE e-invoicing provider (Accredited Service Provider). You send JSON invoices to this service. It validates them, signs in to Marmin, submits them over Peppol to the buyer and the FTA, and tells you when they are cleared.

 Your app ──X-API-Key──▶ E-Invoice Integration API ──HMAC+JWT──▶ Marmin ──Peppol──▶ Buyer + FTA
    ▲                         │  validates, retries, signs in        │
    └──── forwarded webhook ◀─┴──── stores webhook events ◀──────────┘ (status updates)

This guide covers how to use this service. The document fields (what goes inside an invoice) are exactly Marmin's, so for those follow the official Marmin documentation linked in §6.

Marmin API version: 2026-05-07. Interactive reference: Swagger UI at /docs.


Contents

  1. One-time setup
  2. Environments
  3. Calling this API
  4. Endpoint catalogue
  5. Document lifecycle and status
  6. Document fields (Marmin documentation)
  7. Webhooks
  8. Errors and retries
  9. Open points to confirm with Marmin

1. One-time setup

# Step Where
1 Get a Marmin account (email support@marmin.ai). Sandbox: https://sandbox.ae.marmin.ai. Production: https://app.ae.marmin.ai. Marmin
2 Ask Marmin support to enable API Integration, then copy the Client ID and Client Secret from Developer Dashboard → Settings → API Configuration. Marmin web app
3 In EmaraTax, select Marmin as your ASP for your TIN. emaratax.tax.gov.ae
4 Claim your business profile: POST /api/v1/business-profiles/claim/request-otp, then POST /api/v1/business-profiles/claim. Keep the returned profile_id (MBP-...). This API
5 Complete the profile (address, trade licence) with PUT /api/v1/business-profiles/{profile_id} or in the Marmin web app. This API / Marmin
6 Register https://<your-host>/api/v1/webhooks/marmin in Developer Dashboard → Webhooks and reveal the Webhook Signing Secret. Marmin web app
7 Configure the service for your environment (§2), start it, and call GET /api/v1/health/marmin to check the credentials. This service

2. Environments

APP_ENV selects the environment. Each one has a committed settings file without secrets.

development staging production
Settings file .env.development .env.staging .env.production
Marmin sandbox sandbox production (URL from Marmin at go-live)
Swagger UI /docs on on off
This guide /guide on on on

Secrets (MARMIN_CLIENT_ID, MARMIN_CLIENT_SECRET, MARMIN_WEBHOOK_SECRET, SERVICE_API_KEYS, WEBHOOK_FORWARD_SECRET) are provided as environment variables on the server, or in a git-ignored .env.<APP_ENV>.local file on a developer machine. Staging and production refuse to start if a required secret is missing. Use different credentials and keys for each environment.

Setting Meaning
DEFAULT_BUSINESS_PROFILE_ID Profile used when a request doesn't pass ?business_profile_id=.
SERVICE_API_KEYS Comma-separated keys your applications send in X-API-Key (one per app).
WEBHOOK_FORWARD_URL / WEBHOOK_FORWARD_SECRET Optional: push status changes to your app (§7).

3. Calling this API

curl -X POST "http://localhost:8000/api/v1/sales-invoices" \
  -H "X-API-Key: dev-local-key" -H "Content-Type: application/json" \
  -d @sales_invoice.json

Ready-made request bodies are in Swagger UI: open an endpoint, click Try it out, and pick from the Examples dropdown.


4. Endpoint catalogue

All paths are relative to /api/v1.

Sales and purchase documents

The same operations exist for each collection:

Collection What it is
/sales-invoices Tax invoice you issue to a customer
/sales-credit-notes Credit note against one of your sales invoices
/purchase-invoices Self-billed invoice: you, the buyer, issue it for your supplier
/purchase-credit-notes Self-billed credit note
Method Path Purpose
POST /{collection} Create and submit. Returns 201 with the stored document; keep its id (UUID).
GET /{collection} List. Filters: document_number, billed_by_name, billed_to_name, billed_by_vat, billed_to_vat, status, document_date, page, size (+ invoice_number on sales credit notes).
GET /{collection}/uuids?created_on=yyyy-MM-dd UUIDs created on a date (reconciliation).
GET /{collection}/{id} Full document with totals and status.
GET /{collection}/{id}/status Simplified status (§5). Only in this service.
PUT /{collection}/{id} Correct and resubmit, only when status is VALIDATION_FAILED. Send the complete document.
GET /{collection}/{id}/pdf PDF.
GET /{collection}/{id}/xml UBL 2.1 XML.
GET /{collection}/{id}/attachments/{attachment_id} A supporting attachment you uploaded.
GET /{collection}/{id}/peppol-status Detailed Peppol transmission status.
GET /{collection}/{id}/peppol-status-logs Peppol event log.

Other endpoints

Method Path Purpose
POST /business-profiles/claim/request-otp Send claim OTP to the EmaraTax email (valid 2 minutes).
POST /business-profiles/claim Claim the profile with the OTP. Returns profile_id.
GET /business-profiles List profiles.
GET / PUT /business-profiles/{profile_id} Retrieve / update a profile.
POST /proforma-invoices Create a proforma (not a tax document).
POST / PUT /proforma-invoices/drafts, /proforma-invoices/drafts/{id} Create / update a draft proforma.
GET /proforma-invoices, /uuids, /{id}, /{id}/pdf List / UUIDs / retrieve / PDF.
GET /codelists, /codelists/{name} Official code values, e.g. tax-categories, uae-subdivisions, unit-of-measure-codes (cached 1 hour).
POST /webhooks/marmin Receiver for Marmin webhooks. No API key; checked by signature.
GET /webhook-events, /webhook-events/{id} Received webhook events, for polling (§7).
GET /health/marmin Checks the Marmin credentials. /health (no auth) is the liveness probe.

5. Document lifecycle and status

POST ──▶ 201 Created ──▶ PENDING ──▶ APPROVED            (done: delivered to buyer + FTA)
           │                   └──▶ REJECTED            (final)
           └─▶ VALIDATION_FAILED ──▶ PUT /{id} (fix) ──▶ PENDING ...

GET /{collection}/{id}/status returns:

{
  "id": "bd63aefd-df82-430a-858c-ff77108584fe",
  "document_number": "INV-2026-0001",
  "overall_status": "PENDING",
  "participant_status": "PENDING",
  "fta_status": null,
  "is_final": false,
  "can_resubmit": false,
  "guidance": "Accepted by Marmin and in Peppol/FTA processing. Wait for a webhook or poll again later.",
  "validation_results": []
}
overall_status What your app should do
PENDING Show "submitted". Wait for a webhook, or poll every few minutes.
APPROVED Mark as e-invoiced. Download the PDF/XML if needed.
VALIDATION_FAILED Show validation_results[].text, fix the data, then PUT /{id} with the full corrected document.
REJECTED Stop. Review validation_results; usually a new document is needed.
null Not evaluated yet. Treat it as neither success nor failure.

6. Document fields (Marmin documentation)

Request bodies are exactly Marmin's. Use the official pages below for every field, rule and code value.

Base: docs.ae.marmin.ai — Businesses overview

Topic Marmin documentation
Sales invoice fields Create a sales invoice
Sales invoice examples (FTZ, export, summary, non-AED, ...) Sales invoice examples
Sales credit note fields Create a sales credit note
Purchase (self-billed) invoice fields Create a purchase invoice
Purchase credit note fields Create a purchase credit note
Proforma invoice fields Create a proforma invoice
Customer (buyer) party The Customer party object
Supplier (seller) party The Supplier party object
Business profiles About business profiles
Response fields (totals, meta_info) Retrieve a sales invoice
Code lists (tax categories, units, emirates, ...) Code lists. Also available live from this API at GET /api/v1/codelists/{name}.
Validation error codes (IBR-xxx-AE) Common errors
Implementation notes Implementation notes

Differences from Marmin when calling this service: * Use our URLs (/api/v1/...) and the X-API-Key header, not Marmin's URLs or tokens. * business_profile_id goes in the query string (or comes from the default), not the URL path. * Don't send the party that is you: no accounting_supplier_party on sales documents, no accounting_customer_party on purchase documents. Marmin fills it from the business profile.


7. Webhooks

7.1 Marmin → this service

Marmin calls POST /api/v1/webhooks/marmin when a document changes state. The event types are sale.invoice.*, sale.credit_note.*, purchase.invoice.* and purchase.credit_note.* (.create / .update). Payload details are in Marmin webhooks.

For each webhook, this service: 1. Verifies the X-HMAC-Signature header with MARMIN_WEBHOOK_SECRET (and returns 401 if it's wrong). 2. Ignores duplicates (same webhook_event_id) and stores the event. 3. Replies 200 immediately. 4. Fetches the latest document from Marmin in the background and records its overall_status. 5. Forwards it to your application if WEBHOOK_FORWARD_URL is set.

7.2 This service → your application (optional)

POST {WEBHOOK_FORWARD_URL} with:

{
  "event":    { "event_type": "sale.invoice.update", "resource_id": "<document uuid>", "webhook_event_id": "...", "...": "..." },
  "document": { "...": "full latest document (null if it could not be fetched)" }
}
Header Value
X-EInvoice-Event-Id Unique event ID. Use it to ignore duplicates.
X-EInvoice-Event-Type e.g. sale.invoice.update
X-EInvoice-Signature Base64(HMAC-SHA256(WEBHOOK_FORWARD_SECRET, raw body))

Reply with any 2xx. Failed deliveries are retried 3 times. Verify the signature over the raw body before parsing it:

import base64, hashlib, hmac

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = base64.b64encode(hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()).decode()
    return hmac.compare_digest(expected, signature)
static bool Verify(byte[] rawBody, string signature, string secret)
{
    using var h = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToBase64String(h.ComputeHash(rawBody));
    return System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
        System.Text.Encoding.UTF8.GetBytes(expected), System.Text.Encoding.UTF8.GetBytes(signature));
}

7.3 Polling instead

If your app can't receive webhooks, poll GET /api/v1/webhook-events?since=<last ISO time>. Each item has event_type, resource_id, overall_status and the full document.


8. Errors and retries

HTTP Source Body Action
401 this service {"detail": "Missing or invalid X-API-Key header"} Fix the API key.
422 this service {"detail": [{"loc": ["body","document_lines",0,"price","base_amount"], "msg": "..."}]} Fix the field in loc. Nothing was sent to Marmin.
413 this service Payload size should not exceed 8MB Shrink attachments.
400, 403, 404, 409, 5xx Marmin {"error": {"source": "marmin", "status_code": 400, "detail": <Marmin's error>}} Read detail and look up the code in Marmin common errors.
429 Marmin same wrapper, plus Retry-After / X-RateLimit-* headers Slow down.
502 this service {"error": {"source": "marmin", ...}} Marmin unreachable or credentials wrong.

Automatic retries: * 429 (rate limited) is retried automatically for every request. * 5xx and network errors are retried for GET / PUT only. * A create (POST) is never retried on 5xx, so a failure can't produce a duplicate invoice. Before you try again, check GET /{collection}?document_number=....

Marmin's limits are 60 requests/minute for each document type and 30/minute for business profiles (rate limiting). For bulk uploads, send at most about 1 request per second.


9. Open points to confirm with Marmin

# Topic Current handling
1 ~~Token response format~~ Confirmed in sandbox: {"token": "...", "expires_in": 86400} (24 h). Handled; refreshed 60 s before it expires.
2 Production API base URL Set MARMIN_BASE_URL in production at go-live.
3 Whether a fully zero-rated export from a VAT-registered company uses 380 or 480 Not checked locally; Marmin validates it.
4 Proforma retrieve / list-uuids / PDF paths Assumed to follow the sales invoice pattern.
5 Sale / Purchase register Not exposed; Marmin hasn't published these pages.