---
title: "Your first label"
description: "Create a shipment, compare rates, and buy your first label (guía) in test mode — from zero to PDF in minutes."
---

Here you create your first shipping label (guía) in test mode. Test mode uses simulated carriers and a virtual balance. No real money moves. The steps in production are the same. To go live, change the key.

You need a SendIt account and your test key (`sk_test_...`). Find it in the dashboard under **Settings → API keys**, or via `GET /v1/api-keys`.

1. **Create a shipment**

    A shipment describes the origin, the destination, and the parcel. The response carries the new shipment and `rates[]`. `rates[]` holds quotes from every carrier available for that route.

    <CodeGroup>

    ```bash curl
    curl -X POST https://api.sendit.mx/v1/shipments \
      -H "X-API-Key: sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "from": {
          "contactName": "Almacén CDMX",
          "contactPhone": "+5215512345678",
          "street": "Av. Insurgentes Sur",
          "exteriorNumber": "1602",
          "neighborhood": "Crédito Constructor",
          "city": "Ciudad de México",
          "state": "CDMX",
          "postalCode": "03940",
          "country": "MX"
        },
        "to": {
          "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 }
      }'
    ```

    ```js Node.js
    const response = await fetch("https://api.sendit.mx/v1/shipments", {
      method: "POST",
      headers: {
        "X-API-Key": process.env.SENDIT_API_KEY, // sk_test_...
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: {
          contactName: "Almacén CDMX",
          contactPhone: "+5215512345678",
          street: "Av. Insurgentes Sur",
          exteriorNumber: "1602",
          neighborhood: "Crédito Constructor",
          city: "Ciudad de México",
          state: "CDMX",
          postalCode: "03940",
          country: "MX",
        },
        to: {
          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 },
      }),
    });

    const { data: shipment } = await response.json();
    console.log(shipment.id, shipment.rates.length);
    ```

    </CodeGroup>

    The (trimmed) response carries the shipment in `DRAFT` status plus its rates:

    ```json
    {
      "success": true,
      "data": {
        "id": "clxq1w2e3r4t5y6u7i8o9p0a",
        "status": "DRAFT",
        "rates": [
          {
            "id": "DHL_standard_a1b2c3",
            "carrierCode": "DHL",
            "serviceCode": "EXPRESS_WORLDWIDE",
            "serviceName": "DHL Express Nacional",
            "serviceLevel": "standard",
            "totalPrice": 326.82,
            "currency": "MXN",
            "estimatedDays": { "min": 1, "max": 2 },
            "expiresAt": "2026-07-18T14:30:00.000Z"
          },
          {
            "id": "ESTAFETA_economy_x9y8z7",
            "carrierCode": "ESTAFETA",
            "serviceCode": "TERRESTRE",
            "serviceName": "Estafeta Terrestre",
            "serviceLevel": "economy",
            "totalPrice": 289.50,
            "currency": "MXN",
            "estimatedDays": { "min": 3, "max": 5 },
            "expiresAt": "2026-07-18T14:30:00.000Z"
          }
        ],
        "ratesStatus": "ready",
        "ratesExpiresAt": "2026-07-18T14:30:00.000Z"
      }
    }
    ```

2. **Pick a rate**

    Each element of `rates[]` is a firm offer. Its `totalPrice` includes tax and is **exactly** what your wallet is charged. Choose by price, speed, or carrier. For the next step you only need the rate's `id`.

    ```js Node.js
    const cheapest = shipment.rates
      .slice()
      .sort((a, b) => a.totalPrice - b.totalPrice)[0];

    console.log(cheapest.id); // "ESTAFETA_economy_x9y8z7"
    ```

    Rates stay valid for 24 hours. If they expire, fetch fresh ones with `GET /v1/shipments/:id/rates?refresh=true`.

    :::note
    If you already know your carrier, or want the cheapest, add the `purchase` object when you create the shipment. The label arrives in the same response. See [One-call buy](/en/shipping/shipments#one-call-buy).
    :::

3. **Buy the label**

    Send the chosen `rateId`. The `Idempotency-Key` header is optional, but send one on this call. If your request is interrupted and you retry with the same key, there is no double charge.

    <CodeGroup>

    ```bash curl
    curl -X POST https://api.sendit.mx/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/label \
      -H "X-API-Key: sk_test_..." \
      -H "Idempotency-Key: 1f0e6f0e-59a4-4a6f-9d2e-9b1a7b2c3d4e" \
      -H "Content-Type: application/json" \
      -d '{ "rateId": "ESTAFETA_economy_x9y8z7" }'
    ```

    ```js Node.js
    const purchase = await fetch(
      `https://api.sendit.mx/v1/shipments/${shipment.id}/label`,
      {
        method: "POST",
        headers: {
          "X-API-Key": process.env.SENDIT_API_KEY,
          "Idempotency-Key": crypto.randomUUID(),
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ rateId: cheapest.id }),
      }
    );

    const { data: label } = await purchase.json();
    console.log(label.trackingNumber, label.labelUrl);
    ```

    </CodeGroup>

    ```json
    {
      "success": true,
      "data": {
        "shipmentId": "clxq1w2e3r4t5y6u7i8o9p0a",
        "labelId": "clxlbl456abc789def012ghi",
        "trackingNumber": "TEST-ESTAFETA-A1B2C3D4",
        "labelUrl": "https://labels.sendit.mx/test/clxq1w2e3r4t5y6u7i8o9p0a/TEST-ESTAFETA-A1B2C3D4.pdf",
        "carrierCode": "ESTAFETA",
        "serviceName": "Estafeta Terrestre",
        "charged": "289.50",
        "currency": "MXN",
        "walletBalanceAfter": "9710.50",
        "breakdown": {
          "ivaAmount": "39.93",
          "overageCharge": "0.00",
          "total": "289.50"
        }
      }
    }
    ```

    Download the PDF from `labelUrl`. That is your label. The charge came out of the virtual test balance, not real money.

4. **Watch your shipment move**

    In test mode the parcel simulates its journey on its own. It goes from `PICKED_UP` to `DELIVERED` in under half an hour. To skip the wait, advance the status by hand:

    ```bash
    curl -X POST https://api.sendit.mx/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/test/advance-status \
      -H "X-API-Key: sk_test_..."
    ```

    Each call advances one step:

    ```text
    LABEL_PURCHASED → READY_FOR_PICKUP → PICKED_UP → IN_TRANSIT → OUT_FOR_DELIVERY → DELIVERED
    ```

    Check the status and the event history any time with `GET /v1/shipments/:id`.

## Next steps

<CardGroup cols={2}>
  <Card title="Test mode in depth" href="/en/getting-started/test-mode" icon="flask-conical">
    Failure scenarios, virtual balance, and everything the sandbox simulates.
  </Card>
  <Card title="Webhooks" href="/en/webhooks-and-events/webhooks" icon="webhook">
    Stop polling. Receive every event on your server.
  </Card>
  <Card title="Shipments in depth" href="/en/shipping/shipments" icon="package">
    Saved addresses, insurance, external references, and more.
  </Card>
  <Card title="Idempotency" href="/en/api-conventions/idempotency" icon="repeat">
    How to retry money endpoints safely.
  </Card>
</CardGroup>
