# Integration Updates Guide

Every change below is strictly additive: existing endpoints, response fields, HTTP status codes, webhook payload bytes, and the legacy `Signature` header are unchanged. Integrations that do nothing keep working exactly as before. Adopt each feature independently, in any order.

Served live at https://api.nbexp.ca/api/documentation/integration-updates (`?download=1` to save). Companion documents: OpenAPI (https://api.nbexp.ca/api/documentation), Postman collection (https://api.nbexp.ca/api/documentation/postman-collection), status dictionary & flow (https://api.nbexp.ca/api/documentation/order-status-flow).

---

## 1. Cancel by tracking number (idempotent)

`POST https://api.nbexp.ca/api/v1/orders/cancel` — provide exactly one of `order_id`, `tracking_number`, `external_tracking_number`.

```json
POST /api/v1/orders/cancel
Authorization: Bearer <token>
{ "external_tracking_number": "WP1234567890" }
```

- Success: `200 {"result":true,"id":123456,"already_cancelled":false,...}`
- Already cancelled (safe retry): `200` with `"already_cancelled": true`
- Number matches several live orders: `409` with `"matched_order_ids": [...]` — retry with `order_id`.
- The legacy `GET /v1/orders/{orderId}/cancel` is unchanged.

## 2. Incremental reconciliation feeds

Sweep everything that changed inside a window; never miss or double-count a record thanks to cursor pagination.

- `GET https://api.nbexp.ca/api/v1/orders-reconciliation?updated_from=&updated_to=&per_page=&cursor=`
- `GET https://api.nbexp.ca/api/v1/tracking-events/reconciliation?...`

`updated_from` / `updated_to` accept unix timestamps or `Y-m-d H:i:s` strings in the platform timezone. Follow `next_cursor` until `has_more` is false; compare with the `*_timestamp` unix fields, never with local-time strings. Tracking-event items include `occurred_at` / `occurred_timestamp` (business occurrence time; equals created time for historical rows).

## 3. Machine-readable error codes & request tracing

High-frequency create/cancel error responses now carry a stable `code` field next to the unchanged `message`:
`VALIDATION_FAILED`, `MISSING_IDENTIFIER`, `ORDER_NOT_FOUND`,
`ORDER_CANCEL_UNAUTHORIZED`, `ORDER_STATUS_NOT_CANCELLABLE`,
`ORDER_CANCEL_BLOCKED_THIRD_PARTY`, `LABEL_CANCEL_FAILED`,
`DUPLICATE_TRACKING_NUMBER`, `MULTIPLE_ORDERS_MATCHED`.
Codes are stable identifiers — new ones may appear, existing ones never change meaning. Every API response also echoes `X-Request-ID`; quote it when reporting an issue.

## 4. Duplicate detection upgrades

With `auto_deduplication=1`, a blocked create's `exist_package_ref` / `exist_external_tracking_number` entries now include the existing `order_id` and `order_ref`, and the envelope carries `"duplicate": true`. Optional strict mode: send `strict_duplicate_check=1` to receive HTTP `409` instead of the legacy `200` + `result:false` (omit the flag to keep the legacy behavior).

## 5. Batch creation options

- `per_order_transaction: 1` (top level of the batch body): each order commits independently — one failure no longer rolls back its siblings. If processing aborts early you still receive rows for everything that completed, plus a final `{"result":false,"batch_aborted":true}` row.
- Batches over 100 orders receive an advisory `X-Batch-Size-Warning` response header; prefer `POST /v1/client/batchOrderCreateAsync` for large volumes.

## 6. Proof-of-delivery file sync

Tracking responses' `proofs[]` entries now also carry:

```json
{ "url": "...", "full_url": "...", "type": 2,
  "file_id": 234567, "uploaded_at": "2026-07-20 14:30:05",
  "uploaded_timestamp": 1784745005,
  "signed_url": "https://.../files/pod/234567?expires=...&signature=...",
  "signed_url_expires_at": 1784745005 }
```

Use `file_id` as your dedup / incremental-sync key. `signed_url` is an expiring link (7 days) — re-query tracking for a fresh one after expiry. The permanent `url` / `full_url` links are unchanged.

## 7. Webhooks: v2 signature headers (all deliveries)

Every webhook now ALSO sends, alongside the unchanged legacy `Signature` header:

| Header | Meaning |
|---|---|
| `X-Webhook-Event-Id` | UUID, stable across retries — use for dedup |
| `X-Webhook-Timestamp` | unix seconds at first dispatch |
| `X-Webhook-Signature-V2` | HMAC-SHA256 of `"<timestamp>.<raw body>"` |

Verification (any language): `expected = HMAC_SHA256(timestamp + "." + raw_body, your_secret)`; constant-time compare with the header. The timestamp is fixed at first dispatch and reused on retries (retries can run up to ~3 hours), so pair a generous tolerance window with `X-Webhook-Event-Id` dedup rather than a tight replay cutoff. Your existing legacy-`Signature` verification keeps working unchanged — adopt v2 whenever you like. Test both signatures against your endpoint from the webhook guide page (`/webhooks-guide`).

## 8. Webhooks: opt-in payload envelope

Set `webhook_payload_envelope = 1` in your webhook settings to receive, inside the JSON body of object-shaped events: `event_id`, `event_time` (ISO8601 with offset), `event_timestamp` (unix), plus — where applicable — `is_correction: true` (a previously delivered/handed-over order re-entered the flow) and `occurred_at` / `occurred_timestamp` on tracking events. Without the flag your payload stays byte-identical to before. List-shaped payloads (`order.create_async`) are never modified.

## 9. New webhook events (opt-in URLs)

- `pod.files_updated` — a delivery photo/signature was added or removed after the fact. Configure `pod_files_webhook_url`. Payload:
  `{"action":"added|removed","order_id":...,"order_ref":...,
  "tracking_number":[...],"external_tracking_number":[...],
  "file":{"file_id":...,"type":1|2,"note":null}, "event_id":..., ...}`
- `order.deleted` — an order was permanently deleted. Configure `order_deleted_webhook_url`. Payload: `{"action":"deleted","order_id":...,
  "order_ref":...,"tracking_number":[...],"external_tracking_number":[...],
  "event_id":..., ...}`

Both events always include the envelope fields and are only sent when their URL is configured.

## 10. Webhooks: delivery options

- **Multiple endpoints per event**: every webhook URL setting accepts a single URL (as before), a comma-separated list, or a JSON array. Each endpoint gets its own delivery and retry cycle.
- **TLS verification**: set `webhook_verify_ssl = 1` to have outbound calls verify your certificate. Default stays off (historical behavior).
- **order.created for all order types**: set `order_created_webhook_all_types = 1` to extend the order-created event beyond local-delivery orders. Default keeps the historical scope.
- **Signing secrets**: first-time setups get a strong random secret; existing secrets are never rotated automatically.

## 11. API token scopes & IP allowlists (optional)

Individual tokens can be limited to `orders:read`, `orders:write`, and/or `webhooks:manage`, and to a list of allowed IPs. Unrestricted tokens (the default, and every pre-existing token) behave exactly as before. A restricted token calling outside its scopes/IPs receives `403`. Contact support to restrict a token.

## Compatibility promise

See the change-management policy: additive-only responses, new endpoints alongside old ones, webhook bodies frozen by default, defaults never changed, deprecations announced at least 30 days ahead with `Deprecation` / `Sunset` headers. These guarantees are enforced by automated byte-level compatibility tests on every release.