This is a sandbox for building an ERP integration against the UAE PINT AE e-invoicing API. It implements the same contract as the accredited platform your invoices will eventually flow through — same paths, same request and response shapes, same numeric status codes, same error bodies — so an integration written against this one moves across without rework.
It is a test environment, not an e-invoicing platform. Nothing is signed, nothing reaches the Peppol network, and nothing is filed with the Federal Tax Authority. Downstream results are simulated. See Limits of this environment.
| API base URL | https://erp-uat.actinode.com/api/v1 |
| client_id | actinode-sandbox-sap-demo |
| client_secret | sk_sandbox_9f2c41d7e83b40a5b1c6d0e7a3f85920 |
| Permissions | invoice:view, invoice:submit, document:view, document:upload, document:download, document:delete |
| Seller endpoint | 0235:1203491724 — Actinode Demo Trading LLC |
/. Omitting it produces a redirect that some HTTP clients will not follow on a POST."1050.00", not 1050.00. This is the most common defect in an ERP connector and it is rejected outright.YYYY-MM-DD. Timestamps are ISO 8601 UTC.after cursor work.Delivery is simulated against a participant registry. Sending to a buyer electronic_address outside this list is accepted by the API and then fails at delivery — which is the most common real-world failure, so it is worth testing on purpose.
| Participant | Name | Accepts |
|---|---|---|
| 0235:1999999911 | XYZ Corporation FZCO | 380, 381, 480, 81, 389, 261 |
| 0235:1000000001 | Gulf Distribution LLC | 380, 381, 480, 81 |
| 0235:1000000002 | Emirates Retail Group PJSC | 380, 381 |
| 0235:1000000003 | Falcon Logistics FZE | 380, 381, 480, 81, 389, 261 |
| 0235:1000000004 | Al Noor Contracting LLC | 380, 381, 389, 261 |
There are two routes in, and they differ in when content errors reach you.
Send the whole invoice as detail in the request body. Validation runs before the response: a bad payload returns 400 listing the failing fields, and a 201 means the content passed. Simplest to build against.
# 1 — token curl -X POST "https://erp-uat.actinode.com/api/v1/oauth/token/" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=actinode-sandbox-sap-demo&client_secret=$CLIENT_SECRET" # 2 — submit (see POST /api/v1/invoices/ for the full body) curl -X POST "https://erp-uat.actinode.com/api/v1/invoices/" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d @invoice.json # 3 — track curl -H "Authorization: Bearer $ACCESS_TOKEN" \ "https://erp-uat.actinode.com/api/v1/invoices/$INVOICE_ID/"
Reserve a slot, PUT the XML to the URL you get back, then submit by path. Preferred when your system can already emit conformant UBL, because your bytes stay canonical.
201 and flips to Rejected a moment later. Poll the invoice, or you will silently lose documents.# 1 — reserve a slot
curl -X POST "https://erp-uat.actinode.com/api/v1/documents/" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"INV-2026-0001","extension":"xml"}'
# → { "path": "s3://...", "upload_url": "https://...", "expires_in": 3600 }
# 2 — upload the bytes (no Authorization header)
curl -X PUT -H "Content-Type: application/xml" \
--data-binary @INV-2026-0001.xml "$UPLOAD_URL"
# 3 — submit, referencing the path from step 1
curl -X POST "https://erp-uat.actinode.com/api/v1/invoices/" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Invoice INV-2026-0001","invoice_number":"INV-2026-0001",
"issue_date":"2026-09-15","invoice_type_code":"380",
"source_file_path":"'"$PATH_FROM_STEP_1"'"}'OAuth2 client credentials. Every other endpoint takes the resulting access token as a Bearer credential. Note the content types: the token endpoint is form-encoded, the refresh endpoint is JSON.
Exchange the client_id / client_secret pair for an access + refresh token pair.
Content-Type: application/x-www-form-urlencoded
| Field | Type | Required | Description |
|---|---|---|---|
| client_id | string | required | The API client identifier. |
| client_secret | string | required | The API client secret. |
client_id=actinode-sandbox-sap-demo&client_secret=<your-client-secret>
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMU0xSEM1…",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMU0xSEM1…",
"token_type": "Bearer",
"expires_in": 600,
"organization": {
"id": "01M1HC5EEEZHYHBAPRQ54V0BTS",
"name": "Actinode Demo Trading LLC"
},
"client_name": "SAP Demo API client"
}| Status | When |
|---|---|
| 400 | client_id or client_secret missing{
"error": "invalid_request"
} |
| 401 | Credentials rejected, or the API client is deactivated{
"error": "invalid_client"
} |
Exchange a refresh token for a fresh access + refresh pair.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | required | The refresh token from the previous token or refresh response. |
{
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMU0xSEM1…"
}{
"access_token": "eyJ0eXAiOiJKV1Qi…",
"refresh_token": "eyJ0eXAiOiJKV1Qi…",
"token_type": "Bearer",
"expires_in": 600,
"organization": {
"id": "01M1HC5EEEZHYHBAPRQ54V0BTS",
"name": "Actinode Demo Trading LLC"
},
"client_name": "SAP Demo API client"
}| Status | When |
|---|---|
| 400 | refresh_token missing{
"error": "invalid_request"
} |
| 401 | Expired, invalid, or already-used refresh token{
"error": "invalid_grant"
} |
The main surface. All endpoints need a Bearer token and operate on the organisation the API client is bound to.
Submit an invoice or credit note. Provide exactly one of `detail` (inline JSON) or `source_file_path` (a previously uploaded XML or JSON file).
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| name | string (≤255) | required | Human-readable label for the invoice. |
| invoice_number | string (1–100) | required | IBT-001. Your document number. Unique per organisation per issue year. |
| issue_date | date (YYYY-MM-DD) | required | IBT-002. Date of issue. |
| invoice_type_code | enum | required | IBT-003. One of 380, 381, 480, 81, 389, 261. |
| detail | object | conditional | The full PINT AE field tree. Required when source_file_path is omitted. |
| source_file_path | string (s3:// URI) | conditional | Path returned by POST /api/v1/documents/. Required when detail is omitted. |
{
"id": "01M1HDAFF9XHR7K2QJ4TZ8W3PC",
"name": "Invoice INV-2026-0001",
"invoice_number": "INV-2026-0001",
"issue_date": "2026-09-15",
"invoice_type_code": "380",
"source": "form",
"created_at": "2026-09-15T09:12:30.004Z"
}| Status | When |
|---|---|
| 400 | Content validation failed (inline JSON mode). The body is a dictionary keyed by field path.{
"detail.totals.invoice_total_amount_without_vat": [
"IBR-CO-13: Invoice total amount without VAT (IBT-109) must equal the sum of line net amounts minus document-level allowances plus document-level charges. Expected \"9700.00\", got \"9500.00\"."
]
} |
| 400 | Both detail and source_file_path supplied, or neither{
"non_field_errors": [
"Provide either `detail` or `source_file_path`, not both."
]
} |
| 400 | invoice_number already used in the same issue year{
"invoice_number": [
"An invoice you sent in 2026 already uses the number \"INV-2026-0001\"."
]
} |
| 401 | Token missing, invalid, or expired{
"detail": "Token is invalid or expired"
} |
| 403 | API client deactivated, missing invoice:submit, or organisation not Registered{
"detail": "Organisation must be Registered before submitting invoices."
} |
The full record: the summary status, the three underlying pipeline statuses, the document paths, and the whole PINT AE tree.
| Status | When |
|---|---|
| 401 | Token missing or invalid |
| 403 | API client deactivated, or missing invoice:view |
| 404 | No such invoice, or it belongs to another organisation{
"detail": "No Invoice matches the given query."
} |
Paginated list with filtering, search and ordering. Supports page-based and cursor-based iteration.
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number. Default 1. |
| page_size | integer | optional | Items per page. Default 20, maximum 100. |
| ordering | string | optional | One of name, created_at, id. Prefix with - for descending. Default -id. |
| search | string | optional | Matches against name and invoice_number. |
| status | integer 1–4 | optional | 1 Processing, 2 Completed, 3 Rejected, 4 Failed. |
| direction | integer 1–2 | optional | 1 Sent, 2 Received. |
| invoice_type_code__in | string | optional | Comma-separated type codes, e.g. 380,381. |
| can_resubmit | boolean | optional | Only invoices that can be resubmitted. |
| issue_date_from / issue_date_to | date | optional | IBT-002 bounds, inclusive. |
| created_at_from / created_at_to | date | optional | Creation-date bounds, inclusive of the whole day. |
| completed_at_from / completed_at_to | date | optional | Completion-date bounds, inclusive of the whole day. |
| after / before | invoice id | optional | Cursor. Only invoices created after / before the given id. |
| Status | When |
|---|---|
| 400 | A cursor id your API client cannot see{
"after": [
"Invalid cursor: object with id \"01ZZZ…\" was not found."
]
} |
| 401 | Token missing or invalid |
Replace the payload of an invoice whose can_resubmit is true, and restart the lifecycle. The invoice id is preserved, so any reference you hold stays valid.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | New label. Defaults to the existing one. |
| issue_date | date | optional | Defaults to the existing one. |
| invoice_type_code | enum | optional | Defaults to the existing one. |
| detail | object | conditional | The corrected PINT AE tree. One of detail or source_file_path. |
| source_file_path | string | conditional | Path to the corrected file. |
{
"name": "Invoice INV-2026-0001",
"issue_date": "2026-09-15",
"invoice_type_code": "380",
"detail": "/* the corrected PINT AE tree — same shape as POST /api/v1/invoices/ */"
}{
"id": "01M1HDAFF9XHR7K2QJ4TZ8W3PC",
"name": "Invoice INV-2026-0001",
"invoice_number": "INV-2026-0001",
"issue_date": "2026-09-15",
"invoice_type_code": "380",
"source": "form",
"created_at": "2026-09-15T09:12:30.004Z"
}| Status | When |
|---|---|
| 400 | Validation error in the new payload |
| 403 | can_resubmit is false — still in flight, or already Completed{
"detail": "Can't resubmit this invoice"
} |
| 404 | No such invoice |
File storage, used by the XML and JSON-file submission modes. The pattern is: reserve a slot, PUT the bytes to the returned URL, then reference the returned path when you submit.
Reserves a slot and returns a presigned upload URL valid for one hour, plus the path you reference at submit time.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| name | string (1–255) | required | Filename without the extension. |
| extension | string | required | One of xml, json, pdf, xlsx, csv. |
{
"name": "INV-2026-0001",
"extension": "xml"
}{
"id": "01M1HD8209F0HC2XMXJSCPNFKV",
"name": "INV-2026-0001",
"extension": "xml",
"path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001.xml",
"upload_url": "https://erp-uat.actinode.com/api/v1/uploads/8Kd2mR…",
"expires_in": 3600,
"created_at": "2026-09-15T09:11:04.220Z"
}| Status | When |
|---|---|
| 400 | Missing fields, or an extension outside the allowed list |
| 403 | Missing document:upload, or organisation not Registered |
PUT the raw file to the upload_url from the previous call. Do NOT send an Authorization header.
Content-Type: application/xml (or application/json)
<?xml version="1.0" encoding="UTF-8"?> <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" …> …your PINT AE UBL… </Invoice>
(no body)
| Status | When |
|---|---|
| 400 | Empty upload body |
| 403 | Unknown or expired upload URL |
Exchange a document id or an s3:// path for a temporary download URL. Use it to fetch the signed wire copy, the tax declaration document, or your original upload.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | conditional | Document id. One of id or s3_uri. |
| s3_uri | string | conditional | Any of the *_location_path values from an invoice. |
| filename | string | optional | Filename to suggest to the browser. |
{
"s3_uri": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-wire.xml"
}{
"download_url": "https://erp-uat.actinode.com/api/v1/files/eyJkb2Mi…",
"expires_in": 3600
}| Status | When |
|---|---|
| 400 | Neither id nor s3_uri supplied, or the URI is not yours |
| 403 | Missing document:download |
The organisation's file library.
| Field | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number. Default 1. |
| page_size | integer | optional | Default 20, maximum 100. |
| search | string | optional | Matches the name field. |
{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"id": "01M1HD8209F0HC2XMXJSCPNFKV",
"name": "INV-2026-0001",
"extension": "xml",
"path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001.xml",
"upload_url": null,
"expires_in": null,
"created_at": "2026-09-15T09:11:04.220Z"
}
]
}Metadata for a single document. Same shape as a list row.
{
"id": "01M1HD8209F0HC2XMXJSCPNFKV",
"name": "INV-2026-0001",
"extension": "xml",
"path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001.xml",
"upload_url": null,
"expires_in": null,
"created_at": "2026-09-15T09:11:04.220Z"
}| Status | When |
|---|---|
| 404 | No such document |
Permanently removes the document and its stored bytes.
(no body)
| Status | When |
|---|---|
| 404 | No such document |
Onboarding a taxable person. Unauthenticated, because they are called on behalf of someone who does not have credentials yet. Most ERP integrations never need these — your organisation is already registered.
Step 1. Verifies the TIN and returns a verification_token valid for 30 minutes, which the follow-up call requires.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| tin_number | string (10 digits) | required | The UAE TRN. |
| string (email) | required | Contact email. Must match at register time. | |
| phone_number | string | optional | E.164, e.g. +971501234567. |
| action | enum | optional | register (default), de-register, or re-verify. |
{
"tin_number": "1234567890",
"email": "finance@example.ae",
"phone_number": "+971501234567",
"action": "register"
}{
"verified": true,
"tin_number": "1234567890",
"email": "finance@example.ae",
"phone_number": "+971501234567",
"entity_name_en": "Taxable Person 1234567890",
"entity_name_ar": null,
"vat_trn": "AE123456789000",
"effective_date": "2026-09-15",
"legal_type": "0",
"legal_type_description": "Limited Liability Company (LLC)",
"verification_token": "eyJ0aW4iOiIxMjM0NTY3ODkwIi…",
"token_expires_in_minutes": 30,
"message": "TIN verified successfully"
}| Status | When |
|---|---|
| 400 | Invalid request data, or the organisation is in a state that disallows the action |
| 404 | Organisation not found (de-register and re-verify only) |
| 422 | TIN verification failed at the tax authority{
"detail": "TIN verification failed at the FTA: no taxable person matches this TIN."
} |
Step 2. Creates the organisation and publishes it to the participant registry so it becomes addressable as a buyer.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| verification_token | string | required | From the verify call. |
| string (email) | required | Must match the email used at verify. | |
| organization_name | string (1–255) | required | Legal name in English. |
| legal_type | enum 0–8 | required | 0 LLC, 1 PJSC, 2 PRJSC, 3 Sole Proprietorship, 4 Partnership, 5 Branch of Foreign Company, 6 Free Zone Entity, 7 Government Entity, 8 Other. |
| vat_trn | string (15) | optional | Exactly 15 characters when supplied. |
| legal_name_arabic | string | optional | Legal name in Arabic. |
| registration_date | date | optional | YYYY-MM-DD. |
| country_code | string | optional | ISO 3166-1 alpha-2. Default AE. |
{
"verification_token": "eyJ0aW4iOiIxMjM0NTY3ODkwIi…",
"email": "finance@example.ae",
"organization_name": "Example Trading LLC",
"legal_type": "0",
"vat_trn": "100123456789003",
"legal_name_arabic": "مثال للتجارة ش.ذ.م.م"
}{
"success": true,
"message": "Organization registered successfully",
"organization": {
"id": "01M1HE2K4QW8ZP3R7YT5NVB6XD",
"legal_name": "Example Trading LLC",
"legal_name_arabic": "مثال للتجارة ش.ذ.م.م",
"registration_status": 2,
"created_at": "2026-09-15T09:20:00.000Z",
"updated_at": "2026-09-15T09:20:00.000Z"
},
"user": {
"id": "01M1HE2K7BXCV9QW2ER4TY6UIO",
"email": "finance@example.ae",
"name": "Example Trading LLC"
}
}| Status | When |
|---|---|
| 400 | Validation error in the request fields |
| 401 | Verification token expired, invalid, or the email does not match |
| 409 | An organisation with this TIN already exists |
Applies refreshed entity details after a verify call with action=re-verify.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| verification_token | string | required | From verify with action=re-verify. |
| string (email) | required | Email used at verification. | |
| entity_name_en | string | optional | Updated legal name in English. |
| entity_name_ar | string | optional | Updated legal name in Arabic. |
| vat_trn | string | optional | Updated VAT TRN. |
| legal_type | enum 0–8 | optional | Updated legal entity type. |
{
"verification_token": "eyJ0aW4iOiIxMjM0NTY3ODkwIi…",
"email": "finance@example.ae",
"entity_name_en": "Example Trading LLC"
}{
"success": true,
"message": "Organization re-verified successfully",
"organization": {
"id": "01M1HE2K4QW8ZP3R7YT5NVB6XD",
"legal_name": "Example Trading LLC",
"legal_name_arabic": null,
"registration_status": 2,
"created_at": "2026-09-15T09:20:00.000Z",
"updated_at": "2026-09-15T09:31:00.000Z"
}
}| Status | When |
|---|---|
| 401 | Verification token expired or invalid |
| 404 | Organisation not found |
Removes the participant from the registry. The organisation must already be in Initiating Deregistration status.
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
| organization_id | string | required | The organisation id. |
{
"organization_id": "01M1HE2K4QW8ZP3R7YT5NVB6XD"
}{
"success": true,
"message": "Organization deregistered successfully",
"organization_id": "01M1HE2K4QW8ZP3R7YT5NVB6XD"
}| Status | When |
|---|---|
| 400 | Organisation not in Initiating Deregistration status |
| 404 | Organisation not found |
Four integer fields. status summarises; the other three say where something went wrong and appear only on the invoice detail endpoint.
| 1 | Processing |
| 2 | Completed |
| 3 | Rejected |
| 4 | Failed |
| 0 | Not Applicable |
| 1 | Processing |
| 2 | Validation Failed |
| 3 | Validation Passed |
| 0 | Not Applicable |
| 1 | Yet to Send |
| 2 | Sending to C3 |
| 3 | Waiting for MLS |
| 4 | Accepted |
| 5 | Rejected |
| 6 | Unable to Deliver |
| 0 | Not Applicable |
| 1 | Yet to Send |
| 2 | Sending to C5 |
| 3 | Waiting for MLS |
| 4 | Accepted |
| 5 | Sending Withdraw Request |
| 6 | Waiting for Withdraw MLS |
| 7 | Withdraw Accepted |
Rejected means validation failed: your payload was wrong, the document never left, and correcting the mapping then resubmitting will work. Failed means the payload was fine but a downstream party refused it — resubmitting the same document unchanged will not help. Reporting both to the ERP as a generic error is a real defect.
Use can_resubmit rather than deriving it from status.
| Code | Name |
|---|---|
| 81 | Commercial Credit Note |
| 261 | Self-Billed Credit Note |
| 380 | Tax Invoice |
| 381 | Tax Credit Note |
| 389 | Self-Billing Invoice |
| 480 | Commercial Invoice (Out of Scope) |
Three shapes, chosen by the kind of error. Nothing else appears.
A dictionary keyed by field path; each value a list of messages. Errors not tied to a field appear under non_field_errors.
{
"detail.totals.invoice_total_amount_without_vat": [
"IBR-CO-13: Invoice total amount without VAT (IBT-109) must equal the sum of line net amounts minus document-level allowances plus document-level charges. Expected \"9700.00\", got \"9500.00\"."
],
"invoice_number": ["This field is required."]
}{ "detail": "Authentication credentials were not provided." }{ "error": "invalid_client" }invalid_request a required parameter was missing · invalid_client credentials rejected or client deactivated · invalid_grant refresh token expired, already used, or invalid.
Rule identifiers say where they come from:
| Family | Meaning |
|---|---|
| IBR-### | Core PINT / BIS invoice business rule — presence and cardinality. |
| IBR-CO-## | Computation rule. The totals must actually add up. |
| IBR-CL-## | Code-list rule — the value must come from an external list. |
| IBR-SR-## | Single-recurrence / uniqueness rule. |
| SBX-* | Added by this sandbox. Not a spec rule id — either a documented PINT AE requirement whose upstream id we are not certain of, or a UAE specialisation. |
An SBX- prefix means this is not a specification rule ID— it is a check this environment adds. Don't quote one into a support ticket as if it came from the spec.
Downstream results are decided from the payload, so they are reproducible. To force one, put the marker anywhere in buyer_reference, the invoice name, invoice_note or project_reference. Matching is case-insensitive.
| Marker | Result |
|---|---|
| SANDBOX-FAIL-C3 | Receiving Access Point rejects — c3_mls_status 5, status Failed |
| SANDBOX-UNDELIVERABLE | Cannot deliver — c3_mls_status 6, status Failed |
| SANDBOX-FAIL-C5 | Tax authority rejects the declaration — status Failed |
| SANDBOX-SLOW | Every stage takes five times as long, so the transitions are watchable |
Without a marker the result follows the payload: an unlisted buyer is undeliverable, and a buyer that does not accept the document type is rejected. Validation failures need no trigger — send something wrong and it is caught.
Read this before drawing conclusions from a green result. Every item is a deliberate simplification.
The authoritative format is the PINT AE specification. Where this environment and the spec disagree, the spec is right.