> For the complete documentation index, see [llms.txt](https://pingpay.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://pingpay.gitbook.io/docs/pingpay-api/subscriptions/webhooks.md).

# Webhooks

Ping sends webhooks to your configured endpoint whenever a subscription event occurs. Use these to keep your backend in sync with subscription state changes.

### Webhook Events

| Event                           | Trigger                                                                      |
| ------------------------------- | ---------------------------------------------------------------------------- |
| `subscription.created`          | Subscription successfully activated                                          |
| `subscription.payment_released` | A scheduled payment was released on-chain                                    |
| `subscription.payment_failed`   | A payment release failed (3+ failures marks subscription as PAYMENT\_FAILED) |
| `subscription.cancelled`        | Subscription cancelled by merchant or subscriber                             |
| `subscription.completed`        | All payment periods have been released                                       |

### Payload Format

All webhook deliveries are wrapped in this envelope:

```json
{
  "id": "whevt_abc123",
  "type": "subscription.payment_released",
  "created": "2026-04-21T12:00:00.000Z",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "merchantId": "org_abc123",
    ...
  }
}
```

#### Headers

Every webhook request includes these headers:

| Header              | Description                                              |
| ------------------- | -------------------------------------------------------- |
| `X-Ping-Timestamp`  | Unix timestamp (seconds) when the webhook was sent       |
| `X-Ping-Signature`  | HMAC-SHA256 signature for verification                   |
| `X-Ping-Event-Id`   | Unique event ID (for deduplication)                      |
| `X-Ping-Event-Type` | Event type string (e.g. `subscription.payment_released`) |

#### `subscription.created`

```json
{
  "type": "subscription.created",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "sessionId": "ss_a1b2c3d4e5f6",
    "subscriber": "alice.near",
    "merchant": "merchant.near",
    "amount": "5000000",
    "assetSymbol": "USDC",
    "intervalLabel": "MONTHLY",
    "totalPeriods": 12,
    "merchantId": "org_abc123"
  }
}
```

#### `subscription.payment_released`

```json
{
  "type": "subscription.payment_released",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "period": 3,
    "intentHash": "Abc123...",
    "releasedCount": 3,
    "totalPeriods": 12,
    "nextReleaseTime": 1721476800,
    "merchantId": "org_abc123"
  }
}
```

#### `subscription.payment_failed`

```json
{
  "type": "subscription.payment_failed",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "period": 4,
    "error": "insufficient balance",
    "consecutiveFailures": 3,
    "merchantId": "org_abc123"
  }
}
```

#### `subscription.cancelled`

```json
{
  "type": "subscription.cancelled",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "cancelledBy": "subscriber",
    "cancelledAt": "2026-05-15T10:30:00.000Z",
    "merchantId": "org_abc123"
  }
}
```

#### `subscription.completed`

```json
{
  "type": "subscription.completed",
  "data": {
    "subscriptionId": "sub_x7y8z9",
    "totalPeriods": 12,
    "releasedCount": 12,
    "merchantId": "org_abc123"
  }
}
```

### Webhook Signature Verification

Ping signs all webhooks with HMAC-SHA256. Verify the signature to ensure the webhook is authentic.

#### Verification Steps

1. Get the `X-Ping-Timestamp` and `X-Ping-Signature` headers
2. Construct the signing string: `{timestamp}.{raw_body}`
3. Compute HMAC-SHA256 of the signing string using your endpoint's webhook secret
4. Compare with the signature header (use timing-safe comparison)

#### Example (Node.js)

```javascript
import crypto from 'crypto';

function verifyWebhook(rawBody, timestamp, signature, secret) {
  const signingString = `${timestamp}.${rawBody}`;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(signingString)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express.js example
app.post('/webhooks/ping', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.headers['x-ping-timestamp'];
  const signature = req.headers['x-ping-signature'];
  const rawBody = req.body.toString();

  if (!verifyWebhook(rawBody, timestamp, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(rawBody);
  // Process event...
  res.status(200).send('ok');
});
```

> **Important:** You must verify against the raw request body string, not a re-serialized JSON object. Re-serializing can change key ordering or whitespace, which will break signature verification.

### Retry Policy

Failed deliveries are retried with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 30 minutes |
| 5       | 2 hours    |
| 6       | 24 hours   |

After 6 failed attempts, the delivery is marked as `FAILED`. Deliveries timeout after 30 seconds.

Your endpoint should return a `2xx` status code to acknowledge receipt. Any non-2xx response (or timeout) triggers a retry.

### Configuring Webhooks

Register webhook endpoints via the API:

```bash
# Create a webhook endpoint
curl -s -X POST https://pay.pingpay.io/api/rpc/webhooks/createEndpoint \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_abc123" \
  -d '{
    "url": "https://yourapp.com/webhooks/ping",
    "events": [
      "subscription.created",
      "subscription.payment_released",
      "subscription.payment_failed",
      "subscription.cancelled",
      "subscription.completed"
    ]
  }' | python3 -m json.tool
```

The response includes a `secret` (starts with `whsec_`) — save this for signature verification.

#### Managing Endpoints

```bash
# List your webhook endpoints
curl -s https://pay.pingpay.io/api/rpc/webhooks/listEndpoints \
  -H "x-api-key: pk_live_abc123" | python3 -m json.tool

# Rotate the webhook secret
curl -s -X POST https://pay.pingpay.io/api/rpc/webhooks/rotateSecret \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_abc123" \
  -d '{ "endpointId": "whep_abc123" }' | python3 -m json.tool

# Delete an endpoint
curl -s -X POST https://pay.pingpay.io/api/rpc/webhooks/deleteEndpoint \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_abc123" \
  -d '{ "endpointId": "whep_abc123" }' | python3 -m json.tool
```
