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

Source: /docs/guides/erp-intake

Integrating CargoWise? Start with the [CargoWise integration kit](/docs/guides/cargowise-integration).

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
```

| Connector | Content type | What it reads |
|---|---|---|
| `cargowise` | any (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 |
| `json` | `application/json` | One invoice in the [InvoiceInput](/docs/api-reference/introduction) 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:

| Shape | Example |
|---|---|
| Bare Universal XML | `UniversalInterchange` or `UniversalTransaction` as the body (`application/xml`, `text/xml`) |
| SOAP envelope | The 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 |
| Base64 | The body is base64 of the XML, or of the gzip-compressed XML |
| Form post | `application/x-www-form-urlencoded` with the XML in a field |
| Compressed | `Content-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.

```bash
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

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

What comes back:

| Status | Meaning |
|---|---|
| `202` with `status: processed` | The invoice was created (and submitted when `submit` is on). `invoiceId`, `invoiceNumber` and the full create result are in the body. |
| `200` with `status: duplicate` | The same document was already processed (same branch, type and number). Nothing was created; the original invoice is referenced. |
| `400` `E_VALIDATION` | The 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: failed` | Same 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](/docs/reference/cargowise-field-mapping).

| Alya Books field | Elements read (first found) |
|---|---|
| Number | `TransactionInfo/Number`, `TransactionNumber`, `InvoiceNumber` |
| Type | `TransactionType`: `INV` → tax invoice, `CRD`/`CRN` → credit note, `DBN` → debit note; a three-digit code is taken as is |
| Dates | `TransactionDate`/`PostDate`, `DueDate` (ISO or dd/mm/yyyy) |
| Currency, rate | `Currency/Code`, `ExchangeRate` (used for non-AED) |
| References | `JobNumber`/`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 |
| Customer | `Organization/FullName`, `RegistrationNumber[@Type="TRN"]`, `Address/Address1`, `City`, `State/Code`, `Country/Code`, `Email`, `Code` |
| Lines | `PostingJournalCollection/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.
