{
  "info": {
    "_postman_id": "b3f1c2a4-5d6e-4a7b-8c9d-0e1f2a3b4c5d",
    "name": "Starmile Partner API",
    "description": "Official Postman collection for the Starmile Partner API.\n\nHow to use:\n1. Import this collection AND one of the environment files (Production or Sandbox).\n2. In the environment, set `client_id` and `client_secret` (from your partner).\n3. Run **Authentication › Get access token** — it stores `access_token` in the active environment; every other request inherits it via collection Bearer auth.\n4. Endpoints, request shapes and status codes are identical across environments — only `base_url` and the credential differ.\n\nAll requests use `{{base_url}}` and the collection variables so you can switch environments without editing anything.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      { "key": "token", "value": "{{access_token}}", "type": "string" }
    ]
  },
  "variable": [
    { "key": "base_url", "value": "https://api.starmile.io", "type": "string" },
    { "key": "client_id", "value": "", "type": "string" },
    { "key": "client_secret", "value": "", "type": "string" },
    { "key": "access_token", "value": "", "type": "string" },
    { "key": "order_id", "value": "PO-10294", "type": "string" },
    { "key": "item_id", "value": "PKG-1", "type": "string" },
    { "key": "merchant_tracking", "value": "CN773300012345", "type": "string" },
    { "key": "parcel_id", "value": "STM0000000121", "type": "string" },
    { "key": "since", "value": "0", "type": "string" },
    { "key": "limit", "value": "100", "type": "string" }
  ],
  "item": [
    {
      "name": "Authentication",
      "item": [
        {
          "name": "Get access token",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "var json = {};",
                  "try { json = pm.response.json(); } catch (e) {}",
                  "if (json && json.access_token) {",
                  "  pm.environment.set('access_token', json.access_token);",
                  "  pm.collectionVariables.set('access_token', json.access_token);",
                  "}",
                  "pm.test('200 OK', function () { pm.response.to.have.status(200); });",
                  "pm.test('returns a bearer token', function () {",
                  "  pm.expect(json.access_token, 'access_token').to.be.a('string').and.not.empty;",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "auth": { "type": "noauth" },
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"grant_type\": \"client_credentials\",\n  \"client_id\": \"{{client_id}}\",\n  \"client_secret\": \"{{client_secret}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/oauth/token",
              "host": ["{{base_url}}"],
              "path": ["oauth", "token"]
            },
            "description": "Exchange your client_id + client_secret for a short-lived bearer token (OAuth2 client credentials). The test script stores `access_token` in the active environment. Tokens last ~1 hour; reuse until they expire."
          }
        }
      ]
    },
    {
      "name": "Catalogue",
      "item": [
        {
          "name": "List services",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/services",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "services"]
            },
            "description": "Your published Services. Use a Service's `id` as `service_id` on order creation. Scope: catalogue:read."
          }
        },
        {
          "name": "List rates",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/rates",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "rates"]
            },
            "description": "Your rates, for review. You do NOT send a rate on order creation — Starmile resolves it automatically. Scope: catalogue:read."
          }
        }
      ]
    },
    {
      "name": "Orders",
      "item": [
        {
          "name": "Create an order",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"service_id\": 4,\n  \"order_id\": \"{{order_id}}\",\n  \"gov_id\": \"AZE1234567\",\n  \"customer_name\": \"Aysel M.\",\n  \"customer_phone\": \"+994500000000\",\n  \"customer_email\": \"aysel@example.com\",\n  \"parent_region\": \"1\",\n  \"region\": \"2\",\n  \"pudo_id\": null,\n  \"locker_id\": null,\n  \"address_first\": \"12 Nizami St\",\n  \"address_second\": \"Apt 5\",\n  \"zip\": \"AZ1000\",\n  \"notes\": \"Leave with the concierge\",\n  \"delivery_address\": {\n    \"line1\": \"12 Nizami St\",\n    \"line2\": \"Apt 5\",\n    \"city\": \"Baku\",\n    \"zip\": \"AZ1000\"\n  },\n  \"shipping_cost\": 9.50,\n  \"consolidation_required\": false,\n  \"parcels\": [\n    {\n      \"item_id\": \"{{item_id}}\",\n      \"merchant_tracking\": \"{{merchant_tracking}}\",\n      \"package_type\": \"fragile\",\n      \"weight_grams\": 1500,\n      \"length_mm\": 300,\n      \"width_mm\": 200,\n      \"height_mm\": 150,\n      \"products\": [\n        {\n          \"name\": \"Sneakers\",\n          \"hs_code\": \"640411\",\n          \"declared_value\": 120.00,\n          \"currency\": \"USD\",\n          \"weight_grams\": 1500,\n          \"quantity\": 1,\n          \"description\": \"Running shoes, size 42\"\n        }\n      ]\n    }\n  ]\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "orders"]
            },
            "description": "Create an order. Only `service_id` (from List services), your own `order_id`, and `parcels[]` (each with at least one `products[]` entry whose `name` is required) are required — every other field shown here is OPTIONAL and may be omitted.\n\nDestination is set by the Service's delivery type: home delivery uses `parent_region` (your parent region id/code) + `region` (your own leaf region id/code, mapped per partner) + address lines; PUDO uses `pudo_id`; locker uses `locker_id` — send only the one that matches your Service (the others are shown as null). The flow, corridor and rate all come from the Service; no rate is sent.\n\nAn unmapped home-delivery region does NOT reject the order — the 201 response `data` includes `region_status`: `mapped` (resolved), `pending_mapping` (accepted, awaiting an operator mapping — resolves automatically, do not resend), or `not_applicable` (PUDO / locker / clearance), plus `order_id` (your order's Starmile tracking number) and `items[]` (each parcel's `item_id` → `parcel_id`). Scope: orders:create."
          }
        },
        {
          "name": "Download a parcel label (PDF)",
          "request": {
            "method": "GET",
            "header": [
              { "key": "Accept", "value": "application/pdf" }
            ],
            "url": {
              "raw": "{{base_url}}/api/v1/orders/label?merchant_tracking={{merchant_tracking}}",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "orders", "label"],
              "query": [
                { "key": "merchant_tracking", "value": "{{merchant_tracking}}" },
                { "key": "parcel_id", "value": "{{parcel_id}}", "disabled": true }
              ]
            },
            "description": "One parcel's printable label as a PDF, rendered from your org's default parcel template. Address the parcel by EITHER `merchant_tracking` (the sticker code) OR `parcel_id` (the parcel's Starmile tracking number, returned as items[].parcel_id on create) — enable one query param and disable the other. Strictly one parcel per call; there is no whole-order form. Response is application/pdf. Scope: labels:read."
          }
        },
        {
          "name": "Update a parcel",
          "request": {
            "method": "PATCH",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"merchant_tracking\": \"{{merchant_tracking}}\",\n  \"package_type\": \"fragile\",\n  \"weight_grams\": 1500,\n  \"length_mm\": 300,\n  \"width_mm\": 200,\n  \"height_mm\": 150,\n  \"products\": [\n    {\n      \"name\": \"Sneakers\",\n      \"hs_code\": \"640411\",\n      \"declared_value\": 120.00,\n      \"currency\": \"USD\",\n      \"weight_grams\": 1500,\n      \"quantity\": 1,\n      \"description\": \"Running shoes, size 42\"\n    }\n  ]\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/{{order_id}}/parcels/{{item_id}}",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "orders", "{{order_id}}", "parcels", "{{item_id}}"]
            },
            "description": "Partial update of a not-yet-received parcel. Every field is optional — send only what you want to change. Sending `products` replaces the full list (each product's `name` is required). 409 once received. Scope: orders:update."
          }
        },
        {
          "name": "Cancel an order",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reason\": \"customer changed their mind\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/orders/{{order_id}}/cancel",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "orders", "{{order_id}}", "cancel"]
            },
            "description": "Cancel an order while it is still pre-custody. 409 once any package is in our custody. Scope: orders:cancel."
          }
        }
      ]
    },
    {
      "name": "Status pool",
      "item": [
        {
          "name": "Poll status changes",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/api/v1/partner/changes?since={{since}}&limit={{limit}}",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "partner", "changes"],
              "query": [
                { "key": "since", "value": "{{since}}" },
                { "key": "limit", "value": "{{limit}}" }
              ]
            },
            "description": "Pull every status change after your cursor. `since` (optional, default 0) is your cursor; `limit` (optional, default 100) is the page size. Persist `next_cursor` and pass it as `since` next time; while `has_more` is true, poll again immediately. Scope: status:read."
          }
        }
      ]
    },
    {
      "name": "Partner events",
      "item": [
        {
          "name": "Report an event",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"event_id\": \"evt-7f3a-0001\",\n  \"type\": \"shipment.out_for_delivery\",\n  \"tracking_number\": \"STM000123\",\n  \"occurred_at\": \"2026-06-20T15:02:00Z\",\n  \"data\": {\n    \"driver\": \"Rashad\",\n    \"eta\": \"2026-06-20T17:30:00Z\"\n  }\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": {
              "raw": "{{base_url}}/api/v1/partner/events",
              "host": ["{{base_url}}"],
              "path": ["api", "v1", "partner", "events"]
            },
            "description": "Report an inbound event (carriers / PUDO / customs / leg handoff). Validated before accept — a bad type/field returns 422 with error + hint. Scope: one of events:transport, events:pudo, events:customs, leg:handoff (per event type)."
          }
        }
      ]
    }
  ]
}
