Docs menu

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.

MethodEndpointWhat it does
POST/v1/webhooksRegister a webhook endpoint
GET/v1/webhooksList 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:

AttemptDelay
130 seconds
22 minutes
310 minutes
41 hour
56 hours

An endpoint accumulates consecutive_failures across fully-exhausted deliveries and is marked degraded after 5.