PT EN
Sign in Sign up →
Open Delivery standard (ABRASEL)

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:

  1. The merchant (end customer) generates a client_id + client_secret in its panel and sends them to you.
  2. You exchange those credentials for an access token via OAuth2 (client credentials).
  3. You create deliveries, simulate prices, mark orders ready for pickup, cancel — all with the token in the header.
  4. You receive webhooks on every status change (accepted, picked up, delivered, etc.).

Exchanging information with the merchant

🔁 You send to the merchant

🔐 The merchant sends to you

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
Official partner apps receive a dedicated base URL. The endpoints stay identical — only the host changes. Request publication through the Overview > Submit for review tab of your app.

🔑 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.

POST /oauth/token 200

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.

POST /v1/logistics/availability 200 400, 404 503

Request body — main fields

FieldTypeDescription
pickupAddress requiredobject · AddressLogisticsPickup address at the merchant.
deliveryAddress requiredobject · AddressLogisticsDelivery address of the consumer.
vehicle requiredobject · VehicleAccepted vehicle types.
limitTimes requiredobjectpickupLimit and deliveryLimit in minutes.
totalOrderPrice requiredobject · PriceTotal order amount shown to the consumer.
totalWeight requiredintegerTotal weight in grams.
returnToMerchant requiredbooleanDoes the courier return to the pickup location after delivering?
canCombine requiredbooleanAllow combining with other orders of the same courier?
merchant optionalobjectThe merchant's id + name.
orderDeliveryFee optionalobject · PriceDelivery fee already charged to the consumer.
packageVolume optionalnumberVolume in m³.
packageQuantity optionalintegerNumber of packages.
additionalPricePercentual optionalnumber% above the calculated price you are willing to pay (e.g., 50 = 50%).
specialInstructions optionalstringFree-form instructions for the courier.
onlinePayment optionalbooleanHas 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

FieldTypeDescription
deliveryPriceobjectCalculated delivery price.
vehiclesobjectavailableVehicles, vehiclesAvailable, nextAvailableVehicle (in minutes).
ETAsobjectEstimated 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).

POST /v1/logistics/delivery 202 400, 401, 403, 404, 422 503

Request body — required fields

All the fields of /availability, plus:

FieldTypeDescription
orderId requiredstring (UUID)Order ID generated by the merchant.
orderDisplayId requiredstringID visible to the consumer (e.g., 96b41025).
merchant requiredobjectThe merchant's id and name.
customerName requiredstringRecipient's name.
payments requiredobjectSee below.
customerPhone optionalstringRecipient's phone number (requires consent).
metadata optionalobjectPickup code (metadata.pickUpCode) and data from the source marketplace (metadata.partnerData). See iFood orders.
notifyReadyForPickup optionalbooleanWait for the /readyForPickup endpoint before calling the courier.
confirmationCodeRequired optionalbooleanRequire a confirmation code on delivery.
items optionalarrayList of { name, quantity } for the products.
combinedOrdersIds optionalarray of UUIDsOrders already in progress that can be combined.

The payments object

FieldTypeDescription
methodenumOFFLINEONLINE
wirelessPosbooleanDoes the courier carry a card terminal?
offlineMethod[].typeenumCREDITDEBITMEAL_VOUCHERFOOD_VOUCHERPIXCASHCREDIT_DEBITOTHER
offlineMethod[].amountobject · PriceAmount paid with that method.
changeobject · PriceChange 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"
}
The final status (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

FieldTypeDescription
metadata.partnerData.orderPartnerNamestringName 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.orderDisplayIdstringOrder number on iFood, as shown to the store and to the consumer.
metadata.partnerData.phoneNumberstringiFood 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.pickUpCodestringPickup code the courier presents at pickup.

What we do with each code

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"
    }
  }
}
Receiving iFood orders depends on the merchant having enabled that option in the integration. When it is not enabled, the call is answered with 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.

POST /v1/logistics/readyForPickup/{orderId} 200 400, 401, 403, 404, 422 503

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).

POST /v1/logistics/cancel/{orderId} 202 400, 401, 403, 404, 422 503

Request body

FieldTypeDescription
reason requiredenumCONSUMER_CANCELLATION_REQUESTEDNO_SHOWPROBLEM_AT_MERCHANTHIGH_ACCEPTANCE_TIMEINCORRECT_ORDER_OR_PRODUCT_PICKUPPROBLEM_RESOLUTIONDISCOMBINE_ORDEROTHER
action optionalenumRETURN_TO_STORECANCEL_DELIVERY
message optionalstringFree 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).

GET /v1/logistics/delivery/{orderId} 200 400, 404 503

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).

POST {registered-webhook-url} 204 expected

Required headers

HeaderContent
X-App-IdOur APP-ID (UUID) — always the same; it identifies the logistics service.
X-App-MerchantIdThe merchant.id you shared.
X-App-SignatureHMAC-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.

FieldTypeReq.Description
deliveryIdstring <UUID>requiredDelivery ID generated by the logistics service.
orderIdstring <uuid>requiredThe orderId you sent on creation (the external ID of the address).
orderDisplayIdstringrequiredOrder ID shown to the customer in the ordering application.
merchant.idstring [36..100]requiredUnique merchant identifier. We echo the value sent when that address was created.
merchant.namestring <=500requiredPublic name of the merchant.
event.typeenumrequired PENDINGACCEPTEDREJECTED PICKUP_ONGOINGARRIVED_AT_MERCHANTORDER_PICKED DELIVERY_ONGOINGARRIVED_AT_CUSTOMERORDER_DELIVERED RETURNING_TO_MERCHANTRETURNED_TO_MERCHANT DELIVERY_FINISHEDCANCELLED
event.datetimestring <date-time>requiredWhen the event occurred (ISO 8601 UTC).
event.messagestringoptionalFree-form message with details of the event.
event.rejectionInfoobjectoptionalAccompanies REJECTED/CANCELLED events. Carries reason with the cause.
problemarray<object>optionalList of problems with the delivery. Each item: { reason(enum), message?, datetime, canFinishDeliver(bool), needMerchantAction(bool) }.
customerNamestringrequiredName of the customer at the address.
vehicleobjectconditional{ type(enum), container(enum), containerSize?(enum), instruction?(string) }.
deliveryPriceobjectconditional{ price: { value, currency }, pricingList(NORMAL|DYNAMIC), additionalPricePercentual }.
etaobjectconditional{ updateMethod(OFFLINE|ONLINE), pickupEtaInMinutes, pickupEtaDatetime, deliveryEtaInMinutes, deliveryEtaDatetime, maxDeliveryTime }.
deliveryPersonobjectconditionalWhen a courier is assigned. { id, name, pictureURL?, phone? }.
externalTrackingURLstring <URI>optionalPublic tracking link for the delivery.
combinedOrdersIdsarray<uuid>optionalIDs 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

FieldTypeNotes
country requiredstringISO 3166-1 alpha-2 (e.g., BR).
state requiredstringISO 3166-2 (e.g., BR-SP).
city requiredstring
district requiredstringNeighborhood.
street requiredstring
number requiredstring
postalCode requiredstringPostal code (CEP), with or without a hyphen.
complement requiredstringMay be empty, but the field must be present.
reference optionalstringLandmark.
latitudenumber-90 to 90.
longitudenumber-180 to 180.

Vehicle

FieldTypeNotes
type requiredarray<enum> Accepted types: MOTORBIKE_BAGMOTORBIKE_BOXCARBICYCLESCOOTERVUC. Send more than one to let the logistics operator choose.
container requiredenumNORMALTHERMIC
containerSize optionalenumSMALLMEDIUMLARGEEXTRA_LARGE
instruction optionalstringSpecial instructions for the courier.

Price

FieldTypeNotes
value requirednumberAccepts up to 4 decimal places.
currency requiredstringUse BRL.

Error codes

HTTPWhen
200/202Success. 202 indicates asynchronous processing (final state via webhook).
400Malformed request — invalid JSON or missing required fields.
401Missing, invalid or expired token.
403Valid token, but without permission for the resource.
404orderId not found.
422Valid content, but a business rule prevents it (e.g., canceling an order that has already been delivered).
503Service 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

  1. Open the app in the dashboard.
  2. Go to the Credentials tab.
  3. Copy the client_id and client_secret fields — there is a copy button next to each one.
  4. Use exactly those values in the payload of POST /oauth/token from the authentication section.
In sandbox, this credential pair is FIXED and represents the test merchant already provisioned. In production (after approval), each merchant generates its own pair directly in its panel under Integrações > Open Delivery, and each pair represents a different merchant.

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

  1. Open the app's Webhook tab.
  2. In the Webhook URL field, paste the HTTPS URL where your application receives events (format detailed in Status webhook).
  3. 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.
  4. 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:

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

  1. Members tab > Invite.
  2. Enter your colleague's email.
  3. 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.
  4. 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

How to submit

  1. Open the app in the dashboard.
  2. In the Overview tab, click Submit for review.
  3. 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.
  4. The app moves to the “Awaiting review” status. You receive an email as soon as the team evaluates it.

What changes after approval

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.

Create / access my apps   View the Full API