# Webhooks

SwissPay sends webhook notifications to your server so you can react to payment activity asynchronously -- for example, to email a receipt when a card charge succeeds, or to mark an order as refunded.

This page covers the events we send, how to verify they really came from us, and how delivery retries work.

## Managing endpoints

Webhook endpoints are managed from the dashboard under **Developers -> Webhooks**. From there you can:

* Add a new endpoint URL.
* Roll the signing secret. The previous secret is revealed once, on creation or rotation -- store it somewhere your server can read it. We do not store it in plaintext after the reveal.
* Send a test event to confirm your endpoint is reachable.
* Inspect the event log and the request/response detail for any individual delivery, and retry a failed delivery by hand.

A merchant can register multiple endpoints; every endpoint receives every event the merchant is subscribed to.

### Endpoint URLs

* `https://` is recommended in production.
* `http://` URLs are accepted -- use them for local development against a tunnel (`http://localhost:4242/webhooks`, ngrok, Cloudflare Tunnel, etc.). Malformed or non-HTTP URLs are rejected at save time.

### Test mode vs live mode

Webhook delivery is mode-agnostic: a single endpoint will receive events generated by both test API keys (`sk_test_...`) and live API keys. If you want strict separation, register one endpoint per environment and gate each in your own routing.

## Event catalogue (v1)

| Event               | When it fires                                                                                                                |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `payment.succeeded` | A `POST /api/v1/payments` finished with `status: "succeeded"`, including a 3-D Secure challenge that completed successfully. |
| `payment.failed`    | A payment finished with `status: "failed"`.                                                                                  |
| `refund.succeeded`  | A refund created via `POST /api/v1/payments/:id/refunds` was accepted by the processor.                                      |
| `refund.failed`     | A refund was rejected by the processor.                                                                                      |

Each event carries the same object you would get from `GET /api/v1/payments/:id` or `GET /api/v1/refunds/:id` (see [Payments](/api-reference/payments.md) and [Refunds](/api-reference/refunds.md)) -- so once you have authenticated the request, you can reuse your existing object-handling code.

We add new event types over time. Treat unknown event types as a no-op rather than an error, and never assume the list above is exhaustive.

## Verifying the signature

Every delivery is signed with an HMAC using the signing secret you reveal when you create or roll an endpoint. **Verify the signature on every incoming request and reject anything that doesn't match** -- the URL of your endpoint is the only thing standing between an attacker and a fake `payment.succeeded`.

The signature is computed over the **raw request body**. Parse the body for application logic only after the signature check passes -- frameworks that re-serialise JSON will break the comparison.

You will find your signing secret, plus the exact header and signing-string format your endpoint should expect, on the endpoint's detail page in **Developers -> Webhooks**. Rolling the secret invalidates the previous one immediately, so update your server before rotating.

## Delivery and retries

* Your endpoint must respond with a 2xx status within a reasonable timeout (a few seconds). Any non-2xx response, or no response at all, is treated as a failure and queued for retry.
* Retries are spaced out with an increasing backoff over a multi-hour window. Eventually we give up and mark the delivery as permanently failed; you can still retry it manually from the dashboard.
* Each delivery has a stable ID and you may see the same event delivered more than once (for example, if your server 200s but the connection drops before we record it). Make your handler **idempotent** -- key on the event's payment or refund ID, not on the delivery.
* The dashboard shows the full request/response for every attempt, which is the fastest way to debug a misbehaving endpoint.

## Designing your handler

A handler that survives production usually looks like this:

1. Read the raw body and the signature header.
2. Verify the signature. Reject with `401` on mismatch.
3. Look up the payment or refund by ID in your own database. If you have already processed this state transition, return `200` and stop.
4. Update your local state, side-effect (email, fulfil, refund-on-your-side, etc.), and return `200`.
5. If anything fails, return a 5xx so we retry.

Keep step 4 fast. If you need to do slow work (PDF generation, third-party calls), enqueue a job and return `200` immediately.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://support.swisspay.ai/api-reference/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
