Integration Guide

Create, look up, amend and cancel express shipments in the FastLink system directly from your own platform. Twelve endpoints, one login, no SDK required. Everything in this guide is copy-and-run: the examples are real requests against a live environment.

Quick start

Four calls, in this order. Each one has its own section with the full payload and the rules that apply — this page is the map, not the detail.

  1. Sign in

    Exchange the account we issued you for a token, then send that token on every other call. Any catalog endpoint is a cheap way to confirm it works: no body, changes nothing. Authentication →

  2. Load the reference data

    Service codes, country codes and postal codes are not free text. Fetch them, cache them, use the returned values verbatim. Catalog endpoints →

  3. Create a booking

    You get back a waybill number. That number is permanent and cannot be reissued, so read Avoiding duplicates before you go live. The booking lands at Draft. Create a booking →

  4. Confirm it

    A draft is not a live shipment — nobody is dispatched until you confirm, and this is what sends the confirmation email. Confirm a booking →

Test freely

The staging environment writes to a test database. Create as many bookings as you need while you build — you do not have to clean them up.

Authentication

Every request carries a bearer token. There is no API key: you sign in with a FastLink account and send the token you get back.

Getting a token

POST /api/v1/booking/auth/login with the account and password we issued you — send just the email address we gave you — nothing before it, nothing after it. Anything else is rejected before we even try it. It returns an accessToken good for one hour, and a refreshToken to renew it without sending the password again through POST /api/v1/booking/auth/refresh.

Request
curl -X POST "https://booking-gateway-api.fastlink.dev/api/v1/booking/auth/login" \
  -H "Content-Type: application/json" \
  -d '{ "userName": "YOUR_ACCOUNT", "password": "YOUR_PASSWORD" }'

Then send it on every other call:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6…

What the login gives you besides the token

The response carries customers: the customer codes that account may book for, with their names — ready to put in front of a user, or to check against your own records at startup. It is a convenience, not the permission itself; every booking call re-checks.

Treat the password as a password

Keep it on your server — never in a browser bundle, a mobile app, or a public repository. Store the refresh token the same way. Tell us immediately if either is exposed.

Do not log in on every request. One login per process, then refresh — repeated failed logins lock the account, and a lockout stops your integration, not just that call.

StatusMeaningWhat to do
401Token missing, malformed or expiredCheck the header name and the value. A token lasts one hour — renew with /auth/refresh rather than logging in again on every call.
403Token is valid but your client is not allowed on this routeContact us; the route has to be enabled for your client.

Your account is scoped to a set of customer codes. You can only create and read bookings for those codes — see Common mistakes for how that surfaces.

How that scope is decided

The customers you may act on are the ones your account is assigned to in our internal system with the booking role. Being assigned to a customer for billing or sales does not let you book for it.

Two consequences worth planning for. Adding or removing a customer takes effect immediately, with no change on your side and no redeploy on ours — so a code that worked yesterday can start returning 40, and a new one can start working without us telling you. And a booking for a code outside your scope answers exactly like one that does not exist, so never read "not found" as proof the waybill is unknown to us.

Response envelope

HTTP 200 does not mean the request succeeded. Business outcomes are carried in the code field of the body, not in the HTTP status. Checking only the status code is the single most common integration bug we see.

Every response has the same shape:

Response envelope
{
  "traceId":       "00-dcf0cd16…-9588d2d7…-01",   // give us this when reporting a problem
  "correlationId": null,
  "data":          { … } | null,                  // payload on success, null on failure
  "code":          0,                             // 0 = success, anything else = failure
  "subCode":       null,
  "message":       "Success",                     // human-readable, English
  "errors":        []                             // per-field messages on validation failure
}

Click a code to see what it means and what to do about it.

Reading a response correctly

const res  = await fetch(url, { method: 'POST', headers, body });
const body = await res.json();

if (res.status === 401 || res.status === 403) throw new Error('Auth problem');

if (body.code === 0)  return body.data;          // success
if (body.code === 12) return body.data;          // duplicate — same booking returned, treat as success
if (body.code === 21) throw new Error(body.errors.map(e => e.message).join('; '));

throw new Error(`${body.code}: ${body.message}`); // 11, 20, 40, …
Always log traceId

Store traceId against your own order record. When you report a problem we can find the exact request in our logs from that value alone, which turns a day of back-and-forth into a few minutes.

Booking lifecycle

Creating a booking takes two calls. create returns a booking at 1 — Draft with its tracking number already assigned, and confirm moves it to 10 — Booked. Nobody is dispatched to collect the shipment until you confirm, so a draft left behind is not a live shipment.

The split exists so you can create the booking, show the tracking number to your customer, and only commit once they are ready. If you have no such step, call the two back to back.

StatusMeaningUpdateCancelPrint
1Draft — created but not confirmed; no collection scheduledYesYesNo
10Booked — confirmed, collection scheduledNoYesYes
15Picked up — the shipment is physically with usNoNoYes
20ProcessingNoNoYes
100CancelledNoNoNo

Note the asymmetry at 10: you can still cancel a confirmed booking, but you cannot amend one. Confirming hands the details to our operation, so a change after that point would not reach the people already scheduled to collect. Cancel it and create a new booking instead — the tracking number changes, so use the new one from then on.

Once a shipment reaches 15 it has been physically handled, so amending or cancelling it needs a human process — contact your account manager rather than the API.

Catalog endpoints

Four read-only lists. Cache them on your side and refresh about once a day; they change rarely, and calling them before every booking only slows you down.

GET/api/v1/booking/catalog/services?groupCode=EXP_EXPRESS_V4

Valid values for the service field. Do not hard-code these — they differ per environment. Codes you will typically see: EOCS, ARAMEX, EDHL, EUPS, FEDEX, FBM, KHA.

GET/api/v1/booking/catalog/countries

Feeds deliveryAddress.countryCode.

GET/api/v1/booking/catalog/postal-codes?countryCode=US&postalCode=90001

Returns code (postal code) and name (state or province). Feeds deliveryAddress.zipCode and deliveryAddress.zoneName — both required. A postalCode shorter than 3 characters returns an empty list.

GET/api/v1/booking/catalog/currencies

Feeds invoice.currencyCode. Only needed if you type in an invoice.

Shape of every catalog response
{
  "data": [
    { "code": "EDHL", "name": "DHL" },
    { "code": "FEDEX", "name": "FedEx" }
  ],
  "code": 0,
  "message": "Success"
}

Create a booking

POST/api/v1/booking/express/create

Creates the shipment and returns its waybill number. Written in one transaction — either the booking, all package lines, both addresses and the invoice are stored, or nothing is.

Request — full example
{
  "custCode": "0320XXXX",
  "service": "EDHL",
  "pkgType": "SPX",
  "partnerRefNo": "SHP-2026-00123",
  "estimatePickTime": "2026-12-31T14:00:00",
  "taxCode": "0101234567",
  "termOfTrade": "DDU",
  "paidDutyBy": "PP",
  "hasCustomsDeclaration": true,
  "customsDeclarationType": "HNC_OPENS",
  "customsDeclarationNumber": "103456789012",
  "note": "Fragile, call before pickup",
  "pickupAddress": {
    "senderName": "ACME TRADING",
    "senderNameEng": "ACME TRADING CO LTD",
    "contactPerson": "Jane Doe",
    "mobile": "0901234567",
    "addressDetail": "12 Tran Duy Hung, Ha Noi"
  },
  "deliveryAddress": {
    "consigneeName": "XYZ CORPORATION",
    "contactPerson": "John Smith",
    "mobile": "+1 213 555 0100",
    "email": "john@xyz.com",
    "addressDetail": "1234 S Main St",
    "countryCode": "US",
    "zoneName": "CALIFORNIA",
    "zipCode": "90001"
  },
  "details": [
    {
      "noPieces": 2,
      "originTotalWeight": 5,
      "contentType": "ELECTRON",
      "contentDetail": "Electronic components",
      "vwLength": 40,
      "vwWidth": 30,
      "vwHeight": 20
    }
  ],
  "invoice": {
    "invoiceType": "COMMERCIAL",
    "invoiceNumber": "INV-2026-0001",
    "invoiceDate": "2026-08-01T00:00:00",
    "currencyCode": "USD",
    "sender": "ACME TRADING",
    "recipient": "XYZ CORPORATION",
    "details": [
      { "quantity": 2, "descriptionContent": "Sensor module", "unitValue": 10, "unitWeight": 0.5, "countryOfOrigin": "VN", "hsCode": "8543", "unitCode": "PCE" },
      { "quantity": 3, "descriptionContent": "Cable", "unitValue": 5, "unitWeight": 0.2, "countryOfOrigin": "VN" }
    ]
  }
}
Response — abbreviated, real data
{
  "traceId": "00-dcf0cd1656bfb138646039fd25653311-9588d2d7b1ac1362-01",
  "data": {
    "hawbNo": "893600001782644",     // ← your waybill number
    "status": 10,
    "service": "EDHL",
    "pkgType": "SPX",
    "custCode": "0320XXXX",
    "custName": "ACME TRADING",
    "noPieces": 5,                   // computed by us
    "weight": 10.0,                  // computed by us
    "isInvoice": true,
    "invoiceDbFiles": "",
    "createdTime": "2026-08-07T07:53:07",
    "details": [ … ],
    "pickupAddress": { … },
    "deliveryAddress": { … }
  },
  "code": 0,
  "message": "Success"
}

Field reference

Booking

FieldNotes
custCodeRequiredYour customer code with us. Must be one of the codes your account is authorised for — the login response lists them.
serviceRequiredFrom /catalog/services. Decides how the waybill number is issued, so it cannot be changed later.
pkgTypeRequiredDOX documents · SPX goods. SPX requires an invoice.
estimatePickTimeRequiredMust be in the future. ISO 8601, local time.
taxCodeRequiredStored exactly as you send it — we do not substitute the value on the customer record.
detailsRequiredAt least one package line.
partnerRefNoOptionalYour own order reference. Strongly recommended — see Avoiding duplicates.
termOfTradeOptionale.g. DDU, DDP.
paidDutyByOptionalWho pays duty.
noteOptionalFree text for our operations team.
hasCustomsDeclarationOptionalSet to true when a customs declaration is opened for the shipment.
customsDeclarationTypeOptionalRequired when hasCustomsDeclaration is true and pkgType is SPX. Who opens the declaration: CUSTOMER_OPENS or HNC_OPENS. Any other value is accepted but shows up verbatim on the confirmation email instead of a readable label.
customsDeclarationNumberOptionalThe declaration number, once you have it.
batchCodeOptionalIf you group shipments into batches.
hawbNoWe fillIssued by us. Never send it.
custName, companyCodeWe fillRead from your customer record.
noPieces, weightWe fillSummed from details. Anything you send is ignored.

Address — pickupAddress and deliveryAddress

FieldPickupDeliveryNotes
senderNameRequired—Legal sender name.
consigneeName—RequiredReceiving company.
contactPersonRequiredRequired
mobileRequiredRequired
addressDetailRequiredRequiredStreet address.
countryCode—RequiredFrom /catalog/countries.
zoneName—RequiredState or province, from /catalog/postal-codes.
zipCode—RequiredCountries without postal codes: send a single space.
emailOptionalOptionalRequired when service is ARAMEX — Aramex rejects shipments without a consignee email.
senderNameEngOptional—Used on printed documents.
taxIdOptionalOptionalConsignee tax / EIN / BIN number.

Read-backs contain two fields you never sent: addressFull and countryName. We derive them — the delivery one by joining street, state, postal code and country name, the pickup one from the street alone — because our operations screens and the printed label read those columns. Send them yourself only if you want a specific wording; whatever you send is kept as-is.

Package line — details[]

FieldNotes
noPiecesRequiredGreater than 0.
originTotalWeightRequiredActual weight in kg, greater than 0.
contentTypeRequiredExcept when service is FBM. One of LICENSE · GARMENT · MECHANICAL · ELECTRON · PLASTIC · FOOD · OTHER — the same seven our own booking screen offers. Anything else is stored as sent and reaches our operators as an unknown code.
vwLength, vwWidth, vwHeightOptionalDimensions in cm. Send them and we compute volumetric weight — see below.
totalVwWeightWe fillComputed from the dimensions unless you send your own value.
seqOptionalLine order. Left empty, we number them in the order you sent.
contentDetail, noteOptionalFree text.

Volumetric weight

Send vwLength, vwWidth and vwHeight in centimetres and leave totalVwWeight empty. We work it out and write it back on the booking, so you can read it from /express/get rather than computing it yourself.

Chargeable weight

Billing uses the greater of actual and volumetric weight. Sending dimensions therefore changes the price — send them when they apply, omit them when they do not.

Invoices and attachments

Goods shipments (pkgType = "SPX") need a commercial invoice. Supply it in exactly one of two ways.

Never send both

invoice and invoiceFiles are mutually exclusive and sending both is rejected. Our operations screen hides the attachment panel on bookings that carry a typed-in invoice, so a file sent alongside one would be invisible to the staff handling your shipment.

Option A — type the invoice in

Best when you already hold the commercial data. We derive the totals so you do not have to:

  • Leave subTotal empty → we compute quantity × unitValue
  • Leave totalWeight empty → we compute quantity × unitWeight
  • Leave totalInvoiceAmount empty → we sum the lines

Four invoice header fields come from details[] — the package lines you already sent — not from the invoice lines. Leave them out and they arrive filled:

FieldDerived from
totalNetWeightSum of details[].originTotalWeight
totalGrossWeightSum of the volumetric weights we computed for each line
totalShipmentPiecesSum of details[].noPieces
carrierThe booking's own service

currencyCode defaults to USD. Send any of these five explicitly and we store exactly what you sent — the derivation only fills blanks.

Read it back later with POST /api/v1/booking/express/get-invoice.

Fields that end up on paper

A typed-in invoice also produces a printed invoice PDF attached to the confirmation email, and a few fields that look cosmetic in the payload become visible boxes on it. Leave them out and the box prints empty — nothing errors, so it is worth filling them in:

FieldWhere it prints
details[].descriptionContentVnThe Material column, one row per line
commoditySizeThe Dimension box
importerThe Importer box — left out, it prints Same as Consignee
deliveryAddress.taxIdNot on the invoice, but on the OCS consignment note under the consignee name

Option B — attach a file

Send the document as base64 inside the same create call. No separate upload step.

Request fragment
"invoiceFiles": [
  { "fileName": "INV-2026-0001.pdf", "fileBase64": "JVBERi0xLjQKMSAwIG9iago…" }
]
RuleValue
Accepted types.pdf .jpg .jpeg .png
Maximum size5 MB per file
Number of filesNo fixed limit; send what the shipment needs
ResultRecorded in invoiceDbFiles as a JSON array — see Read a booking

Files are uploaded before the waybill number is issued, so a rejected file never costs you a number — you can fix it and retry cleanly.

Avoiding duplicates

A waybill number, once issued, cannot be withdrawn or reused. That makes retries dangerous by default:

1. You call create
2. We create the booking and issue waybill …600
3. The response is lost — timeout, dropped connection, deploy
4. You do not know it worked, so you retry
5. Without protection: a second booking, waybill …611
   → one shipment, two waybills, two labels, double billing

Send partnerRefNo — your own order reference — and step 5 changes: we recognise the reference, skip creation entirely, and return the original booking with code: 12. Call it ten times and you still get one shipment.

Cancelled bookings are ignored by this check. Once you cancel a booking, the same partnerRefNo is free again — re-book it and you get a fresh waybill number rather than the dead booking back. That is deliberate: a cancelled order being re-placed under the same reference is normal, and you should not have to invent a new one.

Treat code 12 as success

It carries the same data as the original response, waybill number included. Nothing was created, nothing is wrong — your retry simply found the earlier booking.

The reference is compared within one customer code. Two shipments for different customers may carry the same string without interfering, so you can keep numbering per customer if that suits you. Leave it empty and no protection applies — the choice is yours, but we recommend always sending it.

One consequence worth knowing if more than one of our partners books for the same customer code: they share that reference space. The same string sent by either of you resolves to the same booking, which is usually what you want — one order, one waybill — but it does mean the reference is not private to you.

Confirm a booking

POST/api/v1/booking/express/confirm

Moves the booking from 1 — Draft to 10 — Booked and sends the confirmation email. Until you call this, no collection is scheduled.

Request
{
  "hawbNo": "893600001782644",
  "emails": ["ops@yourcompany.com", "customer@example.com"]
}

Who receives the email

emails is your list. We add the address on the customer record and the staff assigned to that customer, so the message usually reaches more people than you listed. That is expected — those are the people who act on the booking.

Every address in emails must be a plain, well-formed address. Send one that is not — a name, a placeholder, two addresses in one string — and the call is rejected with HTTP 400 before anything changes; the booking stays at Draft and you can retry. We validate here rather than let the mail server refuse later, because one malformed address makes the whole message fail: you, the customer and the assigned staff would all receive nothing while the booking had already moved to Booked.

Addresses we add ourselves are treated differently. If the customer record or a staff profile holds something unusable, we drop that one address and still send to everyone else — one stale record should not silence a booking confirmation.

If the list ends up empty — you sent none and the customer record has no address — the call returns code: 20 and the booking stays at Draft. A booking nobody was told about is worse than one you can retry.

What the email contains

A summary of the booking — customer, service, pickup time, package lines, and both addresses — followed by a link to the waybill label PDF under File đính kèm. That label is the sheet to print and attach to the parcel.

Which files you get depends on the shipment. Standard express services get the waybill label. EOCS bookings get an OCS consignment note instead of it, not in addition. And any booking with a typed-in invoice gets a commercial invoice PDF on top of whichever label applies — so a booking can carry two files.

One exception: FBM bookings do not carry the Amazon label, because that template is not present on our reporting service. A booking is still valid without any of these files — you can always request a reprint.

Calling it twice

Only a booking at status 1 can be confirmed. A second call returns code: 20 and changes nothing, so a retry after a network timeout is safe.

The email is queued, not sent inline. A successful response means the booking is confirmed and the message accepted for delivery — delivery itself happens moments later. If mail delivery fails, the booking still stays confirmed; we would rather you have a live shipment than lose it to a mail server.

Look up a booking

POST/api/v1/booking/express/get

Header, package lines ordered by seq, and both addresses.

Request
{ "hawbNo": "893600001782644" }

Two fields worth knowing:

  • isInvoice — whether a typed-in invoice exists. If true, fetch it from /get-invoice.
  • invoiceDbFiles — the attached invoice files, as a JSON array encoded in a string. Parse it if you need the file names back; it is stored exactly as our internal screens read it.
[
  {
    "Category": "fin_trinh_gia_send_mail",
    "OriginalName": "INV-2026-0001.pdf",
    "Code": "0320XXXX",
    "Filename": "5eed8b033afa4216835e6733041e1514.pdf",
    "Ext": ".pdf"
  }
]

Two details that trip people up. The field names inside are PascalCase, unlike every other field in this API — that shape belongs to the storage service, not to us, and we pass it through unchanged. And Code is the customer code, not a file id; Filename is what identifies the stored file.

Bookings created before we moved file storage still hold the older format in this field — a plain comma-separated list of ids. Check whether the value starts with [ before parsing it as JSON.

Several at once

POST/api/v1/booking/express/get-by-codes

The same payload as /get, for a list of waybill numbers — use it to reconcile a batch instead of firing one request per booking. 50 numbers per call.

Request
{ "hawbNos": ["893600001782644", "893600001782651"] }

Unknown numbers are skipped, not reported. A waybill that does not exist — or belongs to a customer code you are not registered for — is simply absent from the response. That is the same reasoning as /get returning not found for both cases: saying which of the two it was would let anyone probe for other partners' waybill numbers. Compare the returned list against what you sent to see what is missing.

Results keep the order you sent, with duplicates removed.

POST/api/v1/booking/express/get-invoice

The commercial invoice and its line items. Only for bookings where the invoice was typed in; bookings carrying attached files return code: 11.

Update a booking

POST/api/v1/booking/express/update

Only while the booking is still at status 1 — Draft. Once you confirm it, cancel and create again.

What you cannot change

These fields are absent from the update payload by design:

FieldWhy
hawbNoIt identifies the booking; it is not an editable value.
custCodeChanging it would move the shipment to a different customer and bypass your access scope.
serviceThe waybill number was issued according to this service. Changing it would leave the number inconsistent with the shipment.
partnerRefNoIt is the duplicate-protection key. Moving it would make a later retry fail to recognise the booking.

How the parts behave

PartBehaviour
AddressesThe same two address rows are kept, but every field is overwritten — this is not a merge. A field you leave out is cleared, not preserved. Send the whole address block exactly as you did on create, including values that did not change. The one most often forgotten is senderNameEng: lose it and the printed label falls back to the customer name on file, while the “sender company” line of the confirmation email goes blank.
detailsReplaced wholesale. Whatever you send is exactly what remains — send the complete list, not just the changes.
invoiceReplaced wholesale. Omit it to remove the invoice entirely.
invoiceFilesOmit to keep the current files; send [] to remove them all.

All of it runs in one transaction: either every part changes, or none does.

Cancel a booking

POST/api/v1/booking/express/cancel

Only while the booking is at status 1 or 10.

Request
{
  "hawbNo": "893600001782644",
  "note": "Customer changed their mind"
}

note is required — our operations team reads it to understand why the shipment disappeared. It is appended to the existing note rather than replacing it, so whatever you wrote at creation survives.

Calling it twice returns code: 20 with a message saying the booking is already cancelled, so you can tell a duplicate call apart from a genuine state error.

Cancellation email

Cancelling a confirmed booking sends a cancellation notice carrying your note as the reason. It goes to exactly the people who received the confirmation — the addresses you passed to confirm, plus the customer record and the assigned staff. Nobody who was told the shipment exists is left thinking it still does.

Cancelling a booking still at Draft sends nothing, because no confirmation was ever sent. As with confirm, a mail failure never undoes the cancellation.

Print the shipping note

POST/api/v1/booking/express/get-shipping-note

Only once the booking is confirmed — status 10, 15 or 20.

Returns the shipping note — the air waybill that travels with the box — as a base64 PDF. It is built from the same template as the copy attached to your confirmation email, from the booking as it stands right now. Details that changed since that email, and the booking date stamp once a shipment moves on through our operation, can therefore differ between the two. The waybill number never does, and that is what identifies the shipment.

Request
{ "hawbNo": "893600001782644" }
Response
{
  "code": 0,
  "data": {
    "fileName": "AWB_893600001782644.pdf",
    "contentType": "application/pdf",
    "fileBase64": "JVBERi0xLjQKMSAwIG9iago…"
  }
}

Decode fileBase64 and you have the file. Nothing is stored on our side: the PDF is rebuilt from the booking on every call, so it always shows the current data and you can call it as often as you need — a jammed printer or a torn label costs you one more request, not a support ticket.

Why a draft has none

A draft is not a live shipment, so it has no shipping note. Printing one would put a label on a box that nobody has been scheduled to collect, and that label would be indistinguishable from a real one. Confirm the booking first. A cancelled booking has no shipping note either, for the same reason in reverse: the label would still work if it reached a warehouse.

Which template we use follows the service on the booking, and you do not choose it — sending a different one would produce a document our operation does not accept.

Common mistakes

Every one of these has cost an integrator real time. They are listed most-frequent first.

1 · Checking only the HTTP status

A failed lookup returns HTTP 200 with code: 11. Treating 200 as success means silently processing an empty result. Always branch on code.

2 · Expecting “not found” and “not allowed” to differ

A booking that exists but belongs to a customer code you are not authorised for returns the same answer as one that does not exist. This is deliberate: distinguishing them would let anyone probe for other partners' waybill numbers.

The check runs on every call and looks only at the customer code on the booking — it does not care who created it. So a booking you created yourself becomes unreachable the moment your access to that customer code is withdrawn, and it comes back as code: 11 like any unknown waybill.

That means code: 11 on a waybill you are sure exists has two possible causes, and the response will not tell you which: the number is wrong, or your access to that customer changed. Check the customer code first — if other bookings for the same customer also stopped resolving, it is access, not the number. Then contact us.

3 · Sending both an invoice and an invoice file

Rejected — see Invoices and attachments. Pick one.

4 · Retrying without partnerRefNo

Duplicate shipments and duplicate billing. See Avoiding duplicates.

5 · Sending only the changed package lines to update

details is replaced wholesale, not merged. Sending one line leaves the booking with exactly one line.

6 · Hard-coding service or country codes

They differ between staging and production. Read them from the catalog endpoints.

7 · Omitting zipCode for countries that have no postal code

The field is required. Send a single space rather than omitting it.

8 · Forgetting the consignee email on ARAMEX

Aramex rejects shipments without one, so we reject them at the door rather than letting the shipment fail later.

Go-live checklist

  • Production account requested, and /auth/login confirmed working against it
  • Account name stored exactly as we issued it, character for character
  • Token cached for its hour and renewed with /auth/refresh, not one login per request
  • Response handling branches on code, not on HTTP status
  • code: 12 handled as success, not as an error
  • partnerRefNo sent on every create, unique per order on your side
  • Every create followed by a confirm — and an alert on your side for drafts left unconfirmed
  • traceId stored against your order record
  • Catalog data cached and refreshed on a schedule
  • Retry policy in place for network failures — safe now that duplicates are handled
  • Password and refresh token held server-side only, never in client code or version control
  • A person or alias on your side we can contact about shipment problems