Skip to content

Events ​

OpenRampKit has two kinds of events, with two types:

  • Browser events (ClientEvent): what the user does in the modal. For analytics and UI. Not trusted.
  • Webhook events (WebhookEvent): what happened to the session, signed by your server and sent to your backend. Trusted after you verify the signature.

Both are unions on type: when you check e.type, TypeScript knows the fields of e.data.object. Both have id, type, an ISO 8601 createdAt, livemode, sessionId and data.object. Nothing else is shared.

ts
type ClientEvent = {
  id: string            // 'evt_...' (made in the browser)
  type: ClientEventType // 'modal.opened' | 'method.selected' | ...
  createdAt: string     // ISO 8601
  livemode: boolean
  sessionId?: string
  data: { object: ClientEventFields[type] }
}

type WebhookEvent = {
  id: string            // 'evt_...' (deterministic)
  object: 'event'
  apiVersion: 1
  type: WebhookEventType // 'session.created' | 'session.succeeded' | ...
  createdAt: string     // ISO 8601
  livemode: boolean
  sessionId: string
  data: { object: { session: Session } & WebhookEventFields[type] }
}

livemode is false unless you pass livemode: true to createOpenRamp.

Browser events ​

Receive them with onEvent on OpenRampProvider, DepositButton, WithdrawButton, OpenRampEmbedded, openDeposit(), openWithdraw() or the controller.

TypeWhendata.object
modal.openedThe controller starts for the first time{}
target.selectedWithdraw: the server accepted the target{ type: 'crypto', chain, token }, { type: 'fiat', currency } or { type: 'lightning', cashApp }
method.selectedThe user picks a method{ method }
quotes.shownQuotes arrived{ method, count }
quote.selectedThe user confirms a quote{ quoteId }
step.changedThe step's state or detail code changed{ state, detail? } (detail is the Step.detail.code)
surface.openedThe user opened a surface (for example the redirect button){ kind }
surface.messageA provider iframe sent a recognized message{ kind, detail }
modal.closedThe modal closed{ screen, state }
tsx
<OpenRampProvider
  baseUrl="/api/openramp"
  onEvent={(e) => analytics.track(`deposit ${e.type}`, { sessionId: e.sessionId, ...e.data.object })}
>
  <DepositButton getClientSecret={getClientSecret} />
</OpenRampProvider>

Browser events are made in the browser. Their ids are not cryptographically random. Never credit anything from them.

Webhook events ​

The server sends these to webhooks.url, signed with webhooks.secret (Standard Webhooks). See Webhooks to your backend for the headers, the verification and the crediting rules. There is one catalog for deposits and withdrawals: data.object.session.direction tells which one it is.

TypeWhen
session.createdA session was created
session.requires_actionA leg waits for the user. Once per leg and attempt.
session.processingThe user paid or acted, and a provider or the chain works. Once per leg and attempt.
session.payment_failedA payment attempt failed, and the user can try again. The status is requires_payment_method again, with lastError.
session.succeededEvery leg succeeded
session.failedFinal failure: no attempts are left, money already arrived on a leg, or an operator resolved the session as FAILED. No event follows it.
session.canceledThe app or the user canceled the session
session.expiredThe deadline passed with no payment in progress, or the provider order expired (found by the sweep, or by a request)
session.refundedThe provider returned the payment before it succeeded
session.reversedThe payment succeeded, then the provider refunded it or took it back (a chargeback). Take back or freeze the credit.
session.late_paymentA payment arrived late. reason: 'after_expiry': the session had expired, and the payment arrived after all; the session goes on and you also get session.succeeded. reason: 'after_grace': the payment arrived after the grace window (latePayments.graceHours, default 72); the session stays expired, so refund or credit it by hand. reason: 'earlier_attempt': a payment that the user left with restart succeeded, but the session already has a final status, or another payment is in progress; refund or credit it by hand.
leg.succeededOne leg finished
leg.failedOne leg failed
deposit_address.depositA deposit arrived at a standing deposit address, and the server made a session for it. The session events follow on the same session.

data.object for every webhook:

ts
{
  session: Session // the backend view after the change: PublicSession plus userId and metadata
  // session.requires_action, session.processing: attempt, index, adapterId, legId
  // session.payment_failed: attempt, index, adapterId, legId, error
  // leg.succeeded: index, adapterId, legId
  // leg.failed: index, adapterId, legId, error
  // session.failed: error, and resolution when an operator resolved it
  // session.canceled: reason ('requested_by_app', 'requested_by_user' or 'abandoned')
  // session.late_payment: reason ('after_expiry', 'after_grace' or 'earlier_attempt'), index, adapterId, legId,
  //   transactions (the leg's transactions, when known), attempt (earlier_attempt only)
  // session.reversed: index, adapterId, legId,
  //   legStatus ('refunded' or 'reversed'), previous (the step state before, e.g. 'COMPLETED'),
  //   attempt (only for an earlier attempt; the session status does not change)
  // deposit_address.deposit: depositAddressId, sessionId, depositId, input (the deposit, when known)
}

WebhookEventFields in @openrampkit/core has these fields per type. WEBHOOK_EVENT_TYPES lists the types.

data.object.session.result (a SessionResult) tells what the user paid and what arrived, once a payment started. result.transactions has every transaction of the payment, each with a role: source (what paid into a leg, for example the user's wallet transaction), hop and destination (the deliveries), and more (see Transactions). result.delivery tells if the reported output matches the quote. Credit the full amount only when result.delivery.status is ok. session.payment has the legs and the provider order ids (providerRef).

Refunds and chargebacks after success ​

A provider can take back a payment after it completed: a refund, or a card chargeback. The adapter reports the leg as refunded or reversed. The server then:

  • keeps the leg with its new status (and the session's result),
  • adds leg.refunded (or leg.reversed) and session.reversed to the timeline,
  • sets the step to REVERSED (with the error PAYMENT_REVERSED) and the session status to reversed,
  • sends session.reversed one time, with a deterministic event id.

REVERSED is final. After it, the server moves no more funds for the session: it does not start a next leg, it does not call the treasury, and later provider events change only the leg data (no leg.* events, no other session state). An earlier attempt that the provider pays later does not become the session's payment: the server sends session.late_payment. The session refuses restart and a new payment. An operator can still set another final state with admin.resolve and an audit note.

A reversal also applies to a session that an operator closed with admin.resolve: the session becomes REVERSED and the server sends session.reversed.

A refund or a chargeback of an earlier attempt (one the user left with restart) does not change the session state. The server sends session.reversed once for it, with the extra field attempt (the attempt number). You may have credited that payment by hand after session.late_payment.

A refund that comes before the payment completed is not a reversal: the step becomes REFUNDED and the server sends session.refunded, as before.

The server learns a reversal from a provider event (a webhook). It does not poll a completed session.

Each event type (with its extra fields) is queued at most once per session. Leg events of a later payment attempt (after restart) are new events.

The event id is deterministic: it is a hash of the session id and the event (type and extra fields). The server saves the event in the session record in the same write as the change that caused it, and sends it only after that write succeeds. So a change that is not saved sends nothing, and a retry of the same change makes the same id. A failed delivery is retried by the sweep with the same id, and a delivery can arrive more than once. Deduplicate by event id: it is safe.

Build your own events ​

createWebhookEvent(type, object, { id, sessionId, livemode }) and createClientEvent(type, object, { sessionId, livemode }) from @openrampkit/core build the envelopes. randomId(prefix) makes ids of the same shape.

MIT licensed