# 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.superroute.ca/api/documentation/integration-updates?lang=en (`?download=1` to save). Companion documents: OpenAPI (https://api.superroute.ca/api/documentation?lang=en), Postman collection (https://api.superroute.ca/api/documentation/postman-collection), status dictionary & flow (https://api.superroute.ca/api/documentation/order-status-flow?lang=en).

---

## 1. Cancel by tracking number (idempotent)

`POST https://api.superroute.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.superroute.ca/api/v1/orders-reconciliation?updated_from=&updated_to=&per_page=&cursor=`
- `GET https://api.superroute.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.

## 12. Carrier package submission (Smart Locker)

External carriers submit packages into a warehouse staging area in a single batch call. Optionally attach a recipient phone/email so the recipient is notified with a pickup code once the package is placed in a smart locker. Recipient fields are optional — the endpoint stays backward compatible.

```json
POST https://api.superroute.ca/api/v1/inventories/carrier-staging-stockin
Authorization: Bearer <token>
{ "carrier_id": 1, "warehouse_id": 10, "data": [
  { "carrier_reference_number": "TRK1", "ref": "REF1",
    "recipient_phone": "+14165550123", "recipient_email": "r@example.com" }
] }
```

```graphql
mutation($carrierId: Int!, $warehouseId: Int, $packages: Json) {
  submitCarrierPackages(carrierId: $carrierId, warehouseId: $warehouseId, packages: $packages)
}
```

- Auth: Bearer token as usual. Customer accounts must hold a carrier upload authorization (carrier + warehouse); client, employee and partner accounts use their own warehouse scope.
- Per package: member_number and/or carrier_reference_number as required by the carrier settings; optional carrier_order_id, batch, ref, grid_code (staff only), recipient_phone, recipient_email.
- Each response row returns a pickup_code; the same code is delivered to the recipient in the pickup notification.

Recipient contact is stored encrypted and never echoed back. When both phone and email are provided, both channels are notified, and phone/email take priority over the member-binding in-app notification.

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