Webhooks

Created by Kalin Ivanov, Modified on Thu, 18 Jun at 7:21 PM by Kalin Ivanov

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 and Refunds) -- 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.

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article