Open Application Platform Docs

API reference

The calls a widget's key makes, with their requests and answers.

Every call a widget may make on the API host, with its parameters, request body and answers. A widget makes only the calls of its own type, as listed on each widget's page; the runtime makes them.

Every call carries the widget's key and the page's origin. The calls of a signed-in visitor also carry the visitor's token:

X-App-Key: sp_widget_…
Origin: https://www.example.com
Authorization: Bearer <the visitor's token>

REST

POST /api/v1/apps/swidget/auth/two-factor

Finish a sign-in held at its second factor

Unified identity: an email, Google or Apple sign-in of a person with two-step verification (or whose client requires it) answers 403 TWO_FACTOR_REQUIRED with data.challenge_id instead of a session. Send the code here from the same device. method is totp (authenticator app; a recovery code is also accepted), email or sms. setup = true: the client requires two-step verification and the person had none; the emailed code turns it on. trust_device = true returns two_factor_device_token: send it as two_factor_device_token with later sign-ins from this device and the code is not asked for 30 days. The response is the sign-in's session (same envelope as email login).

ParameterInRequiredExample
appCodepath✓swidget

Request body

{
    "challenge_id": "r9Qm...",
    "code": "123456",
    "trust_device": true,
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "app_version": "1.0.0"
}

Answer 200 — Session issued

{
    "result": true,
    "access_token": "passport-access-token",
    "token_type": "Bearer",
    "two_factor_device_token": "k2v...64 chars"
}

Answer 401 — TWO_FACTOR_CODE_INVALID

Answer 410 — TWO_FACTOR_CHALLENGE_INVALID: unknown, expired, used, or from another device

Answer 429 — TWO_FACTOR_TOO_MANY_ATTEMPTS: start the sign-in again

Answer 422 — Invalid request

POST /api/v1/apps/swidget/auth/two-factor/resend

Send the code of a held sign-in again

For method email or sms. Returns a new challenge_id; the previous one stops working.

ParameterInRequiredExample
appCodepath✓swidget

Request body

{
    "challenge_id": "r9Qm...",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios"
}

Answer 200 — Code sent

{
    "result": true,
    "data": {
        "challenge_id": "x7Lp...",
        "method": "email",
        "target": "a***@example.com",
        "expires_in": 600
    }
}

Answer 410 — TWO_FACTOR_CHALLENGE_INVALID

Answer 429 — TWO_FACTOR_RESEND_TOO_SOON

Answer 503 — TWO_FACTOR_DELIVERY_FAILED

POST /api/v1/apps/swidget/clients/{clientCode}/auth/email

Sign in to an app with email

Authenticates the normal Superroute account password, then applies the selected app's role and Client ownership policy before issuing an app session.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "email": "customer@example.com",
    "password": "account-password",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "android",
    "app_version": "1.0.0"
}

Answer 200 — App session issued; response matches Google login

Answer 401 — Invalid credentials

Answer 403 — App, method, role or Client access is not allowed

Answer 422 — Invalid request

POST /api/v1/apps/swidget/clients/{clientCode}/auth/join

Join this client with the identity

Unified identity: an email, Google or Apple sign-in in a client's own app by a person whose identity holds no account of this client yet answers 409 IDENTITY_JOIN_REQUIRED with data.join_token (10 minutes, same device), data.fields (required) and data.values (only the name prefilled). Send the client's registration details here. The client's app registration decides: 403 IDENTITY_JOIN_CLOSED when it takes no registrations, 202 pending_review when it reviews new accounts, otherwise the session (or TWO_FACTOR_REQUIRED). Nothing is copied from the person's accounts at other clients.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓q8w2e6r1t5y9u3i7o0p4a2sd

Request body

{
    "join_token": "t4Kp...64 chars",
    "name": "Ann Lee",
    "telephone": "+15145550123",
    "address": "100 Rue Main",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H2X 1Y4",
    "country": "Canada",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios"
}

Answer 200 — Joined; session issued (same envelope as email login)

Answer 202 — Joined; the client reviews new accounts first

{
    "result": true,
    "pending_review": true
}

Answer 403 — IDENTITY_JOIN_CLOSED, or TWO_FACTOR_REQUIRED

Answer 409 — IDENTITY_ALREADY_JOINED

Answer 410 — IDENTITY_JOIN_INVALID: unknown or expired token, other device

Answer 422 — VALIDATION_FAILED with errors per field

POST /api/v1/apps/swidget/clients/{clientCode}/auth/logout

Sign out of an app — needs the visitor's token

Revokes the current Passport token and marks the matching app-device session as revoked.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Answer 200 — Session revoked

{
    "result": true,
    "message": "Signed out successfully."
}

Answer 401 — Invalid or expired app session

Answer 403 — App access is no longer allowed

GET /api/v1/apps/swidget/clients/{clientCode}/guest/pickup-points

The Client's pickup points without signing in

The Client's smart locker locations (ids `L…`) and pickup point counters (ids `C…`) a visitor may choose, nearest first when `latitude` and `longitude` are given, with nothing personal. A pickup point widget's key (`appCode` `swidget`, see `platformAppSwidgetConfig`) narrows the list to the kinds the widget keeps. Public. GraphQL twin: `platformAppGuestPickupPoints`.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓
latitudequery
longitudequery
searchquery
pagequery
per_pagequery
X-App-Keyheader
Originheader

Answer 200 — The pickup points

{
    "result": true,
    "data": {
        "items": [
            {
                "id": "L12",
                "kind": "locker",
                "location_id": 12,
                "name": "Station locker",
                "address": "1 Front St",
                "city": "Toronto",
                "latitude": 43.6,
                "longitude": -79.4,
                "distance_km": 1.2
            }
        ],
        "pagination": {
            "page": 1,
            "per_page": 20,
            "total": 1,
            "has_more": false
        }
    }
}

Answer 404 — The app is not available for this Client

{
    "result": false,
    "code": "APP_NOT_AVAILABLE"
}

GET /api/v1/apps/swidget/clients/{clientCode}/guest/shipments

List a verified contact's guest shipments

Public; needs X-Guest-Contact-Token. The guest shipments of that contact at this Client from the last 90 days, newest first, at most 50: for a guest on a new device.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
X-Guest-Contact-Tokenheader✓
localequeryen

Answer 200 — Guest shipments

{
    "result": true,
    "items": [
        {
            "id": 42,
            "kind": "self_delivery",
            "status": "pending_payment",
            "amount": "12.40",
            "currency": "CAD",
            "contact": "g****@example.com",
            "created_at": "2026-10-09T10:00:00-04:00",
            "pay_url": "https://ship.example.com/pay/r/3f9c0d6a1b2e4f5a6b7c8d9e0f1a2b3c",
            "pay_before": "2026-10-09T10:30:00-04:00",
            "can_cancel": true,
            "order": null
        }
    ]
}

Answer 401 — GUEST_CONTACT_REQUIRED

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer guest shipping in this app

POST /api/v1/apps/swidget/clients/{clientCode}/guest/shipments

Submit a guest shipment for payment

Public; needs X-Guest-Contact-Token. The body is the rated shipment, exactly as rated, with rate_id from POST .../guest/shipments/rate, and optionally note and need_pick_up (self delivery). Nothing is created yet: the shipment waits for its payment on the shared payment page (pay_url, gateways only, no wallet) until pay_before. Once paid, the payment is credited to the Client's attribution customer and the order is created through uniorder (local delivery dispatched, label bought); if that fails the payment is refunded. The guest receives the outcome by email or SMS on the verified contact; the link in it carries no token. order_token is shown once and opens this shipment later.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
X-Guest-Contact-Tokenheader✓

Request body

{
    "from_name": "Ann Lee",
    "from_telephone": "+14165550100",
    "from_address": "1 King St W",
    "from_city": "Toronto",
    "from_province": "ON",
    "from_postcode": "M5H 1A1",
    "from_country": "CA",
    "to_name": "Bo Chen",
    "to_telephone": "+14165550101",
    "to_address": "300 Front St W",
    "to_city": "Toronto",
    "to_province": "ON",
    "to_postcode": "M5V 2T6",
    "to_country": "CA",
    "packages": [
        {
            "weight": 2,
            "length": 30,
            "width": 20,
            "height": 10,
            "weight_unit": 1,
            "dimension_unit": 1
        }
    ],
    "rate_id": "eyJpdiI6Ikd1ZXN0UXVvdGUi...",
    "note": "Back door",
    "locale": "en"
}

Answer 201 — Waiting for payment

{
    "result": true,
    "order_token": "k3J9xQ2mV8pL5tR1wZ7cN4bH6dF0gS2aY9eU3iO8",
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "pending_payment",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": "https://ship.example.com/pay/r/3f9c0d6a1b2e4f5a6b7c8d9e0f1a2b3c",
        "pay_before": "2026-10-09T10:30:00-04:00",
        "can_cancel": true,
        "order": null
    }
}

Answer 401 — GUEST_CONTACT_REQUIRED

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer guest shipping in this app

Answer 422 — RATE_ID_INVALID: expired, another Client, or a different shipment

Answer 429 — GUEST_ORDER_LIMIT: per contact per day, per IP address per hour

POST /api/v1/apps/swidget/clients/{clientCode}/guest/shipments/rate

Price a guest shipment

Public. Prices a local delivery and carrier labels for a guest of a Client that offers guest shipping (capability guest.shipping), as the Client's attribution customer: its price plan, label accounts and service switches. The body is the shipment (from_* and to_* name, telephone, address, city, province, postcode, country; packages; signature_option); any other field, such as a customer or a channel, is ignored. Each rate_id is a guest quote: valid for this Client and this exact shipment only, for 30 minutes. A self delivery rate without a price is reported in errors with code NOT_PRICED: a guest pays before the order exists. Carrier quotes: six per minute per caller and Client.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "from_name": "Ann Lee",
    "from_telephone": "+14165550100",
    "from_address": "1 King St W",
    "from_city": "Toronto",
    "from_province": "ON",
    "from_postcode": "M5H 1A1",
    "from_country": "CA",
    "to_name": "Bo Chen",
    "to_telephone": "+14165550101",
    "to_address": "300 Front St W",
    "to_city": "Toronto",
    "to_province": "ON",
    "to_postcode": "M5V 2T6",
    "to_country": "CA",
    "packages": [
        {
            "weight": 2,
            "length": 30,
            "width": 20,
            "height": 10,
            "weight_unit": 1,
            "dimension_unit": 1
        }
    ],
    "locale": "en"
}

Answer 200 — Rates, as POST /v1/uniorder/rate, with guest rate ids

{
    "result": true,
    "rates": [
        {
            "rate_id": "eyJpdiI6Ikd1ZXN0UXVvdGUi...",
            "type": "self_delivery",
            "shipping_price": "12.40",
            "currency": "CAD"
        },
        {
            "rate_id": "eyJpdiI6Ikd1ZXN0TGFiZWwi...",
            "type": "label_service",
            "shipping_price": "20.00",
            "currency": "CAD",
            "carrier_name": "UPS",
            "service_name": "Ground",
            "transit_days": 2
        }
    ],
    "errors": []
}

Answer 400 — Invalid shipment: result false with message

Answer 403 — SERVICE_NOT_AVAILABLE: the attribution customer may not use these services

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer guest shipping in this app

Answer 429 — TOO_MANY_REQUESTS: carrier quote limit

GET /api/v1/apps/swidget/clients/{clientCode}/guest/shipments/{guestOrderId}

Get a guest shipment

Public; with X-Guest-Order-Token or an X-Guest-Contact-Token of the same contact (90 days). status is pending_payment, paid, placing, placed, expired, cancelled, refunded or refund_required. While unpaid, pay_url and pay_before; once placed, order carries the order's reference, status_group and status_label, the two addresses, packages with tracking numbers and, for a label, label.carrier_name and carrier_tracking_numbers.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
guestOrderIdpath✓42
X-Guest-Order-Tokenheader
X-Guest-Contact-Tokenheader
localequeryen

Answer 200 — The guest shipment

{
    "result": true,
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "placed",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": null,
        "pay_before": null,
        "can_cancel": false,
        "order": {
            "type": "self_delivery",
            "ref": "SR41000123",
            "status_group": "active",
            "status_label": "Out for delivery",
            "packages": [
                {
                    "tracking_number": "SR41000123-1"
                }
            ]
        }
    }
}

Answer 404 — GUEST_ORDER_NOT_FOUND, or APP_CONTEXT_NOT_FOUND

POST /api/v1/apps/swidget/clients/{clientCode}/guest/shipments/{guestOrderId}/cancel

Cancel a guest shipment

Public; with X-Guest-Order-Token or an X-Guest-Contact-Token of the same contact. An unpaid shipment is withdrawn and its payment page closes. A created one is cancelled while its order still can be (no pickup yet, label not used; can_cancel tells): the order is cancelled, its charge returned to the Client's attribution customer, the guest refunded in full and told by email or SMS; when the gateway cannot refund on its own the status is refund_required.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
guestOrderIdpath✓42
X-Guest-Order-Tokenheader
X-Guest-Contact-Tokenheader

Answer 200 — Withdrawn

{
    "result": true,
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "cancelled",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": null,
        "pay_before": null,
        "can_cancel": false,
        "order": null
    }
}

Answer 404 — GUEST_ORDER_NOT_FOUND, or APP_CONTEXT_NOT_FOUND

Answer 409 — GUEST_ORDER_NOT_CANCELLABLE: being paid, being created, or the order can no longer be cancelled

GET /api/v1/apps/swidget/clients/{clientCode}/guest/shipments/{guestOrderId}/label

Get the label of a guest shipment

Public; with X-Guest-Order-Token or an X-Guest-Contact-Token of the same contact. The label PDF of a placed label shipment, base64 in pdf_data.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
guestOrderIdpath✓42
X-Guest-Order-Tokenheader
X-Guest-Contact-Tokenheader

Answer 200 — The label

{
    "result": true,
    "format": "pdf",
    "tracking_numbers": [
        "1Z999AA10123456784"
    ],
    "pdf_data": "JVBERi0xLjQKJ..."
}

Answer 404 — GUEST_ORDER_NOT_FOUND for any other shipment, or APP_CONTEXT_NOT_FOUND

Answer 409 — LABEL_NOT_AVAILABLE: the carrier has not produced it yet

POST /api/v1/apps/swidget/clients/{clientCode}/guest/track

Track any code (guest)

Universal tracking (docs/APP-UNIVERSAL-TRACKING-DESIGN.md): one code of this Client in, whatever it is — `kind` parcel (tracking number), moving (moving code), storage (storage code), return (return code), shipping (shipping order code) or carrier_package (carrier tracking number). `tracking` is the OTEP public view (as /oteptrack); its `details` (addresses, dates, proof of delivery) and `summary.code` (pickup code) are present once `verified`. Send `answer` (last 4 digits of a phone on the shipment, or its postcode) to verify; `can_verify` says whether that is possible. A storage, return or shipping code is the drop-off code the sender holds, so `summary.code` is shown for those. `wallet` holds absolute Apple (.pkpass) and Google (save link) card links valid 2 h, or null when the Client has wallet cards off or the shipment offers no card. Errors: 404 TRACKING_NOT_FOUND (unknown code), 422 TRACKING_VERIFY_FAILED (wrong answer), 422 TRACKING_INVALID, 429 TRACKING_TOO_MANY. Public, no sign-in. Pickup codes are never answered here.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Answer 200 — What the code is and where it stands

{
    "result": true,
    "data": {
        "kind": "return",
        "identifier": "R8K2Q7",
        "owned": false,
        "verified": false,
        "can_verify": true,
        "summary": {
            "stage": "send",
            "status": "awaiting_dropoff",
            "reference": "RMA-1042",
            "place": null,
            "deadline": null,
            "code": "R8K2Q7"
        },
        "tracking": {
            "otep_version": "0.1",
            "profile": "parcel",
            "subject": {
                "identifier": "R8K2Q7",
                "profile": "parcel"
            },
            "current_status": "booking_confirmed",
            "current_phase": "pre_shipment",
            "delivered": false,
            "events": [
                {
                    "occurred_at": "2026-10-18T09:12:00-04:00",
                    "status_code": "information_submitted",
                    "phase": "pre_shipment",
                    "location": null
                },
                {
                    "occurred_at": "2026-10-18T09:40:00-04:00",
                    "status_code": "booking_confirmed",
                    "phase": "pre_shipment",
                    "location": null
                }
            ]
        },
        "wallet": {
            "apple": "https://track.example.com/wallet-card/en/return_request/1042/apple?code=1&expires=1760800000&signature=...",
            "google": "https://track.example.com/wallet-card/en/return_request/1042/google?code=1&expires=1760800000&signature=..."
        },
        "pickup_id": null
    }
}

Answer 404 — TRACKING_NOT_FOUND, or the App / Client is not available

Answer 422 — TRACKING_INVALID or TRACKING_VERIFY_FAILED

Answer 429 — TRACKING_TOO_MANY

POST /api/v1/apps/swidget/clients/{clientCode}/guest/verify/complete

Check a guest verification code

Public. Checks the code of a challenge and returns a contact token: proof, for 20 minutes and at this Client only, that the guest holds the contact. It is not a sign-in, and a sign-in token is never accepted in its place. Five wrong codes void the challenge.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "challenge_id": "Qm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2ll",
    "code": "482913"
}

Answer 200 — Contact verified

{
    "result": true,
    "contact_token": "eyJpdiI6IkNvbnRhY3RUb2tlbiIs...",
    "channel": "email",
    "contact": "g****@example.com",
    "expires_in": 1200
}

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer guest shipping in this app

Answer 422 — INVALID_VERIFICATION_CODE, VERIFICATION_ATTEMPTS_EXCEEDED or INVALID_VERIFICATION_CHALLENGE

POST /api/v1/apps/swidget/clients/{clientCode}/guest/verify/request

Send a guest a verification code

Public. Sends a six-digit code to an email address or a phone number, on the channel the Client chose for guest shipping (settings guest.verification_channel; shown in GET /v1/apps/swidget/guest/clients). The answer is the same for any contact. The code expires in 10 minutes; one code per contact and Client per minute and 20 per IP address per hour.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "contact": "guest@example.com",
    "locale": "en"
}

Answer 200 — Code sent

{
    "result": true,
    "message": "A verification code was sent.",
    "challenge_id": "Qm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2ll",
    "channel": "email",
    "contact": "g****@example.com",
    "expires_in": 600,
    "resend_after": 60
}

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer guest shipping in this app

Answer 422 — INVALID_EMAIL or INVALID_PHONE_NUMBER

Answer 429 — GUEST_VERIFICATION_RATE_LIMITED

Answer 503 — SMS_SERVICE_NOT_CONFIGURED, EMAIL_DELIVERY_FAILED or SMS_DELIVERY_FAILED

GET /api/v1/apps/swidget/clients/{clientCode}/quotes/schema

The quote form a client offers without signing in

Public. The lines and their fields, the same schema the /quotes page renders from. A request with a bearer token of a customer of this Client sees only what that account may use.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
localequeryen

Answer 200 — The quote form

{
    "result": true,
    "schema": {
        "business_name": "Northstar Courier",
        "currency": "CAD",
        "valid_days": 7,
        "lines": []
    }
}

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer quotes in this app

POST /api/v1/apps/swidget/clients/{clientCode}/quotes/{line}

Quote a line without signing in

Public. The same service and per-Client settings as the /quotes page, so the app and the page quote alike; every quote is recorded. The body carries the line's fields as listed in the schema. A request with a bearer token of a customer of this Client is quoted on that account, anyone else as an anonymous visitor. Carrier lines (label, express) are limited to six per minute per caller and Client.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab
linepath✓

Request body

{
    "to_postcode": "M5V 2T6",
    "weight": 2.4,
    "locale": "en"
}

Answer 200 — result true with quote, or result false with not_covered_reason

{
    "result": true,
    "quote": {
        "line": "delivery",
        "currency": "CAD",
        "total": 12.4,
        "token": "abcdefghijabcdefghijabcdefghijabcdefghij",
        "valid_until": "2026-10-15T12:00:00-04:00",
        "drive_minutes": 18,
        "drive_from": "Toronto Depot"
    }
}

Answer 404 — APP_CONTEXT_NOT_FOUND: the Client does not offer quotes in this app

Answer 422 — Invalid input, or a line the Client does not offer: result false with errors

Answer 429 — Carrier-line limit: result false, not_covered_reason too_many

GET /api/v1/apps/swidget/clients/{clientCode}/registration-details

Registration details still to fill in — needs the visitor's token

Unified identity: a customer a client's staff created without the registration details (the client has identity sign-in on). Until they are filled in, every other API call of the account (REST, GraphQL, MCP) answers 403 {"error": "registration_incomplete", "code": "REGISTRATION_INCOMPLETE"}; generic API logins refuse with the same body, and an app login still issues the session with registration_incomplete: true so the app can show this form.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Answer 200 — Required fields, missing ones and values on file

{
    "result": true,
    "data": {
        "complete": false,
        "fields": [
            "name",
            "telephone",
            "address",
            "city",
            "province",
            "postcode",
            "country"
        ],
        "missing": [
            "telephone",
            "address"
        ],
        "values": {
            "name": "Ann Lee",
            "telephone": "",
            "address": "",
            "address_2": "",
            "city": "Montreal",
            "province": "QC",
            "postcode": "H2X 1Y4",
            "country": "Canada"
        }
    }
}

POST /api/v1/apps/swidget/clients/{clientCode}/registration-details

Fill in the registration details — needs the visitor's token

Required: the fields listed by GET (province only where the client's area has provinces). Once complete, the account's API works again.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "telephone": "+15145550123",
    "address": "100 Rue Main"
}

Answer 200 — Saved; same body as GET

{
    "result": true,
    "data": {
        "complete": true,
        "fields": [
            "name",
            "telephone",
            "address",
            "city",
            "province",
            "postcode",
            "country"
        ],
        "missing": [],
        "values": {
            "name": "Ann Lee",
            "telephone": "",
            "address": "",
            "address_2": "",
            "city": "Montreal",
            "province": "QC",
            "postcode": "H2X 1Y4",
            "country": "Canada"
        }
    }
}

Answer 422 — VALIDATION_FAILED with errors per field

POST /api/v1/apps/swidget/clients/{clientCode}/registration/complete

Register a Client customer

Creates the normal Superroute User and Customer records and either issues an app session or returns a pending-review result. Existing accounts are never overwritten or duplicated. Two shapes, chosen by the Client's `registration.require_phone_verification` setting (published as `authentication.registration.phone_verification_required` on bootstrap). When it is on, send `challenge_id` and `code` from /registration/phone/request and the phone is recorded as verified. When it is off, send `phone` instead and the number is stored unverified. A challenge is always honoured if supplied. For an app whose Clients choose the verification channel (the SuperRoute customer app), a challenge is always required and the Client's channel (`authentication.registration.verification_channel` on bootstrap) decides: phone as above, or email — send `channel: email` with `challenge_id` and `code` from /registration/email/request, the same `email`, and `phone` (a contact number, stored unverified and never compared with other accounts); the answer then also carries `email_verified: true`. `channel` defaults to phone. Optionally send `google_id_token` and/or `apple_id_token` (with `apple_authorization_code` and `apple_name`) to link the Google / Apple account the person signed in with; the answer lists what was linked in `linked_providers`. The address is mandatory in both shapes. The new account starts from the customer template the Client selected for this app, or from the business default template when it selected none.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "challenge_id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "code": "123456",
    "name": "MiBox Customer",
    "email": "customer@example.com",
    "password": "account-password",
    "password_confirmation": "account-password",
    "address": "123 King Street West",
    "address_2": "Unit 5",
    "city": "Toronto",
    "province": "ON",
    "postcode": "M5H 1A1",
    "country": "Canada",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "app_version": "1.0.0"
}

Answer 201 — Customer registered. Returns either a Passport app session or REGISTRATION_PENDING_REVIEW.

{
    "result": true,
    "message": "Registration and phone verification completed.",
    "phone_verified": true,
    "requires_approval": false,
    "access_token": "passport-access-token",
    "token_type": "Bearer",
    "linked_providers": []
}

Answer 403 — Registration is disabled for this App and Client

Answer 409 — Existing account, linked phone, concurrent registration, or GOOGLE_ACCOUNT_LINK_CONFLICT / APPLE_ACCOUNT_LINK_CONFLICT for a Google / Apple account already linked to another account

Answer 410 — Verification challenge expired

Answer 422 — Invalid request, missing address, invalid verification code, or PHONE_VERIFICATION_REQUIRED when the Client requires SMS verification and no challenge was sent

Answer 429 — Verification attempt limit exceeded

POST /api/v1/apps/swidget/clients/{clientCode}/registration/email/request

Request a registration email verification code

For an app whose Clients choose the sign-up verification channel (the SuperRoute customer app, appCode customer) and a Client that chose email (bootstrap authentication.registration.verification_channel). Creates a ten-minute, App- and Client-scoped challenge and emails a six-digit code. The answer is the same whether or not the address already has an account: an address that has one is sent no code, only a note that it is registered, so the challenge cannot be completed. Codes are never returned by the API.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓n4x7q2g8w9r3k6m1p5t0z8ab

Request body

{
    "email": "jane.doe@example.com",
    "locale": "en"
}

Answer 200 — Verification challenge created (the same answer for a registered address)

{
    "result": true,
    "message": "A verification email was sent.",
    "challenge_id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "email": "j*******@example.com",
    "expires_in": 600,
    "resend_after": 60
}

Answer 403 — REGISTRATION_DISABLED (sign-up closed for this App and Client) or EMAIL_VERIFICATION_DISABLED (the Client chose phone, or the app has no channels, as MiBox)

Answer 422 — Invalid email address

Answer 429 — EMAIL_VERIFICATION_RATE_LIMITED: another code for this address within 60 seconds, or the request rate limit

Answer 503 — EMAIL_DELIVERY_FAILED: the email could not be sent

GET /api/v1/apps/swidget/clients/{clientCode}/swidget

A website widget's configuration (sWidget)

The configuration of the website widget its key names (docs/SWIDGET-DESIGN.md W28, W32): kind, options, branding, language, the Client's country, and where every later call goes (`api`: base URL, app code and client code). A signed-in order widget's `options.sign_in` says how visitors sign in (`method` `password`), whether sign-up is offered (`registration`: the Client's self-registration switch and the app's sign-up both on) and the Client's password reset page. Every call naming the app `swidget` needs the widget key in `X-App-Key` and a browser `Origin` that is one of the widget's websites. Errors: 401 `invalid_app_key`, 403 `origin_not_allowed`, 403 `widget_call_not_allowed`, 403 `registration_disabled` (sign-up calls), 404 `invalid_client`, 429 `too_many_requests` (with `Retry-After`). GraphQL twin: `platformAppSwidgetConfig`.

ParameterInRequiredExample
appCodepath✓swidget
clientCodepath✓
X-App-Keyheader
Originheader

Answer 200 — The widget's configuration

{
    "result": true,
    "data": {
        "widget": {
            "id": "k2m9q7x4b1c8",
            "type": "order",
            "name": "Ship a parcel",
            "locale": null,
            "options": {
                "order_mode": "sign_in",
                "sign_in": {
                    "method": "password",
                    "registration": true,
                    "password_reset_url": "https://ship.acme.example/portal/password/forgot"
                }
            }
        },
        "branding": {
            "display_name": "Acme Delivery",
            "primary_color": "#E02020",
            "logo_url": null
        },
        "country": "CA",
        "app_code": "swidget",
        "client_code": "k3m9q2",
        "api": {
            "base": "https://api.superroute.ca",
            "app_code": "swidget",
            "client_code": "k3m9q2"
        }
    }
}

Answer 401 — invalid_app_key

{
    "error": "invalid_app_key",
    "error_description": "A valid widget key is required."
}

Answer 403 — origin_not_allowed

{
    "error": "origin_not_allowed",
    "error_description": "This origin is not registered for the widget."
}

Answer 404 — invalid_client

{
    "error": "invalid_client",
    "error_description": "This app is not available for this service provider."
}

Answer 429 — too_many_requests

{
    "error": "too_many_requests",
    "error_description": "Too many requests from this widget."
}

GraphQL

The same calls as GraphQL operations, sent to POST /api/graphql

platformAppCompleteRegistration mutation

Fill in the registration details

Required: the fields listed by platformAppRegistrationDetails. Once complete, the account's API works again.

Query

mutation PlatformAppCompleteRegistration($app_code: String!, $client_code: String!, $name: String, $telephone: String, $address: String, $address_2: String, $city: String, $province: String, $postcode: String, $country: String) {
  platformAppCompleteRegistration(app_code: $app_code, client_code: $client_code, name: $name, telephone: $telephone, address: $address, address_2: $address_2, city: $city, province: $province, postcode: $postcode, country: $country)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "name": "",
    "telephone": "",
    "address": "",
    "address_2": "",
    "city": "",
    "province": "",
    "postcode": "",
    "country": ""
}

Answer

{
    "result": true,
    "data": {
        "complete": true,
        "fields": [
            "name",
            "telephone",
            "address",
            "city",
            "province",
            "postcode",
            "country"
        ],
        "missing": [],
        "values": {
            "name": "Ann Lee",
            "telephone": "",
            "address": "",
            "address_2": "",
            "city": "Montreal",
            "province": "QC",
            "postcode": "H2X 1Y4",
            "country": "Canada"
        }
    }
}

platformAppEmailLogin mutation

Sign in to an app with email

Omit client_code for the normal mobile flow. Superroute resolves the Client from the authenticated Customer membership and returns APP_NOT_ENABLED_FOR_PROVIDER when that provider has not enabled the app. Supplying client_code preserves the existing scoped contract. When the person signs in with their identity (unified identity), the response also carries identity: { choice_required, accounts[] } (additive); platformAppSwitchAccount changes account. A person with two-step verification (or whose client requires it) gets TWO_FACTOR_REQUIRED with data.challenge_id: finish with platformAppTwoFactorVerify. Optional two_factor_device_token: a trusted device skips the code.

Query

mutation PlatformAppEmailLogin($app_code: String!, $client_code: String, $email: String!, $password: String!, $device_id: String!, $device_platform: String!, $app_version: String) {
  platformAppEmailLogin(app_code: $app_code, client_code: $client_code, email: $email, password: $password, device_id: $device_id, device_platform: $device_platform, app_version: $app_version)
}

Variables

{
    "app_code": "swidget",
    "email": "customer@example.com",
    "password": "account-password",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "android",
    "app_version": "1.0.0"
}

Answer

{
    "result": true,
    "access_token": "passport-access-token",
    "token_type": "Bearer"
}

platformAppGuestPickupPoints query

The Client's pickup points without signing in

The Client's smart locker locations (ids `L…`) and pickup point counters (ids `C…`) a visitor may choose, nearest first when latitude and longitude are given, with nothing personal. A pickup point widget's key narrows the list to the kinds the widget keeps. Public.

Query

query ($app_code: String!, $client_code: String!, $latitude: Float, $longitude: Float) {
  platformAppGuestPickupPoints(app_code: $app_code, client_code: $client_code, latitude: $latitude, longitude: $longitude)
}

Variables

{
    "app_code": "swidget",
    "client_code": "k3m9q2",
    "latitude": 43.6,
    "longitude": -79.4
}

Answer

{
    "result": true,
    "data": {
        "items": [
            {
                "id": "L12",
                "kind": "locker",
                "location_id": 12,
                "name": "Station locker",
                "address": "1 Front St",
                "city": "Toronto",
                "latitude": 43.6,
                "longitude": -79.4,
                "distance_km": 1.2
            }
        ],
        "pagination": {
            "page": 1,
            "per_page": 20,
            "total": 1,
            "has_more": false
        }
    }
}

platformAppGuestShipment query

Get a guest shipment

Public; with the order token (REST: header X-Guest-Order-Token) or a contact token of the same contact (90 days). status is pending_payment, paid, placing, placed, expired, cancelled, refunded or refund_required. While unpaid, pay_url and pay_before; once placed, order carries the order's reference, status_group and status_label, the two addresses, packages with tracking numbers and, for a label, label.carrier_name and carrier_tracking_numbers. 404 GUEST_ORDER_NOT_FOUND.

Query

query PlatformAppGuestShipment($app_code: String!, $client_code: String!, $id: Int!, $order_token: String, $contact_token: String, $locale: String) {
  platformAppGuestShipment(app_code: $app_code, client_code: $client_code, id: $id, order_token: $order_token, contact_token: $contact_token, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "id": 42,
    "order_token": "k3J9xQ2mV8pL5tR1wZ7cN4bH6dF0gS2aY9eU3iO8"
}

Answer

{
    "result": true,
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "placed",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": null,
        "pay_before": null,
        "can_cancel": false,
        "order": {
            "type": "self_delivery",
            "ref": "SR41000123",
            "status_group": "active",
            "status_label": "Out for delivery",
            "packages": [
                {
                    "tracking_number": "SR41000123-1"
                }
            ]
        }
    }
}

platformAppGuestShipmentCancel mutation

Cancel a guest shipment

Public; with the order token (REST: header X-Guest-Order-Token) or a contact token of the same contact. An unpaid shipment is withdrawn and its payment page closes. A created one is cancelled while its order still can be (no pickup yet, label not used; can_cancel tells): the order is cancelled, its charge returned to the Client's attribution customer, the guest refunded in full and told by email or SMS; when the gateway cannot refund on its own the status is refund_required. 409 GUEST_ORDER_NOT_CANCELLABLE when being paid, being created, or no longer cancellable; 404 GUEST_ORDER_NOT_FOUND.

Query

mutation PlatformAppGuestShipmentCancel($app_code: String!, $client_code: String!, $id: Int!, $order_token: String, $contact_token: String, $locale: String) {
  platformAppGuestShipmentCancel(app_code: $app_code, client_code: $client_code, id: $id, order_token: $order_token, contact_token: $contact_token, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "id": 42,
    "order_token": "k3J9xQ2mV8pL5tR1wZ7cN4bH6dF0gS2aY9eU3iO8"
}

Answer

{
    "result": true,
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "cancelled",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": null,
        "pay_before": null,
        "can_cancel": false,
        "order": null
    }
}

platformAppGuestShipmentLabel query

Get the label of a guest shipment

Public; with the order token (REST: header X-Guest-Order-Token) or a contact token of the same contact. The label PDF of a placed label shipment, base64 in pdf_data. 404 GUEST_ORDER_NOT_FOUND for any other shipment, LABEL_NOT_AVAILABLE while the carrier has not produced it.

Query

query PlatformAppGuestShipmentLabel($app_code: String!, $client_code: String!, $id: Int!, $order_token: String, $contact_token: String, $locale: String) {
  platformAppGuestShipmentLabel(app_code: $app_code, client_code: $client_code, id: $id, order_token: $order_token, contact_token: $contact_token, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "id": 42,
    "order_token": "k3J9xQ2mV8pL5tR1wZ7cN4bH6dF0gS2aY9eU3iO8"
}

Answer

{
    "result": true,
    "format": "pdf",
    "tracking_numbers": [
        "1Z999AA10123456784"
    ],
    "pdf_data": "JVBERi0xLjQKJ..."
}

platformAppGuestShipmentRate mutation

Price a guest shipment

Public. Prices a local delivery and carrier labels for a guest of a Client that offers guest shipping (capability guest.shipping), as the Client's attribution customer: its price plan, label accounts and service switches. input is the shipment (from_* and to_* name, telephone, address, city, province, postcode, country; packages; signature_option); any other field, such as a customer or a channel, is ignored. Each rate_id is a guest quote: valid for this Client and this exact shipment only, for 30 minutes. A self delivery rate without a price is reported in errors with code NOT_PRICED: a guest pays before the order exists. Carrier quotes are limited to six per minute per caller and Client (429 TOO_MANY_REQUESTS). 404 APP_CONTEXT_NOT_FOUND when the Client does not offer guest shipping in this app.

Query

mutation PlatformAppGuestShipmentRate($app_code: String!, $client_code: String!, $input: Json, $locale: String) {
  platformAppGuestShipmentRate(app_code: $app_code, client_code: $client_code, input: $input, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "input": {
        "from_name": "Ann Lee",
        "from_telephone": "+14165550100",
        "from_address": "1 King St W",
        "from_city": "Toronto",
        "from_province": "ON",
        "from_postcode": "M5H 1A1",
        "from_country": "CA",
        "to_name": "Bo Chen",
        "to_telephone": "+14165550101",
        "to_address": "300 Front St W",
        "to_city": "Toronto",
        "to_province": "ON",
        "to_postcode": "M5V 2T6",
        "to_country": "CA",
        "packages": [
            {
                "weight": 2,
                "length": 30,
                "width": 20,
                "height": 10,
                "weight_unit": 1,
                "dimension_unit": 1
            }
        ]
    }
}

Answer

{
    "result": true,
    "rates": [
        {
            "rate_id": "eyJpdiI6Ikd1ZXN0UXVvdGUi...",
            "type": "self_delivery",
            "shipping_price": "12.40",
            "currency": "CAD"
        },
        {
            "rate_id": "eyJpdiI6Ikd1ZXN0TGFiZWwi...",
            "type": "label_service",
            "shipping_price": "20.00",
            "currency": "CAD",
            "carrier_name": "UPS",
            "service_name": "Ground",
            "transit_days": 2
        }
    ],
    "errors": []
}

platformAppGuestShipmentSubmit mutation

Submit a guest shipment for payment

Public; needs a contact token (REST: header X-Guest-Contact-Token). input is the rated shipment, exactly as rated, with rate_id from platformAppGuestShipmentRate, and optionally note and need_pick_up (self delivery). Nothing is created yet: the shipment waits for its payment on the shared payment page (pay_url, gateways only, no wallet) until pay_before. Once paid, the payment is credited to the Client's attribution customer and the order is created through uniorder (local delivery dispatched, label bought); if that fails the payment is refunded. The guest receives the outcome by email or SMS on the verified contact; the link in it carries no token. order_token is shown once and opens this shipment later. 401 GUEST_CONTACT_REQUIRED, 422 RATE_ID_INVALID (expired, another Client, or a different shipment), 429 GUEST_ORDER_LIMIT (per contact per day, per IP address per hour).

Query

mutation PlatformAppGuestShipmentSubmit($app_code: String!, $client_code: String!, $contact_token: String!, $input: Json, $locale: String) {
  platformAppGuestShipmentSubmit(app_code: $app_code, client_code: $client_code, contact_token: $contact_token, input: $input, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "contact_token": "eyJpdiI6IkNvbnRhY3RUb2tlbiIs...",
    "input": {
        "from_name": "Ann Lee",
        "from_telephone": "+14165550100",
        "from_address": "1 King St W",
        "from_city": "Toronto",
        "from_province": "ON",
        "from_postcode": "M5H 1A1",
        "from_country": "CA",
        "to_name": "Bo Chen",
        "to_telephone": "+14165550101",
        "to_address": "300 Front St W",
        "to_city": "Toronto",
        "to_province": "ON",
        "to_postcode": "M5V 2T6",
        "to_country": "CA",
        "packages": [
            {
                "weight": 2,
                "length": 30,
                "width": 20,
                "height": 10,
                "weight_unit": 1,
                "dimension_unit": 1
            }
        ],
        "rate_id": "eyJpdiI6Ikd1ZXN0UXVvdGUi..."
    }
}

Answer

{
    "result": true,
    "order_token": "k3J9xQ2mV8pL5tR1wZ7cN4bH6dF0gS2aY9eU3iO8",
    "guest_order": {
        "id": 42,
        "kind": "self_delivery",
        "status": "pending_payment",
        "amount": "12.40",
        "currency": "CAD",
        "contact": "g****@example.com",
        "created_at": "2026-10-09T10:00:00-04:00",
        "pay_url": "https://ship.example.com/pay/r/3f9c0d6a1b2e4f5a6b7c8d9e0f1a2b3c",
        "pay_before": "2026-10-09T10:30:00-04:00",
        "can_cancel": true,
        "order": null
    }
}

platformAppGuestShipments query

List a verified contact's guest shipments

Public; needs a contact token (REST: header X-Guest-Contact-Token). The guest shipments of that contact at this Client from the last 90 days, newest first, at most 50: for a guest on a new device. 401 GUEST_CONTACT_REQUIRED.

Query

query PlatformAppGuestShipments($app_code: String!, $client_code: String!, $contact_token: String!, $locale: String) {
  platformAppGuestShipments(app_code: $app_code, client_code: $client_code, contact_token: $contact_token, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "contact_token": "eyJpdiI6IkNvbnRhY3RUb2tlbiIs..."
}

Answer

{
    "result": true,
    "items": [
        {
            "id": 42,
            "kind": "self_delivery",
            "status": "pending_payment",
            "amount": "12.40",
            "currency": "CAD",
            "contact": "g****@example.com",
            "created_at": "2026-10-09T10:00:00-04:00",
            "pay_url": "https://ship.example.com/pay/r/3f9c0d6a1b2e4f5a6b7c8d9e0f1a2b3c",
            "pay_before": "2026-10-09T10:30:00-04:00",
            "can_cancel": true,
            "order": null
        }
    ]
}

platformAppGuestTrack query

Track any code (guest)

Universal tracking (docs/APP-UNIVERSAL-TRACKING-DESIGN.md): one code of this Client in, whatever it is — `kind` parcel (tracking number), moving (moving code), storage (storage code), return (return code), shipping (shipping order code) or carrier_package (carrier tracking number). `tracking` is the OTEP public view (as /oteptrack); its `details` (addresses, dates, proof of delivery) and `summary.code` (pickup code) are present once `verified`. Send `answer` (last 4 digits of a phone on the shipment, or its postcode) to verify; `can_verify` says whether that is possible. A storage, return or shipping code is the drop-off code the sender holds, so `summary.code` is shown for those. `wallet` holds absolute Apple (.pkpass) and Google (save link) card links valid 2 h, or null when the Client has wallet cards off or the shipment offers no card. Errors: 404 TRACKING_NOT_FOUND (unknown code), 422 TRACKING_VERIFY_FAILED (wrong answer), 422 TRACKING_INVALID, 429 TRACKING_TOO_MANY. Public, no sign-in. Pickup codes are never answered here.

Query

query PlatformAppGuestTrack($app_code: String!, $client_code: String!, $identifier: String!, $answer: String, $locale: String) {
  platformAppGuestTrack(app_code: $app_code, client_code: $client_code, identifier: $identifier, answer: $answer, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "identifier": "R8K2Q7"
}

Answer

{
    "result": true,
    "data": {
        "kind": "return",
        "identifier": "R8K2Q7",
        "owned": false,
        "verified": false,
        "can_verify": true,
        "summary": {
            "stage": "send",
            "status": "awaiting_dropoff",
            "reference": "RMA-1042",
            "place": null,
            "deadline": null,
            "code": "R8K2Q7"
        },
        "tracking": {
            "otep_version": "0.1",
            "profile": "parcel",
            "subject": {
                "identifier": "R8K2Q7",
                "profile": "parcel"
            },
            "current_status": "booking_confirmed",
            "current_phase": "pre_shipment",
            "delivered": false,
            "events": [
                {
                    "occurred_at": "2026-10-18T09:12:00-04:00",
                    "status_code": "information_submitted",
                    "phase": "pre_shipment",
                    "location": null
                },
                {
                    "occurred_at": "2026-10-18T09:40:00-04:00",
                    "status_code": "booking_confirmed",
                    "phase": "pre_shipment",
                    "location": null
                }
            ]
        },
        "wallet": {
            "apple": "https://track.example.com/wallet-card/en/return_request/1042/apple?code=1&expires=1760800000&signature=...",
            "google": "https://track.example.com/wallet-card/en/return_request/1042/google?code=1&expires=1760800000&signature=..."
        },
        "pickup_id": null
    }
}

platformAppGuestVerifyComplete mutation

Check a guest verification code

Public. Checks the code of a challenge and returns a contact token: proof, for 20 minutes and at this Client only, that the guest holds the contact. It is not a sign-in, and a sign-in token is never accepted in its place. Five wrong codes void the challenge. 422 INVALID_VERIFICATION_CODE, VERIFICATION_ATTEMPTS_EXCEEDED or INVALID_VERIFICATION_CHALLENGE.

Query

mutation PlatformAppGuestVerifyComplete($app_code: String!, $client_code: String!, $challenge_id: String!, $code: String!, $locale: String) {
  platformAppGuestVerifyComplete(app_code: $app_code, client_code: $client_code, challenge_id: $challenge_id, code: $code, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "challenge_id": "Qm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2ll",
    "code": "482913"
}

Answer

{
    "result": true,
    "contact_token": "eyJpdiI6IkNvbnRhY3RUb2tlbiIs...",
    "channel": "email",
    "contact": "g****@example.com",
    "expires_in": 1200
}

platformAppGuestVerifyRequest mutation

Send a guest a verification code

Public. Sends a six-digit code to an email address or a phone number, on the channel the Client chose for guest shipping (settings guest.verification_channel; shown in platformAppGuestClients). The answer is the same for any contact. The code expires in 10 minutes; one code per contact and Client per minute (429 GUEST_VERIFICATION_RATE_LIMITED) and 20 per IP address per hour. 422 INVALID_EMAIL or INVALID_PHONE_NUMBER, 503 SMS_SERVICE_NOT_CONFIGURED, EMAIL_DELIVERY_FAILED or SMS_DELIVERY_FAILED.

Query

mutation PlatformAppGuestVerifyRequest($app_code: String!, $client_code: String!, $contact: String!, $locale: String) {
  platformAppGuestVerifyRequest(app_code: $app_code, client_code: $client_code, contact: $contact, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "contact": "guest@example.com"
}

Answer

{
    "result": true,
    "message": "A verification code was sent.",
    "challenge_id": "Qm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2llQm9vZ2llV29vZ2ll",
    "channel": "email",
    "contact": "g****@example.com",
    "expires_in": 600,
    "resend_after": 60
}

platformAppIdentityJoin mutation

Join this client with the identity

Unified identity: an email, Google or Apple sign-in in a client's own app by a person whose identity holds no account of this client yet answers 409 IDENTITY_JOIN_REQUIRED with data.join_token (10 minutes, same device), data.fields (required) and data.values (only the name prefilled). Send the client's registration details here. The client's app registration decides: 403 IDENTITY_JOIN_CLOSED when it takes no registrations, 202 pending_review when it reviews new accounts, otherwise the session (or TWO_FACTOR_REQUIRED). Nothing is copied from the person's accounts at other clients.

Query

mutation PlatformAppIdentityJoin($app_code: String!, $client_code: String!, $join_token: String!, $name: String, $telephone: String, $address: String, $address_2: String, $city: String, $province: String, $postcode: String, $country: String, $device_id: String!, $device_platform: String!, $app_version: String) {
  platformAppIdentityJoin(app_code: $app_code, client_code: $client_code, join_token: $join_token, name: $name, telephone: $telephone, address: $address, address_2: $address_2, city: $city, province: $province, postcode: $postcode, country: $country, device_id: $device_id, device_platform: $device_platform, app_version: $app_version)
}

Variables

{
    "app_code": "swidget",
    "client_code": "q8w2e6r1t5y9u3i7o0p4a2sd",
    "join_token": "t4Kp...",
    "name": "",
    "telephone": "",
    "address": "",
    "address_2": "",
    "city": "",
    "province": "",
    "postcode": "",
    "country": "",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "app_version": "1.0.0"
}

Answer

{
    "result": true,
    "pending_review": true
}

platformAppPublicQuote mutation

Quote a line without signing in

Public. The same service and per-Client settings as the /quotes page, so the app and the page quote alike; every quote is recorded. line is one of the schema's lines (delivery, pickup, p2p, express, shipping, label, moving, storage); input carries that line's fields (the REST body). A signed-in customer of the Client is quoted on their own account, anyone else as an anonymous visitor. Carrier lines (label, express) are limited to six per minute per caller and Client (HTTP 429, not_covered_reason too_many). Answers result true with quote, result false with not_covered_reason, 422 with errors, or 404 APP_CONTEXT_NOT_FOUND when the Client does not offer quotes in this app.

Query

mutation PlatformAppPublicQuote($app_code: String!, $client_code: String!, $line: String!, $input: Json) {
  platformAppPublicQuote(app_code: $app_code, client_code: $client_code, line: $line, input: $input)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "line": "delivery",
    "input": {
        "to_postcode": "M5V 2T6",
        "weight": 2.4
    }
}

Answer

{
    "result": true,
    "quote": {
        "line": "delivery",
        "currency": "CAD",
        "total": 12.4,
        "token": "abcdefghijabcdefghijabcdefghijabcdefghij",
        "valid_until": "2026-10-15T12:00:00-04:00",
        "drive_minutes": 18,
        "drive_from": "Toronto Depot"
    }
}

platformAppPublicQuoteSchema query

The quote form a client offers without signing in

Public. The lines and their fields, the same schema the /quotes page renders from (PublicQuoteService). A signed-in customer of the Client sees only what their own account may use. 404 APP_CONTEXT_NOT_FOUND when the Client does not offer quotes in this app.

Query

query PlatformAppPublicQuoteSchema($app_code: String!, $client_code: String!) {
  platformAppPublicQuoteSchema(app_code: $app_code, client_code: $client_code)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab"
}

Answer

{
    "result": true,
    "schema": {
        "business_name": "Northstar Courier",
        "currency": "CAD",
        "valid_days": 7,
        "lines": []
    }
}

platformAppRegisterCustomer mutation

Register a Client customer

Creates the normal Superroute User and Customer records and either issues an app session or returns a pending-review result. Two shapes, chosen by the Client's registration.require_phone_verification setting. When it is on, send challenge_id and code from platformAppRequestRegistrationCode and the phone is recorded as verified. When it is off, send phone instead and the number is stored unverified. A challenge is always honoured if supplied. For an app whose Clients choose the verification channel (the SuperRoute customer app), a challenge is always required and the Client's channel decides: phone as above, or email — send channel email with challenge_id and code from platformAppRequestRegistrationEmailCode, the same email, and phone (a contact number, stored unverified and never compared with other accounts); the answer then also carries email_verified true. channel defaults to phone. Optionally send google_id_token and/or apple_id_token (with apple_authorization_code and apple_name) to link the Google / Apple account the person signed in with; the answer lists what was linked in linked_providers. The address is mandatory in both shapes. The new account starts from the customer template the Client selected for this app, or from the business default template when it selected none. challenge_id, code and country stay nullable in the schema so an existing query document keeps parsing.

Query

mutation PlatformAppRegisterCustomer($app_code: String!, $client_code: String!, $challenge_id: String, $code: String, $phone: String, $name: String!, $email: String!, $password: String!, $password_confirmation: String!, $address: String, $address_2: String, $city: String, $province: String, $postcode: String, $country: String, $device_id: String!, $device_platform: String!) {
  platformAppRegisterCustomer(app_code: $app_code, client_code: $client_code, challenge_id: $challenge_id, code: $code, phone: $phone, name: $name, email: $email, password: $password, password_confirmation: $password_confirmation, address: $address, address_2: $address_2, city: $city, province: $province, postcode: $postcode, country: $country, device_id: $device_id, device_platform: $device_platform)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "challenge_id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "code": "123456",
    "name": "MiBox Customer",
    "email": "customer@example.com",
    "password": "account-password",
    "password_confirmation": "account-password",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "phone": "+14165550123",
    "address": "123 King Street West",
    "address_2": "Unit 5",
    "city": "Toronto",
    "province": "ON",
    "postcode": "M5H 1A1",
    "country": "Canada"
}

Answer

{
    "result": true,
    "phone_verified": true,
    "requires_approval": false,
    "access_token": "passport-access-token",
    "token_type": "Bearer",
    "linked_providers": []
}

platformAppRegistrationDetails query

Registration details still to fill in

Unified identity: a customer a client's staff created without the registration details (the client has identity sign-in on). Until they are filled in, every other API call of the account (REST, GraphQL, MCP) answers 403 {"error": "registration_incomplete", "code": "REGISTRATION_INCOMPLETE"}; generic API logins refuse with the same body, and an app login still issues the session with registration_incomplete: true so the app can show this form.

Query

query PlatformAppRegistrationDetails($app_code: String!, $client_code: String!) {
  platformAppRegistrationDetails(app_code: $app_code, client_code: $client_code)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab"
}

Answer

{
    "result": true,
    "data": {
        "complete": false,
        "fields": [
            "name",
            "telephone",
            "address",
            "city",
            "province",
            "postcode",
            "country"
        ],
        "missing": [
            "telephone",
            "address"
        ],
        "values": {
            "name": "Ann Lee",
            "telephone": "",
            "address": "",
            "address_2": "",
            "city": "Montreal",
            "province": "QC",
            "postcode": "H2X 1Y4",
            "country": "Canada"
        }
    }
}

platformAppRequestRegistrationEmailCode mutation

Request a registration email verification code

For an app whose Clients choose the sign-up verification channel (the SuperRoute customer app, app_code customer) and a Client that chose email. Creates a ten-minute App- and Client-scoped challenge and emails a six-digit code. The answer is the same whether or not the address already has an account: an address that has one is sent no code, only a note that it is registered, so the challenge cannot be completed. Refused with EMAIL_VERIFICATION_DISABLED for a Client on the phone channel and for apps without channels (MiBox), REGISTRATION_DISABLED when sign-up is closed, EMAIL_VERIFICATION_RATE_LIMITED within 60 seconds of the previous request for the same address.

Query

mutation PlatformAppRequestRegistrationEmailCode($app_code: String!, $client_code: String!, $email: String!, $locale: String) {
  platformAppRequestRegistrationEmailCode(app_code: $app_code, client_code: $client_code, email: $email, locale: $locale)
}

Variables

{
    "app_code": "swidget",
    "client_code": "n4x7q2g8w9r3k6m1p5t0z8ab",
    "email": "jane.doe@example.com",
    "locale": "en"
}

Answer

{
    "result": true,
    "message": "A verification email was sent.",
    "challenge_id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "email": "j*******@example.com",
    "expires_in": 600,
    "resend_after": 60
}

platformAppSwidgetConfig query

A website widget's configuration (sWidget)

The configuration of the website widget its key names (header `X-App-Key`, docs/SWIDGET-DESIGN.md W28, W32): kind, options, branding, language, the Client's country and where every later call goes (`api`). With `app_code` `swidget` every operation needs the widget key and a browser `Origin` that is one of the widget's websites, and runs only the operations of the widget's kind. Refused with `invalid_app_key`, `origin_not_allowed`, `invalid_client`, `widget_call_not_allowed`, `registration_disabled` (sign-up operations) or `too_many_requests`.

Query

query ($app_code: String!, $client_code: String!) {
  platformAppSwidgetConfig(app_code: $app_code, client_code: $client_code)
}

Variables

{
    "app_code": "swidget",
    "client_code": "k3m9q2"
}

Answer

{
    "result": true,
    "data": {
        "widget": {
            "id": "k2m9q7x4b1c8",
            "type": "tracking",
            "name": "Track a parcel",
            "locale": null,
            "options": []
        },
        "branding": {
            "display_name": "Acme Delivery",
            "primary_color": "#E02020",
            "logo_url": null
        },
        "country": "CA",
        "app_code": "swidget",
        "client_code": "k3m9q2",
        "api": {
            "base": "https://api.superroute.ca",
            "app_code": "swidget",
            "client_code": "k3m9q2"
        }
    }
}

platformAppTwoFactorResend mutation

Send the code of a held sign-in again

For method email or sms; returns a new challenge_id. Errors: TWO_FACTOR_CHALLENGE_INVALID, TWO_FACTOR_RESEND_TOO_SOON, TWO_FACTOR_DELIVERY_FAILED.

Query

mutation PlatformAppTwoFactorResend($app_code: String!, $challenge_id: String!, $device_id: String!, $device_platform: String!, $app_version: String) {
  platformAppTwoFactorResend(app_code: $app_code, challenge_id: $challenge_id, device_id: $device_id, device_platform: $device_platform, app_version: $app_version)
}

Variables

{
    "app_code": "swidget",
    "challenge_id": "r9Qm...",
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "app_version": "1.0.0"
}

Answer

{
    "result": true,
    "data": {
        "challenge_id": "x7Lp...",
        "method": "email",
        "target": "a***@example.com",
        "expires_in": 600
    }
}

platformAppTwoFactorVerify mutation

Finish a sign-in held at its second factor

Unified identity: a sign-in that answered TWO_FACTOR_REQUIRED (data.challenge_id) is finished with the code, from the same device. trust_device returns two_factor_device_token; pass it as two_factor_device_token to the login mutations to skip the code for 30 days. Errors: TWO_FACTOR_CODE_INVALID, TWO_FACTOR_CHALLENGE_INVALID, TWO_FACTOR_TOO_MANY_ATTEMPTS.

Query

mutation PlatformAppTwoFactorVerify($app_code: String!, $challenge_id: String!, $code: String!, $trust_device: Boolean, $device_id: String!, $device_platform: String!, $app_version: String) {
  platformAppTwoFactorVerify(app_code: $app_code, challenge_id: $challenge_id, code: $code, trust_device: $trust_device, device_id: $device_id, device_platform: $device_platform, app_version: $app_version)
}

Variables

{
    "app_code": "swidget",
    "challenge_id": "r9Qm...",
    "code": "123456",
    "trust_device": true,
    "device_id": "0f3d1c43-7f53-4c16-ae90-62a2bc921812",
    "device_platform": "ios",
    "app_version": "1.0.0"
}

Answer

{
    "result": true,
    "access_token": "passport-access-token",
    "token_type": "Bearer",
    "two_factor_device_token": "k2v..."
}