Skip to content

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 sentWhat happens to the orderWebhooks you receive
Any valid CPF or CNPJ (e.g. 19100000000)Approved in ~5 seconds and then integratedPix: order_pix_created → order_paid, order_approved, order_paid_by_pix → order_integrated
Boleto: order_billet_created → order_paid, order_approved → order_integrated
CPF 40827365144 or CNPJ 40827365000109Stays pending: the payment is not confirmedOnly 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):

json
{
  "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.

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"
  }
}
FieldTypeRequiredDescription
eventstringYesEvent identifier (e.g., order_approved)
event_typestringYesEvent type: order, customer, payment, subscription, or withdraw
site_idstringYesUUID of the site where the event occurred
app_idstringYesUUID of the app receiving the webhook
client_keystring | nullNoKey configured by the merchant for external identification
external_keystring | nullNoExternal key associated with the resource
dataobjectYesResource data -- varies depending on the event_type
partner_merchantobjectYesMerchant data: merchant_email, merchant_document_number, merchant_phone

Event Table ​

Customer ​

Descriptioneventevent_type
Customer createdcustomer_createdcustomer
Customer interestedcustomer_interestedcustomer
Customer contactedcustomer_contactedcustomer

Order ​

Descriptioneventevent_type
Order authorizedorder_authorizedorder
Order approvedorder_approvedorder
Boleto createdorder_billet_createdorder
Order paidorder_paidorder
Order pending integrationorder_pending_integrationorder
Order refundedorder_refundorder
Partial refundorder_partial_refundorder
Upsell paidorder_up_soldorder
Pix generatedorder_pix_createdorder
Pix paidorder_paid_by_pixorder
Pix expiredorder_pix_expiredorder
Order integratedorder_integratedorder
Boleto overdueorder_billet_overdueorder
Order authorized with delayorder_authorized_with_delayorder
Chargeback in treatmentorder_chargeback_in_treatmentorder
Refused by riskorder_refused_by_riskorder
Payment splitsplit_ordersorder

Payment ​

Descriptioneventevent_type
Payment authorized with delaypayment_authorized_with_delaypayment
Payment not authorizedpayment_not_authorizedpayment

Subscription ​

Descriptioneventevent_type
Subscription createdsubscription_createdsubscription
Subscription cancelledsubscription_cancelationsubscription
Subscription pausedsubscription_pausedsubscription
Subscription resumedsubscription_resumedsubscription
Recurring charge succeededsubscription_charge_successsubscription
Recurring charge failedsubscription_charge_failedsubscription
Product addedsubscription_product_addedsubscription
Product removedsubscription_product_removedsubscription
Product quantity changedsubscription_product_quantity_changedsubscription
Frequency changedsubscription_frequency_changedsubscription
Cycle skippedsubscription_cycle_skippedsubscription
Cycle unskippedsubscription_cycle_unskippedsubscription
Billing day changedsubscription_billing_day_changedsubscription
Next billing date changedsubscription_next_billing_day_changedsubscription
Address updatedsubscription_address_updatedsubscription
Payment method updatedsubscription_payment_method_updatedsubscription
Subscription delayed (legacy)subscription_delayedsubscription

Withdraw ​

Descriptioneventevent_type
Withdrawal approvedwithdraw_request_approvedwithdraw
Withdrawal paidwithdraw_request_paidwithdraw
Withdrawal refusedwithdraw_request_refusedwithdraw

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:

FieldTypeDescription
order_idintOrder ID
statusstringCurrent order status
totalfloatTotal amount in BRL (123.00 = R$ 123.00)
freight_valuefloatShipping amount in BRL
merchant_totalfloatMerchant net amount in BRL
merchant_affiliate_totalfloatMerchant affiliate amount in BRL
discountfloatDiscount amount in BRL
interestfloatInterest amount in BRL
upsell_order_idint | nullAssociated upsell order ID
payment_link_idint | nullPayment link ID
paid_atstring | nullPayment date/time
integrated_atstring | nullIntegration date/time
refund_atstring | nullRefund date/time
created_atstringOrder creation date/time
productsarrayList of products in the order
payment_infoobjectPayment information (varies by method)
client_keystring | nullExternal identification key
external_keystring | nullExternal key
cashback_usedfloat | nullCashback used in BRL
cashback_reservedfloat | nullCashback reserved in BRL
cashback_statusstring | nullCashback status
notification_typestringNotification type

products[] fields:

FieldTypeDescription
skustringProduct SKU
namestringProduct name
pricefloatUnit price in BRL
quantityintQuantity

payment_info fields (conditional by payment method):

For Pix:

FieldTypeDescription
pix.end_to_end_idstringPix transaction end-to-end ID
pix.pix_creation_datestringPix creation date
pix.pix_expiration_datestringPix expiration date
pix.pix_emvstringEMV code (copy and paste)
pix.pix_refstringPix reference
pix.pix_qrcodestringQR Code image URL
pix.pix_payment_linkstringPix payment link

For Boleto:

FieldTypeDescription
boleto.boleto_overdue_datestringDue date
boleto.boleto_urlstringBoleto URL
boleto.boleto_digitable_linestringDigitable line

For Credit Card / Apple Pay:

FieldTypeDescription
credit_card.installmentsintNumber of installments
credit_card.card_brandstringCard brand
credit_card.nsustringTransaction NSU
credit_card.authorization_codestringAuthorization code
credit_card.captured_atstringCapture date/time

Example: Order approved with credit card (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": 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) ​

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": 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) ​

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": 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:

FieldTypeDescription
customer_idintCustomer ID
customer_dataobjectCustomer personal data
customer_data.firstnamestringFirst name
customer_data.lastnamestringLast name
customer_data.emailstringEmail
customer_data.telephonestringPhone number
customer_data.document_numberstringCPF or CNPJ
customer_data.custom_txtstring | nullCustom field
customer_addressobjectCustomer address
customer_address.postcodestringPostal code (CEP)
customer_address.streetstringStreet name
customer_address.street_numberstringStreet number
customer_address.street_complementstring | nullAddress complement
customer_address.street_districtstringNeighborhood
customer_address.citystringCity
customer_address.statestringState (UF)
created_atstringCreation date/time
updated_atstringLast update date/time
client_keystring | nullExternal identification key
external_keystring | nullExternal key

Example: Customer created (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 ​

The data field for payment type events contains the following fields:

FieldTypeDescription
customer_idintCustomer ID
order_idintOrder ID
payment_typestringPayment method (e.g., credit_card, pix, boleto)
payment_totalfloatPayment amount in BRL
cashback_usedfloat | nullCashback used in BRL

Example: Payment not authorized (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": 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:

  1. Base fields -- present in all subscription events (null when they do not apply to that event).
  2. Event-specific fields -- vary by event, described in the table further below.

Base fields ​

FieldTypeDescription
subscription_idintSubscription ID. The stable identifier across the whole lifecycle
order_idintID of the order that originated the subscription (on creation) or of the charge that fired it
customer_idintID of the customer who owns the subscription
totalfloatOrder total in BRL (123.00 = R$ 123.00)
intervalstring | nullRecurrence unit: week, month or year
interval_countint | nullNumber of units between charges (interval=month + interval_count=2 = every 2 months)
statusstring | nullSubscription state at the moment of the event (see table below)
cashback_usedfloat | nullCashback applied to the order, when any
client_keystring | nullMerchant external key (repeated from the envelope)
external_keystring | nullSame 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 ​

eventAdditional fields in 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 (subscription UUID)
subscription_charge_failedstatus = failed, uuid (subscription UUID)
subscription_product_addedproducts -- full product list after the change
subscription_product_removedproducts -- full product list after the change
subscription_product_quantity_changedproducts -- full product list after the change
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 or pix)
subscription_address_updatedFields of the updated shipping address
subscription_delayedstatus = 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:

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

Example: Subscription created (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": 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) ​

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": 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) ​

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": 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-paid and withdraw-request-refused permissions to receive each event.
  • The withdrawal belongs to the merchant account, not to a store. Each app receives the event once, with the site_id of 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_key and external_key carry the external_key registered when the app was installed on that store (null if there is none), since the withdrawal has no key of its own.
  • Values in cents. Unlike the other events, net_value, gross_value and withdraw_tax are integers in cents (98400 = R$ 984.00).
  • Centralized settlement. A withdrawal that goes through centralized settlement is notified as withdraw_request_paid once 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:

FieldTypeDescription
withdraw_request_idintWithdrawal request ID
statusstringNotified status: approved, paid or refused
net_valueintNet amount in cents (gross amount minus the withdrawal fee)
gross_valueintGross requested amount in cents
withdraw_taxintWithdrawal fee in cents
currencystringCurrency (BRL)
created_atstring | nullRequest date/time (YYYY-MM-DD HH:MM:SS)
bank_accountobject | nullDestination bank account

bank_account fields:

FieldTypeDescription
idintBank account ID in Appmax
bankstringBank code
agencystringBranch
accountstringAccount number
typestringAccount type (e.g., national_pj)
pix_keystring | nullPix key registered on the account

Example: Withdrawal paid (withdraw_request_paid) ​

json
{
  "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) ​

json
{
  "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_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 -> [overdue: order_billet_overdue]
                                         -> order_paid -> order_approved -> order_integrated

Refund / Chargeback:

[approved order] -> order_refund (full)
                 -> order_partial_refund (partial)
                 -> order_chargeback_in_treatment

Subscription -- 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_resumed

Subscription -- 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_updated

Withdrawal:

[withdrawal requested] -> withdraw_request_approved -> withdraw_request_paid
                       -> withdraw_request_refused

Each 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:

HeaderValue
Content-Typeapplication/json
User-AgentGuzzleHttp/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:

  1. Accept POST requests with Content-Type: application/json
  2. Respond with HTTP 200 within 5 seconds
  3. 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 ​

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}) // 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")
}
javascript
const express = require('express');
const app = express();
app.use(express.json());

// Map to track already processed events (in production, use a database)
const processed = new Set();

app.post('/webhooks/appmax', (req, res) => {
  // Respond 200 immediately to avoid the 5s timeout
  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(`Duplicate event ignored: ${idempotencyKey}`);
    return;
  }

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

  // Process event asynchronously
  // In production, send to a queue (Bull, RabbitMQ, etc.)
});

app.listen(3000, () => console.log('Webhook listener on port 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)

    # Process in background to respond quickly
    threading.Thread(target=process_event, args=(payload,)).start()

    return jsonify(received=True), 200

def process_event(payload):
    print(f"Processing: {payload['event']}")
    # Your logic here

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();

        // Dispatch to async job and respond 200 immediately
        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;

        // Check idempotency
        if (Cache::has("webhook:{$key}")) {
            return;
        }
        Cache::put("webhook:{$key}", true, now()->addHours(24));

        // Process event
        Log::info("Webhook received: {$event}", $this->payload);
    }
}

Best Practices ​

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

  2. Implement idempotency. Use order_id + event (or customer_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.

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

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

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

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

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

AspectAppstore WebhooksDashboard Webhooks
Configured byApp developer, when creating the appMerchant, in the store's admin panel
ScopeAll merchants who install the appOnly that specific merchant's store
Destination URLApp host URL (set in the Appstore)URL defined by the merchant in the panel
Events42 events documented on this pageSubset of events (varies by configuration)
Payload credentialsapp_id, site_id, external_keyDifferent format, no app_id
When to useAppstore 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 ​

ScenarioWhat happensHow to resolve
Endpoint returns non-2xx HTTP statusRetry initiated (up to 4 attempts)Return 200, 201, or 202
Endpoint does not respond within 5sTimeout, retry initiatedProcess async and respond 200 immediately
Endpoint returns 502Invalid URL or server is downVerify the registered URL and server availability
Endpoint returns 401/403Authentication failure, retry initiatedRemove authentication from the endpoint or add a whitelist
Retries exhausted (4 attempts)Webhook permanently discardedMonitor your endpoint actively and request resending from support
Webhook takes long to arriveEvent may be in the retry queueCheck if the endpoint responded 200 on previous attempts
Webhook does not arrive (Yampi order)Webhook suppressedIntentional behavior for orders originating from Yampi
customer_interested does not fireCustomer already has an orderEvent 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:

bash
ngrok http 3000

Use 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