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). 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.

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

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
declared_value_min_centsint | 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_centsint | 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
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.declared_value_min_cents / declared_value_max_centsThe 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.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, 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
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.