Open Delivery Integration
Complete technical documentation for our Open Delivery API. The integration follows the ABRASEL
specification — if you have already implemented the standard on another logistics platform, you reuse the same code
and change only the base URL, client_id and client_secret.
Introduction
The Entregas Expressas Open Delivery API lets software built on the Open Delivery specification integrate with our platform without new development. The typical flow:
- The merchant (end customer) generates a
client_id+client_secretin its panel and sends them to you. - You exchange those credentials for an access token via OAuth2 (client credentials).
- You create deliveries, simulate prices, mark orders ready for pickup, cancel — all with the token in the header.
- You receive webhooks on every status change (accepted, picked up, delivered, etc.).
Exchanging information with the merchant
🔁 You send to the merchant
- Merchant ID — the merchant's identifier in your system (UUID recommended, up to 100 chars).
- Webhook URL — the URL where you will receive update events.
🔐 The merchant sends to you
- Client ID — generated in the Entregas Expressas panel.
- Client Secret — treat it like a password; never expose it in the front end.
Each merchant generates its own pair of credentials — do NOT share credentials between different merchants. The customer fills everything in under Integrações > Open Delivery (Integrations > Open Delivery) and checks “ativar integração” (enable integration).
Base URL & APP-ID
Default base URL:
https://entregasexpressas.com.br/api/opendelivery
Our APP-ID:
d3e3eaf2-e585-4177-962a-7be998efab8e
🔑 Authentication
Standard OAuth 2.0 client_credentials. It issues a JWT access token valid for 24h
(expires_in: 86400). Send the token in the Authorization: Bearer <token> header
of every subsequent call.
Request body
{
"grant_type": "client_credentials",
"client_id": "<merchant_client_id>",
"client_secret": "<merchant_client_secret>"
}
Response 200
{
"access_token": "eyJhbGciOiJIUzI1NiI...",
"token_type": "bearer",
"expires_in": 86400
}
cURL example
curl -X POST https://entregasexpressas.com.br/api/opendelivery/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "...",
"client_secret": "..."
}'
📊 Simulate a delivery (availability and price)
Before creating a delivery, simulate the price, ETAs and vehicle availability. Useful for showing the delivery fee in the merchant's checkout before the consumer completes the purchase.
Request body — main fields
| Field | Type | Description |
|---|---|---|
pickupAddress required | object · AddressLogistics | Pickup address at the merchant. |
deliveryAddress required | object · AddressLogistics | Delivery address of the consumer. |
vehicle required | object · Vehicle | Accepted vehicle types. |
limitTimes required | object | pickupLimit and deliveryLimit in minutes. |
totalOrderPrice required | object · Price | Total order amount shown to the consumer. |
totalWeight required | integer | Total weight in grams. |
returnToMerchant required | boolean | Does the courier return to the pickup location after delivering? |
canCombine required | boolean | Allow combining with other orders of the same courier? |
merchant optional | object | The merchant's id + name. |
orderDeliveryFee optional | object · Price | Delivery fee already charged to the consumer. |
packageVolume optional | number | Volume in m³. |
packageQuantity optional | integer | Number of packages. |
additionalPricePercentual optional | number | % above the calculated price you are willing to pay (e.g., 50 = 50%). |
specialInstructions optional | string | Free-form instructions for the courier. |
onlinePayment optional | boolean | Has the payment already been made in the merchant's app? |
Request body — example
{
"merchant": {
"id": "22815773000169-dbc7e35a-c936-4665-9e13-eb55eb8b6824",
"name": "Pizza Plaza"
},
"pickupAddress": {
"country": "BR", "state": "BR-SP", "city": "São Paulo",
"district": "Moema", "street": "Av. Plaza", "number": "100",
"postalCode": "20111-000", "complement": "",
"latitude": -23.54809, "longitude": -46.63638
},
"deliveryAddress": {
"country": "BR", "state": "BR-SP", "city": "São Paulo",
"district": "Vila Mariana", "street": "R. Domingos de Morais", "number": "1267",
"postalCode": "04009-002", "complement": "Apto 31",
"latitude": -23.59045, "longitude": -46.63876
},
"vehicle": {
"type": ["MOTORBIKE_BAG"],
"container": "THERMIC",
"containerSize": "MEDIUM"
},
"limitTimes": {
"pickupLimit": 20,
"deliveryLimit": 45,
"orderCreatedAt": "2026-05-17T14:32:00Z"
},
"totalOrderPrice": { "value": 89.90, "currency": "BRL" },
"totalWeight": 1200,
"returnToMerchant": false,
"canCombine": true,
"onlinePayment": true
}
Response 200 — returned fields
| Field | Type | Description |
|---|---|---|
deliveryPrice | object | Calculated delivery price. |
vehicles | object | availableVehicles, vehiclesAvailable, nextAvailableVehicle (in minutes). |
ETAs | object | Estimated times (pickup & delivery, online or offline). |
{
"deliveryPrice": { "value": 12.50, "currency": "BRL" },
"vehicles": {
"availableVehicles": 4,
"nextAvailableVehicle": 0
},
"ETAs": {
"updateMethod": "ONLINE",
"pickupEtaInMinutes": 8,
"pickupEtaDatetime": "2026-05-17T14:40:00Z",
"deliveryEtaInMinutes": 32,
"deliveryEtaDatetime": "2026-05-17T15:04:00Z",
"maxDeliveryTime": "2026-05-17T15:17:00Z"
}
}
📦 Create a new delivery
Registers a new delivery request. Returns 202 Accepted — the final acceptance
arrives via webhook (ACCEPTED or REJECTED).
Request body — required fields
All the fields of /availability, plus:
| Field | Type | Description |
|---|---|---|
orderId required | string (UUID) | Order ID generated by the merchant. |
orderDisplayId required | string | ID visible to the consumer (e.g., 96b41025). |
merchant required | object | The merchant's id and name. |
customerName required | string | Recipient's name. |
payments required | object | See below. |
customerPhone optional | string | Recipient's phone number (requires consent). |
metadata optional | object | Pickup code (metadata.pickUpCode) and data from the source marketplace (metadata.partnerData). See iFood orders. |
notifyReadyForPickup optional | boolean | Wait for the /readyForPickup endpoint before calling the courier. |
confirmationCodeRequired optional | boolean | Require a confirmation code on delivery. |
items optional | array | List of { name, quantity } for the products. |
combinedOrdersIds optional | array of UUIDs | Orders already in progress that can be combined. |
The payments object
| Field | Type | Description |
|---|---|---|
method | enum | OFFLINEONLINE |
wirelessPos | boolean | Does the courier carry a card terminal? |
offlineMethod[].type | enum | CREDITDEBITMEAL_VOUCHERFOOD_VOUCHERPIXCASHCREDIT_DEBITOTHER |
offlineMethod[].amount | object · Price | Amount paid with that method. |
change | object · Price | Change due (if type=CASH). |
Response 202 — request accepted for processing
{
"deliveryId": "8f3e9c20-aa14-4f5d-92b2-9bca6e0a3e10",
"orderId": "52122852-6626-11ed-9022-0242ac120002",
"status": "PENDING",
"createdAt": "2026-05-17T14:32:05Z"
}
ACCEPTED or REJECTED) arrives via
webhook. If it is REJECTED, the event carries rejectionInfo.reason
(PRICE_EXCEEDED, VEHICLE_NOT_AVAILABLE, NO_DELIVERYPERSON_AVAILABLE,
DOES_NOT_MEET_REQUESTED_TIMES, REGION_NOT_SERVED, OTHER).
🍔 iFood orders (and orders from other marketplaces)
When the order originated in a marketplace and your system hands the delivery over to us, send the source
and its codes in the metadata object of POST /v1/logistics/delivery.
They are what the courier uses to complete the delivery on iFood, and what the merchant uses to find the order
in its panel.
metadata fields
| Field | Type | Description |
|---|---|---|
metadata.partnerData.orderPartnerName | string | Name of the source marketplace. For iFood, send exactly iFood (with this exact upper and lower case). This value identifies the order as an iFood order. |
metadata.partnerData.orderDisplayId | string | Order number on iFood, as shown to the store and to the consumer. |
metadata.partnerData.phoneNumber | string | iFood contact phone number with the order's locator code. The locator code has 8 digits and must be preceded by the word localizador, for example 0800 200 5011, localizador: 93898428. |
metadata.pickUpCode | string | Pickup code the courier presents at pickup. |
What we do with each code
- Locator code: when the delivery is completed, the courier app shows the 8-digit code with a shortcut to iFood's delivery confirmation page. If the text does not contain the locator code in this format, the delivery proceeds normally, just without that shortcut.
- Order number, phone number and pickup code: they appear in the delivery notes, in the panel and in the courier app.
- The root-level
orderIdandorderDisplayIdremain the IDs of your system. Do not replace them with the iFood IDs: theorderIdis how we prevent duplicate deliveries and how you query, cancel and mark an order ready for pickup.
Example
{
"orderId": "52122852-6626-11ed-9022-0242ac120002",
"orderDisplayId": "4821",
"merchant": { "id": "loja-123", "name": "Hamburgueria Central" },
...
"metadata": {
"pickUpCode": "7391",
"partnerData": {
"orderPartnerName": "iFood",
"orderDisplayId": "6512",
"phoneNumber": "0800 200 5011, localizador: 93898428"
}
}
}
202 and "event": "REJECTED",
and the delivery is not created. An order without metadata.partnerData is treated as the store's own order.
🛎️ Report ready for pickup
Use it when the order is ready ahead of the estimated time and you want to call the courier earlier.
Only required if you set notifyReadyForPickup: true when creating the delivery.
No body. The orderId in the URL is the same UUID sent in POST /v1/logistics/delivery.
❌ Cancel a delivery
Can be called at any time. If the courier has already accepted, the logistics operator may charge an additional fee according to its policy. If the order is part of a combined delivery, it is detached (the other orders proceed normally).
Request body
| Field | Type | Description |
|---|---|---|
reason required | enum | CONSUMER_CANCELLATION_REQUESTEDNO_SHOWPROBLEM_AT_MERCHANTHIGH_ACCEPTANCE_TIMEINCORRECT_ORDER_OR_PRODUCT_PICKUPPROBLEM_RESOLUTIONDISCOMBINE_ORDEROTHER |
action optional | enum | RETURN_TO_STORECANCEL_DELIVERY |
message optional | string | Free text with details. |
Response 202
{
"additionalCharges": false
}
additionalCharges indicates whether the cancellation incurs an additional charge.
🔍 Delivery details
Retrieves the full state of a delivery — the last attempt processed (in case of retries).
Returns an object with all the fields of the DeliveryOrder (the one you sent),
plus the event history, courier data (deliveryMan) and location (geoLocalization)
when available.
📬 Status webhook
On every change in the delivery state, we send a POST to the Webhook URL
configured by the merchant. Respond with HTTP 204 within 10s — anything
else triggers a retry with backoff.
Important: we send the POST to the Webhook URL exactly
as registered, without appending any route suffix. If your endpoint expects to receive
the events at /v1/newLogisticEvent, register that path already included in the
Webhook URL itself (e.g., https://your-domain/v1/opendelivery/webhook/v1/newLogisticEvent).
Required headers
| Header | Content |
|---|---|
X-App-Id | Our APP-ID (UUID) — always the same; it identifies the logistics service. |
X-App-MerchantId | The merchant.id you shared. |
X-App-Signature | HMAC-SHA256 of the body using the client_secret as the key. Validate it to ensure authenticity. |
Payload — main fields
The event type, timestamp and message are nested inside the
event object (not at the top level). The fields below are always sent; the
operational fields (vehicle, deliveryPrice, eta,
deliveryPerson) only appear in progress events — not
in PENDING, REJECTED or CANCELLED.
The structure follows the official OpenDelivery newLogisticEvent schema. The
operational fields (vehicle, deliveryPrice, eta,
deliveryPerson) are conditional: they accompany progress events,
but not PENDING, REJECTED or CANCELLED.
| Field | Type | Req. | Description |
|---|---|---|---|
deliveryId | string <UUID> | required | Delivery ID generated by the logistics service. |
orderId | string <uuid> | required | The orderId you sent on creation (the external ID of the address). |
orderDisplayId | string | required | Order ID shown to the customer in the ordering application. |
merchant.id | string [36..100] | required | Unique merchant identifier. We echo the value sent when that address was created. |
merchant.name | string <=500 | required | Public name of the merchant. |
event.type | enum | required | PENDINGACCEPTEDREJECTED PICKUP_ONGOINGARRIVED_AT_MERCHANTORDER_PICKED DELIVERY_ONGOINGARRIVED_AT_CUSTOMERORDER_DELIVERED RETURNING_TO_MERCHANTRETURNED_TO_MERCHANT DELIVERY_FINISHEDCANCELLED |
event.datetime | string <date-time> | required | When the event occurred (ISO 8601 UTC). |
event.message | string | optional | Free-form message with details of the event. |
event.rejectionInfo | object | optional | Accompanies REJECTED/CANCELLED events. Carries reason with the cause. |
problem | array<object> | optional | List of problems with the delivery. Each item: { reason(enum), message?, datetime, canFinishDeliver(bool), needMerchantAction(bool) }. |
customerName | string | required | Name of the customer at the address. |
vehicle | object | conditional | { type(enum), container(enum), containerSize?(enum), instruction?(string) }. |
deliveryPrice | object | conditional | { price: { value, currency }, pricingList(NORMAL|DYNAMIC), additionalPricePercentual }. |
eta | object | conditional | { updateMethod(OFFLINE|ONLINE), pickupEtaInMinutes, pickupEtaDatetime, deliveryEtaInMinutes, deliveryEtaDatetime, maxDeliveryTime }. |
deliveryPerson | object | conditional | When a courier is assigned. { id, name, pictureURL?, phone? }. |
externalTrackingURL | string <URI> | optional | Public tracking link for the delivery. |
combinedOrdersIds | array<uuid> | optional | IDs of other orders from the same merchant carried by the same courier. |
Example — acceptance event
{
"deliveryId": "32a20413-d5da-4b5d-a7f1-7a36e7f3495f",
"orderId": "2003",
"orderDisplayId": "12",
"merchant": { "id": "2001", "name": "Loja Exemplo" },
"event": {
"type": "ACCEPTED",
"datetime": "2026-05-17T14:32:18.000000Z",
"message": "Pedido aceito pelo serviço de entrega"
},
"problem": [],
"customerName": "João da Silva",
"vehicle": {
"type": "MOTORBIKE_BAG",
"container": "THERMIC",
"containerSize": "MEDIUM",
"instruction": "N/A"
},
"deliveryPrice": {
"price": { "value": 12.5, "currency": "BRL" },
"pricingList": "NORMAL",
"additionalPricePercentual": 0
},
"eta": {
"updateMethod": "OFFLINE",
"pickupEtaInMinutes": 15,
"pickupEtaDatetime": "2026-05-17T14:47:18.000000Z",
"deliveryEtaInMinutes": 30,
"deliveryEtaDatetime": "2026-05-17T15:02:18.000000Z",
"maxDeliveryTime": "2026-05-17T16:02:18.000000Z"
},
"deliveryPerson": {
"id": "1138",
"name": "Carlos Silva",
"phone": "5511999990000",
"pictureURL": "https://cdn.exemplo.com/couriers/1138.jpg"
},
"externalTrackingURL": "https://seu-dominio/tracking/32a20413-d5da-4b5d-a7f1-7a36e7f3495f",
"combinedOrdersIds": []
}
Example — rejection event
{
"deliveryId": "32a20413-d5da-4b5d-a7f1-7a36e7f3495f",
"orderId": "2003",
"orderDisplayId": "12",
"merchant": { "id": "2001", "name": "Loja Exemplo" },
"event": {
"type": "REJECTED",
"datetime": "2026-05-17T14:33:02.000000Z",
"message": "Pedido fora do raio de entrega",
"rejectionInfo": { "reason": "REGION_NOT_SERVED" }
},
"problem": [],
"customerName": "João da Silva",
"externalTrackingURL": "https://seu-dominio/tracking/32a20413-d5da-4b5d-a7f1-7a36e7f3495f",
"combinedOrdersIds": []
}
Validating the signature (Node.js)
const crypto = require('crypto');
function validateSignature(rawBody, headerSignature, clientSecret) {
const expected = crypto
.createHmac('sha256', clientSecret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(headerSignature, 'hex')
);
}
Auxiliary schemas
AddressLogistics
| Field | Type | Notes |
|---|---|---|
country required | string | ISO 3166-1 alpha-2 (e.g., BR). |
state required | string | ISO 3166-2 (e.g., BR-SP). |
city required | string | |
district required | string | Neighborhood. |
street required | string | |
number required | string | |
postalCode required | string | Postal code (CEP), with or without a hyphen. |
complement required | string | May be empty, but the field must be present. |
reference optional | string | Landmark. |
latitude | number | -90 to 90. |
longitude | number | -180 to 180. |
Vehicle
| Field | Type | Notes |
|---|---|---|
type required | array<enum> | Accepted types: MOTORBIKE_BAGMOTORBIKE_BOXCARBICYCLESCOOTERVUC. Send more than one to let the logistics operator choose. |
container required | enum | NORMALTHERMIC |
containerSize optional | enum | SMALLMEDIUMLARGEEXTRA_LARGE |
instruction optional | string | Special instructions for the courier. |
Price
| Field | Type | Notes |
|---|---|---|
value required | number | Accepts up to 4 decimal places. |
currency required | string | Use BRL. |
Error codes
| HTTP | When |
|---|---|
| 200/202 | Success. 202 indicates asynchronous processing (final state via webhook). |
| 400 | Malformed request — invalid JSON or missing required fields. |
| 401 | Missing, invalid or expired token. |
| 403 | Valid token, but without permission for the resource. |
| 404 | orderId not found. |
| 422 | Valid content, but a business rule prevents it (e.g., canceling an order that has already been delivered). |
| 503 | Service temporarily unavailable — retry with backoff. |
Using the developer dashboard
The whole development cycle happens inside the portal: you create the app, receive sandbox credentials, configure the webhook, inspect requests and simulate orders without having to coordinate with a real courier. The steps below follow the natural order of the work.
1. Create an OpenDelivery app
In the dashboard, click Apps > New app and choose the OpenDelivery type. As soon as the app is created, we automatically provision a sandbox merchant in a dedicated hub, with the integration already enabled. There is no manual “connect to a customer” step — the sandbox exists so you can develop without depending on a real customer.
2. Where to get the test client_id and client_secret
- Open the app in the dashboard.
- Go to the Credentials tab.
- Copy the
client_idandclient_secretfields — there is a copy button next to each one. - Use exactly those values in the payload of POST /oauth/token from the authentication section.
Need to regenerate the client_secret? Use the Regenerate credentials button in the
Credentials tab itself. The old pair stops working immediately — update your application before regenerating.
3. Configure the webhook URL
- Open the app's Webhook tab.
- In the Webhook URL field, paste the HTTPS URL where your application receives events (format detailed in Status webhook).
- Check “Allow the merchant to override this URL” if you want to give that option to the end customer — in production, some merchants have their own webhook (POS system, ERP). If you leave it unchecked, the URL is fixed for everyone.
- Click Save. The change takes effect immediately — no deploy is needed on our side.
4. Inspect received requests
The Inspection tab shows every request our platform made to your API (and, in some cases, what your API responded). For each one:
- Method, full path, status code and latency.
- Headers sent and the full payload (request body).
- Filters by endpoint and by status — useful for viewing only 4xx errors or only webhooks.
Use it to debug OAuth, validate payloads, and find the cause of a 4xx your application returned without having to dig through your own logs.
5. Simulate orders and trigger webhooks
Test orders tab: lists the orders created via API in the sandbox merchant. For each one, you can advance the status manually (accept, assign a courier, mark as picked up, delivered, or trigger a rejection). Each change sends the real webhook to your URL — you test the handlers on your side without coordinating a real courier or waiting minutes.
6. Invite other developers on your team
- Members tab > Invite.
- Enter your colleague's email.
- If they already have a portal account, they receive a link to accept. If not, they receive a link to sign up already linked to your app.
- Members see everything: credentials, webhook, inspection, simulation.
7. Adjust the app's name, description, logo and website
Settings tab: app name, a clear description of what it does, public website, and logo (PNG/SVG). This data appears in the public showcase and on the screen where the merchant enables your integration — a clear description and a sharp logo have a direct effect on conversion. The three fields (description, app website, logo) are required to submit the app for approval.
Submit for approval
Once your implementation is working in sandbox, you submit the app for approval by the Plataforma Entregas Expressas team. When approved, the app becomes visible to the 30,000+ merchants that use the platform — they find your app in the public showcase and connect from their own panel, generating real credentials.
Prerequisites before submitting
- Final app name (this is the name the merchant will see).
- A clear description — what your integration does, what the merchant gains.
- App website — a public page where the merchant can understand your product.
- Logo — it will appear in the showcase and on the activation screen.
- Working OAuth: you can exchange
client_id+client_secretfor a token at POST /oauth/token. - Delivery creation tested at POST /delivery.
- Webhook being received, with the HMAC signature validated on your side.
How to submit
- Open the app in the dashboard.
- In the Overview tab, click Submit for review.
- If any of the required fields is empty, the portal shows exactly what is missing before enabling the button — fill it in under Settings and come back.
- The app moves to the “Awaiting review” status. You receive an email as soon as the team evaluates it.
What changes after approval
- The app starts appearing in the public showcase and on the Integrações > Open Delivery screen each merchant sees in its panel.
- Each merchant that enables your integration generates its own pair of
client_id+client_secretin its panel — your application needs to accept multiple pairs and route them by yourmerchantId. - Official partner apps may receive a dedicated base URL (dedicated host). Ask the team if that is relevant for you.
If it is rejected
The reason appears in the dashboard (Overview tab) and by email. The most common fixes are: a vague description, a low-quality logo, a webhook that does not validate the signature, or a partial implementation of the required endpoints. Fix it and click Submit for review again — there is no limit on attempts.