# Webhooks (/docs/direct-debit/webhooks)

Webhooks enable real-time notifications when transaction statuses change.

## Overview [#overview]

The Revio API allows you to:

- Configure webhook URLs for different event types
- Receive real-time callbacks when collections or payments change status
- Test webhook integrations in the sandbox environment

There is no limit to the number of webhooks you can configure.

## Callback Types [#callback-types]

| Type | Description |
|---|---|
| `EFT_COLLECTION` | EFT debit order collection status changes |
| `DEBICHECK_COLLECTION` | DebiCheck collection status changes |
| `DEBICHECK_MANDATE` | DebiCheck mandate status changes |
| `EFT_PAYMENT` | EFT payment status changes |


## Step 1: Create Callback URL [step] [#step-1-create-callback-url-step]

Register a URL to receive webhook notifications.

### Endpoint [step] [#endpoint-step]

```text
POST https://api.revio.co.za/v2/clientcallbackurl/
```

### Request [step] [#request-step]

```bash
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": 109,
    "url": "https://your-server.com/webhooks/revio",
    "callbackType": "EFT_COLLECTION",
    "callbackVersion": "V2"
  }'
```

### Request Fields [step] [#request-fields-step]

| Field | Type | Required | Description |
|---|---|---|---|
| `clientId` | integer | Yes | Your client ID (from merchant profile `createdByClient`) |
| `url` | string | Yes | HTTPS URL to receive callbacks |
| `callbackType` | string | Yes | Event type to subscribe to |
| `callbackVersion` | string | Yes | Callback version (`V2` recommended) |

### Response [step] [#response-step]

```json
{
  "id": "callback-url-123",
  "clientId": 109,
  "url": "https://your-server.com/webhooks/revio",
  "callbackType": "EFT_COLLECTION",
  "callbackVersion": "V2"
}
```

### Register Multiple Callback Types [step] [#register-multiple-callback-types-step]

Create separate callback URLs for each event type:

```bash
# EFT Collections [step]
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -d '{"clientId": 109, "url": "https://...", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2"}'

# DebiCheck Collections [step]
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -d '{"clientId": 109, "url": "https://...", "callbackType": "DEBICHECK_COLLECTION", "callbackVersion": "V2"}'

# DebiCheck Mandates [step]
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -d '{"clientId": 109, "url": "https://...", "callbackType": "DEBICHECK_MANDATE", "callbackVersion": "V2"}'

# EFT Payments [step]
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -d '{"clientId": 109, "url": "https://...", "callbackType": "EFT_PAYMENT", "callbackVersion": "V2"}'
```

## Step 2: Trigger Callback Event [step] [#step-2-trigger-callback-event-step]

Trigger a callback for a specific collection (useful for re-sending notifications).

### Endpoint [step] [#endpoint-step-1]

```text
POST https://api.revio.co.za/v2/callback/collection/eft/{collectionId}/
```

### Request [step] [#request-step-1]

```bash
curl -X POST https://dev.api.revio.co.za/v2/callback/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "collectionStatus": "UNPAID",
    "callbackVersion": "V2"
  }'
```

This sends the current collection status to your configured callback URL.

## Step 3: Test Callback (Sandbox) [step] [#step-3-test-callback-sandbox-step]

Simulate callback events to test your webhook handler without affecting actual transactions.

### Endpoint [step] [#endpoint-step-2]

```text
POST https://api.revio.co.za/v2/callback/test/collection/eft/{collectionId}/
```

### Request [step] [#request-step-2]

```bash
curl -X POST https://dev.api.revio.co.za/v2/callback/test/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "collectionStatus": "UNPAID",
    "callbackVersion": "V2"
  }'
```

### Request Fields [step] [#request-fields-step-1]

| Field | Type | Required | Description |
|---|---|---|---|
| `collectionStatus` | string | Yes | Status to simulate |
| `callbackVersion` | string | Yes | Callback version |
| `overrideUrl` | string | No | Send to different URL for testing |

### Override URL Example [step] [#override-url-example-step]

Send test callback to a different URL:

```json
{
  "overrideUrl": "https://webhook.site/your-test-id",
  "collectionStatus": "COMPLETED",
  "callbackVersion": "V2"
}
```


## Callback Payload [#callback-payload]

When a status change occurs, Revio sends a POST request to your webhook URL.

### EFT Collection Callback [#eft-collection-callback]

```json
{
  "id": "63d94be3-1faf-428b-89f3-bc5271be8015",
  "type": "EFT_COLLECTION",
  "status": "COMPLETED",
  "mandateId": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a",
  "contractReference": "CONTRACT001",
  "amountCents": 10000,
  "collectionDate": "2024-02-15T00:00:00.000Z",
  "statementId": "stmt_12345",
  "timestamp": "2024-02-15T14:30:00.000Z"
}
```

### DebiCheck Mandate Callback [#debicheck-mandate-callback]

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "DEBICHECK_MANDATE",
  "status": "COMPLETED",
  "contractReference": "DC001",
  "authenticationType": "REALTIME",
  "timestamp": "2024-02-15T14:30:00.000Z"
}
```

### DebiCheck Collection Callback [#debicheck-collection-callback]

```json
{
  "id": "coll-123-456-789",
  "type": "DEBICHECK_COLLECTION",
  "status": "COMPLETED",
  "mandateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "amountCents": 10000,
  "timestamp": "2024-02-15T14:30:00.000Z"
}
```

## Handling Webhooks [#handling-webhooks]

### Webhook Handler Example (Node.js) [#webhook-handler-example-nodejs]

```jsx
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/revio', (req, res) => {
  const event = req.body;

  console.log('Received webhook:', event.type, event.status);

  switch (event.type) {
    case 'EFT_COLLECTION':
      handleEftCollection(event);
      break;
    case 'DEBICHECK_COLLECTION':
      handleDebicheckCollection(event);
      break;
    case 'DEBICHECK_MANDATE':
      handleDebicheckMandate(event);
      break;
    case 'EFT_PAYMENT':
      handleEftPayment(event);
      break;
  }

  // Always respond with 200 OK
  res.status(200).send('OK');
});

function handleEftCollection(event) {
  if (event.status === 'COMPLETED') {
    // Mark payment as successful in your system
  } else if (event.status === 'UNPAID') {
    // Handle failed collection
  }
}
```

### Best Practices [#best-practices]

1. **Respond quickly**: Return 200 OK within 5 seconds
2. **Process asynchronously**: Queue events for background processing
3. **Handle duplicates**: Implement idempotency using the event ID
4. **Do not trust the payload**: callbacks are unsigned, so re-read state from the relevant `/status` endpoint before acting — see *Webhook Security* below
5. **Log everything**: Keep detailed logs for debugging
6. **Retry handling**: Handle the same event being sent multiple times

## Webhook Security [#webhook-security]

**Callbacks on this API are not signed.** There is no signature header and no public key, so a callback cannot be authenticated from its content. Anyone who learns your callback URL can post to it. Design on that basis:

1. **Treat the payload as a prompt, not as truth.** On receiving a callback, call the relevant `/status` endpoint — `/v2/mandate/{type}/{id}/status`, `/v2/collection/{type}/{id}/status` or `/v2/payment/eft/{id}/status` — and act on the authoritative response. Never move money or change state on the basis of callback values alone.
2. **Keep the URL unguessable** and treat it as a secret: a long random path segment, rotated if it is ever exposed in a log or a support ticket.
3. **Reconcile independently.** The `/v2/event/*` endpoints are polled by you over an authenticated connection, so they are the trustworthy basis for reconciliation.

<Callout type="warning">
The card APIs *do* sign their callbacks, with an `X-Signature` header over an RSA signature. That mechanism does not exist here — do not carry the assumption across between the two.
</Callout>

### URL Requirements [#url-requirements]

- Must use HTTPS
- Must be publicly accessible
- Should respond within 5 seconds

### Recommendations [#recommendations]

1. **Use a unique path**: Include a secret in your webhook URL
2. **Validate payloads**: Check expected fields are present
3. **Monitor failures**: Alert on repeated delivery failures

## Testing Tools [#testing-tools]

Use these services to test webhooks during development:

- [Webhook.site](https://webhook.site/) - Free webhook testing
- [RequestBin](https://requestbin.com/) - Inspect webhook payloads
- [ngrok](https://ngrok.com/) - Expose local servers

### Example with Webhook.site [#example-with-webhooksite]

1. Go to webhook.site and copy your unique URL
2. Register it as your callback URL:

```bash
curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": 109,
    "url": "https://webhook.site/your-unique-id",
    "callbackType": "EFT_COLLECTION",
    "callbackVersion": "V2"
  }'
```

1. Trigger a test callback to see the payload

## Troubleshooting [#troubleshooting]

### Common Issues [#common-issues]

| Issue | Cause | Solution |
|---|---|---|
| No callbacks received | URL not registered | Verify callback URL configuration |
| 404 errors | Wrong endpoint | Check your webhook handler path |
| Timeout errors | Slow processing | Return 200 immediately, process async |
| Missing events | Wrong callback type | Register for correct event types |

### Verify Callback Registration [#verify-callback-registration]

Get your client ID from merchant profile:

```bash
curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

The `createdByClient` field is your client ID for webhook registration.
