Easy Labs
SDKsRuby

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
end

Pass 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:

FamilyEvents
Paymentspayment.created, payment.updated
Refundsrefund.created, refund.updated
Authorizationsauthorization.created, authorization.updated, authorization.voided
Subscriptionssubscription.created, subscription.updated, subscription.deleted, subscription.paused, subscription.resumed, subscription.trial_will_end, subscription.pending_update_applied, subscription.pending_update_expired
Invoicesinvoice.created, invoice.finalized, invoice.paid, invoice.payment_failed, invoice.upcoming, invoice.voided, invoice.marked_uncollectible
Revenue recoveryrevenue_recovery.action_completed
Coupons / promoscoupon.created, coupon.updated, coupon.deleted, promotion_code.created, promotion_code.updated, promotion_code.deleted
Identitiesidentity.created, identity.updated
Settlementssettlement.created
Disputesdispute.created, dispute.updated
Checkout sessionscheckout.session.completed, checkout.session.crypto_confirmed
Diagnosticstest.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 once

See Resources › Webhook endpoints for the full management surface.

On this page