Webhooks
Verify and consume webhooks from the Easy API.
Easy Labs delivers asynchronous events — payment confirmations, subscription state changes, dispute notifications, settlement closes — as HTTP POSTs to endpoints you register on your account. Webhooks let you react to platform state without polling.
Every delivery is signed with HMAC-SHA256 over the raw request body
using the per-endpoint signing secret. The signature is sent in the
X-Easy-Webhook-Signature header in the form sha256=<hex>.
Verifying signatures
Use EasyLabs::Webhooks.construct_event to verify the signature and
parse the JSON body in one step. It raises
EasyLabs::InvalidRequestError on any failure — never returns silently
on a bad signature.
require "easy_sdk"
# Sinatra example. Same pattern works in Rails (request.raw_post) or Rack.
post "/webhooks/easy" do
begin
event = EasyLabs::Webhooks.construct_event(
payload: request.body.read,
signature: request.headers["X-Easy-Webhook-Signature"],
secret: ENV.fetch("EASY_WEBHOOK_SECRET")
)
rescue EasyLabs::InvalidRequestError => e
# e.code: WEBHOOK_SIGNATURE_MISSING / WEBHOOK_SIGNATURE_FORMAT_INVALID
# / WEBHOOK_SIGNATURE_MISMATCH / WEBHOOK_BODY_INVALID_JSON
halt 400, e.message
end
case event[:type]
when "payment.created" then handle_payment_created(event[:data])
when "subscription.paused" then handle_subscription_paused(event[:data])
when "checkout.session.completed" then fulfill_order(event[:data])
end
status 204
endPass the raw body, not a parsed hash. The signature is computed over
the exact bytes the API sent. Re-serializing a parsed body changes
whitespace and breaks verification. In Rails: request.raw_post. In
Rack: request.body.read (then rewind if anything else needs it).
The secret is the value returned once by
client.webhooks.register(...) — it is only visible at creation
time. Store it next to the rest of your secrets.
Replay protection
The SDK does not bundle a timestamp-tolerance check today —
construct_event validates the signature only.
If you need replay protection in front of an idempotent handler, store
each event[:id] you've successfully processed and skip duplicates:
return if ProcessedWebhook.exists?(event_id: event[:id])
ProcessedWebhook.create!(event_id: event[:id])Common event types
EasyLabs::Webhooks::EVENT_TYPES is a frozen array of every event the
API can emit (37 entries as of 0.1.0). The headline groups:
| Family | Events |
|---|---|
| Payments | payment.created, payment.updated |
| Refunds | refund.created, refund.updated |
| Authorizations | authorization.created, authorization.updated, authorization.voided |
| Subscriptions | subscription.created, subscription.updated, subscription.deleted, subscription.paused, subscription.resumed, subscription.trial_will_end, subscription.pending_update_applied, subscription.pending_update_expired |
| Invoices | invoice.created, invoice.finalized, invoice.paid, invoice.payment_failed, invoice.upcoming, invoice.voided, invoice.marked_uncollectible |
| Revenue recovery | revenue_recovery.action_completed |
| Coupons / promos | coupon.created, coupon.updated, coupon.deleted, promotion_code.created, promotion_code.updated, promotion_code.deleted |
| Identities | identity.created, identity.updated |
| Settlements | settlement.created |
| Disputes | dispute.created, dispute.updated |
| Checkout sessions | checkout.session.completed, checkout.session.crypto_confirmed |
| Diagnostics | test.webhook |
See the API reference for the payload shape of each event.
Registering an endpoint
endpoint = client.webhooks.register(
url: "https://example.com/webhooks/easy",
events: ["payment.created", "subscription.*"] # default is ["*"]
)
ENV["EASY_WEBHOOK_SECRET"] = endpoint[:secret] # capture this onceSee Resources › Webhook endpoints for the full management surface.