Receber pedidos (webhook)
Esta página trata dos eventos de pedido especificamente. O registro do endpoint, os limites do mecanismo e o formato geral do envelope estão em Registrar o webhook.
O caminho do pedido até o seu ERP
O diagrama responde: por onde passa um pedido entre o checkout e o meu endpoint, e por que ele chega com o nome de evento que chega?
Por isso um switch (eventType) que só cria pedido em Order_Created nunca cria
nenhum: o único status que produz esse nome (100) fica abaixo do limiar de notificação,
que começa em PAYMENT_ERROR (250).
E, pelo mesmo motivo, Order_Updated não significa “pedido novo”. Ele é o nome de
catorze situações diferentes, da recusa de pagamento ao estorno concluído. Quem decide o
que fazer é o data.status — a lista fechada está em
Quais valores de data.status podem chegar.
Assinatura para pedidos
Os eventos que interessam a quem recebe pedido no ERP:
{
"eventTypes": [
"Order_Created",
"Order_Updated",
"Order_Cancelled",
"Order_CancelRequested",
"Order_StatusUpdated",
"Order_InvoiceCreated",
"Order_InvoiceUploaded",
"Order_CreationFailed",
"Order_UpdateFailed",
"Order_StatusUpdateFailed",
"Order_CancelFailed",
"Operation_Failed"
]
}Assine a lista inteira, inclusive os eventos que “não vão chegar” — a assinatura é conferida evento a evento no momento da notificação, e o que não está na lista é descartado sem entrega, sem retentativa e sem DLQ. Assinar demais não custa nada; assinar de menos produz silêncio indistinguível de “não aconteceu”.
authToken protege o seu endpoint: a plataforma envia esse valor no header
Authorization de cada entrega. Valide-o antes de processar — sem isso, qualquer um
que descubra sua URL pode injetar pedidos falsos no ERP. O authType e o authToken
são definidos no registro — veja Registrar o webhook.
Eventos de pedido
O que efetivamente chega no seu endpoint
| Código | Evento | Quando dispara |
|---|---|---|
14 | Order_Updated | toda mudança de estado do pedido a partir de PAYMENT_ERROR (250): pagamento aprovado, faturamento, cancelamento, estorno, alteração de itens — e também as mudanças que o seu próprio PATCH faz. É o evento que traz o pedido novo. Os catorze valores possíveis estão em Quais valores de data.status podem chegar |
118 | Order_StatusUpdateFailed | o core recusou o seu PATCH /v1/Orders/{orderNumber}/status com um 4xx |
132 | Order_CancelFailed | o core recusou o seu POST /v1/Orders/{orderNumber}/cancel com um 4xx |
999 | Operation_Failed | o core recusou com 4xx uma operação para a qual não existe evento de falha específico — inclui PUT /v1/Orders/invoiced/{orderNumber} e POST /v1/Storages/invoice/upload |
Quais valores de data.status podem chegar
Esta é a lista fechada. Um Order_Updated traz sempre um destes catorze valores em
data.status — não há outros.
data.status | Nome | O que aconteceu | O que o seu ERP faz |
|---|---|---|---|
250 | PAYMENT_ERROR | pagamento recusado | ignore. Não crie pedido e não reserve estoque |
300 | PAYMENT_APPROVED | pagamento confirmado | crie o pedido. É o único valor que autoriza criar |
350 | ORDER_INVOICED | nota registrada e valor capturado | marque como faturado |
400 | ORDER_BEING_PREPARED | eco do seu PATCH | compare com o estado que você já tem; igual, não faça nada |
500 | READY_FOR_PICKUP_OR_DELIVERY | eco do seu PATCH | idem |
600 | SHIPPED | eco do seu PATCH | idem |
700 | DELIVERED | eco do seu PATCH | idem |
750 | ORDER_CANCELLATION_ANALYSIS | cancelamento em andamento, aguardando estorno | pare a separação; não crie pedido se o number for desconhecido |
800 | ORDER_CANCELLED | cancelado | cancele no ERP; não crie pedido se o number for desconhecido |
900 | DELIVERY_FAILED | eco do seu PATCH | compare e ignore se igual |
1000 | ORDER_RETURNED | eco do seu PATCH | idem |
1100 | REFUND_IN_PROGRESS | estorno iniciado pela plataforma | registre o estorno em andamento |
1200 | REFUND_COMPLETED | estorno concluído pela plataforma | encerre o financeiro do pedido |
1300 | OrderUpdated | o seu PUT /v1/Orders/{orderNumber} terminou: itens e valores foram recalculados e a cobrança ajustada | não grave 1300 como status do pedido. Releia os valores e siga com o status que você já tinha |
O envelope
Todo evento — de pedido, de catálogo, de estoque ou de preço — chega com os mesmos seis campos no primeiro nível:
| Campo do envelope | Sempre presente | Uso no seu ERP |
|---|---|---|
operationId | sim | correlaciona com o lote/operação que originou o evento — guarde-o, é a chave para investigar qualquer problema com o suporte |
contractAccountId | sim | tenant — valide que é o seu |
relatedEntity | sim | tipo da entidade — Order nos eventos desta página; os demais valores são ProductWithSKUs, Category, Brand, Warehouse, WarehouseSKU, SKUPricing e User |
eventType | sim | o nome do evento, em texto |
data | sim | o payload da entidade |
error | só em falhas | diagnóstico — ver Registrar o webhook |
Não existe outro campo no envelope. Não há timestamp, não há assinatura HMAC, não há
identificador de entrega. A chave de deduplicação sai de dentro do data — veja a seção
Deduplicação, mais abaixo nesta página.
O payload de pedido
Este é o data de um Order_Updated de pedido aprovado, com os campos que importam para
a integração. O payload real é maior, por três motivos:
- campos nulos são enviados — a serialização não omite
null; - os blocos das modalidades que você não usa chegam junto: um pedido de entrega agendada
traz
expressDeliveryepickupPointcomonull, eacceptedOrderDetailscarrega os campos de expressa e de retirada zerados ou nulos; - alguns blocos vêm mais completos do que o exemplo:
subtotal.appliedPromotions[]traz a promoção inteira (vigência, escopo, cumulatividade) eacceptedOrderDetailstraz osellercompleto e o endereço do ponto de retirada.
Trate campo desconhecido como campo ignorado. Não valide o payload por lista fechada de chaves e não rejeite entrega por causa de campo a mais — a plataforma acrescenta campos sem aviso prévio.
{
"operationId": "7f3a1b2c-4d5e-4f60-8a71-9b0c1d2e3f40",
"contractAccountId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"relatedEntity": "Order",
"eventType": "Order_Updated",
"data": {
"id": "a1b2c3d4-e5f6-4708-9a1b-2c3d4e5f6071",
"number": 104821,
"status": 300,
"createdAt": "2026-07-29T11:30:02",
"updatedAt": "2026-07-29T11:32:10",
"deviceId": null,
"origin": 1,
"deliveryType": 1,
"currency": "BRL",
"itemAmount": 3,
"canBeCancelled": false,
"invoiceUrl": null,
"couponId": null,
"transactionId": "125300123456",
"acquirerTransactionId": "8471120993",
"user": {
"id": "b2c3d4e5-f607-4819-a02b-3c4d5e6f7081",
"gender": "Prefiro não informar",
"firstName": "João",
"lastName": "Silva",
"email": "joao.silva@email.com",
"document": "12345678900",
"phoneDDI": "55",
"phoneNumber": "21999999999",
"birthDate": "1985-03-14",
"active": true,
"contractAccountId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"contractAccount": null,
"userRole": null
},
"orderAddressId": "c3d4e5f6-0718-492a-b13c-4d5e6f708192",
"orderAddress": {
"id": "c3d4e5f6-0718-492a-b13c-4d5e6f708192",
"country": "Brasil",
"state": "RJ",
"city": "Rio de Janeiro",
"neighborhood": "Centro",
"street": "Av. Rio Branco",
"number": "156",
"zipCode": "20031170",
"addressType": "residence",
"additionalInfo": "Sala 402",
"reference": "Em frente à praça"
},
"items": [
{
"id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
"skuId": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
"ean": "7891234567890",
"reference": "MAC-ESP-500",
"orderedQuantity": 2,
"unitOfMeasure": "Un",
"arithmeticFactor": 1,
"minimumWeightForSale": null,
"brandId": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"categoryId": "8d9e0f1a-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
"commercialPolicyId": "9c1f5a3e-7b2d-4c8e-9f01-2a3b4c5d6e7f",
"promotionName": null,
"salePrice": 8.99,
"arithmeticFactorPrice": 8.99,
"totalPerProduct": 17.98,
"priceStartDateTime": null,
"priceEndDateTime": null,
"manualPrice": 17.98,
"discountApplied": 0
},
{
"id": "9e0f1a2b-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
"skuId": "0f1a2b3c-4d5e-4f6a-9b7c-8d9e0f1a2b3c",
"ean": "2000145000000",
"reference": "PIC-ALC-KG",
"orderedQuantity": 5,
"unitOfMeasure": "Kg",
"arithmeticFactor": 0.1,
"minimumWeightForSale": 0.3,
"brandId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"categoryId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"commercialPolicyId": "9c1f5a3e-7b2d-4c8e-9f01-2a3b4c5d6e7f",
"promotionName": "Quinta da Carne",
"salePrice": 39.9,
"arithmeticFactorPrice": 3.99,
"totalPerProduct": 19.43,
"priceStartDateTime": "2026-07-29T00:00:00",
"priceEndDateTime": "2026-07-31T23:59:59",
"manualPrice": 17.49,
"discountApplied": 1.94
}
],
"orderStatusRegistry": null,
"seller": {
"id": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c",
"name": "Loja Centro",
"country": "Brasil",
"state": "RJ",
"city": "Rio de Janeiro",
"neighborhood": "Centro",
"zipCode": "20031170",
"street": "Av. Rio Branco",
"number": "156",
"complement": "Loja A",
"reference": null,
"phoneNumber": "2133334444",
"description": null,
"sellerCode": "LJ-08",
"active": true
},
"shippingPolicy": {
"id": "b3f1a2c4-5d6e-4f70-8a91-2b3c4d5e6f70",
"name": "Agendada - Loja Centro",
"createdAt": "2026-01-12T09:00:00",
"updatedAt": null,
"sellerId": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c",
"seller": {
"id": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c",
"name": "Loja Centro",
"country": "Brasil",
"state": "RJ",
"city": "Rio de Janeiro",
"neighborhood": "Centro",
"zipCode": "20031170",
"street": "Av. Rio Branco",
"number": "156",
"complement": "Loja A",
"reference": null,
"phoneNumber": "2133334444",
"description": null,
"sellerCode": "LJ-08",
"active": true
},
"expressDelivery": null,
"scheduledDelivery": {
"id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
"shippingPolicyId": "b3f1a2c4-5d6e-4f70-8a91-2b3c4d5e6f70",
"daysLimit": 7,
"minimumQuantityItems": 0,
"maximumQuantityItems": 70,
"minimumPurchaseValue": 50,
"minimumShippingCost": 12.9,
"deliveryActiveModalUndefined": false,
"observations": null
},
"pickupPoint": null
},
"subtotal": {
"subtotal": 37.41,
"discount": 1.94,
"couponDiscount": 0,
"discountMO": 0,
"appliedCouponCode": null,
"frete": 12.9,
"isFreeShipping": false,
"total": 48.37,
"giftcardDiscount": null,
"appliedPromotions": [
{
"id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
"name": "Quinta da Carne",
"type": 1
}
],
"appliedMyOffers": []
},
"acceptedOrderDetails": {
"id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
"sellerId": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c",
"shippingPolicyName": "Agendada - Loja Centro",
"paymentProvider": "MercadoPago",
"paymentMethod": "CreditCard",
"userPaymentData": "{\"cardToken\":\"\",\"cardBrand\":\"Visa\",\"lastFourDigits\":\"6411\"}",
"installments": 1,
"subtotal": 37.41,
"discount": 1.94,
"netDiscount": 1.94,
"frete": 12.9,
"total": 48.37,
"couponDiscount": 0,
"appliedCouponCode": null,
"cashChange": 0,
"scheduledDate": "2026-07-30",
"scheduledDayOfWeek": 4,
"scheduledStartTime": "14:00:00",
"scheduledEndTime": "18:00:00",
"expressStartDate": null,
"expressEndDate": null,
"pickupPointName": null,
"hasGiftcardApplied": false,
"giftcardDiscount": null,
"transactionId": "125300123456",
"brand": "Visa",
"additionalInformation": [
{ "title": "Quem irá receber as compras?", "value": "Eu mesmo", "dataType": 1 },
{ "title": "Aceita substituição de item?", "value": false, "dataType": 2 }
]
},
"paymentAttempts": [
{
"serviceProviderPaymentMethod": {
"id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
"paymentMethod": { "id": 1, "name": "CreditCard" },
"contractServiceProvider": { "name": "MercadoPago" }
},
"status": "Success",
"type": "Authorize",
"brand": "Visa",
"data": { "id": 125300123456, "status": "approved", "status_detail": "accredited" },
"transactionDate": "2026-07-29T11:32:08",
"transactionId": "125300123456",
"acquirerTransactionId": "8471120993",
"paymentId": "0d1e2f3a-4b5c-4d6e-9f7a-8b9c0d1e2f3a",
"value": 48.37,
"authCode": "112233"
}
],
"orderInvoiced": null,
"giftcard": { "applied": false, "appliedAmount": 0 }
}
}O que liga o item vendido ao seu catálogo
| Campo do item | Conteúdo |
|---|---|
items[].reference | o reference que você enviou no SKU em POST /v1/batch/Products |
items[].ean | o EAN do SKU |
items[].skuId | GUID interno do SKU na plataforma |
items[].id | GUID interno do produto (não do item do pedido) |
items[].orderedQuantity | sempre um inteiro. Em item de unidade, a quantidade de unidades. Em item pesável, a quantidade de múltiplos do arithmeticFactor, arredondada — não o peso |
items[].unitOfMeasure | Un, Kg, G ou Mg — nome, não código |
items[].arithmeticFactor | o passo de venda do item pesável (0.1 = de 100 em 100 g) |
items[].salePrice | preço unitário vigente — no pesável, o preço por unitOfMeasure (por quilo, em Kg) |
items[].totalPerProduct | preço × quantidade real, antes de rateio de promoção |
items[].manualPrice | valor do item depois do rateio de desconto — é o que soma no total |
items[].discountApplied | desconto rateado neste item |
orderedQuantity nunca é o peso. O campo é inteiro; o peso é convertido para
múltiplos do arithmeticFactor e arredondado antes de sair. No exemplo acima, 487 g de
picanha com arithmeticFactor: 0.1 chegam como "orderedQuantity": 5, não como
0.487.
Um ERP que trate esse campo como quilo fatura 5 kg de picanha. Um ERP que o trate como unidade fatura 5 peças. Os dois erram, e erram em todo item pesável do pedido.
- Peso aproximado =
orderedQuantity × arithmeticFactor(aqui, 0,5 kg) — é o passo de venda, já arredondado. - O peso exato separado não vem no payload. Quem carrega o valor real é
totalPerProduct(19.43= 0,487 kg × R$ 39,90) emanualPrice. Para cobrança e conferência fiscal, use esses campos, nuncaorderedQuantity × salePrice.
O item do pedido não traz skuExternalReference nem o nome do produto. O campo de
vínculo com o seu catálogo é items[].reference (o reference do SKU) ou
items[].ean — não o externalReference.
Isso importa porque PUT /v1/Orders/{orderNumber} exige skuExternalReference para
informar a separação. Se, no seu ERP, reference e externalReference do SKU não são
o mesmo valor, guarde os dois no cadastro no momento da carga de catálogo: o
webhook devolve um e a alteração de pedido pede o outro. Veja
Alterar itens do pedido.
Como ler os valores dentro de data
Enums são número e datas não têm fuso.
status (300), deliveryType (1 agendada, 2 expressa, 3 retirada) e origin
(0 indefinido, 1 app, 2 web) chegam como inteiros. Só o eventType e o
relatedEntity do envelope vêm como texto. A tabela de códigos de status está em
Ciclo de vida do pedido.
A exceção dentro de items[] é unitOfMeasure, que vem como texto ("Kg") no
webhook e como número (1) em
GET /v1/gateway/orders/{id}/details, no core — dois serviços, duas configurações de
serialização, as duas formas esperadas.
As datas dentro de data são hora local do Brasil (UTC−3), sem sufixo de fuso
("2026-07-29T11:32:10"). Um parser que assume UTC desloca o pedido em três horas.
Deduplicação
O envelope não carrega identificador de entrega, e o operationId não é único por
notificação: um lote de 500 produtos gera 500 notificações com o mesmo operationId.
Deduplicar só por operationId descarta 499 dos 500 eventos de um lote de catálogo.
Foi o erro mais comum nas primeiras integrações.
A chave canônica, válida para todos os tipos de evento, é:
operationId + eventType + identidade da entidade dentro de dataO diagrama responde: o que o meu endpoint faz com cada entrega recebida?
O ramo da esquerda é o que a maioria das integrações erra: duplicata também responde
200. Responder 4xx ou 5xx para um evento que você já processou faz a plataforma
retentar — e são só 3 entregas antes da DLQ silenciosa.
relatedEntity | Identidade dentro de data |
|---|---|
Order | data.number |
ProductWithSKUs | data.product.externalReference |
Brand, Category | data.externalReference |
Warehouse | data.id |
SKUPricing | data.skuId + data.commercialPolicyId |
User | data.email (o id é removido do payload de usuário por segurança) |
WarehouseSKU | não tem campo escalar de identidade — use a regra de hash abaixo |
A regra única, que dispensa a tabela e cobre inclusive WarehouseSKU:
operationId + eventType + sha256(JSON.stringify(data))As retentativas reentregam a mesma mensagem byte a byte, então o hash é estável entre elas.
Verificar o payload de um pedido
Se você perdeu uma entrega, pode buscar o payload que teria sido enviado:
v1 (padrão)
curl $GROCERS_API/v1/Orders/104821/webhook-payload \
-H "Authorization: Basic $BASIC" -H "x-contractAccountId: $TENANT"A resposta da v2 é o objeto que originaria o data do webhook, mas ele não é
idêntico ao data entregue no seu endpoint. Duas diferenças importam:
| Campo | No webhook | Na v2 |
|---|---|---|
contractAccountId | fica no envelope, fora do data | vem dentro do objeto |
eventType | vem no envelope, como texto ("Order_Updated") | vem dentro do objeto, como número (14) |
Não há operationId nem relatedEntity — esses só existem no envelope da notificação. E
orderStatusRegistry e deviceId vêm null, igual ao webhook.
{
"id": "a1b2c3d4-e5f6-4708-9a1b-2c3d4e5f6071",
"contractAccountId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"eventType": 14,
"number": 104821,
"status": 300,
"createdAt": "2026-07-29T11:30:02",
"deviceId": null,
"orderStatusRegistry": null,
"user": { "id": "b2c3d4e5-f607-4819-a02b-3c4d5e6f7081", "firstName": "João", "lastName": "Silva", "document": "12345678900" },
"items": [
{ "skuId": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e", "ean": "7891234567890", "reference": "MAC-ESP-500", "orderedQuantity": 2, "unitOfMeasure": "Un", "salePrice": 8.99, "totalPerProduct": 17.98, "manualPrice": 17.98, "discountApplied": 0 }
],
"seller": { "id": "6f7a8b9c-0d1e-4f2a-9b3c-4d5e6f7a8b9c", "name": "Loja Centro" },
"shippingPolicy": { "id": "b3f1a2c4-5d6e-4f70-8a91-2b3c4d5e6f70", "name": "Agendada - Loja Centro" },
"subtotal": { "subtotal": 37.41, "discount": 1.94, "frete": 12.9, "total": 48.37 },
"acceptedOrderDetails": { "paymentMethod": "CreditCard", "total": 48.37 },
"paymentAttempts": [ { "status": "Success", "type": "Authorize", "value": 48.37 } ],
"currency": "BRL"
}| Código | Significado |
|---|---|
200 | payload devolvido |
404 | pedido não encontrado, ou ainda em status que não gera webhook (CART_CREATED / PaymentInProgress) |
500 | falha interna |
O exemplo acima é o da v2. A v1 lê de uma base de leitura separada, com serviço
e serialização próprios: o formato da resposta dela não é garantido igual ao da
v2. Se você vai depender dessa rota em automação, escolha uma das duas versões e
valide o formato dela contra um pedido real — não presuma que trocar a versão na URL
devolve o mesmo objeto.
Se as duas divergirem no conteúdo, a v2 é a fonte de verdade (ela monta o payload do
zero a partir do core) e a divergência é assunto de chamado.
Nenhuma das duas rotas reenvia a notificação — elas só mostram o payload.