Webhooks
Overview
Appstore webhooks allow your application to receive real-time notifications about events that occur on the Appmax platform. When an event happens (order approved, customer created, subscription cancelled, etc.), Appmax sends a POST request to the URL you configured when creating the app.
The payload is sent as JSON with a standard envelope that includes event metadata and the resource-specific data. There are 5 event types (order, customer, payment, subscription, withdraw) totaling 42 available events.
INFO
Appstore webhooks are dispatched in real time as soon as the event occurs on the platform.
Testing in the sandbox
In the sandbox, Pix and boleto are approved automatically about 5 seconds after they are generated, with no need to pay. This way you receive all the webhooks of a paid order without waiting for a real payment.
The scenario is decided by the document_number sent to POST /v1/payments/pix or POST /v1/payments/boleto:
document_number sent | What happens to the order | Webhooks you receive |
|---|---|---|
Any valid CPF or CNPJ (e.g. 19100000000) | Approved in ~5 seconds and then integrated | Pix: order_pix_created → order_paid, order_approved, order_paid_by_pix → order_integratedBoleto: order_billet_created → order_paid, order_approved → order_integrated |
CPF 40827365144 or CNPJ 40827365000109 | Stays pending: the payment is not confirmed | Only the creation one: order_pix_created or order_billet_created |
Use the documents in the second row to test the "awaiting payment" screen and how your app behaves while the order is pending.
Example to test a pending order with Pix (for boleto, replace pix with boleto):
{
"order_id": 123,
"payment_data": {
"pix": {
"document_number": "40827365000109"
}
}
}Sandbox only
In production nothing is approved automatically: Pix and boleto are approved only when the buyer pays. For credit card, the scenario is defined by the card number: see the test cards.
TIP
For Pix, the move to integrado follows the order integration routine and may take a few hours; order_paid_by_pix and order_approved arrive within seconds. Each webhook is delivered only if the app has the matching permission.
Payload Structure (Envelope)
All webhooks share the same envelope. The data field varies depending on the event_type.
{
"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"
}
}| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event identifier (e.g., order_approved) |
event_type | string | Yes | Event type: order, customer, payment, subscription, or withdraw |
site_id | string | Yes | UUID of the site where the event occurred |
app_id | string | Yes | UUID of the app receiving the webhook |
client_key | string | null | No | Key configured by the merchant for external identification |
external_key | string | null | No | External key associated with the resource |
data | object | Yes | Resource data -- varies depending on the event_type |
partner_merchant | object | Yes | Merchant data: merchant_email, merchant_document_number, merchant_phone |
Event Table
Customer
| Description | event | event_type |
|---|---|---|
| Customer created | customer_created | customer |
| Customer interested | customer_interested | customer |
| Customer contacted | customer_contacted | customer |
Order
| Description | event | event_type |
|---|---|---|
| Order authorized | order_authorized | order |
| Order approved | order_approved | order |
| Boleto created | order_billet_created | order |
| Order paid | order_paid | order |
| Order pending integration | order_pending_integration | order |
| Order refunded | order_refund | order |
| Partial refund | order_partial_refund | order |
| Upsell paid | order_up_sold | order |
| Pix generated | order_pix_created | order |
| Pix paid | order_paid_by_pix | order |
| Pix expired | order_pix_expired | order |
| Order integrated | order_integrated | order |
| Boleto overdue | order_billet_overdue | order |
| Order authorized with delay | order_authorized_with_delay | order |
| Chargeback in treatment | order_chargeback_in_treatment | order |
| Refused by risk | order_refused_by_risk | order |
| Payment split | split_orders | order |
Payment
| Description | event | event_type |
|---|---|---|
| Payment authorized with delay | payment_authorized_with_delay | payment |
| Payment not authorized | payment_not_authorized | payment |
Subscription
| Description | event | event_type |
|---|---|---|
| Subscription created | subscription_created | subscription |
| Subscription cancelled | subscription_cancelation | subscription |
| Subscription paused | subscription_paused | subscription |
| Subscription resumed | subscription_resumed | subscription |
| Recurring charge succeeded | subscription_charge_success | subscription |
| Recurring charge failed | subscription_charge_failed | subscription |
| Product added | subscription_product_added | subscription |
| Product removed | subscription_product_removed | subscription |
| Product quantity changed | subscription_product_quantity_changed | subscription |
| Frequency changed | subscription_frequency_changed | subscription |
| Cycle skipped | subscription_cycle_skipped | subscription |
| Cycle unskipped | subscription_cycle_unskipped | subscription |
| Billing day changed | subscription_billing_day_changed | subscription |
| Next billing date changed | subscription_next_billing_day_changed | subscription |
| Address updated | subscription_address_updated | subscription |
| Payment method updated | subscription_payment_method_updated | subscription |
| Subscription delayed (legacy) | subscription_delayed | subscription |
Withdraw
| Description | event | event_type |
|---|---|---|
| Withdrawal approved | withdraw_request_approved | withdraw |
| Withdrawal paid | withdraw_request_paid | withdraw |
| Withdrawal refused | withdraw_request_refused | withdraw |
INFO
The essential events for integration are order (order_*) and payment (payment_*) events.
WARNING
Every event depends on the matching permission granted to the app. An app without the subscription-product-added permission, for example, does not receive subscription_product_added -- even when the event happens in the store. Review the app permissions in the Appstore before investigating an event that "never arrives".
subscription_delayed is a legacy event, kept only for backward compatibility: the current subscription engine does not emit it. Use subscription_charge_failed to detect a failed recurring charge.
Payloads by Event Type
Monetary values in BRL
In webhooks, monetary values (total, freight_value, price, cashback_used etc.) are sent in BRL as decimals (123.45 = R$ 123.45). This is the opposite of API requests, which take integer values in cents. The exception is withdrawal events (withdraw_*), whose values are sent as integers in cents, like the withdrawal API. See Withdraw Events.
Order Events
The data field for order type events contains the following fields:
| Field | Type | Description |
|---|---|---|
order_id | int | Order ID |
status | string | Current order status |
total | float | Total amount in BRL (123.00 = R$ 123.00) |
freight_value | float | Shipping amount in BRL |
merchant_total | float | Merchant net amount in BRL |
merchant_affiliate_total | float | Merchant affiliate amount in BRL |
discount | float | Discount amount in BRL |
interest | float | Interest amount in BRL |
upsell_order_id | int | null | Associated upsell order ID |
payment_link_id | int | null | Payment link ID |
paid_at | string | null | Payment date/time |
integrated_at | string | null | Integration date/time |
refund_at | string | null | Refund date/time |
created_at | string | Order creation date/time |
products | array | List of products in the order |
payment_info | object | Payment information (varies by method) |
client_key | string | null | External identification key |
external_key | string | null | External key |
cashback_used | float | null | Cashback used in BRL |
cashback_reserved | float | null | Cashback reserved in BRL |
cashback_status | string | null | Cashback status |
notification_type | string | Notification type |
products[] fields:
| Field | Type | Description |
|---|---|---|
sku | string | Product SKU |
name | string | Product name |
price | float | Unit price in BRL |
quantity | int | Quantity |
payment_info fields (conditional by payment method):
For Pix:
| Field | Type | Description |
|---|---|---|
pix.end_to_end_id | string | Pix transaction end-to-end ID |
pix.pix_creation_date | string | Pix creation date |
pix.pix_expiration_date | string | Pix expiration date |
pix.pix_emv | string | EMV code (copy and paste) |
pix.pix_ref | string | Pix reference |
pix.pix_qrcode | string | QR Code image URL |
pix.pix_payment_link | string | Pix payment link |
For Boleto:
| Field | Type | Description |
|---|---|---|
boleto.boleto_overdue_date | string | Due date |
boleto.boleto_url | string | Boleto URL |
boleto.boleto_digitable_line | string | Digitable line |
For Credit Card / Apple Pay:
| Field | Type | Description |
|---|---|---|
credit_card.installments | int | Number of installments |
credit_card.card_brand | string | Card brand |
credit_card.nsu | string | Transaction NSU |
credit_card.authorization_code | string | Authorization code |
credit_card.captured_at | string | Capture date/time |
Example: Order approved with credit card (order_approved)
{
"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": 259.90,
"freight_value": 15.00,
"merchant_total": 234.00,
"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": 259.90,
"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"
}
}Example: Pix paid (order_paid_by_pix)
{
"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": 99.00,
"freight_value": 0,
"merchant_total": 89.10,
"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": 99.00,
"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"
}
}Example: Boleto created (order_billet_created)
{
"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": 349.00,
"freight_value": 20.00,
"merchant_total": 314.10,
"merchant_affiliate_total": 0,
"discount": 5.00,
"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": 167.00,
"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": 5.00,
"cashback_reserved": 10.00,
"cashback_status": "applied",
"notification_type": "order_billet_created"
},
"partner_merchant": {
"merchant_email": "[email protected]",
"merchant_document_number": "98765432000188",
"merchant_phone": "21988887777"
}
}Customer Events
The data field for customer type events contains the following fields:
| Field | Type | Description |
|---|---|---|
customer_id | int | Customer ID |
customer_data | object | Customer personal data |
customer_data.firstname | string | First name |
customer_data.lastname | string | Last name |
customer_data.email | string | |
customer_data.telephone | string | Phone number |
customer_data.document_number | string | CPF or CNPJ |
customer_data.custom_txt | string | null | Custom field |
customer_address | object | Customer address |
customer_address.postcode | string | Postal code (CEP) |
customer_address.street | string | Street name |
customer_address.street_number | string | Street number |
customer_address.street_complement | string | null | Address complement |
customer_address.street_district | string | Neighborhood |
customer_address.city | string | City |
customer_address.state | string | State (UF) |
created_at | string | Creation date/time |
updated_at | string | Last update date/time |
client_key | string | null | External identification key |
external_key | string | null | External key |
Example: Customer created (customer_created)
{
"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
The data field for payment type events contains the following fields:
| Field | Type | Description |
|---|---|---|
customer_id | int | Customer ID |
order_id | int | Order ID |
payment_type | string | Payment method (e.g., credit_card, pix, boleto) |
payment_total | float | Payment amount in BRL |
cashback_used | float | null | Cashback used in BRL |
Example: Payment not authorized (payment_not_authorized)
{
"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": 159.00,
"cashback_used": null
},
"partner_merchant": {
"merchant_email": "[email protected]",
"merchant_document_number": "12345678000199",
"merchant_phone": "11999999999"
}
}Subscription Events
Subscription events are produced by the Appmax recurring-billing engine and delivered to your app with the same envelope as every other event.
The data field of every subscription event is the union of two blocks:
- Base fields -- present in all subscription events (
nullwhen they do not apply to that event). - Event-specific fields -- vary by
event, described in the table further below.
Base fields
| Field | Type | Description |
|---|---|---|
subscription_id | int | Subscription ID. The stable identifier across the whole lifecycle |
order_id | int | ID of the order that originated the subscription (on creation) or of the charge that fired it |
customer_id | int | ID of the customer who owns the subscription |
total | float | Order total in BRL (123.00 = R$ 123.00) |
interval | string | null | Recurrence unit: week, month or year |
interval_count | int | null | Number of units between charges (interval=month + interval_count=2 = every 2 months) |
status | string | null | Subscription state at the moment of the event (see table below) |
cashback_used | float | null | Cashback applied to the order, when any |
client_key | string | null | Merchant external key (repeated from the envelope) |
external_key | string | null | Same value as client_key |
INFO
interval, interval_count and status are only filled in on the events where they make sense. On change events (product, cycle, billing day) they arrive as null -- the subscription itself did not change state.
Event-specific fields
event | Additional fields in data |
|---|---|
subscription_created | status = active, interval, interval_count, next_charge_at (ISO-8601) |
subscription_cancelation | status = canceled |
subscription_paused | status = paused |
subscription_resumed | status, next_billing_date |
subscription_charge_success | status = success, uuid (subscription UUID) |
subscription_charge_failed | status = failed, uuid (subscription UUID) |
subscription_product_added | products -- full product list after the change |
subscription_product_removed | products -- full product list after the change |
subscription_product_quantity_changed | products -- full product list after the change |
subscription_frequency_changed | frequency (week/month/year), interval_count, next_charge_at (ISO-8601) |
subscription_cycle_skipped | next_billing_day (ISO-8601) |
subscription_cycle_unskipped | next_billing_day (ISO-8601) |
subscription_billing_day_changed | billing_day |
subscription_next_billing_day_changed | next_billing_day (ISO-8601) |
subscription_payment_method_updated | payment_method (credit_card or pix) |
subscription_address_updated | Fields of the updated shipping address |
subscription_delayed | status = late |
WARNING
Product events (subscription_product_added, subscription_product_removed, subscription_product_quantity_changed) always send the full consolidated list of products after the change -- not just the item that changed. Replace your local copy with the list you receive instead of applying a delta.
Each products item has this shape:
{
"name": "Produto A - Mensal",
"price": 100.0,
"quantity": 1,
"variant_id": "va-month"
}Example: Subscription created (subscription_created)
{
"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": 49.90,
"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"
}
}Example: Recurring charge succeeded (subscription_charge_success)
{
"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": 49.90,
"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
Every billing cycle creates a new order. order_id changes on each charge; subscription_id stays the same. Use subscription_id to tie charges back to a subscription and order_id to reconcile with the order events (order_*) of that charge.
Example: Product added (subscription_product_added)
{
"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": 49.90,
"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"
}
}Idempotency for subscription events
The subscription envelope does not carry a unique event identifier. To deduplicate, use the combination event + subscription_id + order_id.
Careful: charge events legitimately repeat on every cycle -- the pair subscription_charge_success + subscription_id is not unique over time. It is order_id, new on every charge, that tells one cycle from another. Change events (product, frequency, cycle), on the other hand, can repeat the same order_id; for those, also factor in the payload content or the reception timestamp.
Withdraw Events
Withdrawal events notify a status change of a merchant withdrawal request: approved, paid or refused. The data field has the same format as GET /v1/withdraw-request/{withdraw_request_id}, without the source fields and with a reduced bank account.
Points of attention:
- Permissions. The app needs the
withdraw-request-approved,withdraw-request-paidandwithdraw-request-refusedpermissions to receive each event. - The withdrawal belongs to the merchant account, not to a store. Each app receives the event once, with the
site_idof the first store of the account where the app is installed. Withdrawals by split recipients also notify the apps installed on the account responsible for the split. client_keyandexternal_keycarry theexternal_keyregistered when the app was installed on that store (nullif there is none), since the withdrawal has no key of its own.- Values in cents. Unlike the other events,
net_value,gross_valueandwithdraw_taxare integers in cents (98400= R$ 984.00). - Centralized settlement. A withdrawal that goes through centralized settlement is notified as
withdraw_request_paidonce the settlement completes. - Coverage. Events are emitted for merchants on the standard Appmax withdrawal flow. Merchants on the legacy withdrawal flow or on the settled-installments anticipation model do not generate these events yet.
The data field for withdraw type events contains the following fields:
| Field | Type | Description |
|---|---|---|
withdraw_request_id | int | Withdrawal request ID |
status | string | Notified status: approved, paid or refused |
net_value | int | Net amount in cents (gross amount minus the withdrawal fee) |
gross_value | int | Gross requested amount in cents |
withdraw_tax | int | Withdrawal fee in cents |
currency | string | Currency (BRL) |
created_at | string | null | Request date/time (YYYY-MM-DD HH:MM:SS) |
bank_account | object | null | Destination bank account |
bank_account fields:
| Field | Type | Description |
|---|---|---|
id | int | Bank account ID in Appmax |
bank | string | Bank code |
agency | string | Branch |
account | string | Account number |
type | string | Account type (e.g., national_pj) |
pix_key | string | null | Pix key registered on the account |
Example: Withdrawal paid (withdraw_request_paid)
{
"event": "withdraw_request_paid",
"event_type": "withdraw",
"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": {
"withdraw_request_id": 7731,
"status": "paid",
"net_value": 149633,
"gross_value": 150000,
"withdraw_tax": 367,
"currency": "BRL",
"created_at": "2025-03-17 09:12:40",
"bank_account": {
"id": 4821,
"bank": "341",
"agency": "0512",
"account": "92603-7",
"type": "national_pj",
"pix_key": "[email protected]"
}
},
"partner_merchant": {
"merchant_email": "[email protected]",
"merchant_document_number": "12345678000199",
"merchant_phone": "11999999999"
}
}Example: Withdrawal refused (withdraw_request_refused)
{
"event": "withdraw_request_refused",
"event_type": "withdraw",
"site_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"app_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"client_key": null,
"external_key": null,
"data": {
"withdraw_request_id": 7742,
"status": "refused",
"net_value": 49633,
"gross_value": 50000,
"withdraw_tax": 367,
"currency": "BRL",
"created_at": "2025-03-18 16:40:05",
"bank_account": {
"id": 4821,
"bank": "341",
"agency": "0512",
"account": "92603-7",
"type": "national_pj",
"pix_key": "[email protected]"
}
},
"partner_merchant": {
"merchant_email": "[email protected]",
"merchant_document_number": "12345678000199",
"merchant_phone": "11999999999"
}
}Event Temporal Flow
The diagrams below illustrate the typical sequence of events for each payment method.
Credit Card:
customer_created -> order_authorized -> order_approved -> order_paid -> order_integratedPix:
customer_created -> order_pix_created -> [timeout: order_pix_expired]
-> order_paid_by_pix -> order_approved -> order_integratedBoleto:
customer_created -> order_billet_created -> [overdue: order_billet_overdue]
-> order_paid -> order_approved -> order_integratedRefund / Chargeback:
[approved order] -> order_refund (full)
-> order_partial_refund (partial)
-> order_chargeback_in_treatmentSubscription -- lifecycle:
subscription_created -> subscription_charge_success (every cycle, a new order_id)
-> subscription_charge_failed (charge declined)
-> subscription_cancelation (end of the subscription)
subscription_paused <-> subscription_resumedSubscription -- changes (any time while the subscription is active):
products -> subscription_product_added / subscription_product_removed
subscription_product_quantity_changed
recurrence -> subscription_frequency_changed
subscription_billing_day_changed / subscription_next_billing_day_changed
subscription_cycle_skipped <-> subscription_cycle_unskipped
account -> subscription_payment_method_updated / subscription_address_updatedWithdrawal:
[withdrawal requested] -> withdraw_request_approved -> withdraw_request_paid
-> withdraw_request_refusedEach withdrawal event is sent when the notified status changes. withdraw_request_paid is sent only once, even if the withdrawal later goes through centralized settlement.
INFO
A recurring charge also fires the order events (order_*) of the order created for that cycle. If your app subscribes to both types, expect to receive subscription_charge_success and order_approved for the same order_id.
WARNING
Event ordering is not guaranteed. Network delays, retries, and asynchronous processing can alter the sequence. Always check the current state of the resource before making decisions based on events.
Retry Policy
When the endpoint fails to receive a webhook, Appmax initiates a retry cycle:
Attempt 1 (original) -- after event delay
| failure
Attempt 2 -- +30 minutes
| failure
Attempt 3 -- +2 hours
| failure
Attempt 4 -- +4 hours
| failure
Webhook discarded (no notification)- HTTP Timeout: 5 seconds
- Success codes: 200, 201, 202, 203, 204, 205, 206, 207, 208, 226
- Failure: any other HTTP code or timeout triggers a retry
- Maximum: 4 attempts (1 original + 3 retries)
DANGER
After 4 unsuccessful attempts, the webhook is permanently discarded. There is no notification to the developer. Monitor your endpoint actively.
HTTP Headers
Every webhook request is sent with the following headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | GuzzleHttp/7 |
WARNING
Appmax does not send a signature header (HMAC) or authentication token in webhooks. We recommend validating the origin through other means (see Best Practices).
How to Receive Webhooks
Your endpoint must:
- Accept
POSTrequests withContent-Type: application/json - Respond with HTTP 200 within 5 seconds
- Process the event asynchronously (do not block the response)
WARNING
If the endpoint does not respond 200 within 5 seconds, Appmax initiates the retry cycle. Process the event in the background and respond immediately.
Code Examples
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}) // respond 200 even on error
return
}
// Respond 200 immediately to avoid the 5s timeout
c.JSON(200, gin.H{"received": true})
// Extract ID for idempotency
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 // duplicate
}
// Process in a goroutine -- in production, send to a queue
go func() {
log.Printf("Processing: %s (%s)", payload.Event, payload.EventType)
// Your logic here
}()
})
r.Run(":3000")
}Best Practices
Respond 200 before processing. The timeout is 5 seconds. Synchronous processing causes unnecessary retries. Respond immediately and process in the background (queue, thread, async job).
Implement idempotency. Use
order_id+event(orcustomer_id+event; for subscriptions,subscription_id+order_id+event) as a unique key. Retries legitimately resend the same event, and your system needs to handle duplicates without side effects.Store the raw payload. Save the complete JSON to a database or log before processing. This makes debugging easier and allows manual reprocessing without depending on resending.
Do not rely on event ordering. Network delays and retries can alter the sequence. Always check the current state of the resource (via API, if needed) before making decisions based on an event.
Use HTTPS. Protect data in transit. Appmax sends webhooks to both HTTP and HTTPS URLs, but customer and payment data are included in the payload.
Handle duplicates. Retries legitimately resend the same event. Ensure that processing the same event twice does not cause side effects (double charging, sending duplicate emails, etc.).
Validate the origin. Since there is no HMAC header, consider filtering by source IP, validating the payload structure against the expected schema, or confirming the event via the Appmax API.
Webhooks: Appstore vs Dashboard
There are two types of webhooks in the Appmax platform. Don't confuse them:
| Aspect | Appstore Webhooks | Dashboard Webhooks |
|---|---|---|
| Configured by | App developer, when creating the app | Merchant, in the store's admin panel |
| Scope | All merchants who install the app | Only that specific merchant's store |
| Destination URL | App host URL (set in the Appstore) | URL defined by the merchant in the panel |
| Events | 42 events documented on this page | Subset of events (varies by configuration) |
| Payload credentials | app_id, site_id, external_key | Different format, no app_id |
| When to use | Appstore integrations (this guide) | Direct merchant integrations |
WARNING
If you're integrating via the Appstore (you created an app, merchants install it), use the webhooks documented on this page. Dashboard webhooks are for merchants who configure notifications directly, without an intermediary app.
Errors and Troubleshooting
| Scenario | What happens | How to resolve |
|---|---|---|
| Endpoint returns non-2xx HTTP status | Retry initiated (up to 4 attempts) | Return 200, 201, or 202 |
| Endpoint does not respond within 5s | Timeout, retry initiated | Process async and respond 200 immediately |
| Endpoint returns 502 | Invalid URL or server is down | Verify the registered URL and server availability |
| Endpoint returns 401/403 | Authentication failure, retry initiated | Remove authentication from the endpoint or add a whitelist |
| Retries exhausted (4 attempts) | Webhook permanently discarded | Monitor your endpoint actively and request resending from support |
| Webhook takes long to arrive | Event may be in the retry queue | Check if the endpoint responded 200 on previous attempts |
| Webhook does not arrive (Yampi order) | Webhook suppressed | Intentional behavior for orders originating from Yampi |
customer_interested does not fire | Customer already has an order | Event only fires for leads with no associated order |
Testing and Debugging
webhook.site
A free service to inspect received webhooks. Create a temporary URL at webhook.site, configure it as the app's webhook URL, and view the received payloads in real time.
ngrok
To test webhooks directly in your local environment:
ngrok http 3000Use the HTTPS URL generated by ngrok as the app's webhook URL. Requests will be forwarded to localhost:3000, enabling end-to-end debugging with breakpoints.
General tips
- Check your server logs to confirm that requests are arriving
- Inspect the request headers to confirm
Content-Type: application/json - Validate that the received JSON is well-formed before processing
- Confirm that the app has the necessary permissions to receive the desired events