# Carrier labels

Create a carrier shipment: list methods → quote → create the label → download the PDF → track → subscribe to event notifications → cancel (or close the day, if required).

This playbook documents the following operations only. Complete them in sequence.

Replace `YOUR_HOST`, `ACCESS_TOKEN`, and `shipping_method` with values from your environment. Method identifiers differ per account and must not be hard-coded.

## 1. Authentication

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your_password"}'
```

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL uses the same header on `POST /api/graphql`.

[REST handbook](/api/documentation#/paths/v1-user-login/post) · [GraphQL handbook](/api/graphql/documentation#/user/userLogin)

**Verification:** login returns `access_token`. Subsequent requests without this token return `401`.

## 2. List shipping methods

**REST:** `POST /api/v1/labelservice/getShippingMethodList` — [REST handbook](/api/documentation#/paths/v1-labelservice-getShippingMethodList/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

Each row has:

| Field | Use |
|---|---|
| `id` | `shipping_method` in every later call |
| `name` | Display name |
| `unique_identifier` | Stable code |
| `options.signature_option` | Signature available |
| `options.insurance_option` | Insurance available |
| `options.multi_package` | More than one piece |

**GraphQL:** `labelserviceGetShippingMethodList` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList)) (JSON scalar).

**Verification:** the list is not empty. You picked one `id` and you know whether that method allows signature, insurance, and multiple packages. An empty list means no method is enabled on the account.

## 3. Quote

Dry-run. The carrier is asked for a price; nothing is booked. The body is the same shape you will send to create. `shipping_method` is required.

**REST:** `POST /api/v1/labelservice/rate` — [REST handbook](/api/documentation#/paths/v1-labelservice-rate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

For more than one piece, send `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (address-book id) or `shipping_from_code` can replace the `sender_*` block.

**GraphQL:** `labelserviceRate` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceRate)).

**Verification:** `result` is true and you have a price (and transit days, when the carrier sends them). If there is no rate, fix destination / package / method **before** you create.

## 4. Create the label

This books the shipment with the carrier.

**REST:** `POST /api/v1/labelservice/submitOrder` — [REST handbook](/api/documentation#/paths/v1-labelservice-submitOrder/post)

Same body as step 3. Send `Idempotency-Key`.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-label-001" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)).

```json
{
  "result": true,
  "id": 12345,
  "shipping_price": "12.50",
  "tracking_numbers": ["1Z999AA10123456784"],
  "external_id": "EXT_12345"
}
```

| Field | Use |
|---|---|
| `id` | Superroute order id — download and cancel |
| `tracking_numbers` | Provide these numbers to the recipient; the public tracking endpoint accepts them |
| `external_id` | Carrier shipment id |
| `shipping_price` | Amount charged |

**Verification:** `tracking_numbers` is not empty. Store `id` and the numbers. The same `Idempotency-Key` must not buy a second label.

## 5. Download the PDF

**REST:** `POST /api/v1/labelservice/getShippingLabel` — [REST handbook](/api/documentation#/paths/v1-labelservice-getShippingLabel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "12345",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

`type` may be `ORDER_ID`, `TRACKING_NUMBER`, or `REF`. `base64: 0` streams a PDF. This is the **carrier's official label**. Piece count is fixed by the booking.

**GraphQL:** `labelserviceGetShippingLabel` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)).

**Verification:** the PDF opens and shows the carrier barcode / tracking number from step 4. Print one test copy, then throw it away — do not hand a test label to a carrier.

## 6. Track

No token. Use a number from `tracking_numbers`:

```bash
curl https://YOUR_HOST/api/v1/tracking/1Z999AA10123456784
```

[REST handbook](/api/documentation#/operations/getPublicTracking) · [GraphQL handbook](/api/graphql/documentation#/tracking/trackingPublic)

**GraphQL:**

```graphql
query {
  trackingPublic(trackingNumber: "1Z999AA10123456784") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

`is_third_party_tracking` is true when events come from the carrier. Branch on `tracking_event_status_id` / `tracking_event_key`, not on `description`. Newest event is first in `data`. Early events may still be “information submitted” until the carrier scans the parcel. `500` is delivered; `proofs[]` then may include signature (`type` `1`) or photo (`type` `2`).

**Verification:** the lookup returns the shipment you just created. An unknown number is `result: false` / 404.

## 7. Configure event notifications

| Setting | Event | When |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Persist `id` and carrier tracking numbers |
| `tracking_event_webhook_url` | `tracking.event` | Carrier scans, out for delivery, delivered |
| `order_status_change_webhook_url` | `order.status_change` | Status in your system |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Cancel refused because the carrier already has the parcel |

**REST:** `PUT /api/v1/webhook-settings` — [REST handbook](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL:** `webhookSettingsUpdate` ([GraphQL handbook](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.example.com/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.example.com/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

Verify **v2** over the raw body: `HMAC_SHA256(timestamp + "." + raw_body, secret)` against `X-Webhook-Signature-V2`. Deduplicate on `X-Webhook-Event-Id`. Answer **2xx in under 3 seconds**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**Verification:** one test `submitOrder` produces `order.created` with those `tracking_numbers`. An invalid signature must be rejected by the receiver with `401`.

## 8. Cancel

Only while the carrier still allows it (usually before pickup).

**REST:** `POST /api/v1/labelservice/cancelShippingLabel` — [REST handbook](/api/documentation#/paths/v1-labelservice-cancelShippingLabel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"12345","type":"ORDER_ID"}'
```

**GraphQL:** `labelserviceCancelShippingLabel` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceCancelShippingLabel)).

A carrier that already has the parcel will refuse — that is `order.cancel_failed`.

**Verification:** a second cancel is safe. Tracking no longer treats the shipment as live.

## 9. End of day (only if this method requires it)

Some carriers need a daily manifest.

**REST:** `POST /api/v1/labelservice/endofday` — [REST handbook](/api/documentation#/paths/v1-labelservice-endofday/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shipping_method": 59}'
```

**GraphQL:** `labelserviceEndofday` ([GraphQL handbook](/api/graphql/documentation#/labelservice/labelserviceEndofday)).

Call it once after the last label of the shipping day. Skip this step when step 2's method has no such requirement.

**Verification:** the success body (or the carrier confirmation) lists today's labels. Run this on a test method first.

## Test checklist

Use a destination you control and a method that can be cancelled:

- [ ] Method list is not empty; you captured one `id`.
- [ ] Rate returns a price for that method and destination.
- [ ] Submit returns `tracking_numbers`; the same `Idempotency-Key` does not buy a second label.
- [ ] Label PDF opens and shows the carrier tracking number.
- [ ] Public tracking finds the shipment by that number.
- [ ] `order.created` arrives; v2 signature verifies.
- [ ] Cancel succeeds, **or** you confirmed this method cannot be cancelled after booking.
- [ ] If the method needs end-of-day, a test run completes without error.
