Shipments
Esta página aún no está disponible en tu idioma.
Shipments in BenchKey represent carrier labels purchased through the Shippo or UPS Direct integration. The typical flow is: get rates → purchase label → optionally void within the carrier’s window.
The shipment object
Section titled “The shipment object”{ "object": "shipment", "id": "88", "ticket_id": "314", "carrier_account_id": "ca_abc123", "provider": "shippo", "carrier": "usps", "service_code": "usps_priority", "direction": "outbound", "tracking_number": "9400111899223407602815", "label_url": "https://cdn.shippo.com/labels/88.pdf", "eta": "2024-09-12T23:59:59.000Z", "cost": { "amount": "7.95", "currency": "USD" }, "void_window_days": 28, "voided_at": null, "void_reason": null, "shippo_refund_id": null, "shippo_refund_status": null, "shippo_refund_requested_at": null, "shippo_refund_updated_at": null, "shippo_refund_error": null, "scan_first_seen_at": null, "created_at": "2024-09-10T14:22:00.000Z", "updated_at": "2024-09-10T14:22:00.000Z"}| Field | Type | Description |
|---|---|---|
id | string | Unique shipment ID |
ticket_id | string|null | Repair ticket this label is associated with |
carrier_account_id | string | Internal carrier account used for the purchase |
provider | string | Shipping integration ("shippo", "ups_direct") |
carrier | string | Carrier identifier, for example "usps" or "ups" |
service_code | string | Carrier service level code |
direction | string | "inbound", "outbound", or "unknown" for older records |
tracking_number | string|null | Carrier tracking number |
label_url | string|null | URL to the printable PDF label |
eta | string|null | Estimated delivery date/time (ISO-8601) |
cost.amount | string | Label cost in dollars, e.g. "7.95" |
cost.currency | string | Currency code, e.g. "USD" |
void_window_days | integer | Days from purchase within which the label can be voided |
voided_at | string|null | ISO-8601 timestamp of when the label was voided |
void_reason | string|null | Reason provided when the label was voided |
shippo_refund_id | string|null | Provider refund request ID, when available |
shippo_refund_status | string|null | "REQUESTING", "QUEUED", "PENDING", "SUCCESS", or "ERROR"; null when no status is recorded |
shippo_refund_requested_at | string|null | ISO-8601 time the refund was requested |
shippo_refund_updated_at | string|null | ISO-8601 time the refund status last changed |
shippo_refund_error | string|null | Recorded refund error, if any |
scan_first_seen_at | string|null | Timestamp when the first carrier scan was recorded |
created_at | string | ISO-8601 timestamp of label purchase |
updated_at | string | ISO-8601 timestamp of last update |
List shipments
Section titled “List shipments”GET /api/v1/shipmentsReturns a cursor-paginated list of shipments ordered by ID ascending.
Scope required: shipments.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
ticket_id | string | Filter to shipments linked to this ticket |
carrier | string | Filter by carrier identifier (e.g. "usps", "ups"), max 255 chars |
provider | string | Filter by provider (e.g. "shippo", "ups_direct"), max 255 chars |
voided | boolean | true, voided only; false, active only; omit for all. Must be true, false, 1, or 0; any other value returns 400 invalid_query |
limit | integer | Page size, 1–100 (default: 20) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/shipments?ticket_id=314&voided=false" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/shipments?ticket_id=314&voided=false", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "shipment", "id": "88", "ticket_id": "314", "provider": "shippo", "carrier": "usps", "service_code": "usps_priority", "tracking_number": "9400111899223407602815", "label_url": "https://cdn.shippo.com/labels/88.pdf", "cost": { "amount": "7.95", "currency": "USD" }, "voided_at": null, "created_at": "2024-09-10T14:22:00.000Z", "updated_at": "2024-09-10T14:22:00.000Z" } ], "has_more": false, "next_cursor": null}See Pagination for how to page through results.
Retrieve a shipment
Section titled “Retrieve a shipment”GET /api/v1/shipments/:idReturns a single shipment by its string ID.
Scope required: shipments.read
curl "https://app.benchkey.com/api/v1/shipments/88" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/shipments/88", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const shipment = await res.json();Response
Section titled “Response”Returns the shipment object. Returns 404 if no shipment with that ID exists or if the shipment’s linked ticket has been hidden or soft-deleted.
Get carrier rates
Section titled “Get carrier rates”POST /api/v1/shipments/ratesReturns available carrier rates for a parcel. Pass the complete chosen rate object, including its intentToken, unchanged in the JSON body for Purchase a label. Keep the same ticket and direction used for the quote.
Scope required: shipments.write
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
from_address | yes | Originating address object |
to_address | yes | Destination address object |
parcel | yes | Parcel dimensions and weight |
ticket_id | conditional | Required for inbound quotes and location-restricted keys. Also supply it when quoting a Shippo label, since Shippo purchases require a ticket. Must be a string (the ticket’s order_id) |
carrier_account_id | no | Restrict rates to a specific carrier account. Must be a string |
direction | no | "outbound" (default) or "inbound"; use the same value when purchasing |
Address object fields: name, street1, city, state, zip, country (e.g. "US").
Parcel object fields: length, width, height, distance_unit (e.g. "in"), weight, mass_unit (e.g. "lb").
curl -X POST https://app.benchkey.com/api/v1/shipments/rates \ --fail-with-body --output rates.json \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "from_address": { "name": "BenchKey Repair", "street1": "123 Main St", "city": "Austin", "state": "TX", "zip": "78701", "country": "US" }, "to_address": { "name": "Jane Customer", "street1": "456 Oak Ave", "city": "Dallas", "state": "TX", "zip": "75201", "country": "US" }, "parcel": { "length": 9, "width": 6, "height": 2, "distance_unit": "in", "weight": 0.5, "mass_unit": "lb" }, "ticket_id": "314", "direction": "outbound" }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/shipments/rates", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ from_address: { name: "BenchKey Repair", street1: "123 Main St", city: "Austin", state: "TX", zip: "78701", country: "US", }, to_address: { name: "Jane Customer", street1: "456 Oak Ave", city: "Dallas", state: "TX", zip: "75201", country: "US", }, parcel: { length: 9, width: 6, height: 2, distance_unit: "in", weight: 0.5, mass_unit: "lb" }, ticket_id: "314", direction: "outbound", }),});if (!res.ok) throw new Error(`Rate lookup failed: ${res.status} ${await res.text()}`);const { rates } = await res.json();Response
Section titled “Response”{ "object": "rate_list", "rates": [ { "provider": "shippo", "rateId": "rate_abc123", "carrier": "usps", "serviceLabel": "Priority Mail", "serviceCode": "usps_priority", "amount": 7.95, "currency": "USD", "estDays": 2, "carrierAccountId": "ca_abc123", "_shippoShipmentId": "shipment_abc123", "intentToken": "SERVER_ISSUED_QUOTE_TOKEN" }, { "provider": "shippo", "rateId": "rate_def456", "carrier": "usps", "serviceLabel": "Ground Advantage", "serviceCode": "usps_ground_advantage", "amount": 4.10, "currency": "USD", "estDays": 5, "carrierAccountId": "ca_abc123", "_shippoShipmentId": "shipment_abc123", "intentToken": "ANOTHER_SERVER_ISSUED_QUOTE_TOKEN" } ]}Purchase a label
Section titled “Purchase a label”POST /api/v1/shipmentsPurchases a carrier label using a rate returned by Get carrier rates. Keep the complete selected rate, including its server-issued intentToken; the tokens above are illustrative placeholders. Retry an uncertain purchase with the same request and inspect the result before starting another purchase. If the API reports an expired quote, request fresh rates for the same ticket and direction and select a rate again.
Scope required: shipments.write
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
rate | yes | Complete selected object from POST /shipments/rates, including provider, rateId, amount (positive decimal number or string, max 2 dp), currency (3-letter code), and the unchanged intentToken. Do not reconstruct a partial object |
ticket_id | conditional | Required for Shippo purchases, inbound labels, and location-restricted keys. Must be a string (the ticket’s order_id), matching the quote. The ticket must be visible and within the key’s location scope |
direction | no | "outbound" (default) or "inbound"; must match the quote |
ups_direct_ship_payload | no | Required only when rate.provider is "ups_direct" |
The purchase examples below use a Shippo rate. A UPS Direct purchase also needs ups_direct_ship_payload matching the quoted shipment.
# Review rates.json, then choose the desired Shippo rate from the filtered list.jq -e '[.rates[] | select(.provider == "shippo")][0] // error("No Shippo rates returned") | {ticket_id: "314", direction: "outbound", rate: .}' \ rates.json > label-request.json &&curl --fail-with-body -X POST https://app.benchkey.com/api/v1/shipments \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ --data-binary @label-request.jsonNode.js
Section titled “Node.js”// Continue from the rate lookup above after reviewing the available rates.const selectedRate = rates.find(rate => rate.provider === "shippo");if (!selectedRate?.intentToken) throw new Error("Select a fresh server-issued rate");
const purchaseRes = await fetch("https://app.benchkey.com/api/v1/shipments", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ ticket_id: "314", direction: "outbound", rate: selectedRate, }),});if (!purchaseRes.ok) throw new Error(`Label purchase failed: ${purchaseRes.status} ${await purchaseRes.text()}`);const shipment = await purchaseRes.json(); // HTTP 201Response
Section titled “Response”Returns the shipment object with HTTP 201.
Void a label
Section titled “Void a label”POST /api/v1/shipments/:id/voidRequests a void and refund of a purchased label. Carriers enforce a void window (typically 28 days for USPS; varies by carrier). Once a package has been scanned by the carrier, voiding may be rejected upstream.
Scope required: shipments.write
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
reason | no | Optional reason string (defaults to "manual") |
curl -X POST "https://app.benchkey.com/api/v1/shipments/88/void" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "reason": "Customer picked up in store" }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/shipments/88/void", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ reason: "Customer picked up in store" }),});const shipment = await res.json();Response
Section titled “Response”HTTP 200 returns the shipment object with voided_at set after the void is confirmed. HTTP 202 returns the shipment with a pending provider refund request; voided_at remains null while confirmation is outstanding.
Read shippo_refund_status and retrieve the shipment again to check the outcome. REQUESTING, QUEUED, and PENDING do not establish a completed refund. Confirm SUCCESS and a populated voided_at before treating a Shippo label as refunded; inspect shippo_refund_error if the status is ERROR.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_query | A list filter parameter (ticket_id, carrier, provider, voided) was supplied as an array or object instead of a scalar value, or voided is not a recognized boolean (true/false/1/0) |
400 | invalid_cursor | The cursor value is malformed |
400 | invalid_id | The :id path parameter is not a valid identifier |
400 | invalid_field | A field value is wrong type or invalid, from_address, to_address, or parcel is not an object (POST rates); rate.provider is not one of "shippo", "ups_direct"; rate.amount is not a positive decimal representable at 2 dp; rate.currency is not a 3-letter code; reason is not a string (void); ticket_id in body is not a string; carrier or provider filter exceeds 255 characters |
400 | missing_field | Required label-purchase field is absent, or ticket_id is missing for an inbound quote, inbound label, or Shippo purchase |
403 | location_required | A location-restricted key omitted the ticket required to establish location access |
422 | quote_intent_required | The selected rate omitted intentToken; request fresh rates and pass the complete selected rate |
422 | quote_intent_<reason> | The quote token could not be verified, for example quote_intent_malformed; request fresh rates |
409 | quote_intent_expired | The quote expired; request fresh rates and select again |
409 | quote_intent_ticket_mismatch, quote_intent_direction_mismatch | The purchase ticket or direction differs from the quote |
404 | not_found | No shipment with that ID exists; or ticket_id on label purchase or rates refers to a hidden or nonexistent ticket |
409 | conflict | Label purchase is already in progress; or void was rejected because the package has already been scanned by the carrier |
422 | rate_failed | Carrier rate lookup failed (check address/parcel details) |
422 | purchase_failed | Carrier label purchase failed |
422 | void_failed | Carrier rejected the void request (label may have been scanned) |
403 | insufficient_scope | API key lacks shipments.read or shipments.write |
See Errors for the full error envelope format.