Saltar al contenido
SendIt está en desarrollo y todavía no opera comercialmente · SendIt is in development and not yet commercially available.
SendItdocs
English
Esc
navigateopen⌘Jpreview
En esta página

Shipments

The shipment object, its lifecycle, and how to create, retrieve, and cancel shipments.

The shipment is the API’s central entity: it describes a parcel traveling from an origin to a destination. It starts in DRAFT status, becomes a label (guía) when you buy a rate, and moves through its lifecycle until delivery.

The lifecycle

DRAFT → LABEL_PURCHASED → READY_FOR_PICKUP → PICKED_UP → IN_TRANSIT → OUT_FOR_DELIVERY → DELIVERED
Status Meaning
DRAFT Just created; has rates, editable and cancelable at no cost
PENDING Submitted, awaiting label purchase
LABEL_PURCHASED Label bought, awaiting pickup
READY_FOR_PICKUP Carrier notified for pickup
PICKED_UP Carrier picked up the package
IN_TRANSIT On the way
OUT_FOR_DELIVERY Last-mile delivery
DELIVERED Delivered
RETURNED Returned to sender
FAILED Delivery failed
CANCELLED Cancelled

Two ways to buy a label

POST /v1/shipments serves two flows. Pick the one that fits each shipment:

1. Two-step (compare and choose). Create the shipment; the response carries rates[] with quotes from the available carriers. You compare price and speed, pick one, and buy with its rateId at POST /v1/shipments/:id/label. Best when cost or speed decides on each shipment. If you know the carrier, pass carrierCode and serviceLevel at create to filter rates to a category.

2. One-call. If you already know the carrier and service, or just want the cheapest, add a purchase object at create. The label comes back in the same response, with no second step. Use it for automation and predictable volume. See One-call buy.

Two-step One-call
When to use it Price or speed decides on each shipment You know the service, or want the cheapest rate
Requests Create → buy with rateId One: POST /v1/shipments with purchase
What it returns rates[] to compare The label (label + purchasedRate) ready
Ideal for Stores optimizing for cost Automation and predictable volume

Endpoints

Method Path Scope Description
POST /v1/shipments shipments:write Create a shipment (returns rates[] inline; with purchase, buys the label in the same call)
GET /v1/shipments shipments:read List shipments (paginated and filterable)
GET /v1/shipments/stats shipments:read Shipment counts by status
GET /v1/shipments/:id shipments:read Detail with snapshots, label, and events
PUT /v1/shipments/:id shipments:write Update a DRAFT shipment
DELETE /v1/shipments/:id shipments:write Cancel a shipment
POST /v1/shipments/:id/return shipments:write Create a return shipment (route reversed)

Create a shipment

For each address role (from, to, return) send exactly one of two variants: a saved address ID (fromAddressId) or an inline object (fromAddress). Inline addresses can be persisted to your address book with saveToAddressBook: true.

Body parameters

PropType
parcelobject

The package: length, width, height (cm) and weight (kg). Accepts dimensionUnit (cm | in), weightUnit (kg | g | lb | oz), and description.

Typeobject
fromAddressId?string

Saved origin. Send exactly one of fromAddressId or fromAddress.

Typestring
fromAddress?object

Inline origin (see the address object). Can be persisted with saveToAddressBook: true.

Typeobject
toAddressId?string

Saved destination. Send exactly one of toAddressId or toAddress.

Typestring
toAddress?object

Inline destination (see the address object).

Typeobject
returnAddressId?string

Saved return address (at most one; defaults to the origin).

Typestring
returnAddress?object

Inline return address.

Typeobject
purchase?object

Buy the label in the same call: carrierCode + serviceCode (exact service) or strategy: cheapest. See One-call buy.

Typeobject
externalId?string

Your own reference, such as an order number or folio. Query it later with ?externalId=.

Typestring
carrierCode?string

Optional: scope the quotes to this carrier from creation (does not buy on its own).

Typestring
serviceLevel?string

Optional: scope the service (e.g. standard, express).

Typestring
requestInsurance?boolean

Request insurance on the declared value. Buying an insured rate creates a real policy (see Insurance & claims).

Typeboolean
Defaultfalse
declaredValue?number

Declared value in MXN (insurance coverage).

Typenumber
metadata?object

Your own key-value pairs; returned as-is.

Typeobject
curl -X POST https://api.sendit.mx/v1/shipments \
  -H "X-API-Key: sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "ORD-2026-001",
    "fromAddressId": "clx_origin_address",
    "toAddress": {
      "contactName": "María López",
      "contactPhone": "+5213312345678",
      "street": "Av. López Mateos",
      "exteriorNumber": "45",
      "neighborhood": "Jardines del Sol",
      "city": "Zapopan",
      "state": "JAL",
      "postalCode": "45050",
      "country": "MX",
      "saveToAddressBook": false
    },
    "parcel": {
      "length": 30, "width": 20, "height": 15, "weight": 2.5,
      "description": "Electronics"
    },
    "requestInsurance": true,
    "declaredValue": 5000,
    "metadata": { "orderId": "shopify-12345" }
  }'

The 201 response returns the shipment in DRAFT with its address snapshots, the event history, and inline rates (rates[], see Rates):

{
  "success": true,
  "data": {
    "id": "clxq1w2e3r4t5y6u7i8o9p0a",
    "status": "DRAFT",
    "externalId": "ORD-2026-001",
    "fromAddressSnapshot": {
      "contactName": "Bruno Sánchez",
      "city": "Ciudad de México",
      "postalCode": "03100",
      "savedAddressId": "clx_origin_address"
    },
    "toAddressSnapshot": {
      "contactName": "María López",
      "city": "Zapopan",
      "postalCode": "45050",
      "savedAddressId": null
    },
    "parcel": { "length": 30, "width": 20, "height": 15, "weight": 2.5 },
    "rates": [
      {
        "id": "DHL_standard_a1b2c3",
        "carrierCode": "DHL",
        "serviceCode": "EXPRESS_WORLDWIDE",
        "serviceLevel": "standard",
        "totalPrice": 326.82,
        "currency": "MXN"
      }
    ],
    "events": [
      { "status": "DRAFT", "description": "Shipment created", "occurredAt": "2026-07-17T12:00:00.000Z" }
    ],
    "createdAt": "2026-07-17T12:00:00.000Z"
  }
}

One-call buy

Add a purchase object to POST /v1/shipments to create the shipment and buy the label in one request. Pick one way to select the service: carrierCode + serviceCode for the exact product, or strategy: "cheapest" for the lowest-priced rate. Never send both, and never omit both. With a scoped API key you need labels:write in addition to shipments:write.

purchase parameters

PropType
carrierCode?string

Exact carrier; used together with serviceCode. Mutually exclusive with strategy.

Typestring
serviceCode?string

Carrier-native service code; used together with carrierCode.

Typestring
strategy?string

cheapest: auto-selects the lowest-priced rate. Mutually exclusive with carrierCode + serviceCode.

Typestring
labelFormat?string

Format of the generated label: PDF | ZPL.

Typestring
DefaultPDF
async?boolean

true returns labelPurchaseAttempt inside the 201 response. Retrieve its statusUrl.

Typeboolean
Defaultfalse
curl -X POST https://api.sendit.mx/v1/shipments \
  -H "X-API-Key: sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "fromAddressId": "clx_origin_address",
    "toAddress": {
      "contactName": "María López",
      "contactPhone": "+5213312345678",
      "street": "Av. López Mateos",
      "exteriorNumber": "45",
      "neighborhood": "Jardines del Sol",
      "city": "Zapopan",
      "state": "JAL",
      "postalCode": "45050",
      "country": "MX"
    },
    "parcel": { "length": 30, "width": 20, "height": 15, "weight": 2.5 },
    "purchase": { "strategy": "cheapest", "labelFormat": "PDF" }
  }'

On success, the 201 response additionally includes label (with trackingNumber, labelUrl, charged) and purchasedRate:

{
  "success": true,
  "data": {
    "id": "clxq1w2e3r4t5y6u7i8o9p0a",
    "status": "LABEL_PURCHASED",
    "label": {
      "trackingNumber": "1234567890",
      "labelUrl": "https://labels.sendit.mx/clxq1w2e3r4t5y6u7i8o9p0a/1234567890.pdf",
      "charged": "326.82",
      "currency": "MXN"
    },
    "purchasedRate": {
      "id": "DHL_standard_a1b2c3",
      "carrierCode": "DHL",
      "serviceCode": "EXPRESS_WORLDWIDE",
      "serviceLevel": "standard",
      "totalPrice": 326.82
    }
  }
}

With purchase.async: true, the outer response remains 201. You receive labelPurchaseAttempt instead of label. Retrieve its statusUrl or use label.purchase.completed. See Asynchronous operations.

The shipment is always created first: if the purchase fails, you’re left with a DRAFT you can finish through the two-step flow.

Code When How to resolve it
402 INSUFFICIENT_BALANCE The wallet can’t cover the label The shipment stays DRAFT; fund the wallet and buy with POST /v1/shipments/:id/label
422 ONE_CALL_BUY_RATES_PENDING Carriers were slow (>8s) to quote Poll ratesPollUrl, then buy with the rateId
422 ONE_CALL_BUY_NO_MATCHING_RATE No rate matched your selection Pick one from details.availableRates
400 INVALID_INPUT purchase had both selection modes, or neither Send carrierCode + serviceCode or strategy, not both

Send several boxes together

A shipment carries one parcel. To dispatch several boxes in the same operation, create one shipment per box and group them into a batch. You buy every label in one call and generate one manifest (manifiesto) for the carrier.

Addresses freeze into snapshots

At creation, every address is copied into an immutable snapshot (fromAddressSnapshot, toAddressSnapshot, returnAddressSnapshot). Editing or deleting the saved address later never alters historical shipments.

Always read addresses from the snapshot, not the ID:

// Correct: the snapshot is the canonical value
const origin = shipment.fromAddressSnapshot;

// Wrong: the saved address may have changed or been deleted
const origin = await getAddress(shipment.fromAddressId);

Inside each snapshot, savedAddressId records which address-book entry it came from (null for one-time addresses).

List and filter

GET /v1/shipments?status=IN_TRANSIT&carrierCode=DHL&limit=50
Filter Match
status Exact (DRAFT, IN_TRANSIT, …)
statuses Several statuses at once (repeat the param or comma-separate)
carrierCode Exact (DHL, FEDEX, ESTAFETA, …)
trackingNumber Partial (contains)
externalId Exact
search Free-text over tracking number and externalId
createdFrom / createdTo Creation date range (ISO; from inclusive, to exclusive)

Cursor pagination and the advanced operators are covered in Pagination & filtering. For dashboards, GET /v1/shipments/stats returns per-status counts in one call.

Retrieve the detail

GET /v1/shipments/:id returns the full shipment: snapshots, parcel, label summary (if any), and the event history:

{
  "success": true,
  "data": {
    "id": "clxq1w2e3r4t5y6u7i8o9p0a",
    "status": "IN_TRANSIT",
    "trackingNumber": "TEST-DHL-A1B2C3D4",
    "publicTrackingToken": "2f68c611-30af-4a86-9cde-793413af5f65",
    "trackingUrl": "https://app.sendit.mx/track/2f68c611-30af-4a86-9cde-793413af5f65",
    "events": [
      { "status": "IN_TRANSIT", "description": "Package in transit", "occurredAt": "2026-07-17T09:12:00.000Z" },
      { "status": "PICKED_UP", "description": "Package picked up", "occurredAt": "2026-07-17T08:03:00.000Z" }
    ]
  }
}

Update a draft

Only DRAFT shipments can be edited; after that they lock:

curl -X PUT https://api.sendit.mx/v1/shipments/{id} \
  -H "X-API-Key: sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "ORD-2026-001-v2" }'

A shipment that already moved on returns 409 SHIPMENT_ALREADY_PROCESSED.

Cancel a shipment

curl -X DELETE https://api.sendit.mx/v1/shipments/{id} \
  -H "X-API-Key: sk_test_..."

You cannot cancel in DELIVERED, RETURNED, FAILED, or if it is already CANCELLED. If the shipment already has a purchased label and you want the money back, void the label first. See Refunds & voids.

Create a return shipment

POST /v1/shipments/:id/return creates a new DRAFT shipment with the route reversed, linked to the original via returnForShipmentId:

  • from = the original destination (where the package is now)
  • to = the original return address, falling back to the origin
  • parcel = copied from the original (override it if the return is re-boxed)

The response is identical to POST /v1/shipments, with inline rates[]. You buy the return label through the normal flow. Rules:

  • The original must have a label (DRAFT, PENDING, CANCELLED, and RETURNED originals are rejected).
  • One active return per shipment. Cancelling the return frees the slot.
  • No returns of returns.
  • Works end-to-end in test mode.

¿Te ha resultado útil esta página?