/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). If you want a floor or a ceiling on what the cards in this box are carried and insured at, send declared_value_min_cents and/or declared_value_max_cents (USD cents): whenever we price a card from this box ourselves, a value below the minimum is raised to the minimum and a value above the maximum is lowered to the maximum; a declared value you set is never touched, and a card we cannot price stays null. 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.
declared_value_min_cents / declared_value_max_cents clamp OUR prices, not yours. Each card we intake from this box carries our snapshot of the catalog price as its declared_value (source "RIP") until you set one; with bounds on the box, that snapshot — and the insured_price fallback and pricing.market_price built from it — is raised to the minimum or lowered to the maximum whenever it falls outside them. A value you submit with PUT /declared-values is never changed, and a card we cannot price stays null rather than taking the minimum. The bounds in force when a card is priced are the ones applied: change them with PATCH /inbound-shipments/:id while the box is EXPECTED; once it is RECEIVED the announcement is frozen.
POST /api/v1/inbound-shipments write {
"tracking_number": "9400111899223197428490",
"site": "oregon-1",
"carrier": "usps",
"card_count": 120,
"declared_value_min_cents": 100,
"declared_value_max_cents": 50000,
"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,"declared_value_min_cents":100,"declared_value_max_cents":50000,"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 |
declared_value_min_cents | int | null | — | A floor, in USD cents (a whole number ≥ 0), for every card in this box that WE price: when our snapshot of the catalog price comes in below it, the card is carried at the minimum instead. Omit or null for no floor. Applies only to values we set (declared_value.source "RIP") — a declared value you submit yourself is never changed — and a card we cannot price stays null rather than taking the minimum |
declared_value_max_cents | int | null | — | A ceiling, in USD cents (a whole number ≥ 0), for every card in this box that WE price: when our snapshot of the catalog price comes in above it, the card is carried at the maximum instead. Omit or null for no ceiling. When both bounds are set the minimum must not exceed the maximum (400). Same scope as the minimum: our values only, never yours |
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.declared_value_min_cents / declared_value_max_cents | The declared-value bounds you set for the cards in this box, in USD cents — each null when unset. On every shipment object: create, update, cancel, list and get |
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, declared_value_min_cents or declared_value_max_cents not a whole number ≥ 0 (or null), declared_value_min_cents greater than declared_value_max_cents when both are set, 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.