{"openapi":"3.1.0","info":{"title":"Tekipost Merchant API","version":"1.0.0","description":"The merchant-facing API. Everything here is scoped to your own account by the bearer token you send.\n\n`webhook.tekipost.com` remains a working alias for every path, so existing integrations need no change."},"servers":[{"url":"https://api.tekipost.com","description":"Production"}],"tags":[{"name":"Create orders","description":"One at a time, in bulk, or as a return. Creating does not book — see Booking & couriers."},{"name":"Booking & couriers","description":"Price a lane, book it, cancel it, and choose which couriers carry your parcels."},{"name":"Orders & tracking","description":"Read what you have created: status, lists, scans. Creating happens under Create orders."},{"name":"Labels & manifests","description":"The paperwork: labels, packing slips, and the handover manifest."},{"name":"Warehouses","description":"Your pickup addresses. A courier will not collect from one it does not know."},{"name":"Failed deliveries (NDR)","description":"Parcels the courier could not deliver, and what to tell them to do next."},{"name":"Lookups","description":"Shared reference data. Nothing here is specific to your account."}],"paths":{"/v1/intake/order":{"post":{"tags":["Create orders"],"summary":"Create an order","description":"Creates a forward order. It does **not** book — call `POST /v1/shipping/book` next.\n\nIdempotent on `customer_order_no`: re-sending the same reference answers `200 {order_id, deduped:true}` instead of creating a second order. `order.pickup_address_id` must be one of your own warehouses; an id that is not yours is `400 invalid_pickup_address`, never a 403 that would confirm it exists.\n\nRequires the `orders.write` capability.","operationId":"post_v1_intake_order","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customer_order_no":{"type":"string","description":"Your reference. The idempotency key."},"channel":{"type":"string","description":"Where the order came from."},"receiver":{"type":"object","description":"`{name, address_1, address_2?, landmark?, city?, state?, pincode, mobile_no}`."},"product":{"type":"object","description":"`{product_name, product_sku?, physical_weight, declared_order_total, cod_amount?, length?, width?, height?}`."},"order":{"type":"object","description":"`{pickup_address_id (required), rto_address_id?, order_date?, order_type (\"0\" prepaid | \"1\" COD), order_from_source?}`."}},"required":["customer_order_no","receiver","product","order"]}}}},"responses":{"201":{"description":"`{order_id}`. A duplicate answers `200 {order_id, deduped:true}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`validation_failed` — `details` lists every field that failed.\n\n`invalid_pickup_address` — `pickup_address_id` is not one of your warehouses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`cannot create order for another sender` — The body named a sender that is not yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/intake/bulk":{"post":{"tags":["Create orders"],"summary":"Bulk create","description":"`multipart/form-data`. Answers 202 with a `job_id`; poll `GET /v1/intake/jobs/{id}`. Rows that need a human decision land in the job's review queue rather than failing the upload.\n\nRequires the `orders.write` capability.","operationId":"post_v1_intake_bulk","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"import_file":{"type":"string","description":"The spreadsheet. Maximum 25 MB."},"sender_address_id":{"type":"integer","description":"Pickup warehouse for every row in the file."}},"required":["import_file","sender_address_id"]}}}},"responses":{"202":{"description":"`{job_id, status:\"queued\"}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`sender_address_id (pickup warehouse) required` — Absent.\n\n`invalid_pickup_address` — Not one of your warehouses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/intake/reverse":{"post":{"tags":["Create orders"],"summary":"Create a reverse order","description":"Same block shape as a forward order, plus the original leg. Your reference is stored with a `-R` suffix so a return never collides with its forward order.\n\nRequires the `orders.write` capability.","operationId":"post_v1_intake_reverse","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"object","description":"As forward, plus `forward_source` (`tekipost` | `external`), `old_fwd_awb_no` (≤50 chars), `reverse_pickup_address_id?`, `return_address?`."}},"required":["order"]}}}},"responses":{"201":{"description":"`{order_id}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`validation_failed` — e.g. `forward_source must be 'tekipost' or 'external'`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/intake/jobs/{id}":{"get":{"tags":["Create orders"],"summary":"Bulk job status","description":"Progress and per-row outcome for a bulk upload.","operationId":"get_v1_intake_jobs_id_","parameters":[{"name":"id","in":"path","required":true,"description":"The `job_id` from the upload.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The job object with counts and status.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`not found` — Not your job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/serviceable":{"get":{"tags":["Booking & couriers"],"summary":"Rate and serviceability","description":"Which couriers will carry this lane and what each charges, cheapest decided by you. `dropped_couriers` is the honest half: every courier that was considered and excluded, with the reason — `no_rate_card`, `cod_config_missing`, `reverse_not_supported` and so on.\n\n`performance` carries the delivered/RTO percentages and TAT for the lane where there is enough history; `null` where there is not. It is never invented.","operationId":"get_v1_shipping_serviceable","parameters":[{"name":"from_pin","in":"query","required":true,"description":"Pickup pincode.","schema":{"type":"string","pattern":"^\\d{6}$"}},{"name":"to_pin","in":"query","required":true,"description":"Delivery pincode.","schema":{"type":"string","pattern":"^\\d{6}$"}},{"name":"physical_weight","in":"query","required":true,"description":"Kilograms, greater than zero.","schema":{"type":"number"},"example":0.5},{"name":"volumetric_weight","in":"query","required":false,"description":"Kilograms. Defaults to 0.","schema":{"type":"number"}},{"name":"order_type","in":"query","required":false,"description":"`\"1\"` for COD, anything else prepaid.","schema":{"type":"string","enum":["0","1"]}},{"name":"collectable_amount","in":"query","required":false,"description":"COD amount to collect.","schema":{"type":"number"}},{"name":"operation","in":"query","required":false,"description":"Set `reverse` to price the return leg.","schema":{"type":"string","enum":["reverse"]}}],"responses":{"200":{"description":"`{data:[{logistic_id, courier_name, total, cod_charge, freight_charge, shipping_charge_tax, zone, weight, rto_charge, performance}], dropped_couriers:[{logistic_id, logistic_name, reason, detail}]}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`from_pin, to_pin and physical_weight are required` — Any of the three is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`lane_not_priced` — No courier on your rate card prices this lane.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`rate_unavailable` — The pricing engine is unreachable. Retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/book":{"post":{"tags":["Booking & couriers"],"summary":"Book (asynchronous)","description":"Queues the booking and answers **202 immediately** — the AWB is not in this response and will not be.\n\nThat is deliberate: returning anything else restarts the courier-fallback loop and risks booking one parcel with two couriers and debiting the wallet twice. The AWB usually exists within 8 seconds; collect it from `GET /v1/orders/status?order_no=…`.\n\nOmit `logistic_id` for AUTO mode, where the courier is chosen by your ranking and rules. Supply it to force one.","operationId":"post_v1_shipping_book","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"integer","description":"From create."},"logistic_id":{"type":"integer","description":"Force a courier. Omit for AUTO."}},"required":["order_id"]}}}},"responses":{"202":{"description":"`{status:\"queued\", idem_key}` — or `{status:\"queued\", mode:\"auto\"}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`order_id must be a positive integer` — Missing or malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`cannot create booking for another sender` — The order is not yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/book/sync":{"post":{"tags":["Booking & couriers"],"summary":"Book (synchronous)","description":"Books inside the request and returns the AWB. It BLOCKS for the courier call — typically 30–40 seconds — so set your client timeout to at least 45s.\n\nPrefer the asynchronous `POST /v1/shipping/book` unless your integration genuinely cannot poll. A transient courier failure here is handed to the durable worker, so a timeout on your side does not mean the parcel was not booked — check `GET /v1/orders/status` before retrying.","operationId":"post_v1_shipping_book_sync","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"integer","description":"From create."},"logistic_id":{"type":"integer","description":"Force a courier. Omit for AUTO (forward only)."},"operation":{"type":"string","description":"Forward booking or a return.","enum":["book","reverse"]}},"required":["order_id"]}}}},"responses":{"200":{"description":"`{status, awb, forward_id, logistic_id, order_id, courier_name, label_url}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`order_id and logistic_id must be positive integers` — Malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`booking_failed` — The courier refused. `message` is the courier's own words.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/cancel":{"post":{"tags":["Booking & couriers"],"summary":"Cancel a shipment","description":"Cancels with the courier and releases the order number for reuse. `refunded` reports what went back to your wallet.","operationId":"post_v1_shipping_cancel","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"integer","description":"The order."},"logistic_id":{"type":"integer","description":"The courier it was booked with."}},"required":["order_id","logistic_id"]}}}},"responses":{"200":{"description":"`{status, awb, forward_id, refunded, order_id, mode}`.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"`cannot cancel for another sender` — Not your order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`cancel_failed` — The courier refused the cancellation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`cancel_unavailable` — The courier is unreachable. Retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/ndr":{"post":{"tags":["Booking & couriers"],"summary":"NDR action","description":"Tells the courier what to do with a parcel they could not deliver.","operationId":"post_v1_shipping_ndr","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_id":{"type":"integer","description":"The order."},"logistic_id":{"type":"integer","description":"The courier."},"ndr_act":{"type":"string","description":"What to do. Defaults to `RE-ATTEMPT`.","enum":["RE-ATTEMPT","DEFER_DLV","EDIT_DETAILS"]},"deferred_date":{"type":"string","description":"Required for `DEFER_DLV`."}},"required":["order_id","logistic_id"]}}}},"responses":{"200":{"description":"`{status, awb, forward_id, order_id, ndr_act}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_ndr_act` — Not one of the three values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`reattempt_failed` — The courier refused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/couriers":{"get":{"tags":["Booking & couriers"],"summary":"Courier list and ranking","description":"Every courier on your rate card, whether it is enabled, its position in your AUTO ranking, and how many pincodes it serves.","operationId":"get_v1_shipping_couriers","parameters":[{"name":"shipment_type","in":"query","required":false,"description":"Forward or reverse. Defaults to `\"0\"`.","schema":{"type":"string","enum":["0","1","2"]}}],"responses":{"200":{"description":"`{data:[{logistic_id, logistic_name, enabled, priority, serviceable_pincodes}], strategy, unique_pincodes, rules, restrictions, shipments_30d}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`shipment_type must be 0 (forward), 1 or 2 (reverse)` — Anything else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/shipping/couriers/priority":{"patch":{"tags":["Booking & couriers"],"summary":"Set courier priority","description":"Reorder only. You must send the **complete** ranking as a permutation of 1..n over the couriers already on your active rate card — this cannot add or remove a courier.\n\nRequires the `courier.rank` capability.","operationId":"patch_v1_shipping_couriers_priority","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shipment_type":{"type":"string","description":"Forward or reverse.","enum":["0","1"]},"order":{"type":"array","description":"`[{logistic_id, priority}]` — every courier, each priority used once."}},"required":["shipment_type","order"]}}}},"responses":{"200":{"description":"The re-read ranking.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`incomplete_ranking` — Not every courier on the card was included.\n\n`duplicate_logistic` — A courier appears twice.\n\n`unknown_logistic` — A courier that is not on your card.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/status":{"get":{"tags":["Orders & tracking"],"summary":"Booking status by your own reference","description":"The supported way to collect an AWB after `POST /v1/shipping/book` answers 202. Poll this, not the order row — `awb_number` on the pending-order table is never populated, by design.\n\n`state` is one of `booked` (done; `awb_number` and `courier` are filled), `booking_in_progress` (still on the worker — poll again in a few seconds), `failed` (it will not complete, and `failure_reason` says why in plain words), or `not_shipped` (the order exists, no ship was requested).\n\nBack off to every 10–15 seconds. A booking unsettled after a couple of minutes has almost certainly failed, and the response will say so.","operationId":"get_v1_orders_status","parameters":[{"name":"order_no","in":"query","required":true,"description":"Your own reference, exactly as you sent it on create.","schema":{"type":"string"},"example":"ORD-4471"}],"responses":{"200":{"description":"`{order_no, state, awb_number, courier, status, failure_reason, created_at}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`order_no is required` — The query parameter is absent or empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`order_not_found` — No order with that reference **on your account**. Never means \"not booked yet\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders":{"get":{"tags":["Orders & tracking"],"summary":"List shipments","description":"Booked shipments, newest first, cursor-paginated. `bucket` is the lifecycle slice — the same slices the portal draws — and is the filter you usually want.","operationId":"get_v1_orders","parameters":[{"name":"bucket","in":"query","required":false,"description":"Lifecycle slice.","schema":{"type":"string","enum":["rts","pickup","manifest","transit","delivered","rto","exceptions","cancelled","all"]}},{"name":"limit","in":"query","required":false,"description":"Page size, 1–1000.","schema":{"type":"integer"},"example":50},{"name":"cursor","in":"query","required":false,"description":"From the previous page's `nextCursor`.","schema":{"type":"string"}},{"name":"awb_number","in":"query","required":false,"description":"Exact AWB.","schema":{"type":"string"}},{"name":"order_no","in":"query","required":false,"description":"Your own reference.","schema":{"type":"string"}},{"name":"shipment_type","in":"query","required":false,"description":"Forward or reverse.","schema":{"type":"string","enum":["0","1"]}},{"name":"date_from","in":"query","required":false,"description":"Inclusive, `YYYY-MM-DD`.","schema":{"type":"string"}},{"name":"date_to","in":"query","required":false,"description":"Inclusive, `YYYY-MM-DD`.","schema":{"type":"string"}},{"name":"city","in":"query","required":false,"description":"Destination city, ≤100 characters.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{data:[shipment…], pagination:{mode:\"cursor\",limit,nextCursor}}`. Each row carries `channel`, `order_value` and `tracking_url`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`bucket: unrecognised` — A bucket outside the list above.\n\n`cursor: unrecognised shape` — A cursor we did not issue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/{id}":{"get":{"tags":["Orders & tracking"],"summary":"Shipment detail","description":"The full row for one order, plus `channel` and `tracking_url`.","operationId":"get_v1_orders_id_","parameters":[{"name":"id","in":"path","required":true,"description":"The `order_id` from create.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The shipment object.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`order not found` — No such order on your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/{id}/tracking":{"get":{"tags":["Orders & tracking"],"summary":"Tracking scans","description":"Courier scans oldest-first. `truncated` tells you the list was capped at `scan_limit`.","operationId":"get_v1_orders_id_tracking","parameters":[{"name":"id","in":"path","required":true,"description":"The `order_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"`{scans:[…], truncated, scan_limit, status_name, order_status_id}`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`order not found` — No such order on your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/unshipped":{"get":{"tags":["Orders & tracking"],"summary":"List unshipped orders","description":"Created but not yet booked, with the reason each one is stuck. Cursor-paginated and heavily filterable.","operationId":"get_v1_orders_unshipped","parameters":[{"name":"limit","in":"query","required":false,"description":"Page size, 1–1000.","schema":{"type":"integer"}},{"name":"cursor","in":"query","required":false,"description":"From `nextCursor`.","schema":{"type":"string"}},{"name":"error_type","in":"query","required":false,"description":"One of the values `/v1/orders/unshipped/error-types` returns.","schema":{"type":"string"}},{"name":"customer_order_no","in":"query","required":false,"description":"Your own reference.","schema":{"type":"string"}},{"name":"pickup_address_id","in":"query","required":false,"description":"Warehouse id.","schema":{"type":"integer","pattern":"^[1-9]\\d*$"}},{"name":"order_type","in":"query","required":false,"description":"Prepaid or COD.","schema":{"type":"string","enum":["0","1"]}},{"name":"amount_min","in":"query","required":false,"description":"Rupee amount.","schema":{"type":"string","pattern":"^\\d{1,9}(\\.\\d{1,2})?$"}},{"name":"amount_max","in":"query","required":false,"description":"Rupee amount.","schema":{"type":"string","pattern":"^\\d{1,9}(\\.\\d{1,2})?$"}}],"responses":{"200":{"description":"`{data:[…], pagination}`. Rows carry `order_value`, `channel`, `confirmation` and `address`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`order_type: must be 0 (prepaid) or 1 (COD)` — Anything else.\n\n`amount_min: must be a rupee amount` — Not a plain decimal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/unshipped/counts":{"get":{"tags":["Orders & tracking"],"summary":"Unshipped counts","description":"Forward answers `{total, unshipped, failed, in_queue}`; reverse answers `{unshipped, failed}`.","operationId":"get_v1_orders_unshipped_counts","parameters":[{"name":"shipment_type","in":"query","required":false,"description":"Forward or reverse.","schema":{"type":"string","enum":["0","1"]}}],"responses":{"200":{"description":"The counts object.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`shipment_type: must be 0 (forward) or 1 (reverse)` — Anything else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/unshipped/{id}":{"patch":{"tags":["Orders & tracking"],"summary":"Edit an unshipped order","description":"Change buyer details on an order that has not shipped. The writable set is a closed allow-list; `sender_id` is stripped. Once booked, an order is no longer editable.\n\nRequires the `orders.write` capability.","operationId":"patch_v1_orders_unshipped_id_","parameters":[{"name":"id","in":"path","required":true,"description":"The `order_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"`{order_id, status, changed:[field…]}` — `changed` is what actually moved.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`<validator>` — The body carries `field` naming the offending key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`order not found` — No such order on your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_editable` — Already booked or otherwise past the editable window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/orders/{id}/hide":{"post":{"tags":["Orders & tracking"],"summary":"Hide an order","description":"Soft-removes an order from your lists. Closes any open cancellation and reports what was refunded.\n\nRequires the `orders.write` capability.","operationId":"post_v1_orders_id_hide","parameters":[{"name":"id","in":"path","required":true,"description":"The `order_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"`{order_id, order_no, hidden:true, closed, refunded}`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`order not found` — No such order on your account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/documents/label/{orderId}":{"get":{"tags":["Labels & manifests"],"summary":"Shipping label","description":"The label PDF, or the carrier's own file where they issue it. Fetching it stamps `label_downloaded_at`.","operationId":"get_v1_documents_label_orderId_","parameters":[{"name":"orderId","in":"path","required":true,"description":"The `order_id`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"PDF bytes.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`{\"message\":\"not found\"}` — not yours, or no such order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`carrier_label_missing` — The courier issues its own label and we have not captured it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/documents/label":{"post":{"tags":["Labels & manifests"],"summary":"Bulk labels","description":"One merged PDF, pages in the order you asked for. Maximum 200 orders. One missing label fails the whole batch rather than silently shipping a short PDF.","operationId":"post_v1_documents_label","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_ids":{"type":"array","description":"Positive integers, 1–200."}},"required":["order_ids"]}}}},"responses":{"200":{"description":"One merged PDF.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`{\"message\":\"max 200 orders per request\"}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`carrier_label_missing` — At least one label has not been captured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/documents/manifest":{"post":{"tags":["Labels & manifests"],"summary":"Manifest a batch","description":"Builds the handover manifest, one section per courier, and **moves those packages to Ready-for-pickup**. This is a write with side effects, not a report: it inserts manifest rows and fires `shipment.status_changed`.","operationId":"post_v1_documents_manifest","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"order_ids":{"type":"array","description":"Positive integers, 1–200."}},"required":["order_ids"]}}}},"responses":{"200":{"description":"PDF, with `x-manifest-no` and `x-manifest-count` headers.","content":{"application/json":{"schema":{"type":"object"}}}},"409":{"description":"`not_ready_to_ship` — Body lists `order_ids` and `already_manifested_order_ids`.\n\n`already_manifested` — One or more AWBs are on a manifest already.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/documents/label/t/{token}":{"get":{"tags":["Labels & manifests"],"summary":"Label by signed link","description":"Open — **no Authorization header**. The token is `\"{awb}.{hmac}\"` and is itself the credential, so a label link can be forwarded to a packer who has no login. Rate-limited at the edge.\n\nServed with `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff` and a `sandbox` CSP. Treat the URL as a secret: anyone holding it can fetch that label.","operationId":"get_v1_documents_label_t_token_","parameters":[{"name":"token","in":"path","required":true,"description":"`{awb}.{hmac}`, as issued.","schema":{"type":"string"}}],"responses":{"200":{"description":"The label bytes.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Plain-text `Not found` for a bad token, an unknown AWB, or a courier-issued label we never captured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/v1/warehouses":{"get":{"tags":["Warehouses"],"summary":"List warehouses","description":"Every pickup address, with its courier-registration state and 30-day shipped count.","operationId":"get_v1_warehouses","parameters":[{"name":"include","in":"query","required":false,"description":"CSV. `inactive` also returns retired rows.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{data:[{id, …address, active, retired, is_default, gstin, warehouse_code, warehouse_type, courier_registered, shipped_30d}]}`.","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["Warehouses"],"summary":"Create a warehouse","description":"The validation here is the courier's, not ours: Delhivery rejects a name under three characters and a sparse address, and a warehouse that passes here but fails there is one no courier will collect from.\n\nSet `is_return_same` truthy to mirror the forward address onto the return leg; otherwise every `return_*` field is required and validated the same way. Creating a warehouse also adopts any orders that arrived before you had one.\n\nRequires the `warehouses.write` capability.","operationId":"post_v1_warehouses","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"warehouse_name":{"type":"string","description":"Unique to you. (3–191 characters)"},"contact_person_name":{"type":"string","description":"Who the courier asks for."},"contact_no":{"type":"string","description":"Collection contact.","pattern":"^\\d{10}$"},"address_line_1":{"type":"string","description":"Street address."},"city":{"type":"string","description":"City."},"state":{"type":"string","description":"State."},"pincode":{"type":"string","description":"Pickup pincode.","pattern":"^\\d{6}$"},"contact_email":{"type":"string","description":"Optional, validated when present."},"gstin":{"type":"string","description":"GSTIN for the state the goods are stocked in.","pattern":"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$"},"warehouse_type":{"type":"string","description":"Defaults to `fulfillment_pickup`.","enum":["fulfillment_pickup","pickup","fulfillment"]},"is_return_same":{"type":"boolean","description":"Mirror the forward address onto the return leg."}},"required":["warehouse_name","contact_person_name","contact_no","address_line_1","city","state","pincode"]}}}},"responses":{"201":{"description":"`{data:{id, orders_repaired}}` — `orders_repaired` is how many stranded orders were adopted.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`validation_failed` — `details` names every failure; return-leg messages are prefixed `return_`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/warehouses/{id}":{"delete":{"tags":["Warehouses"],"summary":"Delete a warehouse","description":"A soft delete, and it refuses while orders still ship from the address. The 409 is the point of the endpoint: deleting a warehouse an open order points at strands that order at a collection point nothing resolves.\n\nRequires the `warehouses.write` capability.","operationId":"delete_v1_warehouses_id_","parameters":[{"name":"id","in":"path","required":true,"description":"The warehouse id.","schema":{"type":"integer"}}],"responses":{"200":{"description":"`{data:{id, deleted:1}}`.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"`not_found` — Not yours, never existed, or already deleted — one answer for all three.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`warehouse_in_use` — Body carries `count`, `unshipped`, `awaiting_pickup`, and up to 20 `order_ids` so you can see exactly what is blocking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/ndr":{"get":{"tags":["Failed deliveries (NDR)"],"summary":"List NDR shipments","description":"Failed-delivery parcels by tab, cursor-paginated, each with the notification we sent the buyer and how it landed.","operationId":"get_v1_ndr","parameters":[{"name":"tab","in":"query","required":true,"description":"Which slice of the queue.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size.","schema":{"type":"integer"}},{"name":"cursor","in":"query","required":false,"description":"From `nextCursor`.","schema":{"type":"string"}},{"name":"awb_number","in":"query","required":false,"description":"Exact AWB.","schema":{"type":"string"}},{"name":"date_from","in":"query","required":false,"description":"Inclusive.","schema":{"type":"string"}}],"responses":{"200":{"description":"`{data:[row + order_value + notification], pagination}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`bad_tab` — `message` lists the accepted tabs.\n\n`bad_date` — Not `YYYY-MM-DD`, or not a real date — `2026-02-31` is refused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/ndr/counts":{"get":{"tags":["Failed deliveries (NDR)"],"summary":"NDR counts","description":"One number per tab, so you can badge a queue without pulling every page of it. The keys are the same tab values `GET /v1/ndr` accepts, so you can drive both the badge and the link from one response.","operationId":"get_v1_ndr_counts","responses":{"200":{"description":"`{<tab>: n, …}` — one key per tab.","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/ndr/{id}/comment":{"post":{"tags":["Failed deliveries (NDR)"],"summary":"Add an NDR comment","description":"Adds a note to the NDR thread. Note `merchant_support` holds the `ndr.act` capability but is a read-only role, so the mount refuses it here — a known inconsistency, stated rather than hidden.","operationId":"post_v1_ndr_id_comment","parameters":[{"name":"id","in":"path","required":true,"description":"The order id.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"string","description":"The note. (1–255 characters)"}},"required":["comment"]}}}},"responses":{"201":{"description":"The created comment row.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`empty_comment` — Blank.\n\n`comment_too_long` — Over 255 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/pincode/{pincode}":{"get":{"tags":["Lookups"],"summary":"Pincode lookup","description":"An unknown pincode is **200 with `found:false`**, not a 404 — so you can call it on every keystroke of an address form without treating \"not yet six digits\" as an error.","operationId":"get_v1_pincode_pincode_","parameters":[{"name":"pincode","in":"path","required":true,"description":"Six digits.","schema":{"type":"string","pattern":"^[0-9]{6}$"}}],"responses":{"200":{"description":"`{found:true, pincode, city, state}` or `{found:false, pincode, city:\"\", state:\"\"}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`pincode must be 6 digits` — Not six digits after whitespace is stripped.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`lookup_failed` — The lookup is unavailable. Retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"A GoTrue RS256 JWT — the same credential the merchant portal signs in with. The account and role are read from the verified token, never from the request."}},"schemas":{"Error":{"type":"object","description":"The standard failure envelope. Match on `error`, never on `message`. `/v1/documents/*` is the one exception: it answers `{message}` on 400/404 and `{code, message}` on 409.","properties":{"error":{"type":"string","description":"Stable machine-readable code."},"message":{"type":"string","description":"Human-readable detail. May change."},"details":{"type":"array","items":{"type":"string"},"description":"Present when a validator rejected more than one field."}},"required":["error"]}}},"security":[{"bearerAuth":[]}]}