Skip to content

Webhooks

Visão Geral

Webhooks da Appstore permitem que seu aplicativo receba notificações em tempo real sobre eventos que ocorrem na plataforma Appmax. Quando um evento acontece (pedido aprovado, cliente criado, assinatura cancelada, etc.), a Appmax envia uma requisição POST para a URL que você configurou durante a criação do app.

O payload é enviado em JSON com um envelope padrão que inclui metadados do evento e os dados específicos do recurso. Existem 4 tipos de evento (order, customer, payment, subscription) totalizando 40 eventos disponíveis.

INFO

Os webhooks da Appstore são despachados em tempo real assim que o evento ocorre na plataforma.

Estrutura do Payload (Envelope)

Todos os webhooks compartilham o mesmo envelope. O campo data varia conforme o event_type.

json
{
  "event": "order_approved",
  "event_type": "order",
  "site_id": "uuid-do-site",
  "app_id": "uuid-do-app",
  "client_key": "chave-externa",
  "external_key": "chave-externa",
  "data": { },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}
CampoTipoObrigatórioDescrição
eventstringSimIdentificador do evento (ex: order_approved)
event_typestringSimTipo do evento: order, customer, payment ou subscription
site_idstringSimUUID do site no qual o evento ocorreu
app_idstringSimUUID do app que está recebendo o webhook
client_keystring | nullNãoChave configurada pelo merchant para identificação externa
external_keystring | nullNãoChave externa associada ao recurso
dataobjectSimDados do recurso — varia conforme o event_type
partner_merchantobjectSimDados do merchant: merchant_email, merchant_document_number, merchant_phone

Tabela de Eventos

Customer

Descriçãoeventevent_type
Cliente criadocustomer_createdcustomer
Cliente interessadocustomer_interestedcustomer
Cliente contatadocustomer_contactedcustomer

Order

Descriçãoeventevent_type
Pedido autorizadoorder_authorizedorder
Pedido aprovadoorder_approvedorder
Boleto criadoorder_billet_createdorder
Pedido pagoorder_paidorder
Pedido pendente integraçãoorder_pending_integrationorder
Pedido estornadoorder_refundorder
Estorno parcialorder_partial_refundorder
Upsell pagoorder_up_soldorder
Pix geradoorder_pix_createdorder
Pix pagoorder_paid_by_pixorder
Pix expiradoorder_pix_expiredorder
Pedido integradoorder_integratedorder
Boleto vencidoorder_billet_overdueorder
Pedido autorizado com atrasoorder_authorized_with_delayorder
Chargeback em tratamentoorder_chargeback_in_treatmentorder
Chargeback vencido (favor merchant)order_charge_back_gainorder
Recusado por riscoorder_refused_by_riskorder
Split de pagamentosplit_ordersorder

Payment

Descriçãoeventevent_type
Pagamento autorizado com atrasopayment_authorized_with_delaypayment
Pagamento não autorizadopayment_not_authorizedpayment

Subscription

Descriçãoeventevent_type
Assinatura criadasubscription_createdsubscription
Assinatura canceladasubscription_cancelationsubscription
Assinatura pausadasubscription_pausedsubscription
Assinatura retomadasubscription_resumedsubscription
Cobrança recorrente aprovadasubscription_charge_successsubscription
Cobrança recorrente recusadasubscription_charge_failedsubscription
Produto adicionadosubscription_product_addedsubscription
Produto removidosubscription_product_removedsubscription
Quantidade de produto alteradasubscription_product_quantity_changedsubscription
Frequência alteradasubscription_frequency_changedsubscription
Ciclo puladosubscription_cycle_skippedsubscription
Ciclo despuladosubscription_cycle_unskippedsubscription
Dia de cobrança alteradosubscription_billing_day_changedsubscription
Data da próxima cobrança alteradasubscription_next_billing_day_changedsubscription
Endereço atualizadosubscription_address_updatedsubscription
Forma de pagamento atualizadasubscription_payment_method_updatedsubscription
Assinatura atrasada (legado)subscription_delayedsubscription

INFO

Os eventos essenciais para integração são os de pedido (order_*) e pagamento (payment_*).

WARNING

Cada evento depende da permissão correspondente concedida ao app. Um app sem a permissão subscription-product-added, por exemplo, não recebe subscription_product_added — mesmo que o evento ocorra na loja. Revise as permissões do app na Appstore antes de investigar um evento que "não chega".

subscription_delayed é um evento legado, mantido apenas por compatibilidade: o motor de assinaturas atual não o emite. Use subscription_charge_failed para detectar falha de cobrança recorrente.

Payloads por Tipo de Evento

Order Events

O campo data para eventos do tipo order contém os seguintes campos:

CampoTipoDescrição
order_idintID do pedido
statusstringStatus atual do pedido
totalintValor total em centavos (12300 = R$ 123,00)
freight_valueintValor do frete em centavos
merchant_totalintValor líquido do merchant em centavos
merchant_affiliate_totalintValor do afiliado do merchant em centavos
discountintValor do desconto em centavos
interestintValor de juros em centavos
upsell_order_idint | nullID do pedido de upsell associado
payment_link_idint | nullID do link de pagamento
paid_atstring | nullData/hora do pagamento
integrated_atstring | nullData/hora da integração
refund_atstring | nullData/hora do estorno
created_atstringData/hora de criação do pedido
productsarrayLista de produtos do pedido
payment_infoobjectInformações do pagamento (varia por método)
client_keystring | nullChave de identificação externa
external_keystring | nullChave externa
cashback_usedint | nullCashback utilizado em centavos
cashback_reservedint | nullCashback reservado em centavos
cashback_statusstring | nullStatus do cashback
notification_typestringTipo da notificação

Campos de products[]:

CampoTipoDescrição
skustringSKU do produto
namestringNome do produto
priceintPreço unitário em centavos
quantityintQuantidade

Campos de payment_info (condicional por método de pagamento):

Para Pix:

CampoTipoDescrição
pix.end_to_end_idstringID end-to-end da transação Pix
pix.pix_creation_datestringData de criação do Pix
pix.pix_expiration_datestringData de expiração do Pix
pix.pix_emvstringCódigo EMV (copia e cola)
pix.pix_refstringReferência do Pix
pix.pix_qrcodestringURL da imagem do QR Code
pix.pix_payment_linkstringLink de pagamento Pix

Para Boleto:

CampoTipoDescrição
boleto.boleto_overdue_datestringData de vencimento
boleto.boleto_urlstringURL do boleto
boleto.boleto_digitable_linestringLinha digitável

Para Cartão de Crédito / Apple Pay:

CampoTipoDescrição
credit_card.installmentsintNúmero de parcelas
credit_card.card_brandstringBandeira do cartão
credit_card.nsustringNSU da transação
credit_card.authorization_codestringCódigo de autorização
credit_card.captured_atstringData/hora da captura

Exemplo: Pedido aprovado com cartão (order_approved)

json
{
  "event": "order_approved",
  "event_type": "order",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": "ext-order-456",
  "data": {
    "order_id": 3531,
    "status": "aprovado",
    "total": 25990,
    "freight_value": 1500,
    "merchant_total": 23400,
    "merchant_affiliate_total": 0,
    "discount": 0,
    "interest": 0,
    "upsell_order_id": null,
    "payment_link_id": null,
    "paid_at": "2025-03-15 14:30:00",
    "integrated_at": null,
    "refund_at": null,
    "created_at": "2025-03-15 14:28:00",
    "products": [
      {
        "sku": "PROD-001",
        "name": "Curso de Marketing Digital",
        "price": 25990,
        "quantity": 1
      }
    ],
    "payment_info": {
      "credit_card": {
        "installments": 3,
        "card_brand": "visa",
        "nsu": "0012345678",
        "authorization_code": "AUTH9876",
        "captured_at": "2025-03-15 14:30:00"
      }
    },
    "client_key": "merchant-key-123",
    "external_key": "ext-order-456",
    "cashback_used": null,
    "cashback_reserved": null,
    "cashback_status": null,
    "notification_type": "order_approved"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Exemplo: Pix pago (order_paid_by_pix)

json
{
  "event": "order_paid_by_pix",
  "event_type": "order",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": null,
  "external_key": null,
  "data": {
    "order_id": 4201,
    "status": "aprovado",
    "total": 9900,
    "freight_value": 0,
    "merchant_total": 8910,
    "merchant_affiliate_total": 0,
    "discount": 0,
    "interest": 0,
    "upsell_order_id": null,
    "payment_link_id": 789,
    "paid_at": "2025-03-15 15:10:00",
    "integrated_at": null,
    "refund_at": null,
    "created_at": "2025-03-15 15:05:00",
    "products": [
      {
        "sku": "EBOOK-042",
        "name": "E-book Receitas Fit",
        "price": 9900,
        "quantity": 1
      }
    ],
    "payment_info": {
      "pix": {
        "end_to_end_id": "E123456782025031515100001",
        "pix_creation_date": "2025-03-15 15:05:00",
        "pix_expiration_date": "2025-03-15 15:35:00",
        "pix_emv": "00020126580014br.gov.bcb.pix0136a1b2c3d4-e5f6-7890-abcd-ef12345678905204000053039865802BR5925APPMAX PAGAMENTOS LTDA6009SAO PAULO62070503***63041D3D",
        "pix_ref": "PIX-REF-4201",
        "pix_qrcode": "https://api.appmax.com.br/pix/qrcode/4201.png",
        "pix_payment_link": "https://pay.appmax.com.br/pix/4201"
      }
    },
    "client_key": null,
    "external_key": null,
    "cashback_used": null,
    "cashback_reserved": null,
    "cashback_status": null,
    "notification_type": "order_paid_by_pix"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Exemplo: Boleto criado (order_billet_created)

json
{
  "event": "order_billet_created",
  "event_type": "order",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": null,
  "data": {
    "order_id": 4305,
    "status": "aguardando_pagamento",
    "total": 34900,
    "freight_value": 2000,
    "merchant_total": 31410,
    "merchant_affiliate_total": 0,
    "discount": 500,
    "interest": 0,
    "upsell_order_id": null,
    "payment_link_id": null,
    "paid_at": null,
    "integrated_at": null,
    "refund_at": null,
    "created_at": "2025-03-16 09:00:00",
    "products": [
      {
        "sku": "KIT-PREMIUM",
        "name": "Kit Premium de Suplementos",
        "price": 16700,
        "quantity": 2
      }
    ],
    "payment_info": {
      "boleto": {
        "boleto_overdue_date": "2025-03-19 23:59:59",
        "boleto_url": "https://api.appmax.com.br/boleto/4305.pdf",
        "boleto_digitable_line": "23793.38128 60000.000003 00000.000400 1 84340000034900"
      }
    },
    "client_key": "merchant-key-123",
    "external_key": null,
    "cashback_used": 500,
    "cashback_reserved": 1000,
    "cashback_status": "applied",
    "notification_type": "order_billet_created"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "98765432000188",
    "merchant_phone": "21988887777"
  }
}

Customer Events

O campo data para eventos do tipo customer contém os seguintes campos:

CampoTipoDescrição
customer_idintID do cliente
customer_dataobjectDados pessoais do cliente
customer_data.firstnamestringPrimeiro nome
customer_data.lastnamestringSobrenome
customer_data.emailstringE-mail
customer_data.telephonestringTelefone
customer_data.document_numberstringCPF ou CNPJ
customer_data.custom_txtstring | nullCampo customizado
customer_addressobjectEndereço do cliente
customer_address.postcodestringCEP
customer_address.streetstringLogradouro
customer_address.street_numberstringNúmero
customer_address.street_complementstring | nullComplemento
customer_address.street_districtstringBairro
customer_address.citystringCidade
customer_address.statestringEstado (UF)
created_atstringData/hora de criação
updated_atstringData/hora de atualização
client_keystring | nullChave de identificação externa
external_keystring | nullChave externa

Exemplo: Cliente criado (customer_created)

json
{
  "event": "customer_created",
  "event_type": "customer",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": null,
  "data": {
    "customer_id": 2023,
    "customer_data": {
      "firstname": "Junior",
      "lastname": "Almeida",
      "email": "[email protected]",
      "telephone": "51983655100",
      "document_number": "12345678900",
      "custom_txt": null
    },
    "customer_address": {
      "postcode": "90010-000",
      "street": "Rua dos Andradas",
      "street_number": "1234",
      "street_complement": "Sala 501",
      "street_district": "Centro Histórico",
      "city": "Porto Alegre",
      "state": "RS"
    },
    "created_at": "2025-03-15 14:25:00",
    "updated_at": "2025-03-15 14:25:00",
    "client_key": "merchant-key-123",
    "external_key": null
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Payment Events

O campo data para eventos do tipo payment contém os seguintes campos:

CampoTipoDescrição
customer_idintID do cliente
order_idintID do pedido
payment_typestringMétodo de pagamento (ex: credit_card, pix, boleto)
payment_totalintValor do pagamento em centavos
cashback_usedint | nullCashback utilizado em centavos

Exemplo: Pagamento não autorizado (payment_not_authorized)

json
{
  "event": "payment_not_authorized",
  "event_type": "payment",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": null,
  "external_key": null,
  "data": {
    "customer_id": 2023,
    "order_id": 3532,
    "payment_type": "credit_card",
    "payment_total": 15900,
    "cashback_used": null
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Subscription Events

Eventos de assinatura são produzidos pelo motor de recorrência da Appmax e entregues ao seu app com o mesmo envelope dos demais eventos.

O campo data de todo evento de assinatura é a união de dois blocos:

  1. Campos base — presentes em todos os eventos de assinatura (com null quando não se aplicam ao evento).
  2. Campos específicos do evento — variam conforme o event, descritos na tabela mais abaixo.

Campos base

CampoTipoDescrição
subscription_idintID da assinatura. É o identificador estável ao longo de todo o ciclo de vida
order_idintID do pedido de origem da assinatura (na criação) ou da cobrança que gerou o evento
customer_idintID do cliente dono da assinatura
totalintValor total do pedido em centavos (12300 = R$ 123,00)
intervalstring | nullUnidade da recorrência: week, month ou year
interval_countint | nullQuantidade de unidades entre cobranças (interval=month + interval_count=2 = a cada 2 meses)
statusstring | nullEstado da assinatura no momento do evento (ver tabela abaixo)
cashback_usedfloat | nullCashback aplicado no pedido, quando houver
client_keystring | nullChave externa do merchant (repetida do envelope)
external_keystring | nullMesmo valor de client_key

INFO

interval, interval_count e status só são preenchidos nos eventos em que fazem sentido. Nos eventos de alteração (produto, ciclo, dia de cobrança) eles chegam como null — a assinatura em si não mudou de estado.

Campos específicos por evento

eventCampos adicionais em data
subscription_createdstatus = active, interval, interval_count, next_charge_at (ISO-8601)
subscription_cancelationstatus = canceled
subscription_pausedstatus = paused
subscription_resumedstatus, next_billing_date
subscription_charge_successstatus = success, uuid (UUID da assinatura)
subscription_charge_failedstatus = failed, uuid (UUID da assinatura)
subscription_product_addedproducts — lista completa dos produtos após a alteração
subscription_product_removedproducts — lista completa dos produtos após a alteração
subscription_product_quantity_changedproducts — lista completa dos produtos após a alteração
subscription_frequency_changedfrequency (week/month/year), interval_count, next_charge_at (ISO-8601)
subscription_cycle_skippednext_billing_day (ISO-8601)
subscription_cycle_unskippednext_billing_day (ISO-8601)
subscription_billing_day_changedbilling_day
subscription_next_billing_day_changednext_billing_day (ISO-8601)
subscription_payment_method_updatedpayment_method (credit_card ou pix)
subscription_address_updatedCampos do endereço de entrega atualizado

WARNING

Os eventos de produto (subscription_product_added, subscription_product_removed, subscription_product_quantity_changed) enviam sempre a lista consolidada completa dos produtos após a alteração — não apenas o item que mudou. Substitua sua cópia local pela lista recebida em vez de aplicar um delta.

Cada item de products tem o formato:

json
{
  "name": "Produto A - Mensal",
  "price": 100.0,
  "quantity": 1,
  "variant_id": "va-month"
}

Exemplo: Assinatura criada (subscription_created)

json
{
  "event": "subscription_created",
  "event_type": "subscription",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": "merchant-key-123",
  "data": {
    "subscription_id": 501,
    "order_id": 3532,
    "customer_id": 2023,
    "total": 4990,
    "interval": "month",
    "interval_count": 1,
    "status": "active",
    "next_charge_at": "2025-04-15T14:30:00-03:00",
    "cashback_used": null,
    "client_key": "merchant-key-123",
    "external_key": "merchant-key-123"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Exemplo: Cobrança recorrente aprovada (subscription_charge_success)

json
{
  "event": "subscription_charge_success",
  "event_type": "subscription",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": "merchant-key-123",
  "data": {
    "subscription_id": 501,
    "order_id": 3987,
    "customer_id": 2023,
    "total": 4990,
    "interval": null,
    "interval_count": null,
    "status": "success",
    "uuid": "6f1e9a5c-8d24-4a1b-9f30-2c7b5e0a1d44",
    "cashback_used": null,
    "client_key": "merchant-key-123",
    "external_key": "merchant-key-123"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

INFO

Cada ciclo de cobrança gera um novo pedido. O order_id muda a cada cobrança; o subscription_id permanece o mesmo. Use subscription_id para relacionar as cobranças a uma assinatura e order_id para conciliar com os eventos de pedido (order_*) daquela cobrança.

Exemplo: Produto adicionado (subscription_product_added)

json
{
  "event": "subscription_product_added",
  "event_type": "subscription",
  "site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
  "client_key": "merchant-key-123",
  "external_key": "merchant-key-123",
  "data": {
    "subscription_id": 501,
    "order_id": 3532,
    "customer_id": 2023,
    "total": 4990,
    "interval": null,
    "interval_count": null,
    "status": null,
    "products": [
      { "name": "Produto A - Mensal", "price": 100.0, "quantity": 1, "variant_id": "va-month" },
      { "name": "Produto B - Mensal", "price": 100.0, "quantity": 1, "variant_id": "vb-month" }
    ],
    "cashback_used": null,
    "client_key": "merchant-key-123",
    "external_key": "merchant-key-123"
  },
  "partner_merchant": {
    "merchant_email": "[email protected]",
    "merchant_document_number": "12345678000199",
    "merchant_phone": "11999999999"
  }
}

Idempotência em eventos de assinatura

O envelope de assinatura não traz um identificador único de evento. Para deduplicar, use a combinação event + subscription_id + order_id.

Atenção: eventos de cobrança se repetem legitimamente a cada ciclo — o par subscription_charge_success + subscription_id não é único ao longo do tempo. É o order_id, novo a cada cobrança, que distingue um ciclo do outro. Já os eventos de alteração (produto, frequência, ciclo) podem repetir o mesmo order_id; para esses, combine também o conteúdo recebido ou o instante do recebimento.

Fluxo Temporal dos Eventos

Os diagramas abaixo ilustram a sequência típica de eventos para cada método de pagamento.

Cartão de Crédito:

customer_created → order_authorized → order_approved → order_paid → order_integrated

Pix:

customer_created → order_pix_created → [timeout: order_pix_expired]
                                     → order_paid_by_pix → order_approved → order_integrated

Boleto:

customer_created → order_billet_created → [vencimento: order_billet_overdue]
                                        → order_paid → order_approved → order_integrated

Estorno / Chargeback:

[pedido aprovado] → order_refund (total)
                  → order_partial_refund (parcial)
                  → order_chargeback_in_treatment → order_charge_back_gain

Assinatura — ciclo de vida:

subscription_created → subscription_charge_success   (a cada ciclo, um novo order_id)
                     → subscription_charge_failed    (cobrança recusada)
                     → subscription_cancelation      (fim da assinatura)

subscription_paused ⇄ subscription_resumed

Assinatura — alterações (a qualquer momento, enquanto a assinatura está ativa):

produtos   → subscription_product_added / subscription_product_removed
             subscription_product_quantity_changed
recorrência → subscription_frequency_changed
             subscription_billing_day_changed / subscription_next_billing_day_changed
             subscription_cycle_skipped ⇄ subscription_cycle_unskipped
cadastro   → subscription_payment_method_updated / subscription_address_updated

INFO

Uma cobrança recorrente também dispara os eventos de pedido (order_*) do pedido criado para aquele ciclo. Se seu app assina os dois tipos, espere receber subscription_charge_success e order_approved para o mesmo order_id.

WARNING

A ordem dos eventos não é garantida. Atrasos de rede, retries e processamento assíncrono podem alterar a sequência. Sempre verifique o estado atual do recurso antes de tomar decisões baseadas em eventos.

Política de Retry

Quando o endpoint falha em receber um webhook, a Appmax inicia um ciclo de retentativas:

Tentativa 1 (original) — após delay do evento
        ↓ falha
Tentativa 2 — +30 minutos
        ↓ falha
Tentativa 3 — +2 horas
        ↓ falha
Tentativa 4 — +4 horas
        ↓ falha
Webhook descartado (sem notificação)
  • Timeout HTTP: 5 segundos
  • Códigos de sucesso: 200, 201, 202, 203, 204, 205, 206, 207, 208, 226
  • Falha: qualquer outro código HTTP ou timeout inicia o retry
  • Máximo: 4 tentativas (1 original + 3 retries)

DANGER

Após 4 tentativas sem sucesso, o webhook é descartado permanentemente. Não há notificação ao desenvolvedor. Monitore seu endpoint ativamente.

Headers HTTP

Toda requisição de webhook é enviada com os seguintes headers:

HeaderValor
Content-Typeapplication/json
User-AgentGuzzleHttp/7

WARNING

A Appmax não envia header de assinatura (HMAC) ou token de autenticação nos webhooks. Recomendamos validar a origem por outros meios (ver Boas Práticas).

Como Receber Webhooks

Seu endpoint deve:

  1. Aceitar requisições POST com Content-Type: application/json
  2. Responder com HTTP 200 em até 5 segundos
  3. Processar o evento de forma assíncrona (não bloqueie a resposta)

WARNING

Se o endpoint não responder 200 dentro de 5 segundos, a Appmax inicia o ciclo de retry. Processe o evento em background e responda imediatamente.

Exemplos de Código

go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"sync"

	"github.com/gin-gonic/gin"
)

var (
	processed sync.Map
)

func main() {
	r := gin.Default()

	r.POST("/webhooks/appmax", func(c *gin.Context) {
		var payload struct {
			Event     string          `json:"event"`
			EventType string          `json:"event_type"`
			Data      json.RawMessage `json:"data"`
		}

		if err := c.ShouldBindJSON(&payload); err != nil {
			c.JSON(200, gin.H{"received": true}) // responder 200 mesmo com erro
			return
		}

		// Responder 200 imediatamente para evitar timeout de 5s
		c.JSON(200, gin.H{"received": true})

		// Extrair ID para idempotência
		var data struct {
			OrderID    int `json:"order_id"`
			CustomerID int `json:"customer_id"`
		}
		json.Unmarshal(payload.Data, &data)

		id := data.OrderID
		if id == 0 {
			id = data.CustomerID
		}
		key := fmt.Sprintf("%d-%s", id, payload.Event)

		if _, loaded := processed.LoadOrStore(key, true); loaded {
			return // duplicado
		}

		// Processar em goroutine — em produção, envie para uma fila
		go func() {
			log.Printf("Processando: %s (%s)", payload.Event, payload.EventType)
			// Sua lógica aqui
		}()
	})

	r.Run(":3000")
}
javascript
const express = require('express');
const app = express();
app.use(express.json());

// Map para rastrear eventos já processados (em produção, use banco de dados)
const processed = new Set();

app.post('/webhooks/appmax', (req, res) => {
  // Responder 200 imediatamente para evitar timeout de 5s
  res.status(200).json({ received: true });

  const { event, event_type, data } = req.body;
  const idempotencyKey = `${data.order_id || data.customer_id}-${event}`;

  if (processed.has(idempotencyKey)) {
    console.log(`Evento duplicado ignorado: ${idempotencyKey}`);
    return;
  }

  processed.add(idempotencyKey);
  console.log(`Processando: ${event} (${event_type})`);

  // Processar evento de forma assíncrona
  // Em produção, envie para uma fila (Bull, RabbitMQ, etc.)
});

app.listen(3000, () => console.log('Webhook listener na porta 3000'));
python
from flask import Flask, request, jsonify
import threading

app = Flask(__name__)
processed = set()

@app.route('/webhooks/appmax', methods=['POST'])
def webhook():
    payload = request.get_json()
    event = payload.get('event')
    data = payload.get('data', {})

    key = f"{data.get('order_id') or data.get('customer_id')}-{event}"

    if key in processed:
        return jsonify(received=True), 200

    processed.add(key)

    # Processar em background para responder rápido
    threading.Thread(target=process_event, args=(payload,)).start()

    return jsonify(received=True), 200

def process_event(payload):
    print(f"Processando: {payload['event']}")
    # Sua lógica aqui

if __name__ == '__main__':
    app.run(port=3000)
php
// routes/api.php
Route::post('/webhooks/appmax', [WebhookController::class, 'handle']);

// app/Http/Controllers/WebhookController.php
class WebhookController extends Controller
{
    public function handle(Request $request)
    {
        $payload = $request->all();

        // Despachar para job assíncrono e responder 200 imediatamente
        ProcessWebhook::dispatch($payload);

        return response()->json(['received' => true]);
    }
}

// app/Jobs/ProcessWebhook.php
class ProcessWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private array $payload) {}

    public function handle()
    {
        $event = $this->payload['event'];
        $data = $this->payload['data'];
        $key = ($data['order_id'] ?? $data['customer_id']) . '-' . $event;

        // Verificar idempotência
        if (Cache::has("webhook:{$key}")) {
            return;
        }
        Cache::put("webhook:{$key}", true, now()->addHours(24));

        // Processar evento
        Log::info("Webhook recebido: {$event}", $this->payload);
    }
}

Boas Práticas

  1. Responda 200 antes de processar. O timeout é de 5 segundos. Processamento síncrono causa retry desnecessário. Responda imediatamente e processe em background (fila, thread, job assíncrono).

  2. Implemente idempotência. Use order_id + event (ou customer_id + event; em assinaturas, subscription_id + order_id + event) como chave única. Retries legitimamente reenviam o mesmo evento, e seu sistema precisa tratar duplicatas sem efeitos colaterais.

  3. Armazene o payload cru. Salve o JSON completo em banco de dados ou log antes de processar. Isso facilita debug e permite reprocessamento manual sem depender de reenvio.

  4. Não confie na ordem dos eventos. Atrasos de rede e retries podem alterar a sequência. Sempre verifique o estado atual do recurso (via API, se necessário) antes de tomar decisões baseadas em um evento.

  5. Use HTTPS. Proteja dados em trânsito. A Appmax envia webhooks para URLs HTTP e HTTPS, mas dados de cliente e pagamento transitam no payload.

  6. Trate duplicatas. Retries legitimamente reenviam o mesmo evento. Garanta que processar o mesmo evento duas vezes não cause efeitos colaterais (cobrar duas vezes, enviar dois e-mails, etc.).

  7. Valide a origem. Como não há header HMAC, considere filtrar por IP de origem, validar a estrutura do payload contra o schema esperado, ou confirmar o evento via API da Appmax.

Webhooks: Appstore vs Painel

Existem dois tipos de webhook na plataforma Appmax. Não confunda:

AspectoWebhooks da AppstoreWebhooks do Painel
Quem configuraDesenvolvedor do app, na criação do aplicativoMerchant, no painel admin da loja
EscopoTodos os merchants que instalam o appApenas a loja específica do merchant
URL destinoURL do host do app (definida na Appstore)URL definida pelo merchant no painel
Eventos40 eventos documentados nesta páginaSubconjunto de eventos (varia por configuração)
Credenciais no payloadapp_id, site_id, external_keyFormato diferente, sem app_id
Quando usarIntegrações via Appstore (este guia)Integrações diretas do merchant

WARNING

Se você está integrando via Appstore (criou um app, merchants instalam), use os webhooks documentados nesta página. Os webhooks do painel são para merchants que configuram notificações diretamente, sem app intermediário.

Erros e Troubleshooting

CenárioO que aconteceComo resolver
Endpoint retorna HTTP diferente de 2xxRetry iniciado (até 4 tentativas)Retornar 200, 201 ou 202
Endpoint não responde em 5sTimeout, retry iniciadoProcessar async e responder 200 imediatamente
Endpoint retorna 502URL inválida ou servidor fora do arVerificar URL cadastrada e disponibilidade do servidor
Endpoint retorna 401/403Autenticação falha, retry iniciadoRemover autenticação do endpoint ou adicionar whitelist
Retry esgotado (4 tentativas)Webhook descartado permanentementeMonitorar endpoint ativamente e solicitar reenvio ao suporte
Webhook demora para chegarEvento pode estar em fila de retryVerificar se o endpoint respondeu 200 nas tentativas anteriores
Webhook não chega (pedido Yampi)Webhook suprimidoComportamento intencional para pedidos originados da Yampi
customer_interested não disparaCliente já possui pedidoEvento só dispara para leads sem pedido associado

Testes e Debug

webhook.site

Serviço gratuito para inspecionar webhooks recebidos. Crie uma URL temporária em webhook.site, configure como URL de webhook do app e visualize os payloads recebidos em tempo real.

ngrok

Para testar webhooks diretamente no seu ambiente local:

bash
ngrok http 3000

Use a URL HTTPS gerada pelo ngrok como URL de webhook do app. As requisições serão redirecionadas para localhost:3000, permitindo debug end-to-end com breakpoints.

Dicas gerais

  • Verifique os logs do seu servidor para confirmar que as requisições estão chegando
  • Inspecione os headers da requisição para confirmar Content-Type: application/json
  • Valide que o JSON recebido está bem formado antes de processar
  • Confirme que o app possui as permissões necessárias para receber os eventos desejados