Skip to main content

Webhooks

Webhooks push real-time notifications to a URL you control whenever a payment, deposit, or client event changes state—so you can react without polling.

Subscribe to events

Register an endpoint and the event types you want with POST /webhooks.

curl -X POST "https://api.sandbox.capay.com.au/webhooks" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://yourdomain.com/webhooks/capay",
"events": ["PAYMENT", "DEPOSIT", "CLIENT"]
}'

Event types

Event typeFires on
PAYMENTPayment status changes
DEPOSITDeposit status changes
CLIENTClient status changes

You subscribe at the event-type level; the specific status is carried inside each notification's payload.

Managing subscriptions

ActionEndpoint
List subscriptionsGET /webhooks
Update a subscriptionPUT /webhooks/{id}
Delete a subscriptionDELETE /webhooks/{id}

How delivery works

Encrypted payloads

For security, every webhook body is encrypted with AES-256-CBC. You receive a POST containing the initialization vector and the ciphertext:

{
"iv": "c6407a24ca95deaec313786321ec1e39",
"encrypted": "encrypted_payload_here"
}

Decrypt it with your API secret to recover the notification.

DetailValue
AlgorithmAES-256-CBC
Key size32 bytes (256 bits)
IV size16 bytes (128 bits)
EncodingHex for both iv and encrypted
const { createDecipheriv } = require("crypto");

function decryptWebhookPayload(encryptedData, secret, iv) {
// Create decipher with AES-256-CBC
const decipher = createDecipheriv(
"aes-256-cbc",
Buffer.from(secret, "utf-8"),
Buffer.from(iv, "hex")
);

// Decrypt the data
let decrypted = decipher.update(encryptedData, "hex", "utf8");
decrypted += decipher.final("utf8");

// Parse and return JSON
return JSON.parse(decrypted);
}

// Usage
const secret = "your_32_byte_secret_key_here"; // 32 characters for AES-256
const iv = "1234567890abcdef1234567890abcdef"; // 16 bytes in hex (32 chars)
const encryptedData = "your_encrypted_data_here"; // Hex encoded encrypted data

try {
const decryptedPayload = decryptWebhookPayload(encryptedData, secret, iv);
console.log("Decrypted payload:", decryptedPayload);
} catch (error) {
console.error("Decryption failed:", error.message);
}

Decrypted notification

Once decrypted, the payload is a WebhookNotification:

{
"subscriptionId": "4e408b3d-5f70-423d-b940-f7192cd77252",
"eventType": "PAYMENT",
"eventStatus": "SCHEDULED",
"timestamp": "2025-12-08T05:53:17.372Z",
"messageId": "123e4567-e89b-12d3-a456-426614174000",
"eventObject": {
"id": "4e408b3d-5f70-223d-b940-f7192cd77252",
"referenceNo": "20251208-PTW122",
"currencyCode": "USD",
"amount": 123,
"status": "SCHEDULED"
}
}
FieldDescription
subscriptionIdThe subscription that produced this event
eventTypePAYMENT, DEPOSIT, or CLIENT
eventStatusCurrent status of the object
timestampWhen the event was published
messageIdUnique message ID—use it to deduplicate deliveries
eventObjectFull object: a payment, deposit, or client, depending on eventType
Idempotent handling

Because a webhook may be delivered more than once, use messageId to deduplicate on your side and make event processing idempotent.

Retry policy

If your endpoint does not return a 2xx, CAPAY retries delivery:

  • Timeout: each attempt has a 30-second timeout.
  • Duration: retries continue for up to 1 hour.
  • Backoff: exponential—short intervals at first, lengthening with each failure.

Troubleshooting

SymptomCheck
Decryption failsConfirm the secret matches your dashboard, and the IV comes from the payload
Invalid JSON after decryptEnsure PKCS7 padding is removed
Webhook never arrivesConfirm the endpoint is publicly reachable and accepts POST