## How it works

SpicePay posts signed event envelopes to the shop's configured endpoint. Payment, refund, dispute, mandate, payout, subscription and invoice updates can all arrive through this channel.

## Implementation

Verify the signature before parsing trusted content, persist or queue the event, then acknowledge with 2xx. Deduplicate by event_id and make order updates idempotent. Events may overlap or arrive out of order, so retrieve the resource when reconciling uncertain state. Check test_mode before fulfilment and compare amount and currency to the order. Full capture produces payment_succeeded; partial capture should not automatically fulfil an entire order. Unknown event types should be acknowledged without side effects.

## Payload

| `content.type` | `content.object` |
| --- | --- |
| `payment_details` | The payment, as `GET /payments/{payment_id}` returns it |
| `refund_details` | The refund |
| `dispute_details` | The dispute |
| `mandate_details` | The mandate |
| `payout_details` | The payout |
| `subscription_details` | See [Subscription payloads](#subscription-payloads) |
| `merchant_invoice_details` | The invoice or credit note |
| `shop_risk_details` | The shop's risk band escalation |

## Payments

| Event | Payment status |
| --- | --- |
| `payment_succeeded` | `succeeded` (includes a full capture) |
| `payment_failed` | `failed` or `declined` |
| `payment_processing` | `processing` or `partially_captured_and_processing` |
| `payment_authorized` | `requires_capture` |
| `payment_partially_authorized` | `partially_authorized_and_requires_capture` |
| `payment_captured` | `partially_captured` or `partially_captured_and_capturable` |
| `payment_cancelled` | `cancelled` |
| `payment_cancelled_post_capture` | `cancelled_post_capture` |
| `payment_expired` | `expired` |
| `action_required` | `requires_customer_action`, `requires_merchant_action` or `conflicted` |

## Refunds, disputes and mandates

| Event | Meaning |
| --- | --- |
| `refund_succeeded` | Refund completed |
| `refund_failed` | Refund failed |
| `dispute_opened` | Dispute opened |
| `dispute_challenged` | Evidence submitted |
| `dispute_accepted` | You accepted |
| `dispute_won` | Resolved in your favour |
| `dispute_lost` | Resolved in the buyer's favour |
| `dispute_expired` | Response deadline passed |
| `dispute_cancelled` | Withdrawn |
| `mandate_active` | Mandate can be charged |
| `mandate_revoked` | Mandate revoked |

## Subscriptions

| Event | Meaning |
| --- | --- |
| `subscription_created` | Created through the API; waiting for the buyer |
| `subscription_active` | Active for the first time |
| `subscription_past_due` | Now `unpaid` |
| `subscription_paused` | Paused |
| `subscription_resumed` | Active after a pause, or scheduled cancellation undone |
| `subscription_cancelled` | Cancelled, or cancellation scheduled for period end |
| `subscription_expired` | Now `in_active` |
| `invoice_paid` | Billing cycle paid |
| `invoice_payment_failed` | Renewal attempt failed; not necessarily past due |
| `invoice_disputed` | A cycle the billing processor collected was charged back |

## Invoices and risk

| Event | Meaning |
| --- | --- |
| `merchant_invoice_finalized` | [Invoice](/docs/payment-api/invoices/) or credit note issued; `invoice.kind` says which |
| `merchant_invoice_voided` | Issued invoice voided |
| `shop_risk_band_escalated` | Shop's risk band rose between two assessments |

## Subscription payloads

| Event | Object |
| --- | --- |
| `invoice_paid` | Subscription with the paid cycle in `invoice`; `payment` is `null` when the billing processor collected it |
| `invoice_disputed` | The chargeback: `dispute_id`, `subscription_id`, `invoice_id`, `amount`, `currency`, `dispute_status`, `test_mode`, processor IDs |
| Other subscription events | Snapshot: `subscription_id`, `status`, `transition_id`, `test_mode`, `current_period_end`, `cancelled_at`, `cancellation_reason`, `cancel_at_period_end` |

## Retries

| Response | Result |
| --- | --- |
| `2xx` | Delivered |
| Network error, timeout, `5xx`, `408`, `429` | Retried; `Retry-After` (seconds) on `429` respected |
| `401`, `403` | 6 retries over about 41 minutes |
| Other `4xx` | No retry |

## HTTP example

```json
{
  "merchant_id": "mer_xxxxx",
  "event_id": "evt_xxxxx",
  "event_type": "payment_succeeded",
  "timestamp": "2026-07-13T10:24:31Z",
  "content": {
    "type": "payment_details",
    "object": {
      "payment_id": "pay_xxxxx",
      "status": "succeeded",
      "amount": 1000,
      "currency": "USD",
      "test_mode": false,
      "metadata": { "order_id": "2042" }
    }
  }
}
```

## Related guides

- [Webhook verification](/docs/payment-api/webhook-verification/)
- [Webhook events](/docs/dashboard/webhook-events/)
- [Payment lifecycle](/docs/general/payment-lifecycle/)
- [Invoices & credit notes](/docs/payment-api/invoices/)

## Request examples

```bash
curl -X POST https://api.spicepay.net/events/mer_xxxxx \
  -H 'api-key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"object_id": "pay_xxxxx", "limit": 20}'
```

```bash
curl https://api.spicepay.net/events/mer_xxxxx/evt_xxxxx/attempts \
  -H 'api-key: YOUR_API_KEY'

curl -X POST https://api.spicepay.net/events/mer_xxxxx/evt_xxxxx/retry \
  -H 'api-key: YOUR_API_KEY'
```
