Create reservation
Reserve a compartment at a location for a parcel. The response carries the compartment and all three PINs.
Compartment matching and allow_auto_upgrade are documented once, below, under How the parcel is sized. Read that before you build.
Request
Method: POST
URL: https://api.bobbox.co.za/v2/bob-box/reservations
Request body parameters
| Key | Description | Type | Required |
|---|---|---|---|
| location_id | The location to reserve a compartment at. | integer | Yes |
| reservation_type | client-dropoff, client-pickup, or client-dropoff-and-pickup. |
string | Yes |
| parcel_length | Parcel length in cm. | float | See sizing below |
| parcel_width | Parcel width in cm. | float | See sizing below |
| parcel_height | Parcel height in cm. | float | See sizing below |
| volumetric_weight | Parcel volumetric weight in kg — length × width × height ÷ 4000, sides in cm. Used for sizing only when no dimensions are sent. |
float | See sizing below |
| allow_auto_upgrade | Use a larger compartment when none of the matched size is free. Defaults to false. See How the parcel is sized. |
boolean | No |
| reference | Your own reference for this reservation (e.g. an order number). Max 255 bytes — accented and non-Latin letters use more than one byte each. | string | No |
| contact_name | Name of the person collecting or dropping off. Max 255 bytes — accented and non-Latin letters use more than one byte each. | string | No |
| contact_number | Contact number for the optional Bob Box collection SMS. Must be a valid South African number — see the note below. | string | No |
| parcel_count | How many parcels go in the compartment. Must not be negative. | integer | No |
| custom_data | Arbitrary JSON object for your own metadata. Echoed back on every read of this reservation and on every webhook. Max 4096 bytes when serialized. | object | No |
| bob_box_location_id | Deprecated — alias for location_id, still accepted. Use location_id. |
integer | No |
| custom_reference | Deprecated — alias for reference, still accepted. Use reference. |
string | No |
location_id and bob_box_location_id are interchangeable; send one of them. Same for reference and custom_reference. When both are sent, the new name wins.
Field limits
Requests that exceed these limits will be rejected with a 400.
| Field | Limit |
|---|---|
| reference | 255 characters |
| contact_name | 255 characters |
| custom_data | 4 KB (4096 bytes) serialized |
The character limits are counted in bytes, so accented and non-Latin letters use more than one each. The limit on reference applies to custom_reference too.
How the parcel is sized
This is the single reference for compartment matching. Other pages link here rather than repeat it.
Bob Box works in two steps:
- Match a size. The parcel is matched to the smallest size it fits —
Small, thenMedium, thenLarge. Both sets of sides are sorted before they are compared, so orientation does not matter, and neither does the order you send the dimensions in. - Claim a compartment. The first free compartment of the matched size is claimed.
Compartment sizes
Every compartment is the same width and length. Only the height changes.
| Size | Width (cm) | Length (cm) | Height (cm) | Max volumetric weight (kg) |
|---|---|---|---|---|
Small |
32 | 56 | 10 | 4.48 |
Medium |
32 | 56 | 24 | 10.752 |
Large |
32 | 56 | 52 | 23.296 |
Volumetric factor: 4000. Volumetric weight in kg is length × width × height ÷ 4000, with the sides in centimetres. Each max_volumetric_weight above is just that compartment's own volume put through the same formula, so a parcel that fits a size by its sides also fits it by volumetric weight.
allow_auto_upgrade decides what happens when step 2 finds nothing. It defaults to false, so the reservation fails when the matched size is full — even when bigger compartments are standing empty. Send true and Bob Box falls back to any larger size instead.
Send allow_auto_upgrade: true unless you have a reason not to. It is also the only setting under which compartment_availability predicts this call exactly — see "Checking space before you reserve" in the Workflows section.
What to send
No single sizing field is required, but always send one of these three sets:
-
The three dimensions — the normal case.
-
volumetric_weightwith no dimensions — each compartment is checked against its ownmax_volumetric_weightinstead of against the parcel's sides.Watch out: step 1 still reads the dimensions, and with none sent it always lands on
Small. With the defaultallow_auto_upgrade: falseyou can only ever get a Small compartment, however heavy the parcel is. Sizing by weight needsallow_auto_upgrade: true. -
Nothing at all — the request still succeeds and takes the smallest compartment. Only do this when you know the parcel is small.
A parcel larger than the biggest compartment is rejected with 400 Parcel dimensions exceed maximum compartment size before any compartment is checked.
Contact number format
The number is parsed as a South African number and must be valid for that region. Both +27833771167 and 0833771167 are accepted, and the value is stored in E.164 form. A valid number from another country is rejected.
Reference uniqueness
A reference must be unique among active reservations at the same location. Reusing one that is still tied to an active reservation there returns 400 There is already an active reservation with this reference at this location. The same reference is fine at a different location, or once the earlier reservation has finished.
If you send no reference but your custom_data contains a string field named shipment_reference, that value is used as the reservation's reference. That fallback is not length-checked and not uniqueness-checked — send reference directly if you need either.
Response
Status code: 200 OK Content-Type: application/json
All three PINs are returned here, immediately. They are read off the compartment as it is claimed — you do not need to poll or call the PINs endpoint to get them.
Response body example:
See the example response.
Response fields
| Key | Description | Type |
|---|---|---|
| id | Unique identifier for the reservation. | integer |
| status | waiting-for-dropoff, waiting-for-pickup, expired, cancelled, returned, complete, or cleared. |
string |
| type | The reservation type you requested, echoed back. | string |
| bob_box_location | The location the compartment was reserved at. See the location fields below — this is a narrower object than the one GET /account/locations returns. |
object |
| locker_id | ID of the physical locker unit at the location. | integer |
| compartment | The compartment claimed for this reservation. | object |
| contact_name | The contact name, if you sent one. | string |
| contact_number | The contact number in E.164 form, if you sent one. | string |
| reference | Your reference for this reservation. | string |
| custom_reference | Deprecated — the same value as reference. Read reference. |
string |
| custom_data | Your metadata object, if you sent one. | object |
| dropoff_pin | PIN for the person placing the parcel in the locker. See Using the PINs. | string |
| pickup_pin | PIN for the person collecting the parcel. See Using the PINs. | string |
| return_pin | PIN for removing a parcel that was never collected. See Using the PINs. | string |
| flat_fee_end_time | When the flat-fee window ends and overstay charges begin. Derived from your account's flat-fee duration. | string (datetime) |
| intervention_time | When the reservation becomes eligible for manual intervention (clearing the compartment). Sits after flat_fee_end_time. |
string (datetime) |
| time_dropped_off | When the parcel was dropped off. null until it happens. |
string (datetime) |
| time_completed | When the parcel was collected. null until it happens. |
string (datetime) |
| time_created | When the reservation was created. | string (datetime) |
| time_modified | When the reservation was last modified. | string (datetime) |
Compartment fields:
| Key | Description | Type |
|---|---|---|
| number | Compartment number within the locker. | integer |
| width / length / height | Compartment internal dimensions in cm. Fixed per size — see Compartment sizes. | float |
| max_volumetric_weight | Maximum volumetric weight the compartment holds, in kg. | float |
| type | Compartment size, capitalised: Small, Medium, or Large. |
string |
Location fields (as returned here):
| Key | Description | Type |
|---|---|---|
| id | Unique identifier for the location. | integer |
| name | Internal location name. | string |
| human_name | Display name for the location. | string |
| type | bob-box-locker. An account user cannot reserve at a bob-box-counter. |
string |
| description | Free-text description, e.g. where in the building it is. | string |
| structured_address | Structured address fields. Same shape as on GET /account/locations — see the Locations section for the field list. |
object |
| address | Human-readable address string. | string |
| structured_trading_hours | Per-day opening and closing times. Empty start/end time means closed that day. | object |
| trading_hours | Human-readable trading hours string. | string |
| image_urls | Location image URLs. | array of string |
| status | The location's status, e.g. active. |
string |
| compartment_errors | Reasons no compartment could serve the request, when applicable. | array of string |
| decommissioning_date | When the location will be decommissioned. ISO 8601 UTC timestamp, only present when status is decommissioning. See "Decommissioning locations" in the Locations section. |
string (datetime) |
This object does not carry s3_image_url, s3_image_urls, hidden, compartment_availability, time_created, or time_modified — those belong to GET /account/locations only.
Using the PINs
Store the three PINs on your side and use them however suits your workflow: show them in your app, send them by SMS or email, print them on a label, or pass them to your courier's system. Each PIN is for a different person at a different point in the parcel's journey:
| PIN | Who uses it | When |
|---|---|---|
| Drop-off PIN | The person placing the parcel in the locker. | To open the compartment and put the parcel in. |
| Pickup PIN | The person collecting the parcel. | To open the compartment and take the parcel out. |
| Return PIN | Your courier, or the Bob Box team if the compartment needs to be cleared. | When the parcel was never collected and has to be taken out of the locker. |
Things to know:
- Only give each PIN to the person who needs it. Anyone with a PIN can open the compartment at that point, so the PIN is what keeps the parcel safe.
- The drop-off PIN only works until the parcel is dropped off. After that, the locker no longer accepts it.
- PINs can change. If a reservation's PINs are reissued, the Pin reissued webhook sends you the new PINs, so subscribe to it if you store PINs. Replace the PINs you stored, and make sure anyone you already sent them to gets the new ones. See the Webhooks section.
Errors
| Status | Message | Cause |
|---|---|---|
| 400 | location_id is required |
Neither location_id nor bob_box_location_id was sent. |
| 400 | reservation_type is required |
reservation_type was missing. |
| 400 | invalid reservation_type |
Not one of the three accepted values. |
| 400 | could not find location for location ID |
No location with that ID. |
| 400 | location is not active |
The location is draft, prospect, inactive, or decommissioned. |
| 400 | location is being decommissioned and is no longer accepting new reservations |
The location's decommissioning date has passed. |
| 400 | Parcel dimensions exceed maximum compartment size |
The parcel does not fit the largest compartment. |
| 400 | Parcel matched compartment size <size>; no <sizes> compartments available at this location |
Nothing free in the sizes that were checked. With allow_auto_upgrade: false only the matched size is checked. |
| 400 | There is already an active reservation with this reference at this location. |
Duplicate reference at that location. |
| 400 | cannot create a reservation for a counter |
An account user tried to reserve at a bob-box-counter location. |
| 400 | invalid contact_number |
contact_number could not be read as a phone number at all. |
| 400 | contact_number must be a valid South African phone number |
The number was read, but is not valid for South Africa. |
| 400 | invalid custom_data |
custom_data could not be turned into JSON. |
| 400 | parcel_count cannot be negative |
A negative parcel_count was sent. |
| 400 | account_id is required when creating a reservation |
Your token carries no account, so there is nothing to bill the reservation to. |
| 400 | counter is not available |
A counter location has reached its shipment limit. |
| 400 | reference must be at most 255 characters / contact_name must be at most 255 characters / custom_data must be at most 4096 bytes when serialized |
A length limit was exceeded. |
| 403 | this location is not available for the specified account |
The location is held for another account. Treat it as if it does not exist. |
| 403 | account is frozen |
New reservations are limited while an account is frozen. Existing reservations are unaffected. |