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

# Webhooks

> Real-time notifications for payment events

# Webhooks

Webhooks notify your system in real-time when payment events occur. Instead of polling the API, register a URL and we'll send HTTP POST requests with event data as payments are confirmed, expire, or fail.

## Event Types

| Event                      | Description                                                          | When It Fires                                                                      |
| -------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `payment_intent.confirmed` | Payment received and confirmed on-chain.                             | \~3-15 seconds after customer sends USDC.                                          |
| `payment_intent.expired`   | Payment intent expired without receiving payment.                    | At the `expires_at` timestamp.                                                     |
| `payment_intent.cancelled` | Payment intent was cancelled by the integrator.                      | Immediately after cancellation API call.                                           |
| `transaction.created`      | A new USDC transaction was detected at a merchant's payment address. | When any USDC transfer is confirmed, even if not tied to an active payment intent. |
| `settlement.completed`     | Merchant funds were sent to their bank account.                      | When the ACH transfer is initiated.                                                |
| `settlement.failed`        | Bank transfer failed (e.g., invalid account).                        | When the ACH return is received (1-4 business days).                               |

## Webhook Payload

Every webhook has the same envelope structure:

```json theme={null}
{
  "id": "evt_abc123",
  "object": "event",
  "type": "payment_intent.confirmed",
  "api_version": "2026-04-01",
  "created_at": "2026-04-01T20:00:12Z",
  "data": {
    // Event-specific payload (see below)
  }
}
```

### payment\_intent.confirmed

```json theme={null}
{
  "id": "pi_xyz789",
  "object": "payment_intent",
  "status": "confirmed",
  "amount": 4.50,
  "currency": "usd",
  "net_amount": 4.455,
  "fee": 0.045,
  "fee_rate": 0.01,
  "merchant_id": "mer_abc123",
  "payment_address": "0x1a2b3c4d5e6f...abcdef",
  "tx_hash": "0xabc123...def456",
  "chain": "base",
  "token": "USDC",
  "sender_address": "0x9876...5432",
  "confirmed_at": "2026-04-01T20:00:12Z",
  "block_number": 12345678,
  "metadata": {
    "order_id": "order_456",
    "terminal_id": "pos_01"
  }
}
```

### payment\_intent.expired

```json theme={null}
{
  "id": "pi_xyz789",
  "object": "payment_intent",
  "status": "expired",
  "amount": 4.50,
  "currency": "usd",
  "merchant_id": "mer_abc123",
  "expired_at": "2026-04-01T20:05:00Z",
  "metadata": {
    "order_id": "order_456"
  }
}
```

## Handling Webhooks

### 1. Return 200 quickly

Your endpoint must return a `200` status code within **5 seconds**. Do any heavy processing asynchronously after responding.

```javascript theme={null}
app.post('/webhooks/stablegenius', async (req, res) => {
  // Return 200 immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  const event = req.body;
  await processEvent(event);
});
```

### 2. Handle duplicates

Webhooks may be delivered more than once. Use the `evt_*` ID to deduplicate:

```javascript theme={null}
async function processEvent(event) {
  // Check if we've already processed this event
  const exists = await db.events.findOne({ event_id: event.id });
  if (exists) return; // Already processed

  // Record the event
  await db.events.insert({ event_id: event.id, processed_at: new Date() });

  // Handle the event
  switch (event.type) {
    case 'payment_intent.confirmed':
      await markOrderPaid(event.data);
      break;
    case 'payment_intent.expired':
      await handleExpiredPayment(event.data);
      break;
  }
}
```

### 3. Verify signatures

Every webhook includes a signature header for verification. See [Webhook Security](/api-reference/webhook-security) for details.

## Retry Policy

If your endpoint returns a non-2xx status code or doesn't respond within 5 seconds, we retry with exponential backoff:

| Attempt | Delay            |
| ------- | ---------------- |
| 1       | Immediate        |
| 2       | 1 minute         |
| 3       | 5 minutes        |
| 4       | 30 minutes       |
| 5       | 2 hours          |
| 6       | 12 hours (final) |

After 6 failed attempts, the webhook is marked as failed. You can view and manually retry failed webhooks in the [dashboard](https://app.stablegenius.co).

## Testing Webhooks

In sandbox mode (`sk_test_*` keys), you can trigger test webhook events from the dashboard to verify your endpoint is working correctly without sending real USDC.
