API reference
Webhooks
Cashwo sends a signed HTTPS POST to your endpoint whenever an agreement, payment or payout changes in a way you need to act on.
Set up
- In the merchant portal, open Settings → Webhooks.
- Enter your endpoint. It must be
https://with a valid certificate; redirects aren't followed. - Create a signing secret and store it with your API key.
Events
| Event | Sent when |
|---|---|
agreement_activated | The customer authorised the agreement at their bank. |
agreement_suspended | Paused by you, or by the customer at their bank. |
agreement_reactivated | A suspended agreement is active again. |
agreement_cancelled | Cancelled by you, the customer, or because it expired. |
agreement_failed | The authorisation failed at the bank. |
payment_settled | Money arrived. |
payment_failed | The debit was declined. payment.final says whether it will be retried. |
payment_returned | A settled payment was returned by the bank. |
payout_settled, payout_failed, payout_returned | A payout sent from the merchant portal changed status. |
Common fields
Every event carries event, event_id (a UUID that stays the same on every retry), created_at and merchant_id, plus:
| Events | Fields |
|---|---|
agreement_* | agreement_id, merchant_ref, previous_status, agreement (the agreement object) |
payment_* | transaction_id, transaction_ref, merchant_ref, amount, currency, status, agreement_id, payment (the payment object), payment_agreement; failures and returns add failure_reason |
payout_* | payout_id, payout (amount, currency, reference, status, failure_reason) |
Example
Content-Type: application/json X-Cashwo-Event: payment_settled X-Cashwo-Event-Id: 9199bfcb-6c27-4107-b1b3-909a3fa9f2e0 X-Cashwo-Delivery-Attempt: 1 X-Cashwo-Signature: sha256=5f2c9a…e81b { "event": "payment_settled", "event_id": "9199bfcb-6c27-4107-b1b3-909a3fa9f2e0", "created_at": "2027-01-01T09:00:41+11:00", "merchant_id": 12, "transaction_id": 88213, "transaction_ref": "3 Inst - INV-10482", "merchant_ref": "CUST-2291", "amount": 120.00, "currency": "AUD", "status": "SETTLED", "agreement_id": 4127, "payment": { /* payment object */ }, "payment_agreement": { /* agreement object */ } }
Verify the signature
X-Cashwo-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret. Compute it over the exact bytes you received, before parsing the JSON, compare in constant time, and reject the request if it doesn't match.
$raw = file_get_contents('php://input'); $given = $_SERVER['HTTP_X_CASHWO_SIGNATURE'] ?? ''; $expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('CASHWO_WEBHOOK_SECRET')); if (!hash_equals($expected, $given)) { http_response_code(401); exit; } $event = json_decode($raw, true); // Record $event['event_id'] first; skip it if you've seen it before. http_response_code(200);
const crypto = require('crypto'); app.post('/cashwo/webhook', express.raw({ type: 'application/json' }), (req, res) => { const expected = 'sha256=' + crypto .createHmac('sha256', process.env.CASHWO_WEBHOOK_SECRET) .update(req.body) // raw Buffer, not parsed JSON .digest('hex'); const given = req.get('X-Cashwo-Signature') || ''; const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); if (!ok) return res.sendStatus(401); const event = JSON.parse(req.body); // Record event.event_id first; skip it if you've seen it before. res.sendStatus(200); });
Delivery and retries
- Answer with any
2xxstatus within 15 seconds. Do slow work (emails, fulfilment) after responding. - Anything else (an error status, a timeout, no connection) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: 7 attempts over about 32 hours. After that the event is marked failed.
- Every attempt is listed under Settings → Webhooks, where you can also replay it by hand.
- Retries and replays have the same
event_idand body. Make your handler idempotent onevent_id;X-Cashwo-Delivery-Attempttells you which attempt it is. - Events can arrive out of order. Use the object's
statusandupdated_at, or fetch it from the API, rather than assuming order.