Webhooks
Verify and consume webhooks from the Easy API.
Webhooks let your server react to events that happen asynchronously on
Easy Labs — payments completing, subscriptions renewing, invoices
finalizing, disputes opening — without polling for changes. Easy Labs
delivers each event as a signed POST to a URL you register with
client.webhooks_management.register(...). The SDK ships a small,
framework-agnostic verifier that turns the raw request into a typed
WebhookEvent.
The webhook surface has two halves:
| API | Purpose |
|---|---|
easylabs.Webhooks.construct_event(...) | Verify a signature + parse the event payload. |
client.webhooks_management.* | Register / list / delete endpoints, list deliveries. |
Endpoint management lives on its own Webhook Endpoints page; this page covers verification.
Verifying signatures
import os
from easylabs import Webhooks, InvalidRequestError
SECRET = os.environ["EASY_WEBHOOK_SECRET"] # returned once on register()
def handle(request):
try:
event = Webhooks.construct_event(
payload=request.body, # bytes preferred
signature=request.headers.get("x-easy-webhook-signature", ""),
secret=SECRET,
)
except InvalidRequestError as e:
# Signature missing / malformed / mismatched, or body wasn't JSON.
return ("invalid signature", 400)
if event.type == "payment.created":
# event.data carries the resource payload (dict)
...
elif event.type == "subscription.trial_will_end":
...
return ("", 204)Notes:
-
Pass the raw request body, not a re-serialized JSON string. The signature is computed over the exact bytes Easy Labs sent — pretty- printing or re-encoding will fail verification.
-
payload=acceptsbytes(preferred) orstr. -
Webhooks.construct_eventraiseseasylabs.InvalidRequestError(HTTP 400) on every verification failure. Inspecte.codeto distinguish cases:e.codeMeaning WEBHOOK_SIGNATURE_MISSINGThe x-easy-webhook-signatureheader was empty or absent.WEBHOOK_SIGNATURE_FORMAT_INVALIDHeader didn't start with sha256=or wasn't valid hex.WEBHOOK_SIGNATURE_MISMATCHSignature didn't match what we computed from the body. WEBHOOK_BODY_INVALID_JSONBody verified but failed to parse as JSON.
Comparison uses hmac.compare_digest, so verification is constant-time.
Replay protection
The SDK verifies the HMAC signature over the body. Easy Labs will re-deliver the same event with the same signature on retries, which is exactly what makes webhooks resilient — but it also means a leaked payload could in principle be re-played by an attacker who learns the URL.
Two recommendations:
-
Idempotency on your side. Treat
event.idas a primary key. Skip processing if you've already seen it.if WebhookEventLog.exists(event.id): return ("", 204) WebhookEventLog.record(event.id, event.type) -
Tight network exposure. Restrict the receive endpoint to TLS, authenticate the source IP / proxy, and rotate the signing secret if you suspect compromise (
webhooks_management.update).
Common event types
The full list is exported as easylabs.EVENT_TYPES:
from easylabs import EVENT_TYPES
print(EVENT_TYPES)A representative subset:
| Event | Fired when… |
|---|---|
payment.created / payment.updated | A Transfer is created or transitions state. |
refund.created / refund.updated | A reversal is created or transitions state. |
authorization.created / .updated / .voided | Auth-and-capture lifecycle changes. |
subscription.created / .updated / .deleted | Subscription lifecycle. |
subscription.paused / .resumed | Pause collection toggles. |
subscription.trial_will_end | 3 days before a trial ends. |
subscription.pending_update_applied / .pending_update_expired | Scheduled changes resolve. |
invoice.created / .finalized / .paid / .payment_failed / .upcoming / .voided / .marked_uncollectible | Invoice lifecycle. |
revenue_recovery.action_completed | A dunning step ran. |
coupon.* / promotion_code.* | Discount config changes. |
identity.created / .updated | Customer / identity changes. |
settlement.created | A settlement was issued. |
dispute.created / .updated | Chargeback / dispute lifecycle. |
checkout.session.completed | A hosted / embedded checkout finished. |
checkout.session.crypto_confirmed | A crypto checkout payment was confirmed on-chain. |
test.webhook | Sent when you click "Send test event" in the dashboard. |
The full payload for each event is documented in the Easy Labs API reference.