Delivery flow
Learn how quotes become paid transfers and how to track their state.
A quote and a transfer are separate resources. The quote discovers available products and prices; the transfer commits to one product and charges the merchant balance.
Quote a delivery
Call POST /options with:
- A pickup: an active
warehouseId, or a custompickupLocation - A destination: the coordinates and address in
destinationLocation, or one of your warehouses asdestinationWarehouseId - A parcel size (
parcelSize) or item dimensions (items) - Fragile and perishable flags
Pickup and destination
Send exactly one pickup and exactly one destination. Any combination works:
destinationLocation | destinationWarehouseId | |
|---|---|---|
warehouseId | Warehouse to address | Warehouse to warehouse |
pickupLocation | Custom address to address | Custom address to warehouse |
A custom pickup is any address, with the same shape as destinationLocation (lat, lng, display_name, city). Who the courier meets there does not affect the price, so you give it later, when you create the transfer.
{
"pickupLocation": {
"lat": 41.7225,
"lng": 44.7925,
"display_name": "37 Chavchavadze Avenue, Tbilisi",
"city": "Tbilisi"
},
"destinationWarehouseId": "33333333-3333-4333-8333-333333333333",
"parcelSize": "SMALL",
"isFragile": false,
"isPerishable": false
}Every warehouse you send, as pickup or destination, must belong to your merchant and be active, and the pickup and destination cannot be the same warehouse. Another merchant's warehouse returns 403 UNAUTHORIZED.
You are always the paying merchant
Whichever pickup and destination you choose, the transfer belongs to the merchant that requested the quote. Its balance is charged and refunded, its webhooks fire, and only it can read or cancel the transfer. A custom pickup does not change this.
When the destination is a warehouse, recipientInfo is still required on POST /transfer. It is the person Melo notifies and the courier calls on arrival.
Parcel sizes
Send parcelSize directly, or let Melo derive it from items.
| Value | Name | Description | Max dimensions |
|---|---|---|---|
SMALL | Pocket Parcel | Small items like documents, keys, jewelry, or phone accessories. | 20cm × 15cm × 5cm |
MEDIUM | Bag Parcel | Medium items like clothing, books, or small electronics. | 40cm × 30cm × 20cm |
LARGE | Box Size | Large items like appliances, furniture parts, or multiple items. | 80cm × 60cm × 40cm |
CARGO | Cargo Freight | Very large or heavy items requiring special handling. | Custom dimensions and weight specifications |
parcelSize is required unless items is sent with at least one entry. If both are present, parcelSize is used and items is ignored.
Deriving size from items
Instead of parcelSize, send items — one entry per distinct item, each with a dimensions object (length, width, height in centimeters, all positive numbers), an optional quantity (integer, minimum 1, default 1), and an optional title (up to 255 characters, not currently used). dimensions is required on every entry unless parcelSize is also sent:
"items": [
{ "dimensions": { "length": 20, "width": 15, "height": 10 }, "quantity": 2, "title": "Phone case" },
{ "dimensions": { "length": 12, "width": 8, "height": 4 } }
]Each entry's dimensions are counted quantity times, then summed independently across all entries into one combined bounding box, which is classified (checked in any rotation) against the max dimensions above. In the example above, the first entry contributes 40×30×20 (20×15×10 doubled for quantity: 2) and the second contributes 12×8×4, summing to 52×38×24 — which classifies as LARGE.
The response contains a quote id and a products object of available delivery options — unrelated to the request's items array. Each key in the response's products is a valid productKey for transfer creation.
Persist the exact pair
Keep the quote id and selected product key together. A product key is meaningful only within the quote that returned it.
Quotes expire after 10 minutes
A quote can only be turned into a transfer within 10 minutes of being created. After that POST /transfer returns 400 QUOTE_EXPIRED and you must request a fresh quote. Quote at the point of purchase rather than caching prices.
Create and pay
Call POST /transfer using the quote ID as matchmakingId, the selected productKey, and recipient details.
If the quote starts from a custom pickupLocation, also send pickupContact: who the courier meets there, in the same shape as recipientInfo (firstName, lastName and a phone the courier can call). A warehouse supplies its own contact details, so pickupContact is required for a custom pickup and rejected for a warehouse pickup.
{
"matchmakingId": "22222222-2222-4222-8222-222222222222",
"productKey": "YOUR_PRODUCT_KEY",
"recipientInfo": { "firstName": "Nino", "lastName": "Beridze", "phone": "+995555123456" },
"pickupContact": { "firstName": "Giorgi", "lastName": "Kapanadze", "phone": "+995555987654" }
}Getting this wrong returns 400 PICKUP_CONTACT_REQUIRED or 400 PICKUP_CONTACT_NOT_ALLOWED, and nothing is created: the quote stays pending, so correct the request and send it again within the quote's ten minutes.
The operation performs the following work atomically:
- Validates that the quote exists, is still pending, and has not expired.
- Resolves the selected product and payable amount.
- Creates the transfer, parcel, delivery metadata, and tracking steps.
- Subtracts the amount from the merchant balance.
- Marks the quote as completed.
A completed quote cannot be used again. If the request succeeds but your client loses the response, do not blindly retry with the same quote. Query your internal records or contact Melo support before creating a new quote.
Track state
Call GET /transfer/{transferId} to retrieve the current state:
| State | Meaning |
|---|---|
inProgress | The delivery is active. |
completed | The delivery reached its final successful state. |
failed | The delivery ended unsuccessfully. |
The response also contains the public trackingCode, the parcel size, pickup and destination details, and the selected delivery option. Pickup and destination fields are omitted when a transfer has no delivery metadata.
warehouseId is the pickup warehouse and is null for a custom pickup. destinationWarehouseId is the destination warehouse and is null when the transfer was delivered to an address. The pickup and destination address fields are filled in either way.
Steps
GET /transfer/{transferId} also returns a steps array — the transfer's fulfilment history, which is more precise than state:
| Field | Description |
|---|---|
step | created, payed, shipped, or delivered. |
order | Position in the workflow, 0 through 3. |
status | completed, pending, or failed. |
occuredAt | When the step reached its current status. |
A delivery is finished when delivered is completed. Any step reaching failed also moves state to failed.
You can be pushed these updates
Rather than polling, register a webhook to receive an event each time a transfer's steps change. See Webhooks.
Cancel a transfer
Call POST /transfer/{transferId}/cancel to cancel a transfer before shipping starts. Send an empty JSON object ({}) as the body.
A transfer can be cancelled only while it is at the created or payed step. Once a courier picks up the parcel, it can no longer be cancelled.
| Current step | Cancellable |
|---|---|
created (awaiting payment) | Yes |
payed (paid, waiting for courier pickup) | Yes |
shipped (courier picked up the parcel) | No |
delivered | No |
| Already failed or cancelled | No |
On cancel:
- The first pending step gets status
failed. For a paid transfer, that is theshippedstep. - The transfer
statebecomesfailed. - If the transfer was paid, its price is refunded to your merchant balance.
- A webhook event fires, with the same payload as any other step change.
The response contains the transfer id, state, and steps. A later GET /transfer/{transferId} returns the same values.
curl -X POST "https://api.melo.delivery/v1/transfer/3f1c2a8e-5b7d-4e0a-9c6f-1a2b3c4d5e6f/cancel" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'Cancelling after pickup
Once shipping has started, the request returns 409 TRANSFER_NOT_CANCELLABLE. Do not retry it; contact Melo support instead.
Recommended integration behavior
- Store Melo's transfer ID with your own order ID.
- Store the tracking code for customer-facing support.
- Poll with backoff rather than on every page view, and stay within the rate limits described in Errors.
- Prefer webhooks for timely updates, reconciling with a periodic poll.
- Treat network timeouts as an unknown outcome, especially for transfer creation.
- Log Melo error
typevalues, but redact credentials and personal data.