Core Concepts
Webhooks
Payment lifecycle events are pushed to a URL you register — signed, and retried with backoff if your endpoint is briefly unreachable.
Registering an endpoint
Tenant-scoped, requires an Idempotency-Key (see Idempotency). A URL whose hostname resolves — or which is itself a bare IP literal — to a private, loopback, link-local, multicast, or reserved address (e.g. a cloud metadata endpoint) is rejected with 400 invalid_request, code unsafe_webhook_url. The same check runs again immediately before every delivery attempt, since DNS can rebind between registration and delivery.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /v1/webhooks | Register a webhook endpoint |
| GET | /v1/webhooks | List your webhook endpoints |
| GET | /v1/webhooks/{id} | Retrieve an endpoint |
| PATCH | /v1/webhooks/{id} | Update an endpoint's url/events |
| DELETE | /v1/webhooks/{id} | Deactivate an endpoint (soft — marks disabled) |
Event types
Fired after the corresponding transition's own database commit, delivered out-of-band — never blocking or failing the triggering payment API call.
- payment.authorized
- payment.captured
- payment.failed
- payment.voided
- payment.refunded
Verifying the signature
Each delivery carries a signature header of the form:
X-ProsoftPay-Signature: t=1720000210,v1=<hex_hmac_sha256>Verify with HMAC_SHA256(webhook_secret, "{t}.{raw_body}"):
def verify_webhook(secret: str, header: str, raw_body: bytes) -> bool:
# header format: "t=<ts>,v1=<sig>"
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.{raw_body.decode()}"
expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Respond 2xx quickly. Long-running work on your end should be enqueued, with a 200 returned immediately.
Retry & backoff
A non-2xx response is retried up to 5 times with the following backoff, before being marked exhausted:
| Attempt | Delay |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 10 minutes |
| 4 | 1 hour |
| 5 | 6 hours |
An endpoint accumulates consecutive_failures across fully-exhausted deliveries and is marked degraded after 5.