Announce a package

POST /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.

Try it POST /api/v1/inbound-shipments write
request body
object · 7 keys
{
  "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"
}
response

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 (tracks the inputs above)
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"}'

Request fields

FieldTypeRequiredDescription
tracking_numberstringyesThe carrier tracking number, up to 64 chars. Must contain 8–64 letters or digits once spaces and punctuation are removed
sitestringyesThe 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
carrierstring—Free text, up to 32 chars, stored lower-cased: "usps", "ups", "fedex", "dhl", …
card_countint—How many cards are in the box, 0..100000. Shown to the receiving operator; informational — nothing is billed on it
referencestring—Your own reference for the box (PO, batch, consignment id), up to 128 chars
notestring—Up to 512 chars
sub_accountstring | 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
metadataobject—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_keystring—Up to 128 chars; a replay returns the original shipment with 200 and created: false

Response fields (data)

FieldDescription
inbound_shipment.id / tracking_number / carrier / card_count / reference / noteWhat 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.metadataYour 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_atTimestamps; received_at and cancelled_at are null until they happen
createdtrue (201) when this request recorded the announcement; false (200) when the box was already on file

Errors

StatusCodeWhen
400invalid sitesite missing, unknown, or a warehouse that is not receiving packages ("texas-1 is not receiving packages — send them to: oregon-1")
409no site receivingno warehouse is taking packages at all right now — contact your account manager
400invalid inputtracking_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
400invalid sub_accountsub_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
403Insufficient permissionskey is tenant-scoped but carries neither write:inbound_shipment nor admin

See Errors for the response envelope and the full code list.