/api/v1/inbound-shipments write:inbound_shipment Tells us a package of your cards is on its way. Send the carrier tracking number as soon as the label exists, and the site — the warehouse the label is addressed to (list them with GET /sites). Each warehouse works only its own incoming list, so a package with the wrong site is not expected where it lands; it is still received there, and received_site then differs from destination_site. When the box reaches the warehouse, the receiving operator scans its label and the shipment moves to RECEIVED. Read that back with GET /inbound-shipments/:id, or wait for the inbound_shipment.received event on your activity log. Received means the box is on our floor, not that its cards are intaken; those still appear one by one in GET /cards as they are scanned, each naming this shipment in its inbound_shipment field (id, tracking_number, carrier, reference), so GET /cards?inbound_shipment_id=<id> lists exactly the cards that came out of this box. If the cards are for one of your sub-accounts, say so with sub_account (a slug from GET /sub-accounts): the receiving bench sees it and files the intake under it, and the shipment shows up under GET /inbound-shipments?sub_account=. Anything else you want on the box goes in metadata, a JSON object of your own that we store and show back; documents — the packing list, the invoice, a manifest, photos — go on as attachments once the shipment exists (POST /inbound-shipments/:id/attachments). Announcing the same box twice is safe. If you already have a live announcement for this tracking number, or we received a box with it in the last 120 days, you get that shipment back with 200 and created: false instead of a duplicate. Re-announcing a box we already have is therefore a quick way to learn it arrived. Everything but the tracking number and the site can be changed with PATCH /inbound-shipments/:id while the package is EXPECTED; to change where a box is going, cancel it and announce it again with the right site. We match on letters and digits only (spaces, dashes and case are ignored), and a USPS or FedEx label barcode matches even though it carries extra digits around the tracking number. Send exactly the number the carrier gave you.
This endpoint needs its own write:inbound_shipment scope. Ask your account manager to add it to your key; no existing scope stands in for it.
sub_account is advice to the bench, not an automatic filing rule: the operator picks the sub-account when the cards are intaken, with yours pre-selected. Check GET /cards?sub_account= after intake, and use POST /cards/sub-account if anything landed in the wrong place.
POST /api/v1/inbound-shipments write {
"tracking_number": "9400111899223197428490",
"site": "oregon-1",
"carrier": "usps",
"card_count": 120,
"reference": "PO-1001",
"metadata": {
"po": "PO-1001",
"contact": "ops@example.com"
},
"idempotency_key": "docs-inbound-9400111899223197428490"
}Not run yet. Press Run to make a live call against https://service.rip.fun (through this demo's server-side proxy; the API key never reaches the browser).
curl -X POST 'https://service.rip.fun/api/v1/inbound-shipments' \
-H 'X-API-Key: rip_…' \
-H 'Content-Type: application/json' \
-d '{"tracking_number":"9400111899223197428490","site":"oregon-1","carrier":"usps","card_count":120,"reference":"PO-1001","metadata":{"po":"PO-1001","contact":"ops@example.com"},"idempotency_key":"docs-inbound-9400111899223197428490"}' | Field | Type | Required | Description |
|---|---|---|---|
tracking_number | string | yes | The carrier tracking number, up to 64 chars. Must contain 8–64 letters or digits once spaces and punctuation are removed |
site | string | yes | The warehouse the package is addressed to, by site code — today "oregon-1", the only site receiving packages. Matched case-insensitively. Must be one of the codes GET /sites returns; a missing code, an unknown one, or a site that is not receiving packages returns 400 naming the valid ones |
carrier | string | — | Free text, up to 32 chars, stored lower-cased: "usps", "ups", "fedex", "dhl", … |
card_count | int | — | How many cards are in the box, 0..100000. Shown to the receiving operator; informational — nothing is billed on it |
reference | string | — | Your own reference for the box (PO, batch, consignment id), up to 128 chars |
note | string | — | Up to 512 chars |
sub_account | string | null | — | Which of your sub-accounts the cards are for, by slug (GET /sub-accounts), matched case-insensitively. Omit, null or "main" for your own inventory. Must be one live sub-account: "all" is a filter, not a destination, and a retired sub-account is refused. An unknown slug returns 400 naming the valid ones. A label, like sub_account everywhere else: it changes no handling or billing |
metadata | object | — | Any JSON object of your own — ids, a contact, a manifest summary. Stored verbatim, never read by us, shown back on every response and to the receiving bench. At most 8 KB once serialised; larger material belongs in an attachment. Not merged on update: PATCH replaces it whole |
idempotency_key | string | — | Up to 128 chars; a replay returns the original shipment with 200 and created: false |
data)| Field | Description |
|---|---|
inbound_shipment.id / tracking_number / carrier / card_count / reference / note | What you submitted (tracking_number as sent, trimmed) |
inbound_shipment.sub_account | { slug, name } of the sub-account you named, or null for your own inventory |
inbound_shipment.metadata | Your object as stored, or null |
inbound_shipment.attachments[] | The files and links on the box, oldest first — empty on create. Each: id, kind, type ("file" = bytes we hold, "link" = a URL you gave us), filename, content_type, size_bytes (files), url, url_expires_at (files: the download link is signed and lasts an hour; fetch the shipment again for a fresh one), description, created_at |
inbound_shipment.destination_site | { code, name } — the site you addressed it to. null only on packages announced before sites were required |
inbound_shipment.received_site | { code, name } — where it was actually scanned in; null until it is received. Differs from destination_site when a box arrived at the other warehouse |
inbound_shipment.status | "EXPECTED" on create; EXPECTED → RECEIVED when the warehouse scans it in, or EXPECTED → CANCELLED if you cancel it first |
inbound_shipment.source | "tenant" for a package you announced; "warehouse" for one that arrived unannounced and was logged against your account at the door |
inbound_shipment.created_at / received_at / cancelled_at / updated_at | Timestamps; received_at and cancelled_at are null until they happen |
created | true (201) when this request recorded the announcement; false (200) when the box was already on file |
| Status | Code | When |
|---|---|---|
| 400 | invalid site | site missing, unknown, or a warehouse that is not receiving packages ("texas-1 is not receiving packages — send them to: oregon-1") |
| 409 | no site receiving | no warehouse is taking packages at all right now — contact your account manager |
| 400 | invalid input | tracking_number missing or too short/long, card_count not a whole number in range, a text field over its limit, metadata not an object or over 8 KB, or idempotency_key over 128 chars |
| 400 | invalid sub_account | sub_account is not one of your live sub-accounts ("Unknown sub_account "x". Valid values: main, underdog"), is "all", or names a retired sub-account |
| 403 | Insufficient permissions | key is tenant-scoped but carries neither write:inbound_shipment nor admin |
See Errors for the response envelope and the full code list.