Webhooks

Registering URLs to be told about settlement as it happens.

Webhook Signature Verification

To ensure webhook requests are sent by our API and not forged by third parties, we sign each request using HMAC-SHA256 (secret-keyed).

How it works

Each webhook request includes a header:

X-Signature: <signature>

This signature is generated by signing the raw request payload with the secret you provided during webhook registration.

How to verify

  1. Retrieve the raw payload from the request exactly as received — do not parse or modify it before validation.
  2. Use your webhook secret to compute a SHA-256 HMAC of the raw payload.
  3. Compare the computed HMAC with the value from the X-Signature header.
  4. If they match, the request is authentic.
🚧

Always use the raw payload for signature validation. Parsing or re-encoding the body before computing the signature can lead to mismatches.

Implementation Code Example

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Locale;

class WebhookSignatureExample {

    /**
     * Computes the same signature the gateway sends in the X-Signature header:
     * HMAC-SHA256 of the raw request payload using the webhook secret,
     * hex-encoded lowercase.
     */
    public static String sign(String payload, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] raw = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));

        StringBuilder hex = new StringBuilder(raw.length * 2);
        for (byte b : raw) {
            hex.append(String.format(Locale.ROOT, "%02x", b));
        }
        return hex.toString();
    }

    /**
     * Constant-time comparison
     */
    public static boolean verify(String payload, String secret, String signature) throws Exception {
        String expected = sign(payload, secret);
        return MessageDigest.isEqual(
                expected.getBytes(StandardCharsets.UTF_8),
                signature.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * After the signature check passes, route on the event and process the data.
     */
    public static void handleWebhook(String rawBody) {
        System.out.println("event handled: " + rawBody);
    }

    // Demo: verifies the exact payload + secret + signature produced by the gateway.
    public static void main(String[] args) throws Exception {
        // The webhook secret you set in the gateway dashboard or via the API. Keep it safe and never share it.
        String secret = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234";

        // Exactly the bytes the gateway sends (compact JSON, {"data":...,"event":...}).
        String payload = "{\"data\":{\"id\":\"4c430b09-ae6c-4f35-b459-32cecc0e9b0a\",\"reference_code\":\"ABC5e3bbb797418882b4b52208e03ca6\",\"amount\":2.01,\"end_to_end_id\":\"E46026562202608201538s5yfppvhnqz\",\"status\":\"done\",\"error\":\"\",\"created_at\":\"2026-08-20T15:38:22.843355Z\",\"completed_at\":\"2026-08-20T15:38:25.784831Z\",\"account_id\":123,\"recipient\":{\"name\":\"BRUNO EDUARDO ARAÚJO SOUZA\",\"pix_key\":\"dd464c53-a855-49d1-8c88-21e464f71713\",\"document_number\":\"65354373026\",\"account_ispb\":\"20855875\",\"account_bank\":\"\"},\"payer\":{\"account_bank\":\"monetarie\",\"account_number\":\"1002988\",\"document_number\":\"29067364000170\",\"name\":\"COMPANY NAME LTDA\"},\"refunds\":null},\"event\":\"PayOutCompleted\"}";

        // X-Signature header value example, sent by the gateway. This is the expected signature for the payload above.
        String signature = "000202b1e9ca52148853d3d0931462e56c975c96583cec664b7e4dcae1acbe04";

        String computed = sign(payload, secret);
        System.out.println("computed signature   : " + computed);
        System.out.println("expected X-Signature : " + signature);
        System.out.println("signature MATCH      : " + computed.equals(signature));

        System.out.println("verify(original) : " + verify(payload, secret, signature));
        System.out.println("verify(tampered) : " + verify(payload + " ", secret, signature));
        System.out.println("verify(wrong key): " + verify(payload, "wrong-secret", signature));

        handleWebhook(payload);
    }
}
import * as crypto from 'crypto';

class WebhookSignatureExample {

    /**
     * Computes the same signature the gateway sends in the X-Signature header:
     * HMAC-SHA256 of the raw request payload using the webhook secret,
     * hex-encoded lowercase.
     */
    public static sign(payload: string, secret: string): string {
        return crypto
            .createHmac('sha256', secret)
            .update(payload, 'utf8')
            .digest('hex');
    }

    /**
     * Constant-time comparison to prevent timing attacks
     */
    public static verify(payload: string, secret: string, signature: string): boolean {
        const expected = this.sign(payload, secret);
        
        const expectedBuffer = Buffer.from(expected, 'utf8');
        const signatureBuffer = Buffer.from(signature, 'utf8');
        
        // Both buffers must be the same length for timingSafeEqual
        if (expectedBuffer.length !== signatureBuffer.length) {
            return false;
        }
        
        return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
    }

    /**
     * After the signature check passes, route on the event and process the data.
     */
    public static handleWebhook(rawBody: string): void {
        console.log("event handled: " + rawBody);
    }

    // Demo: verifies the exact payload + secret + signature produced by the gateway.
    public static main(): void {
        // The webhook secret you set in the gateway dashboard or via the API. Keep it safe and never share it.
        const secret = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234";

        // Exactly the bytes the gateway sends (compact JSON, {"data":...,"event":...}).
        const payload = "{\"data\":{\"id\":\"4c430b09-ae6c-4f35-b459-32cecc0e9b0a\",\"reference_code\":\"ABC5e3bbb797418882b4b52208e03ca6\",\"amount\":2.01,\"end_to_end_id\":\"E46026562202608201538s5yfppvhnqz\",\"status\":\"done\",\"error\":\"\",\"created_at\":\"2026-08-20T15:38:22.843355Z\",\"completed_at\":\"2026-08-20T15:38:25.784831Z\",\"account_id\":123,\"recipient\":{\"name\":\"BRUNO EDUARDO ARAÚJO SOUZA\",\"pix_key\":\"dd464c53-a855-49d1-8c88-21e464f71713\",\"document_number\":\"65354373026\",\"account_ispb\":\"20855875\",\"account_bank\":\"\"},\"payer\":{\"account_bank\":\"monetarie\",\"account_number\":\"1002988\",\"document_number\":\"29067364000170\",\"name\":\"COMPANY NAME LTDA\"},\"refunds\":null},\"event\":\"PayOutCompleted\"}";

        // X-Signature header value example, sent by the gateway. This is the expected signature for the payload above.
        const signature = "000202b1e9ca52148853d3d0931462e56c975c96583cec664b7e4dcae1acbe04";

        const computed = this.sign(payload, secret);
        console.log("computed signature   : " + computed);
        console.log("expected X-Signature : " + signature);
        console.log("signature MATCH      : " + (computed === signature));

        console.log("verify(original) : " + this.verify(payload, secret, signature));
        console.log("verify(tampered) : " + this.verify(payload + " ", secret, signature));
        console.log("verify(wrong key): " + this.verify(payload, "wrong-secret", signature));

        if (this.verify(payload, secret, signature)) {
            this.handleWebhook(payload);
        }
    }
}

WebhookSignatureExample.main();

Delivery, Retries and Acknowledgment

Each webhook delivery is acknowledged by the HTTP status code you return:

  • 2xx (200–299) — the callback was processed. The delivery stops here.
  • Anything else, a timeout, or a network failure — we retry the same payload.

Retry schedule

Each attempt has a 5-second timeout. Failed deliveries are retried up to 20 attempts with the following backoff:

AttemptDelay before next retry
15 seconds
210 seconds
320 seconds
430 seconds
540 seconds
61 minute
7–1915 minutes

After the 20th attempt the delivery is dropped.

Integration notes

  • Return 2xx only after the event has been processed (or durably queued). Responding early with 2xx and processing later can lose events on crash.
  • The same event may be delivered more than once (e.g. a retry after a timeout where your side actually processed it). Make your processing idempotent — dedupe on data.id, data.reference_code, or data.end_to_end_id (data.original_end_to_end_id for refunds).
  • Verify the X-Signature header before processing the payload.

Events

  • PayInCompleted
  • PayOutCompleted
  • PayOutRefunded
  • PayInRefunded
  • InfractionCreated
  • InfractionStatusChanged

Webhook Event Examples

🔹 PayInCompleted : Payment confirmed.

Check the possible pay-in statuses at: https://docs.nxp.techworks.app/reference/get-payin

{
  "data": {
    "account_id": 2,
    "amount": 3,
    "created_at": "2025-08-27T16:59:36.9111Z",
    "end_to_end_id": "E18236120202508271700s00a9b2c354",
    "expiration": 300,
    "id": "8217f189-39dc-4902-a562-243e59cdfd8d",
    "payer": {
      "account_bank": "NU PAGAMENTOS - IP",
      "account_ispb": "18236120",
      "document_number": "04870348306",
      "name": "BRUNO EDUARDO ARAÚJO SOUZA"
    },
    "qr_code_string": "00020101021226870014br.gov.bcb.pix2565pix.delbank.com.br/v2/12573115/cob/TX2025082713598Jqnpeb9TIgbLGsR5204000053039865802BR5907DELBANK6009SAO PAULO62070503***6304C540",
    "refunds": null,
    "status": "paid",
    "txid": "TX2025082713598Jqnpeb9TIgbLGsR"
  },
  "event": "PayInCompleted"
}

🔹 PayOutCompleted : Outgoing payment completed.

Check the possible pay-out statuses at: https://docs.nxp.techworks.app/reference/get-out

{
  "data": {
    "account_id": 2,
    "amount": 1,
    "created_at": "2025-08-27T17:06:24.190536Z",
    "end_to_end_id": "E12573115202508271706Z8hFFYdvcwb",
    "error": null,
    "id": "24147420-095d-4583-8da0-8558776ed8ed",
    "payer": {
      "account_bank": "nel3",
      "account_number": "31216",
      "document_number": "59614054000190",
      "name": "Nel3 New GTW"
    },
    "recipient": {
      "account_bank": "NU PAGAMENTOS - IP",
      "account_ispb": "18236120",
      "document_number": "04870348306",
      "name": "Bruno Eduardo Araújo Souza",
      "pix_key": "04870348306"
    },
    "reference_code": "33043d54-02ab-4069-808b-10b7d1fe17af",
    "refunds": null,
    "status": "done"
  },
  "event": "PayOutCompleted"
}

🔹PayOutRefunded : Return of an outgoing payment (the recipient or the bank returned the funds).

{
  "data": {
    "amount": 1,
    "created_at": "2025-08-27T17:08:35.279990789Z",
    "end_to_end_id": "D18236120202508271708s00737bbb7d",
    "id": "cf04ea0e-e22e-4ffc-befb-7f2c338ce2ee",
    "original_end_to_end_id": "E12573115202508271706Z8hFFYdvcwb",
    "reference_code": "33043d54-02ab-4069-808b-10b7d1fe17af"
  },
  "event": "PayOutRefunded"
}

🔹PayInRefunded : Return of funds to the payer's account.

This event will be sent when there is support for the return of a receipt (Pay In) initiated via API and will be used every time a PIX receipt is returned to the payer.

{
  "data": {
    "id": "6e543181-4f25-48b4-9eec-03cd22d62cad",
    "txid": "7eff145ff45c418dbe4c46ec5eb8084",
    "amount": 1,
    "end_to_end_id": "E00000000202512192133284b76675fe",
    "original_end_to_end_id": "E18236120202508271700s00a9b2c353",
    "created_at": "2025-08-27T17:08:35.279990789Z"
  },
  "event": "PayInRefunded"
}

🔹InfractionCreated

Sent once, when a MED claim against one of the account's pay-ins is recorded. The med_status is always open on this event.

{
  "event": "InfractionCreated",
  "data": {
    "id": "5e8a1b20-7c4d-4e9f-b0a1-c2d3e4f50617",
    "infraction_type": "REFUND_REQUEST",
    "amount": 149.9,
    "end_to_end_id": "E1234567820260918140500abcdef123",
    "reference_code": "order-2026-09-18-0001",
    "situation_type": "SCAM",
    "report_details": "Payer reports the goods were never delivered.",
    "status": "OPEN",
    "med_status": "open",
    "defense_deadline": "2026-09-21T15:02:00Z",
    "created_at": "2026-09-18T15:02:00Z"
  }
}

🔹InfractionStatusChanged

Sent each time the case's med_status moves. data is the case as it stands after the move; only med_status differs from the previous delivery unless the provider has updated other fields in between.

{
  "event": "InfractionStatusChanged",
  "data": {
    "id": "5e8a1b20-7c4d-4e9f-b0a1-c2d3e4f50617",
    "infraction_type": "REFUND_REQUEST",
    "amount": 149.9,
    "end_to_end_id": "E1234567820260918140500abcdef123",
    "reference_code": "order-2026-09-18-0001",
    "situation_type": "SCAM",
    "report_details": "Payer reports the goods were never delivered.",
    "status": "OPEN",
    "med_status": "sent_to_customer",
    "defense_deadline": "2026-09-21T15:02:00Z",
    "created_at": "2026-09-18T15:02:00Z"
  }
}

med_status values

ValueMeaning
openThe case has arrived and has not been triaged. Every case starts here.
under_reviewBeing assessed internally.
sent_to_customerThe claim has been forwarded to the account holder for their account of it.
awaiting_customerSent, still unanswered.
sent_for_defenseA defense has been filed with the provider.
defense_acceptedThe defense was accepted.
defense_rejectedThe defense was rejected.
not_applicableA case no defense can be mounted for.
closedFinished, whatever the outcome.