Skip to main content
Webhooks allow your application to be notified automatically when something happens to your transactions.

How it works

When you create a transaction with callbackUrl, Safefy sends an HTTP POST request to that URL whenever the status changes.

Available events

Transaction events

EventDescriptionWhen it happens
transaction.completedPayment confirmedWhen PIX is paid and confirmed by the bank
transaction.expiredTransaction expiredWhen the payment window ends (default: 30 min)
transaction.failedProcessing failedWhen there is an acquirer or validation error
transaction.refundedPayment refundedWhen a confirmed payment is refunded

Cashout events

EventDescriptionWhen it happens
cashout.completedCashout completedWhen PIX is sent to the destination account
cashout.failedCashout failedWhen a cashout is rejected or fails

Transaction statuses

Understand the transaction lifecycle and when each webhook is fired:

Details for each status

The transaction has been created and is waiting for the customer to pay.What to do: Show the QR Code/PIX copy-and-paste string and wait for confirmation.Webhook: No webhook is sent for this status.
The payment was received and confirmed by the bank. The amount is now available in your balance.What to do: Release the product/service to the customer.Webhook: transaction.completedImportant fields in the webhook:
  • data.completedAt - Confirmation date/time
  • data.pix.endToEndId - Unique transaction ID at the Central Bank
  • data.pix.payerName - Payer name
  • data.pix.payerDocument - Payer document (masked)
  • data.pix.payerBank - Payer bank
The payment window expired and the transaction was automatically cancelled.What to do: Inform the customer and offer a new attempt.Webhook: transaction.expired
An error occurred while processing the transaction.What to do: Check the failureReason field and take the appropriate action.Webhook: transaction.failedImportant field: data.failureReason - Failure reason
The confirmed payment was refunded. The amount was returned to the payer.What to do: Revoke access to the product/service.Webhook: transaction.refundedImportant field: data.refundedAt - Refund date/time
The transaction was manually cancelled before payment.What to do: No action required.Webhook: No webhook is sent for manual cancellations.

Example payloads

transaction.completed

Sent when a PIX payment is confirmed:
{
  "id": "whk_550e8400-e29b-41d4-a716-446655440000",
  "event": "transaction.completed",
  "createdAt": "2024-01-15T10:30:00Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "externalId": "order-123",
    "amount": 1000,
    "fee": 15,
    "netAmount": 985,
    "status": "Completed",
    "method": "Pix",
    "pix": {
      "txId": "SAFEFY2024011512345678901234",
      "endToEndId": "E12345678202401151030ABC123",
      "payerName": "Joao Silva",
      "payerDocument": "***456789**",
      "payerBank": "Banco do Brasil"
    },
    "completedAt": "2024-01-15T10:30:00Z"
  }
}

transaction.expired

Sent when the payment window expires:
{
  "id": "whk_661f9511-f3c8-52e5-b827-557766551111",
  "event": "transaction.expired",
  "createdAt": "2024-01-15T11:00:00Z",
  "data": {
    "id": "661f9511-f3c8-52e5-b827-557766551111",
    "externalId": "order-456",
    "amount": 5000,
    "fee": 75,
    "netAmount": 4925,
    "status": "Expired",
    "method": "Pix",
    "pix": {
      "txId": "SAFEFY2024011598765432101234",
      "endToEndId": null,
      "payerName": null,
      "payerDocument": null
    },
    "expiresAt": "2024-01-15T11:00:00Z"
  }
}

transaction.refunded

Sent when a payment is refunded:
{
  "id": "whk_772a0622-g4d9-63f6-c938-668877662222",
  "event": "transaction.refunded",
  "createdAt": "2024-01-15T14:00:00Z",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "externalId": "order-123",
    "amount": 1000,
    "fee": 15,
    "netAmount": 985,
    "status": "Refunded",
    "method": "Pix",
    "pix": {
      "txId": "SAFEFY2024011512345678901234",
      "endToEndId": "E12345678202401151030ABC123",
      "payerName": "Joao Silva",
      "payerDocument": "***456789**"
    },
    "completedAt": "2024-01-15T10:30:00Z",
    "refundedAt": "2024-01-15T14:00:00Z"
  }
}

cashout.completed

Sent when a cashout is processed successfully:
{
  "id": "whk_883b1733-h5e0-74g7-d049-779988773333",
  "event": "cashout.completed",
  "createdAt": "2024-01-15T16:00:00Z",
  "data": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "externalId": "cashout_001",
    "amount": 50000,
    "fee": 200,
    "netAmount": 49800,
    "status": "Completed",
    "pix": {
      "pixKeyType": "CPF",
      "pixKey": "***456789**",
      "endToEndId": "E12345678202401151600xyz789abc012"
    },
    "requestedAt": "2024-01-15T15:50:00Z",
    "completedAt": "2024-01-15T16:00:00Z"
  }
}

Sent headers

HeaderDescription
X-Safefy-SignatureHMAC-SHA256 signature for validation
X-Safefy-EventEvent type (transaction.completed, etc)
X-Safefy-DeliveryUnique delivery ID
X-Safefy-AttemptAttempt number (1, 2, 3…)

Validating the signature

To ensure the webhook is from Safefy, validate the signature:
const crypto = require('crypto');

function validateWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
Always validate the signature before processing the webhook to avoid spoofing attacks.

Retries

If your application does not respond with 2xx, Safefy will retry:
AttemptInterval
1Immediately
21 minute
35 minutes
430 minutes
52 hours

Best practices

Respond quickly

Return 200 OK immediately and process the webhook asynchronously.

Be idempotent

Use the webhook id to avoid processing the same event twice.

Validate the signature

Always verify X-Safefy-Signature before trusting the payload.

Use HTTPS

Configure your callbackUrl with HTTPS in production.