> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gosurge.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Use webhooks to receive real-time updates on payment events and fulfill orders safely.

## The Source of Truth

**Important:** Do not use the frontend `onSuccess` callback as the trigger to fulfill orders. Frontend state can be manipulated. **Always rely on Webhooks** as the final source of truth.

### Registering your Webhook

Set your `webhookUrl` and `webhookSecret` in the [Surge Merchant Dashboard](https://merchant.gosurge.xyz/settings/api) under **Settings → Webhooks**.

### Security

Surge signs every outbound webhook with an **HMAC-SHA256** signature in the `X-Surge-Signature` header. Verify this against your `webhook_secret` before processing any event.

```javascript theme={null}
const crypto = require('crypto');

function verifySurgeWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // timingSafeEqual throws if buffers differ in length
  const expBuf = Buffer.from(expected);
  const sigBuf = Buffer.from(signature ?? '');
  return expBuf.length === sigBuf.length && crypto.timingSafeEqual(expBuf, sigBuf);
}
```

<Warning>
  Always verify the signature **before** acting on webhook data. Never trust the payload without this check.
</Warning>

***

## Outbound Events (Surge → Your Server)

These are the events Surge sends to your registered `webhookUrl`.

### `payment.success`

Fires when the customer pays the initial deposit and the installment plan is activated.

```json theme={null}
{
  "eventId": "evt_abc123...",
  "eventType": "payment.success",
  "merchantId": "mer_998877",
  "createdAt": "2026-03-11T12:25:00Z",
  "data": {
    "resourceType": "payment_plan",
    "resourceId": "pp_554433",
    "orderReference": "ORD-9912384",
    "amount": 120000,
    "currency": "NGN"
  }
}
```

### `payment.failed`

Fires when a deposit or installment charge attempt fails (e.g. insufficient funds, expired card).

```json theme={null}
{
  "eventId": "evt_fail789...",
  "eventType": "payment.failed",
  "merchantId": "mer_998877",
  "createdAt": "2026-04-11T06:15:00Z",
  "data": {
    "resourceType": "payment_plan",
    "resourceId": "pp_554433",
    "orderReference": "ORD-9912384",
    "reason": "insufficient_funds"
  }
}
```

### `payment_plan.installment_paid`

Fires each time the **Auto-Debit engine** successfully collects a scheduled installment. Also emitted as `installment.paid` — both are identical events with different names for compatibility.

<Note>
  You receive `payment.success` once when the plan is created, and one `payment_plan.installment_paid` event **for each subsequent installment** collected.
</Note>

```json theme={null}
{
  "eventId": "evt_def456...",
  "eventType": "payment_plan.installment_paid",
  "merchantId": "mer_998877",
  "createdAt": "2026-04-11T06:15:00Z",
  "data": {
    "resourceType": "installment",
    "resourceId": "inst_778899",
    "paymentPlanId": "pp_554433",
    "customerId": "cus_112233",
    "orderReference": "ORD-9912384",
    "amount": 40000,
    "currency": "NGN",
    "processorReference": "PST_abc123"
  }
}
```

### `goods.release`

Fires when the merchant's configured release rule is satisfied — typically after the deposit is paid. This is the trigger to ship physical goods or unlock digital access.

```json theme={null}
{
  "eventId": "evt_rel321...",
  "eventType": "goods.release",
  "merchantId": "mer_998877",
  "createdAt": "2026-04-11T06:15:00Z",
  "data": {
    "resourceType": "payment_plan",
    "resourceId": "pp_554433",
    "orderReference": "ORD-9912384"
  }
}
```

### `debit.scheduled`

Fires when an auto-debit job is registered for a future installment.

### `transfer.success`

Fires when a merchant payout (settlement transfer) to your registered bank account completes successfully.

***

## Fulfillment Trigger

When your server receives `goods.release` or `payment.success` (and the signature is valid):

1. Mark the order as **Paid** in your database.
2. Trigger the **release or shipping** of physical or digital goods to the customer.

<Note>
  Return a `200 OK` immediately and process fulfillment asynchronously to avoid timeout-based retries from Surge.
</Note>

***

## Inbound Events (Paystack → Surge)

<Note>
  This section describes **Surge's internal infrastructure** — you do not need to configure, call, or handle any of these endpoints. They are listed here for transparency only.
</Note>

Surge maintains endpoints that receive events from Paystack (the underlying payment processor). These handle charge confirmations, payout confirmations, and payment method revocations on Surge's behalf.

### `POST /api/v1/webhooks/paystack`

Used internally by Surge to receive Paystack charge and transfer events. Surge validates every inbound Paystack event using **HMAC-SHA512** (Paystack's standard signing algorithm).

### `POST /api/v1/webhooks/payment-gateway`

Used internally by Surge to handle payment method revocation events from Paystack (`authorization.revoked`, `mandate.cancelled`, etc.). When Surge receives a revocation, it deactivates the customer's payment method and recalculates their Surge Score automatically.

***

## Testing Your Webhook

You can send a signed test event to your configured endpoint directly from the API:

```bash theme={null}
POST /api/v1/webhooks/test
Authorization: Bearer <YOUR_JWT_TOKEN>
Content-Type: application/json

{
  "webhook_url": "https://yourstore.com/webhooks/surge",
  "webhook_secret": "whsec_..."
}
```

**Response**

```json theme={null}
{
  "ok": true,
  "delivered": true,
  "status_code": 200,
  "latency_ms": 142
}
```

If delivery fails, `delivered` is `false` and an `error` field describes the failure.

***

## Replaying Failed Events

If your server was down when a webhook was delivered, you can manually replay any event from the dashboard or via the API:

```bash theme={null}
POST /api/v1/webhooks/events/{event_id}/replay
Authorization: Bearer <YOUR_JWT_TOKEN>
```

Failed delivery attempts are visible via:

```bash theme={null}
GET /api/v1/webhooks/failed-attempts
Authorization: Bearer <YOUR_JWT_TOKEN>
```
