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
- Retrieve the raw payload from the request exactly as received — do not parse or modify it before validation.
- Use your webhook secret to compute a SHA-256 HMAC of the raw payload.
- Compare the computed HMAC with the value from the X-Signature header.
- 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:
| Attempt | Delay before next retry |
|---|---|
| 1 | 5 seconds |
| 2 | 10 seconds |
| 3 | 20 seconds |
| 4 | 30 seconds |
| 5 | 40 seconds |
| 6 | 1 minute |
| 7–19 | 15 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, ordata.end_to_end_id(data.original_end_to_end_idfor refunds). - Verify the
X-Signatureheader 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
med_status values| Value | Meaning |
|---|---|
open | The case has arrived and has not been triaged. Every case starts here. |
under_review | Being assessed internally. |
sent_to_customer | The claim has been forwarded to the account holder for their account of it. |
awaiting_customer | Sent, still unanswered. |
sent_for_defense | A defense has been filed with the provider. |
defense_accepted | The defense was accepted. |
defense_rejected | The defense was rejected. |
not_applicable | A case no defense can be mounted for. |
closed | Finished, whatever the outcome. |

