Alya Books

    Developer docs

    REST API, webhooks, MCP server and CLI for UAE e-invoicing

    ERP intake: CargoWise and pushed messages

    Point your ERP's outbound integration at one endpoint. Alya Books stores the message, maps it, validates it, creates and submits the invoice, and keeps a log you can replay.

    Integrating CargoWise? Start with the CargoWise integration kit.

    The intake endpoint takes the message your ERP already produces when an invoice is posted and turns it into a Alya Books invoice through the same path as POST /invoices: numbering, PINT AE validation, submission to the service provider and webhooks are all shared. Every message is stored with its raw body, so a mapping fix can be replayed without asking the ERP to resend.

    POST {BASE}/erp/{connector}/messages
    
    ConnectorContent typeWhat it reads
    cargowiseany (see below)A CargoWise UniversalTransaction (AR invoice, credit or debit note) as eAdaptor or eAdaptor Next delivers it: bare, in a UniversalInterchange, a SOAP envelope, a JSON wrapper, base64, gzip or a form post
    jsonapplication/jsonOne invoice in the InvoiceInput shape, keyed by externalId

    Authentication for push transports

    Every API route accepts the key in X-API-KEY. The intake routes also accept HTTP Basic authentication with the API key as the password (any user name), because outbound integrations such as CargoWise eAdaptor can only send basic credentials to a partner endpoint:

    Authorization: Basic base64("api:sk_test_…")
    

    Use a test key while integrating: the invoices it creates are test documents and never reach the live service provider.

    What the CargoWise endpoint accepts

    eAdaptor (SOAP) and eAdaptor Next (REST) can deliver the same Universal XML in different shapes, depending on how the outbound endpoint is configured. POST /erp/cargowise/messages accepts any Content-Type and finds the document itself:

    ShapeExample
    Bare Universal XMLUniversalInterchange or UniversalTransaction as the body (application/xml, text/xml)
    SOAP envelopeThe document as elements in the Body, as escaped text in a field such as Payload, or as base64 of the gzip-compressed stream
    JSON{ "MessageId": "…", "Payload": "<UniversalInterchange …>" }; the XML may sit in any field, as text or base64
    Base64The body is base64 of the XML, or of the gzip-compressed XML
    Form postapplication/x-www-form-urlencoded with the XML in a field
    CompressedContent-Encoding: gzip

    Up to four layers are unwrapped. The message log shows how each message arrived (for example json:Payload → base64 → gzip). A body with no Universal XML inside is stored as a failed message with the reason.

    Message id. Send your message id in X-Message-Id (also read: Message-Id, X-Correlation-Id, X-Request-Id), or as MessageId / TrackingId in a JSON wrapper or the SOAP header. Alya Books stores it on the message and returns it as messageId, so both sides can trace a document. Resending the same document is still recognised by its CargoWise number, whatever the message id.

    Configure the connector once

    Charge codes decide the VAT treatment, so the mapping is set per organisation before the first message. Have your tax adviser sign it.

    curl -X PUT "$BASE/erp/connectors/cargowise" -H "X-API-KEY: $KEY" -H "Content-Type: application/json" -d '{
      "chargeMap": {
        "FRT":  { "category": "zero_rated",   "itemType": "S", "serviceAccountingCode": "996521", "label": "Ocean freight" },
        "THC":  { "category": "standard",     "itemType": "S", "serviceAccountingCode": "996751" },
        "DUTY": { "category": "out_of_scope", "itemType": "S", "serviceAccountingCode": "996799", "label": "Customs duty disbursement" }
      },
      "vatCodeMap": { "S": "standard", "Z": "zero_rated", "O": "out_of_scope", "E": "exempt", "RC": "reverse_charge" },
      "submit": true,
      "alwaysAck": false,
      "designTemplateId": "6f0b0123-…",
      "mappingSignedOffBy": "A. Adviser, Firm LLP"
    }'
    

    As shown, chargeMap replaces the stored map, which is what a settings screen sending every row needs. To add or change a few codes and keep the rest, send "mode": "merge"; delete codes in merge mode with "removeChargeCodes": ["OLD"] (and removeVatCodes for the VAT-code map). Scripts and assistants should always use merge. The audit log keeps the previous maps, so any change can be undone.

    Resolution order for each charge line: chargeMap[chargeCode], then vatCodeMap[vatCode], then the built-in VAT code list (S, Z, E, O, RC…), then defaultCategory. When nothing decides, the message fails and names the charge code, so a new charge code in the ERP never slips through as the wrong category.

    designTemplateId gives the connector's invoices a PDF design (unless the message names one); choose one with the Logistics layout to print the job number, house bill and Incoterms in a Shipment panel. GET /design-templates lists the ids.

    GET /erp/connectors lists the connectors, the endpoint URL for each and your settings.

    Send a message

    curl -X POST "$BASE/erp/cargowise/messages" -u "api:$KEY" -H "Content-Type: application/xml" --data-binary @universal-transaction.xml
    

    What comes back:

    StatusMeaning
    202 with status: processedThe invoice was created (and submitted when submit is on). invoiceId, invoiceNumber and the full create result are in the body.
    200 with status: duplicateThe same document was already processed (same branch, type and number). Nothing was created; the original invoice is referenced.
    400 E_VALIDATIONThe message could not be mapped (unknown charge code, missing customer) or the invoice failed a PINT AE rule. The message is stored as failed with the reason; extra.message has it.
    202 with status: failedSame as above when ?ack=always or the connector's alwaysAck is on: push transports that retry on non-2xx get an acknowledgement and the failure stays in the log.

    The job, house bill and branch from the message land in the invoice's custom fields job, house_bill and branch (define them under Settings → Custom fields so they are validated and shown).

    Check before you send

    POST /invoices/validate?connector=cargowise takes the same XML, maps it and runs every rule without creating anything. The answer carries the validation report, mapping notes and a preview of the mapped invoice (type, dates, buyer, totals, lines with categories). The same route takes JSON without the connector parameter.

    Try it without writing code

    The test console on the Alya Books site (/console, under Developers in the menu) sends the freight samples behind this guide, CargoWise XML or JSON, through the dry run, the intake and POST /invoices. It runs in the browser without a key (same reader, mapper and rules, against a sample seller), or against your sandbox with a test key that stays in the page. Its dummy data feed generates a day of invoices with planted mistakes at a rate you choose, sends each one, and checks the answer against what the sample promises; with send each twice, every valid document is posted again and must come back as a duplicate. Add the sample charge codes adds those of the samples' charge codes (FRT, AIR, THC, TRK, WHS, DOC, DUTY) that the CargoWise connector does not have yet; codes already mapped are left exactly as they are.

    The message log

    • GET /erp/messages?status=failed lists what needs attention; keyword searches document numbers, keys and errors.
    • GET /erp/messages/{id} returns the message with its raw body (the document inside, for a wrapped message), the outcome, the sender's messageId and the envelope it arrived in.
    • POST /erp/messages/{id}/replay processes a failed message again after the charge map or the customer master was fixed. A processed message cannot be replayed, so a replay never creates a duplicate.

    Each outcome is written to the audit log, and the invoice events (invoice.created, invoice.submitted, invoice.reported, invoice.rejected) fire as for any other invoice, so the ERP's write-back can run from webhooks.

    What the CargoWise reader looks at

    Local element names only; namespaces and wrappers are ignored. The full table, with PINT AE terms and rules, is in CargoWise field mapping.

    Alya Books fieldElements read (first found)
    NumberTransactionInfo/Number, TransactionNumber, InvoiceNumber
    TypeTransactionType: INV → tax invoice, CRD/CRN → credit note, DBN → debit note; a three-digit code is taken as is
    DatesTransactionDate/PostDate, DueDate (ISO or dd/mm/yyyy)
    Currency, rateCurrency/Code, ExchangeRate (used for non-AED)
    ReferencesJobNumber/Job/Key, HouseBill, Branch/Code, Incoterms from ShipmentIncoTerm/Code, IncoTerm/Code or Incoterms (an unknown code is left out with a note); credit notes: OriginalTransactionNumber and the UAE reason code in CreditNoteReason (or ReasonCode), which a credit note must carry
    CustomerOrganization/FullName, RegistrationNumber[@Type="TRN"], Address/Address1, City, State/Code, Country/Code, Email, Code
    LinesPostingJournalCollection/PostingJournal or TransactionLineCollection/TransactionLine: ChargeCode, Description, Quantity, Amount, VATCode

    The Universal XML schema is published by WiseTech to licensees; send us a sample from your tenant and we confirm the mapping against it before UAT.