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 4 event types (order, customer, payment, subscription) totaling 40 available events.
INFO
Appstore webhooks are dispatched in real time as soon as the event occurs on the platform.
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, or subscription |
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 |
| Chargeback won (merchant favor) | order_charge_back_gain | 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 |
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
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 | int | Total amount in cents (12300 = R$ 123.00) |
freight_value | int | Shipping amount in cents |
merchant_total | int | Merchant net amount in cents |
merchant_affiliate_total | int | Merchant affiliate amount in cents |
discount | int | Discount amount in cents |
interest | int | Interest amount in cents |
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 | int | null | Cashback used in cents |
cashback_reserved | int | null | Cashback reserved in cents |
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 | int | Unit price in cents |
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": 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"
}
}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": 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"
}
}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": 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
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 | int | Payment amount in cents |
cashback_used | int | null | Cashback used in cents |
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": 15900,
"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 | int | Order total in cents (12300 = 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 |
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": 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"
}
}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": 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
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": 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"
}
}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.
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_treatment -> order_charge_back_gainSubscription -- 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_updatedINFO
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 | 40 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