AN
Actinode Sandbox — API Reference
PINT AE v1.0.4 · UAE eInvoicing
Test environment — simulated network

Getting started

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.

Connection details

API base URLhttps://erp-uat.actinode.com/api/v1
client_idactinode-sandbox-sap-demo
client_secretsk_sandbox_9f2c41d7e83b40a5b1c6d0e7a3f85920
Permissionsinvoice:view, invoice:submit, document:view, document:upload, document:download, document:delete
Seller endpoint0235:1203491724 — Actinode Demo Trading LLC

Conventions

  • Trailing slashes are required. Every path ends in /. Omitting it produces a redirect that some HTTP clients will not follow on a POST.
  • Monetary amounts are strings, never JSON numbers. Send "1050.00", not 1050.00. This is the most common defect in an ERP connector and it is rejected outright.
  • Dates are YYYY-MM-DD. Timestamps are ISO 8601 UTC.
  • Ids are opaque 26-character strings. They sort chronologically, which is what makes the after cursor work.
  • Currency codes are ISO 4217 alpha-3; country codes ISO 3166-1 alpha-2; unit codes UN/ECE Recommendation 20.

Buyers you can reach

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.

ParticipantNameAccepts
0235:1999999911XYZ Corporation FZCO380, 381, 480, 81, 389, 261
0235:1000000001Gulf Distribution LLC380, 381, 480, 81
0235:1000000002Emirates Retail Group PJSC380, 381
0235:1000000003Falcon Logistics FZE380, 381, 480, 81, 389, 261
0235:1000000004Al Noor Contracting LLC380, 381, 389, 261

The submission flow

There are two routes in, and they differ in when content errors reach you.

Inline JSON — one call, errors are immediate

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/"

PINT AE XML — three calls, errors arrive later

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.

A 201 here is not a pass.
In file mode the response means “accepted and queued”. A file that fails validation still returns 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"'"}'

Authentication

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.

POSTGet an access token
auth: None
POST https://erp-uat.actinode.com/api/v1/oauth/token/

Exchange the client_id / client_secret pair for an access + refresh token pair.

Content-Type: application/x-www-form-urlencoded

  • This endpoint does NOT accept JSON. It is form-encoded to match the OAuth2 spec.
  • The access token is valid for 600 seconds (10 minutes). Cache it in memory and refresh before it expires rather than requesting a new one per call.
Request body
FieldTypeRequiredDescription
client_idstringrequiredThe API client identifier.
client_secretstringrequiredThe API client secret.
Sample request
client_id=actinode-sandbox-sap-demo&client_secret=<your-client-secret>
Response
200OK
{
  "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"
}
Errors
StatusWhen
400client_id or client_secret missing
{
  "error": "invalid_request"
}
401Credentials rejected, or the API client is deactivated
{
  "error": "invalid_client"
}
POSTRefresh the access token
auth: None (the refresh token is the credential)
POST https://erp-uat.actinode.com/api/v1/oauth/token/refresh/

Exchange a refresh token for a fresh access + refresh pair.

Content-Type: application/json

  • JSON here, unlike the token endpoint above.
  • Refresh tokens ROTATE. Redeeming one invalidates it — store the new pair immediately. Presenting a spent refresh token returns 401 invalid_grant.
  • The refresh token is valid for 7 days.
Request body
FieldTypeRequiredDescription
refresh_tokenstringrequiredThe refresh token from the previous token or refresh response.
Sample request
{
  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMU0xSEM1…"
}
Response
200OK — identical shape to the token endpoint
{
  "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"
}
Errors
StatusWhen
400refresh_token missing
{
  "error": "invalid_request"
}
401Expired, invalid, or already-used refresh token
{
  "error": "invalid_grant"
}

Invoices

The main surface. All endpoints need a Bearer token and operate on the organisation the API client is bound to.

POSTSubmit an invoice
auth: Bearerinvoice:submit
POST https://erp-uat.actinode.com/api/v1/invoices/

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

  • With `detail`, validation is SYNCHRONOUS — a bad payload returns 400 with the failing fields, and a 201 means the content passed.
  • With `source_file_path`, validation is ASYNCHRONOUS — a 201 means "accepted and queued", NOT "valid". A bad file still returns 201 and then flips to Rejected. Do not treat 201 as success in that mode.
  • invoice_number must be unique per organisation per issue year, counting only invoices you send. A duplicate returns 400 — this is your protection against accidental double submission.
  • Monetary amounts are STRINGS, never JSON numbers. "1050.00" is right; 1050.00 is rejected.
Request body
FieldTypeRequiredDescription
namestring (≤255)requiredHuman-readable label for the invoice.
invoice_numberstring (1–100)requiredIBT-001. Your document number. Unique per organisation per issue year.
issue_datedate (YYYY-MM-DD)requiredIBT-002. Date of issue.
invoice_type_codeenumrequiredIBT-003. One of 380, 381, 480, 81, 389, 261.
detailobjectconditionalThe full PINT AE field tree. Required when source_file_path is omitted.
source_file_pathstring (s3:// URI)conditionalPath returned by POST /api/v1/documents/. Required when detail is omitted.
Sample request
{
  "name": "Invoice INV-2026-0001",
  "invoice_number": "INV-2026-0001",
  "issue_date": "2026-09-15",
  "invoice_type_code": "380",
  "detail": {
    "invoice_currency_code": "AED",
    "vat_point_date": "2026-09-15",
    "payment_due_date": "2026-10-15",
    "buyer_reference": "BUYER-REF-12345",
    "purchase_order_reference": "PO-2026-001",
    "invoice_note": "Thank you for your business. Payment terms are net 30 days.",
    "issue_time": "14:30:00",
    "transaction_type_code": "00000000",
    "process_control": {
      "profile_id": "urn:peppol:bis:billing",
      "customization_id": "urn:peppol:pint:billing-1@ae-1"
    },
    "seller": {
      "name": "Actinode Demo Trading LLC",
      "trading_name": "Actinode Demo",
      "legal_registration_identifier": "TL-123456",
      "legal_registration_identifier_scheme": "0234",
      "vat_identifier": "100123456789003",
      "tax_scheme": "VAT",
      "electronic_address": "1203491724",
      "electronic_address_scheme": "0235",
      "address_line_1": "123 Trade Street",
      "address_line_2": "Building A",
      "city": "Dubai",
      "post_code": "12345",
      "country_subdivision": "DXB",
      "country_code": "AE",
      "contact_point": "Ahmad Al Mansouri",
      "contact_telephone_number": "+971 4 123 4567",
      "contact_email_address": "sales@actinode.example"
    },
    "buyer": {
      "name": "XYZ Corporation FZCO",
      "trading_name": "XYZ Corp",
      "legal_registration_identifier": "LRI-789012",
      "legal_registration_identifier_scheme": "0234",
      "vat_identifier": "100987654321003",
      "tax_scheme": "VAT",
      "electronic_address": "1999999911",
      "electronic_address_scheme": "0235",
      "address_line_1": "456 Commerce Avenue",
      "city": "Abu Dhabi",
      "post_code": "54321",
      "country_subdivision": "AUH",
      "country_code": "AE",
      "contact_point": "Fatima Al Hashemi",
      "contact_email_address": "procurement@xyzco.example"
    },
    "delivery": {
      "actual_delivery_date": "2026-09-15",
      "address": {
        "address_line_1": "999 Logistics Road",
        "city": "Ajman",
        "post_code": "88888",
        "country_subdivision": "AJM",
        "country_code": "AE"
      }
    },
    "totals": {
      "sum_of_invoice_line_net_amount": "10000.00",
      "sum_of_allowances_on_document_level": "500.00",
      "sum_of_charges_on_document_level": "200.00",
      "invoice_total_amount_without_vat": "9700.00",
      "invoice_total_vat_amount": "485.00",
      "invoice_total_amount_with_vat": "10185.00",
      "paid_amount": "0.00",
      "rounding_amount": "0.15",
      "amount_due_for_payment": "10185.15",
      "invoice_total_amount_with_vat_in_aed": "10185.15",
      "tax_included_indicator": false
    },
    "allowances": [
      {
        "amount": "500.00",
        "base_amount": "10000.00",
        "percentage": "5.00",
        "vat_category_code": "S",
        "tax_scheme_code": "VAT",
        "vat_rate": "5.00",
        "reason": "Early payment discount",
        "reason_code": "95"
      }
    ],
    "charges": [
      {
        "amount": "200.00",
        "base_amount": "10000.00",
        "percentage": "2.00",
        "vat_category_code": "S",
        "tax_scheme_code": "VAT",
        "vat_rate": "5.00",
        "reason": "Shipping and handling",
        "reason_code": "AA"
      }
    ],
    "vat_breakdowns": [
      {
        "taxable_amount": "9700.00",
        "tax_amount": "485.00",
        "vat_category_code": "S",
        "tax_scheme_code": "VAT",
        "vat_category_rate": "5.00"
      }
    ],
    "payment_instructions": [
      {
        "payment_means_type_code": "30",
        "payment_means_text": "Bank Transfer",
        "payment_instructions_id": "PAY-INSTR-001",
        "payment_account_identifier": "AE070331234567890123456",
        "payment_account_identifier_scheme": "IBAN",
        "payment_account_name": "Actinode Demo Trading LLC",
        "payment_service_provider_identifier": "AEBAAEAD"
      }
    ],
    "terms": [
      {
        "payment_terms": "Net 30 days from invoice date",
        "terms_amount": "10185.15",
        "terms_installment_due_date": "2026-10-15"
      }
    ],
    "lines": [
      {
        "line_id": "1",
        "note": "Supply and delivery of office supplies as per quotation QT-2026-001",
        "invoiced_quantity": "100.000000",
        "invoiced_quantity_unit_of_measure_code": "EA",
        "line_net_amount": "10000.00",
        "purchase_order_line_reference": "PO-2026-001-L1",
        "item_net_price": "100.000000",
        "item_price_base_quantity": "1.000000",
        "item_price_base_quantity_unit_of_measure_code": "EA",
        "item_name": "Office Stationery Package",
        "item_description": "Premium assorted office supplies including A4 paper, pens and folders",
        "item_sellers_identifier": "ITEM-SEL-12345",
        "item_country_of_origin": "AE",
        "line_amount_in_aed": "10000.00",
        "vat_line_amount_in_aed": "500.00",
        "vat_info": [
          {
            "vat_category_code": "S",
            "vat_rate": "5.00",
            "tax_scheme": "VAT"
          }
        ],
        "classifications": [
          {
            "classification_identifier": "49019900",
            "classification_identifier_scheme": "HS",
            "classification_identifier_scheme_version": "2022"
          }
        ]
      }
    ]
  }
}
Response
201Created — echoes what you sent, plus id, source and created_at
{
  "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"
}
Errors
StatusWhen
400Content 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\"."
  ]
}
400Both detail and source_file_path supplied, or neither
{
  "non_field_errors": [
    "Provide either `detail` or `source_file_path`, not both."
  ]
}
400invoice_number already used in the same issue year
{
  "invoice_number": [
    "An invoice you sent in 2026 already uses the number \"INV-2026-0001\"."
  ]
}
401Token missing, invalid, or expired
{
  "detail": "Token is invalid or expired"
}
403API client deactivated, missing invoice:submit, or organisation not Registered
{
  "detail": "Organisation must be Registered before submitting invoices."
}
GETGet one invoice
auth: Bearerinvoice:view
GET https://erp-uat.actinode.com/api/v1/invoices/{id}/

The full record: the summary status, the three underlying pipeline statuses, the document paths, and the whole PINT AE tree.

  • The three status fields — internal_validation_status, c3_mls_status, c5_mls_status — are ONLY on this endpoint. A list row does not carry them.
  • An invoice belonging to another organisation returns 404, not 403.
Response
200OK
{
  "id": "01M1HDAFF9XHR7K2QJ4TZ8W3PC",
  "name": "Invoice INV-2026-0001",
  "invoice_number": "INV-2026-0001",
  "issue_date": "2026-09-15",
  "invoice_type_code": "380",
  "invoice_xml_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-wire.xml",
  "pdf_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/invoices/INV-2026-0001.pdf",
  "tdd_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-tdd.xml",
  "direction": 1,
  "user_submitted": null,
  "api_client_submitted": {
    "id": "01M1HC5ETXGPHQ4V08SDK494JW",
    "name": "SAP Demo API client",
    "client_id": "actinode-sandbox-sap-demo",
    "is_active": true,
    "created_at": "2026-09-02T10:00:00.000Z",
    "updated_at": "2026-09-02T10:00:00.000Z"
  },
  "internal_validation_status": 3,
  "internal_validation_error_message": null,
  "c3_mls_status": 4,
  "c3_mls_reason_code": null,
  "c3_mls_error_message": null,
  "c5_mls_status": 4,
  "c5_mls_reason_code": null,
  "c5_mls_error_message": null,
  "status": 2,
  "can_resubmit": false,
  "completed_at": "2026-09-15T09:12:42.118Z",
  "detail": "/* the full PINT AE tree you submitted — see POST /api/v1/invoices/ */",
  "created_at": "2026-09-15T09:12:30.004Z",
  "updated_at": "2026-09-15T09:12:42.118Z"
}
Errors
StatusWhen
401Token missing or invalid
403API client deactivated, or missing invoice:view
404No such invoice, or it belongs to another organisation
{
  "detail": "No Invoice matches the given query."
}
GETList invoices
auth: Bearerinvoice:view
GET https://erp-uat.actinode.com/api/v1/invoices/

Paginated list with filtering, search and ordering. Supports page-based and cursor-based iteration.

  • A list row is a summary. It carries status and can_resubmit but NOT internal_validation_status, c3_mls_status or c5_mls_status, and no detail tree. Fetch the id you care about to see why something failed.
  • For polling, keep the id of the last invoice you processed and pass it as after= on the next call. Ids are time-sortable.
Query parameters
FieldTypeRequiredDescription
pageintegeroptionalPage number. Default 1.
page_sizeintegeroptionalItems per page. Default 20, maximum 100.
orderingstringoptionalOne of name, created_at, id. Prefix with - for descending. Default -id.
searchstringoptionalMatches against name and invoice_number.
statusinteger 1–4optional1 Processing, 2 Completed, 3 Rejected, 4 Failed.
directioninteger 1–2optional1 Sent, 2 Received.
invoice_type_code__instringoptionalComma-separated type codes, e.g. 380,381.
can_resubmitbooleanoptionalOnly invoices that can be resubmitted.
issue_date_from / issue_date_todateoptionalIBT-002 bounds, inclusive.
created_at_from / created_at_todateoptionalCreation-date bounds, inclusive of the whole day.
completed_at_from / completed_at_todateoptionalCompletion-date bounds, inclusive of the whole day.
after / beforeinvoice idoptionalCursor. Only invoices created after / before the given id.
Response
200OK
{
  "count": 42,
  "next": "https://erp-uat.actinode.com/api/v1/invoices/?page=2",
  "previous": null,
  "results": [
    {
      "id": "01M1HDAFF9XHR7K2QJ4TZ8W3PC",
      "name": "Invoice INV-2026-0001",
      "invoice_number": "INV-2026-0001",
      "issue_date": "2026-09-15",
      "invoice_type_code": "380",
      "invoice_xml_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-wire.xml",
      "pdf_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/invoices/INV-2026-0001.pdf",
      "tdd_location_path": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-tdd.xml",
      "direction": 1,
      "user_submitted": null,
      "api_client_submitted": {
        "id": "01M1HC5ETXGPHQ4V08SDK494JW",
        "name": "SAP Demo API client",
        "client_id": "actinode-sandbox-sap-demo",
        "is_active": true,
        "created_at": "2026-09-02T10:00:00.000Z",
        "updated_at": "2026-09-02T10:00:00.000Z"
      },
      "status": 2,
      "can_resubmit": false,
      "completed_at": "2026-09-15T09:12:42.118Z",
      "created_at": "2026-09-15T09:12:30.004Z"
    }
  ]
}
Errors
StatusWhen
400A cursor id your API client cannot see
{
  "after": [
    "Invalid cursor: object with id \"01ZZZ…\" was not found."
  ]
}
401Token missing or invalid
PUTResubmit a rejected or failed invoice
auth: Bearerinvoice:submit
PUT https://erp-uat.actinode.com/api/v1/invoices/{id}/resubmit/

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

  • Only works when can_resubmit is true. Read that field rather than inferring it from status.
  • invoice_number is read-only here: it is ignored rather than applied, so the invoice keeps the number it was created with.
  • A Completed invoice cannot be resubmitted. To correct one, issue a credit note (type 381) that references it.
  • PATCH on the same path accepts a partial body. A PATCH carrying neither detail nor source_file_path is a plain update and does NOT restart the lifecycle.
Request body
FieldTypeRequiredDescription
namestringoptionalNew label. Defaults to the existing one.
issue_datedateoptionalDefaults to the existing one.
invoice_type_codeenumoptionalDefaults to the existing one.
detailobjectconditionalThe corrected PINT AE tree. One of detail or source_file_path.
source_file_pathstringconditionalPath to the corrected file.
Sample request
{
  "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/ */"
}
Response
200OK
{
  "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"
}
Errors
StatusWhen
400Validation error in the new payload
403can_resubmit is false — still in flight, or already Completed
{
  "detail": "Can't resubmit this invoice"
}
404No such invoice

Documents

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.

POSTReserve a document slot
auth: Bearerdocument:upload
POST https://erp-uat.actinode.com/api/v1/documents/

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

  • Allowed extensions: xml, json, pdf, xlsx, csv.
  • Save the `path`. That is what goes into source_file_path.
Request body
FieldTypeRequiredDescription
namestring (1–255)requiredFilename without the extension.
extensionstringrequiredOne of xml, json, pdf, xlsx, csv.
Sample request
{
  "name": "INV-2026-0001",
  "extension": "xml"
}
Response
201Created
{
  "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"
}
Errors
StatusWhen
400Missing fields, or an extension outside the allowed list
403Missing document:upload, or organisation not Registered
PUTUpload the file bytes
auth: None — the signature in the URL is the credential
PUT {upload_url}

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)

  • This goes to the URL returned by the reserve call, not to a fixed path.
  • A 200 means the bytes are stored. Nothing is validated at this step — validation happens when you submit.
Sample request
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" …>
  …your PINT AE UBL…
</Invoice>
Response
200OK — empty body, ETag header
(no body)
Errors
StatusWhen
400Empty upload body
403Unknown or expired upload URL
POSTGet a download URL
auth: Bearerdocument:download
POST https://erp-uat.actinode.com/api/v1/documents/download/

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

  • Supply one of id or s3_uri.
  • Fetch the returned URL with a plain GET and no Authorization header, within expires_in seconds.
  • An s3_uri belonging to another organisation returns 400, not 404.
Request body
FieldTypeRequiredDescription
idstringconditionalDocument id. One of id or s3_uri.
s3_uristringconditionalAny of the *_location_path values from an invoice.
filenamestringoptionalFilename to suggest to the browser.
Sample request
{
  "s3_uri": "s3://actinode-sandbox/organization/01M1HC5EEEZHYHBAPRQ54V0BTS/documents/INV-2026-0001-wire.xml"
}
Response
201Created
{
  "download_url": "https://erp-uat.actinode.com/api/v1/files/eyJkb2Mi…",
  "expires_in": 3600
}
Errors
StatusWhen
400Neither id nor s3_uri supplied, or the URI is not yours
403Missing document:download
GETList documents
auth: Bearerdocument:view
GET https://erp-uat.actinode.com/api/v1/documents/

The organisation's file library.

  • upload_url is only populated at creation. On a listing it is always null.
Query parameters
FieldTypeRequiredDescription
pageintegeroptionalPage number. Default 1.
page_sizeintegeroptionalDefault 20, maximum 100.
searchstringoptionalMatches the name field.
Response
200OK
{
  "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"
    }
  ]
}
GETGet one document
auth: Bearerdocument:view
GET https://erp-uat.actinode.com/api/v1/documents/{id}/

Metadata for a single document. Same shape as a list row.

Response
200OK
{
  "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"
}
Errors
StatusWhen
404No such document
DELETEDelete a document
auth: Bearerdocument:delete
DELETE https://erp-uat.actinode.com/api/v1/documents/{id}/

Permanently removes the document and its stored bytes.

Response
204No Content — empty body
(no body)
Errors
StatusWhen
404No such document

Organisations

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.

POSTVerify a TIN
auth: None
POST https://erp-uat.actinode.com/api/v1/organizations/verify/

Step 1. Verifies the TIN and returns a verification_token valid for 30 minutes, which the follow-up call requires.

Content-Type: application/json

  • Sandbox behaviour: any 10-digit TIN verifies; one starting with 9 fails at the tax authority with 422.
Request body
FieldTypeRequiredDescription
tin_numberstring (10 digits)requiredThe UAE TRN.
emailstring (email)requiredContact email. Must match at register time.
phone_numberstringoptionalE.164, e.g. +971501234567.
actionenumoptionalregister (default), de-register, or re-verify.
Sample request
{
  "tin_number": "1234567890",
  "email": "finance@example.ae",
  "phone_number": "+971501234567",
  "action": "register"
}
Response
200OK
{
  "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"
}
Errors
StatusWhen
400Invalid request data, or the organisation is in a state that disallows the action
404Organisation not found (de-register and re-verify only)
422TIN verification failed at the tax authority
{
  "detail": "TIN verification failed at the FTA: no taxable person matches this TIN."
}
POSTRegister the organisation
auth: verification_token
POST https://erp-uat.actinode.com/api/v1/organizations/register/

Step 2. Creates the organisation and publishes it to the participant registry so it becomes addressable as a buyer.

Content-Type: application/json

Request body
FieldTypeRequiredDescription
verification_tokenstringrequiredFrom the verify call.
emailstring (email)requiredMust match the email used at verify.
organization_namestring (1–255)requiredLegal name in English.
legal_typeenum 0–8required0 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_trnstring (15)optionalExactly 15 characters when supplied.
legal_name_arabicstringoptionalLegal name in Arabic.
registration_datedateoptionalYYYY-MM-DD.
country_codestringoptionalISO 3166-1 alpha-2. Default AE.
Sample request
{
  "verification_token": "eyJ0aW4iOiIxMjM0NTY3ODkwIi…",
  "email": "finance@example.ae",
  "organization_name": "Example Trading LLC",
  "legal_type": "0",
  "vat_trn": "100123456789003",
  "legal_name_arabic": "مثال للتجارة ش.ذ.م.م"
}
Response
201Created — registration_status 2 means Registered
{
  "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"
  }
}
Errors
StatusWhen
400Validation error in the request fields
401Verification token expired, invalid, or the email does not match
409An organisation with this TIN already exists
POSTRe-verify an organisation
auth: verification_token
POST https://erp-uat.actinode.com/api/v1/organizations/reverify/

Applies refreshed entity details after a verify call with action=re-verify.

Content-Type: application/json

Request body
FieldTypeRequiredDescription
verification_tokenstringrequiredFrom verify with action=re-verify.
emailstring (email)requiredEmail used at verification.
entity_name_enstringoptionalUpdated legal name in English.
entity_name_arstringoptionalUpdated legal name in Arabic.
vat_trnstringoptionalUpdated VAT TRN.
legal_typeenum 0–8optionalUpdated legal entity type.
Sample request
{
  "verification_token": "eyJ0aW4iOiIxMjM0NTY3ODkwIi…",
  "email": "finance@example.ae",
  "entity_name_en": "Example Trading LLC"
}
Response
200OK
{
  "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"
  }
}
Errors
StatusWhen
401Verification token expired or invalid
404Organisation not found
POSTDeregister an organisation
auth: None
POST https://erp-uat.actinode.com/api/v1/organizations/deregister/

Removes the participant from the registry. The organisation must already be in Initiating Deregistration status.

Content-Type: application/json

  • Call verify with action=de-register first — that is what moves the organisation into the required state.
Request body
FieldTypeRequiredDescription
organization_idstringrequiredThe organisation id.
Sample request
{
  "organization_id": "01M1HE2K4QW8ZP3R7YT5NVB6XD"
}
Response
200OK
{
  "success": true,
  "message": "Organization deregistered successfully",
  "organization_id": "01M1HE2K4QW8ZP3R7YT5NVB6XD"
}
Errors
StatusWhen
400Organisation not in Initiating Deregistration status
404Organisation not found

Status model

Four integer fields. status summarises; the other three say where something went wrong and appear only on the invoice detail endpoint.

status
1Processing
2Completed
3Rejected
4Failed
internal_validation_status
0Not Applicable
1Processing
2Validation Failed
3Validation Passed
c3_mls_status
0Not Applicable
1Yet to Send
2Sending to C3
3Waiting for MLS
4Accepted
5Rejected
6Unable to Deliver
c5_mls_status
0Not Applicable
1Yet to Send
2Sending to C5
3Waiting for MLS
4Accepted
5Sending Withdraw Request
6Waiting for Withdraw MLS
7Withdraw Accepted

Rejected vs Failed — handle these differently

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.

Document types

CodeName
81Commercial Credit Note
261Self-Billed Credit Note
380Tax Invoice
381Tax Credit Note
389Self-Billing Invoice
480Commercial Invoice (Out of Scope)

Error shapes

Three shapes, chosen by the kind of error. Nothing else appears.

Validation — 400

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."]
}

Generic — 401, 403, 404, 409, 422, 5xx

{ "detail": "Authentication credentials were not provided." }

OAuth — the two token endpoints only

{ "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.

Validation rules

Rule identifiers say where they come from:

FamilyMeaning
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.

The ones that will bite

SBX-TYPE-01A monetary amount or quantity arrived as a JSON number instead of a string.
PINT AE transports amounts as strings so precision survives exactly as sent. This is the single easiest bug to ship from an ERP connector, and it is silent until a total is off by a cent.
IBR-CO-13invoice_total_amount_without_vat ≠ line totals − allowances + charges.
Document-level allowances and charges are the usual culprit: ERPs often net them into the line amounts as well, so they get counted twice.
IBR-CO-17A VAT breakdown's tax_amount ≠ taxable_amount × rate ÷ 100.
Rounding per line and then summing gives a different answer from rounding the total. PINT AE wants the latter.
SBX-AE-01A UAE party's electronic_address_scheme is not 0235.
UAE participants always use scheme 0235. A wrong scheme means the SMP lookup finds nothing and the document is undeliverable.
SBX-AE-05An out-of-scope document (480 / 81) carries a VAT category other than E, O or Z.
Type 480 changes several rules at once — seller VAT identifier becomes optional, buyer legal registration identifier becomes mandatory, categories are restricted. Treat it as a distinct validation mode in the mapping layer.
SBX-AE-06A credit note carries no preceding_invoice_references.
A credit note must name the invoice it corrects, otherwise the FTA cannot pair them.
SBX-AE-08A non-AED invoice is missing invoice_total_amount_with_vat_in_aed (BTAE-20).
The FTA reports in AED, so a foreign-currency invoice must carry the AED equivalent.

Testing error paths

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.

MarkerResult
SANDBOX-FAIL-C3Receiving Access Point rejects — c3_mls_status 5, status Failed
SANDBOX-UNDELIVERABLECannot deliver — c3_mls_status 6, status Failed
SANDBOX-FAIL-C5Tax authority rejects the declaration — status Failed
SANDBOX-SLOWEvery 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.

Limits of this environment

Read this before drawing conclusions from a green result. Every item is a deliberate simplification.

  • Nothing is signed. The wire copy is real PINT AE UBL, but carries no digital signature.
  • Nothing is dispatched. No Peppol traffic leaves this deployment and no TDD reaches the FTA.
  • Downstream outcomes are simulated from the payload, not observed. C3 and C5 results are decided at submission time and revealed on a timer.
  • Validation is a representative subset of the PINT AE schematron, not the whole of it. Passing here does not guarantee passing on the real platform.
  • Object storage is emulated. Paths keep the s3:// shape but resolve to this deployment.
  • The FTA does not really verify TINs. Any 10-digit TIN verifies; one starting with 9 fails.
  • C5 has no "rejected" value in the documented enum (§14.5). On a simulated FTA rejection the sandbox leaves c5_mls_status at 3 (Waiting for MLS), sets c5_mls_error_message, and flips status to 4 (Failed). Worth confirming with the platform vendor how the real thing represents this.
  • Rate limiting is not implemented, matching the documented "not currently configured".
  • The access token's `aud` claim is `actinode_sandbox_client_apps`, which is this sandbox's own value. The live platform uses a different audience — do not hardcode either one.

The authoritative format is the PINT AE specification. Where this environment and the spec disagree, the spec is right.