# Create a shipment

`POST /v2/shipments`

**Base URLs:**

- Sandbox: `https://api.sandbox.bobgo.co.za`
- Production: `https://api.bobgo.co.za`

**Authentication:** Bearer token required (see the Authentication page).

## Example request

```bash
curl -X POST 'https://api.sandbox.bobgo.co.za/v2/shipments' \
  -H "content-type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
  "provider_slug": "tcg",
  "service_level_code": "ECO",
  "custom_order_number": "ORD-10045",
  "collection_address": {
    "type": "business",
    "company": "Bob Go",
    "street_address": "24 Rothesay Avenue",
    "local_area": "Craighall Park",
    "city": "Johannesburg",
    "zone": "Gauteng",
    "country": "ZA",
    "code": "2196"
  },
  "delivery_address": {
    "type": "residential",
    "street_address": "5 Long Street",
    "local_area": "Cape Town City Centre",
    "city": "Cape Town",
    "zone": "Western Cape",
    "country": "ZA",
    "code": "8001"
  },
  "parcels": [
    {
      "submitted_length_cm": 30,
      "submitted_width_cm": 20,
      "submitted_height_cm": 10,
      "submitted_weight_kg": 2,
      "description": "Box of books"
    }
  ],
  "declared_value": 1500,
  "collection_contact_name": "Jane",
  "collection_contact_surname": "Seller",
  "collection_contact_mobile_number": "0821234567",
  "collection_contact_email": "seller@example.com",
  "delivery_contact_name": "John",
  "delivery_contact_surname": "Buyer",
  "delivery_contact_mobile_number": "0839876543",
  "delivery_contact_email": "buyer@example.com",
  "instructions_collection": "Ring the bell at reception",
  "instructions_delivery": "Leave with the concierge"
}'
```

Books a courier for a set of parcels. You choose the courier and service level,
usually one of the rates returned by [`POST /v2/rates`](../8_Rates/rates.md), and
Bob Go creates the shipment, charges your account and submits it to the courier.

The response returns as soon as the shipment exists, unless you ask it to wait
with `timeout`. Rating and submission to the courier happen asynchronously, so
the price and the courier's own waybill number appear on the shipment a short
while after it is created.

## Request

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `provider_slug` | string | yes | The courier, e.g. `tcg`. Take it from the rate you selected. |
| `service_level_code` | string | yes | The service level, e.g. `ECO`. Take it from the rate you selected. |
| `collection_address` | object | yes | Same shape as on `POST /v2/rates`. Not needed when you send `collection_pickup_point_location_id`. |
| `delivery_address` | object | yes | Not needed when you send `delivery_pickup_point_location_id`. |
| `parcels` | array | yes | At least one parcel. |
| `collection_contact_name` | string | yes | First name. |
| `delivery_contact_name` | string | yes | First name. |
| `selected_rate_request_id` | integer | no | The `id` from the rates response. Send it so the quoted rate is used for the funds check. |
| `declared_value` | number | no | Value of the contents in ZAR. |
| `custom_order_number` | string | no | Your own order number. Shows on the shipment and on labels, and comes back as `meta.custom_order_number`. |
| `custom_tracking_reference` | string | no | Your own tracking reference. |
| `collection_min_date` | timestamp | no | Earliest date the courier may collect. Leave it out, or send a date in the past, for the earliest possible. Bob Go moves it to the next business day, and past the service level's cut-off time. |
| `collection_after` / `collection_before` | string | no | Collection window on the collection date, `hh:mm:ss` (`hh:mm` is padded), e.g. `09:00:00` / `17:00:00`. Send both or neither, with the hour zero-padded, between `08:00:00` and `17:00:00`. Defaults to the whole of that window. |
| `instructions_collection` | string | no | Free-text note for the collection driver. |
| `instructions_delivery` | string | no | Free-text note for the delivery driver. |
| `shipping_label_emails` | array of string | no | Email the label to these addresses once it is generated. |
| `collection_contact_surname` | string | no | |
| `collection_contact_mobile_number` | string | no | |
| `collection_contact_email` | string | no | Checked when you send it. |
| `delivery_contact_surname` | string | no | |
| `delivery_contact_mobile_number` | string | no | Used for delivery notifications, so worth sending. |
| `delivery_contact_email` | string | no | Used for tracking emails. Checked when you send it. |
| `collection_pickup_point_location_id` | integer | no | Collect from a pickup point instead of an address. |
| `delivery_pickup_point_location_id` | integer | no | Deliver to a pickup point / locker instead of an address. Ignored on a door-to-door service level. |
| `timeout` | integer | no | Milliseconds to wait for the submission to settle before responding, capped at `25000`. As on `POST /v2/rates`, the last five seconds are reserved, so anything up to 5000 does not wait. Defaults to `0`. |

Contact fields take a first name and surname here. `POST /v2/rates` takes a
single `*_full_name`, but also accepts `*_contact_name` and `*_contact_surname`
and joins them, so you can send the same contact fields to both.

## Response

```json
{
  "id": 4455123,
  "account_id": 1234,
  "provider_slug": "tcg",
  "service_level_code": "ECO",
  "status": "",
  "submission_status": "pending-rates",
  "tracking_reference": "UATCG4X7Q2",
  "provider_tracking_reference": null,
  "declared_value": 1500,
  "meta": { "custom_order_number": "ORD-10045" },
  "collection_address": { "...": "..." },
  "delivery_address": { "...": "..." },
  "parcels": [{ "...": "..." }],
  "time_created": "2026-09-05T08:31:12Z"
}
```

A new shipment starts with `submission_status: "pending-rates"` and an empty
`status`. Unless you sent a long enough `timeout`, that is what the response
shows. `charged_amount` and `charged_weight_kg` fill in once Bob Go has rated
the shipment.

Store `tracking_reference`: it is Bob Go's identifier for the shipment and
what you use to look it up, track it and match webhook events.
`provider_tracking_reference` is the courier's own waybill number and is filled
in once the courier accepts the submission.

## Statuses

`submission_status` tracks getting the shipment to the courier:

| Status | Meaning |
| --- | --- |
| `pending-rates` | Created, waiting on the courier's rate. |
| `pending-submission` | Rated, queued for the courier. |
| `success` | The courier accepted the shipment. |
| `no-rates` | No bookable rate for this lane. |
| `failed-will-retry` | Transient failure. Bob Go will retry. |
| `failed-indefinitely` | Failed for good; `failed_reason` says why. |

`status` tracks the parcel in the real world. It is empty until the courier
accepts the shipment, then one of `pending-collection`, `awaiting-dropoff`,
`collected`, `in-transit`, `out-for-delivery`, `ready-for-pickup`,
`delivered`, `finalised`, `returned-to-sender`, `collection-exception`,
`delivery-exception`, `failed-collection`, `failed-delivery`, `cancelled`,
`cancelled-by-courier`.

## Notes

- Creating a shipment charges your Bob Go account once it has been rated.
  Before that, Bob Go checks your funds: against the quoted rate when you send
  `selected_rate_request_id`, and against your balance and credit limit either
  way. Either check fails with a `400` saying the account has insufficient
  funds.
- Funds are checked again when the courier's rate arrives. A shipment that
  fails there ends `failed-indefinitely`, with the insufficient-funds message
  as `failed_reason`.
- Shipments on a courier account connected with your own credentials skip the
  funds checks.
- Rates can move between quoting and booking. `charged_amount` on the shipment
  is authoritative, not the earlier quote.
