PayintoDocs

Webhooks

Receive signed payment events, verify them, and handle retries.

Webhooks tell your server when money moves, so you don't need to poll.

Set up an endpoint

In the dashboard, open Settings > API Keys & Webhooks and choose the environment. Each environment has one webhook with:

  • URL: your HTTPS endpoint
  • Secret token: a long random value that you create and use to verify signatures
  • Events: the events you want, a wildcard such as transaction.*, or * for all

Saving a webhook needs your transaction PIN.

Events

EventSent whenUse it to
checkout.payment.succeededA verified bank transfer or card checkout payment succeedsFulfill checkout orders
virtual_account.transfer.receivedA virtual account deposit is creditedCredit a customer's balance
transaction.credit.succeededA credit is recorded on your accountKeep your ledger up to date
transaction.debit.succeededA debit is recorded on your accountKeep your ledger up to date
bulk_transfer.finishedEvery recipient in a bulk transfer has reached a final statusReconcile a payout

One payment can send several events

A checkout bank transfer can send the first three events for a single payment. Fulfill checkout orders from checkout.payment.succeeded only. Pending, failed, expired, and refunded checkout events aren't sent.

Delivery format

Every delivery is a POST with these headers:

Content-Type: application/json
X-Payinto-Webhook-Id: 01J1234567890ABCDEFGHJKMNP
X-Payinto-Event: virtual_account.transfer.received
X-Payinto-Timestamp: 1793404800
X-Payinto-Signature: t=1793404800,v1=5f2b8c...

The body uses a stable envelope:

{
  "event": "virtual_account.transfer.received",
  "id": "01J1234567890ABCDEFGHJKMNP",
  "created_at": "2026-10-31T09:00:00.000000Z",
  "api_version": "2026-05-27",
  "data": {
    "id": "0196431e-1b44-7d10-a2c9-8e5f7b1c2d3e",
    "reference": "PIF01EXAMPLE",
    "merchant_reference": "order-10001",
    "currency": "NGN",
    "amount": 150000,
    "settled_amount": 150000,
    "status": "successful"
  }
}

The API reference lists the full data schema for each event. All five events include merchant_reference, which can be null.

Verify the signature

Verify every delivery before you trust it. To do this:

  1. Read the raw request body, before any JSON parsing.
  2. Split X-Payinto-Signature into t and v1.
  3. Compute HMAC-SHA256 of {t}.{raw_body} with your secret token, as lowercase hex.
  4. Compare it with v1 using a constant-time function.
  5. Reject deliveries where t is more than five minutes from your clock.
# Check a signature by hand from a saved delivery.
TIMESTAMP=1793404800
SIGNATURE="5f2b8c..."

EXPECTED=$(printf '%s.%s' "$TIMESTAMP" "$(cat body.json)" \
  | openssl dgst -sha256 -hmac "$PAYINTO_WEBHOOK_SECRET" -hex \
  | awk '{print $NF}')

[ "$EXPECTED" = "$SIGNATURE" ] && echo "valid" || echo "invalid"

Sign the raw bytes. If you parse the JSON and encode it again, the signature won't match.

The PHP SDK includes webhook verification.

Retries

Respond with any 2xx status to acknowledge a delivery. Any other status, a connection failure, or a slow response counts as a failure. Payinto waits up to 10 seconds to connect and 60 seconds for a response.

A failed webhook is retried up to 14 attempts in total, over about 11 days:

After attemptWait before the next attempt
1Within seconds
21 minute
35 minutes
415 minutes
530 minutes
61 hour
76 hours
812 hours
91 day
101.5 days
112 days
122.5 days
133 days

After the 14th attempt, the delivery is marked as failed. You can review deliveries and replay them from the webhook logs in the dashboard.

Each attempt is signed again with a new timestamp, so a retry still passes your timestamp check. The X-Payinto-Webhook-Id stays the same.

Handle events reliably

  • Respond quickly. Save the event, return 200, and do slow work in a background job.
  • Process events once. Retries and replays can deliver an event again. Store the id of processed events, or use data.id or data.reference as a unique key.
  • Handle events in any order. Delivery order isn't guaranteed.
  • Check the amount. Before you give value, match merchant_reference, currency, and the expected amount.
  • Fall back to a status query. If you're unsure about a payment, call GET /transactions/tsq/{reference}.

Test in the sandbox

Register a webhook in the test environment, then simulate a deposit from the dashboard or create a sandbox transfer. Test and live webhooks are separate, and each has its own secret.

On this page