Skip to content

Webhook Specification ​

Adopay delivers Payment, Refund, cashout, Exchange, and MED events asynchronously to integrators via webhooks (HTTP callbacks). This page defines the delivery rules shared by all callbacks: endpoint configuration, transport and timeout, signature verification, success responses, retries, and idempotency. Event types and payload fields for each product are defined on the corresponding event pages: merchant events are in the Webhooks group of the Merchant API, and partner events are in the Webhook group of the Partner API.

Core principle: a webhook is a notification, not the source of truth. Notifications may be delayed, duplicated, or arrive out of order. The final state of an order is always what the order query API returns; fall back to active polling when no notification arrives.

1. Delivery flow ​

mermaid
sequenceDiagram
    autonumber
    participant PLAT as ADOPAY Platform
    participant MER as Integrator endpoint

    PLAT->>MER: POST event payload (with signature headers)
    MER->>MER: Verify signature and timestamp
    MER->>MER: Idempotency check (ack duplicates immediately)
    MER-->>PLAT: Return 2xx within 3 seconds (SUCCESS recommended)
    Note over MER: Business processing (booking, fulfillment) runs asynchronously after the ack
    alt Timeout / non-2xx / non-conforming body
        PLAT->>MER: Automatic retries (the same event may be delivered multiple times)
    end

2. Endpoint configuration ​

Callback URLs are provided to Adopay through one of the following two methods. Different products are configured independently:

MethodApplies toNotes
Merchant dashboardMerchant API order callbacks, MED notificationsConfigure a fixed callback URL per product in the merchant dashboard (or through operations); API references show it as the /merchant-callback-url placeholder.
Request parameterMerchant API / Partner APIcashin refund requests use notifyUrl; when omitted, the merchant's configured and enabled refund notification URL is used. For other scenarios, refer to the endpoint documentation for the field name and behavior when omitted.

Endpoint requirements:

  1. Must be a complete HTTPS URL (scheme and domain included).
  2. Must be reachable from the public internet; internal-only or local development addresses are not acceptable.
  3. The receiver must respond within 3 seconds (see section 5); the backend behind the URL must meet this performance requirement.
  4. Configure and test endpoints separately for sandbox and production; complete webhook integration testing before going live (see the Onboarding Guide).

3. Common delivery rules ​

RuleRequirement
TransportHTTPS callback URL
Request methodPOST with Content-Type: application/json
TimeoutRespond within 3 seconds
Success criteriaHTTP 200-299 within 3 seconds; some products additionally require the body to be SUCCESS / OK (see section 5)
Failure handlingTimeouts, non-success responses, or connection failures count as delivery failures and trigger automatic retries
IdempotencyThe same event may be delivered multiple times; receivers must handle duplicates safely
SignatureVerify the platform signature; never process a payload before verification succeeds

4. Headers and signature verification ​

The platform sends the following headers with each delivery:

HeaderAlways presentDescription
X-Merchant-IdYesMerchant number receiving the notification.
X-TimestampYesUnix timestamp in seconds; receivers should reject requests outside an allowed skew (±300 seconds recommended).
X-NonceYesRandom string for this delivery, used against replay; it should not repeat within the validity window.
DigestYesBody digest in the format SHA-256=<Base64(SHA-256(raw body bytes))>.
AuthorizationYesPlatform ES256 signature; verify with the platform public key (keyId is currently fixed to v1).
X-Secret-Key-VersionNoSigning key version, present when the system provides one.
X-Trace-IdNoPlatform trace ID; provide it to Adopay when troubleshooting.

Verification steps:

  1. Recompute the digest from the raw request body bytes and compare it with the Digest header; do not deserialize the JSON and re-serialize it.
  2. Check that X-Timestamp is within the allowed window and that X-Nonce has not been seen before (replay protection).
  3. Verify the ES256 signature in Authorization with the platform public key, following the canonical string rules in sections 3 and 4 of Request Signing.
  4. If any step fails: do not process the event, return a non-2xx status so the platform retries, and log the request for investigation.

Python verification example:

python
import base64
import hashlib
import re

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec


def verify_webhook(path, raw_query, headers, raw_body, platform_public_key_pem):
    # 1. Verify the Digest: computed over the raw request body bytes
    expected_digest = "SHA-256=" + base64.b64encode(hashlib.sha256(raw_body).digest()).decode()
    if expected_digest != headers["Digest"]:
        return False

    # 2. Rebuild the canonical string (same rules as Request Signing)
    request_target = "post " + path + ("?" + raw_query if raw_query else "")
    canonical = "\n".join([
        f"(request-target): {request_target}",
        f"x-timestamp: {headers['X-Timestamp']}",
        f"x-nonce: {headers['X-Nonce']}",
        f"digest: {headers['Digest']}",
    ])

    # 3. Extract the signature from Authorization and verify with the platform public key
    signature_b64 = re.search(r'signature="([^"]+)"', headers["Authorization"]).group(1)
    public_key = serialization.load_pem_public_key(platform_public_key_pem.encode())
    try:
        public_key.verify(base64.b64decode(signature_b64), canonical.encode(), ec.ECDSA(hashes.SHA256()))
        return True
    except InvalidSignature:
        return False

Timestamp-window checks and nonce deduplication are implemented by the integrator. Always discard events that fail verification; never downgrade to a log-only mode.

5. Success response requirements ​

The minimum requirement for Adopay to consider a delivery successful: HTTP 200-299 received within 3 seconds. Some products (such as cashout) additionally require the body, after trimming whitespace, to be exactly SUCCESS or OK (case-sensitive). To satisfy every product, receivers should always return plain text:

http
HTTP/1.1 200 OK
Content-Type: text/plain

SUCCESS

Recommended handling order: verify signature -> idempotency check -> persist the event (database or queue) -> return SUCCESS immediately. Run business logic such as booking and fulfillment asynchronously after the response; never let it occupy the 3-second response window.

6. Retries, duplicates, and ordering ​

  • Retries: failed deliveries are retried automatically by Adopay; counts and intervals are defined on each product's event page (for example, cashout notifications retry up to 9 times after the first failure, 10 delivery attempts in total; design for "multiple duplicate deliveries" for other products).
  • Duplicates: retries and network jitter can both cause the same event to arrive more than once. Build the idempotency key from "business object ID + event type" (for example, merchant number + platOrderNo + event for order events, or medId + event for MED events); acknowledge duplicates immediately with a success response.
  • Ordering: delivery order is not guaranteed to match business order, and one order may receive events with different statuses over time. Apply state-machine checks so an older status never overwrites a newer one; when uncertain, query the order API instead of inferring the final state from notifications (see Idempotency).

7. Event payload structure ​

Business status fields in transaction notifications ​

Cashin, cashin refund, cashout, cashout refund, and Exchange webhooks carry the following business status fields at the top level of the notification JSON, at the same level as event, without an extra data nesting. These fields do not apply to MED notifications; the MED status is a flow status string and must not be treated as an integer response code.

FieldTypeDescription
statusintResponse code
msgstringCorresponds to status

status is of type int; status = 200 means the business processing is OK, in which case msg = "sucesso"; any value other than 200 indicates a business error, with the reason in msg. Judge by the integer status code, not by whether the field has a value; a missing, empty, or wrongly typed field must not be treated as business success. Business success does not mean the transaction is complete; combine event, orderStatus, and other business fields to determine the processing result. The status and msg in a webhook indicate the business processing result carried by the notification; the outer status and msg of a query API indicate the API response result. The business status fields do not affect acknowledging receipt of the notification as required in section 5.

Production payloads fall into two categories; fields are defined on each product's event page:

  1. Order events (Payment / Refund / cashout / Exchange): flat JSON where event identifies the type, alongside order fields:
json
{
  "event": "PIX_QRCODE_PAID",
  "status": 200,
  "msg": "sucesso",
  "merchantOrderNo": "20220721144249483",
  "platOrderNo": "3KITxA1dEXhZ2IRXvwck",
  "orderStatus": "SUCCESS",
  "amount": 10000,
  "payAmount": 10000
}

(Partial fields of a payment event; see the Cashin Webhook page for the full schema.)

  1. MED events: event identifies the event type; the payload contains the full fields of the Query MED Details data plus event-specific fields, and does not use the common API response envelope of status, msg, data. Only some fields are shown below:
json
{
  "event": "MED_APPROVED",
  "medId": "medc2874510938274639021",
  "occurredAt": "2026-01-16T10:30:00Z",
  "status": "ACCEPTED_BY_PSP",
  "amount": 1000.50,
  "analysisResult": "ACCEPTED"
}

(Example; see MED Webhook for each event's fields and triggers.)

New event types should adopt a unified envelope to simplify generic routing and idempotency for integrators:

json
{
  "eventId": "evt_xxx",
  "eventType": "PAYMENT.SUCCEEDED",
  "status": 200,
  "msg": "sucesso",
  "createdAt": 1786190400,
  "data": {}
}

Partner envelope ​

The Partner envelope additionally carries partnerId and merchantId alongside data:

json
{
  "eventId": "evt_xxx",
  "eventType": "PAYMENT.SUCCEEDED",
  "status": 200,
  "msg": "sucesso",
  "partnerId": "P10001",
  "merchantId": "M20001",
  "data": {
    "paymentId": "PAY10001",
    "merchantOrderNo": "ORDER10001",
    "amount": 100.00,
    "currency": "BRL",
    "status": "SUCCEEDED"
  }
}

Partner platforms should route events by merchantId to downstream merchants.

8. Security requirements ​

  1. Verify before processing: signature verification is the only strong authentication; do not replace it with IP allowlists, and never downgrade handling when verification fails.
  2. Protect sensitive data: callbacks may contain payee names, document numbers, and Pix Keys; mask them in logs and storage.
  3. Key management: verify callbacks with the platform public key (provided with onboarding materials); Adopay identifies signing key versions via keyId / X-Secret-Key-Version when keys rotate.

9. Receiver implementation checklist ​

10. Troubleshooting ​

SymptomWhere to look
No notifications receivedIs the callback URL publicly reachable with a valid HTTPS certificate; is notifyUrl in the cashin refund request or the merchant's configured and enabled refund notification URL correct (check the endpoint documentation for other scenarios); do gateways / firewalls allow Adopay requests.
Signature verification failsWas the digest computed over raw body bytes; do the four canonical lines match the header values and order; is the timestamp inside the allowed window.
Retries keep arrivingDid the receiver return 2xx within 3 seconds; for products with strict body rules, was the plain-text SUCCESS / OK returned (case-sensitive).
Duplicate notificationsExpected behavior (retry mechanism); confirm idempotent handling is in place.

Keep the X-Trace-Id value and contact Adopay when escalating an issue.