For the complete documentation index, see llms.txt. This page is also available as Markdown.

Webhooks

How UR delivers business events to partner-registered HTTPS endpoints. Covers protocol, signatures, retries, idempotency, and the full event catalog.

This document is the canonical reference for partner backend engineers integrating with UR's webhook delivery service. It covers the wire protocol, signature verification, retry semantics, idempotency contract, and every event type currently in production.

If you only have a few minutes, read §1 Overview and §4 Signature verification. Everything else is reference material.


1. Overview

UR pushes asynchronous business events (transactions confirmed on-chain, KYC outcomes, allowance changes, card-authorization failures, partner-KYC session results) to an HTTPS endpoint you register in your partner contract.

The high-level contract:

Aspect
Behavior

Transport

HTTPS POST, Content-Type: application/json

Body

JSON envelope {event, data, timestamp}

Authentication

EIP-191 ECDSA signature in HTTP headers (V1 legacy + V2 recommended, both delivered in parallel)

Delivery semantics

At-least-once, ~48 h retry window, ±20 %-jittered exponential backoff

Success criterion

Your endpoint returns HTTP 2xx within the configured timeout (default 30 s)

Idempotency

We send X-Webhook-Request-Id (UUID v4); partners MUST dedupe on this

Ordering

Sequential per-partner; across partners is unordered

User-Agent: URBank-Webhook/2.0 (pre-v2.0 partners saw URBank-Webhook/1.0). Whitelist by domain, not UA. The URBank-Webhook string is a fixed wire-protocol constant retained for backward compatibility; it is not a product name.


2. Endpoint requirements

Your endpoint MUST:

  1. Be reachable via HTTPS with a CA-issued certificate (no self-signed).

  2. Accept POST with Content-Type: application/json.

  3. Return HTTP 2xx on success within the configured timeout (default 30 s).

  4. Return HTTP 4xx on permanent failure; we will NOT retry.

  5. Return HTTP 5xx (or time out) on transient failure; we WILL retry.

  6. Be idempotent: handle the same X-Webhook-Request-Id more than once gracefully (every retry reuses the same id).

Your endpoint SHOULD:

  • Verify at least one of the two signatures (see §4) before processing.

  • Reject any request whose X-Webhook-Timestamp skews more than ±5 minutes from your clock (replay protection).

  • Ack fast (< 1 s ideal) and process asynchronously. A queue + worker pattern works best because our delivery worker holds a TCP connection open until you respond.


3. HTTP headers

Every outbound request carries these headers. X-Webhook-* are envelope metadata; X-Api-* are signature material.

Header
Example
Meaning

Content-Type

application/json

Always JSON

User-Agent

URBank-Webhook/2.0

Stable for the v2.0 generation of the delivery service

X-Webhook-Request-Id

b3e8…-… (UUID v4)

Idempotency key. Same value across the first delivery and every retry of the same message. Dedupe on this.

X-Webhook-Timestamp

1735689600

Unix seconds at the moment the HTTP request is assembled

X-Webhook-Event-Type

transaction

Canonical event name (see §6 Event catalog)

X-Webhook-Attempt

1, 2, …

1-indexed delivery attempt. First delivery is 1, first retry is 2.

X-Api-Signature

0x…

V1 (legacy) signature. Present whenever V1 is enabled in your contract.

X-Api-PublicKey

0x…

V1 signer Ethereum address. Same address as V2.

X-Api-Signature-V2

0x…

V2 (recommended) signature. Present whenever V2 is enabled.

X-Api-PublicKey-V2

0x…

V2 signer Ethereum address (= V1 address).

3.1 Response headers you can send

Header
When
Effect

X-Webhook-No-Retry: true

On a 4xx or 5xx response when you've decided this message should never be retried

We move the message to DEAD immediately and stop retrying.

Everything else in your response is ignored. Status code is what we key on (see §5).


4. Signature verification

All webhooks are signed with the EIP-191 "personal sign" scheme used by Ethereum wallets:

The signing key is the same for V1 and V2; only the signed message differs.

4.1 V1 (legacy)

V1 is what legacy partners have always verified. It will remain on the wire indefinitely; a retirement announcement will run on a ≥3-month deprecation window if we ever do retire it.

V2 binds the timestamp and request id into the signature. Without that binding an attacker who captures a webhook can replay the body with a fresh timestamp; with V2 the recovered address won't match. All new integrations should validate V2.

4.3 Address pinning

The signer address is published in your partner contract. Pin it on your side; do not trust whatever X-Api-PublicKey(-V2) says without comparing to the pinned value. The header is a convenience; the contract is the source of truth.

4.4 Reference implementation (Node.js)

The verifier call moved namespace between ethers major versions. Both forms are shown below; the rest of the function is identical.

ethers v6 (current; published 2023+):

ethers v5 (legacy; for partners on the v5 line):

If you're not sure which version you're on, run node -p "require('ethers').version". 6.x.x is v6; 5.x.x is v5. The hash + secp256k1 recovery underneath is the same EIP-191 algorithm in both versions, so the recovered address is byte-identical.

4.5 Reference implementation (Go)

4.6 Verifying both versions during the migration window

A partner who verifies V2 only and rejects on failure will break the day UR rotates a key (rare but planned). Defense-in-depth:

A common partner-side bug is parsing JSON first and then re-serializing for signature verification. That breaks because Go's encoding/json does not preserve key order. Read the raw request body byte-for-byte before deserializing. The req.rawBody / c.Request.GetBody() pattern in most frameworks does this for you.


5. Retry policy

5.1 Classification by response

Outcome
Class

HTTP 2xx

Success (message marked DELIVERED).

HTTP 4xx (except 408, 429)

Non-retryable (message marked DEAD).

HTTP 408, 429

Retryable (server-side hint that you were rate-limited or slow).

HTTP 5xx

Retryable.

Network error / timeout / DNS failure

Retryable.

HTTP response header X-Webhook-No-Retry: true

Treated as non-retryable regardless of status.

5.2 Backoff schedule

±20 % jittered exponential backoff, base 30 s, cap 6 h, max 20 attempts over roughly a 48-hour window:

Attempt
Approx. delay since previous

1

(initial)

2

~30 s

3

~1 min

4

~2 min

5

~4 min

doubling each step, jittered

11

~4 h

12

~6 h (cap)

13 … 20

~6 h each

After attempt 20 the message is moved to DEAD and persisted for 90 days. UR operations can manually re-queue a DEAD message on partner request.

5.3 Circuit breaker

If we observe 20 consecutive failures (5xx / timeout / connection-refused) for a given partner, a per-partner circuit opens for 60 s. Open-circuit attempts are not sent to your endpoint; they're requeued for the next backoff slot. Your retry count is not incremented while the circuit is open.

5.4 What X-Webhook-Attempt tells you

The header is 1-indexed:

  • Attempt: 1: first delivery (fresh message)

  • Attempt: 2+: retry

You can use this to bias logging (verbose on 1, terse on retries) or to detect partner-side hot loops where retries keep failing for the same root cause.


6. Event catalog

Subscription is per-partner, per-event-type. You only receive event types your contract subscribes to. Asking for an event you're not subscribed to is the most common silent-fail in integrations; if you don't see events you expect, check your subscription before debugging anything else.

New event types may be added; existing event-type bodies are append-only. We may add new optional fields, never rename or remove existing ones.

6.0 Field-naming quirks (read this first)

The customer identifier appears under one of two field names depending on event type, and its JSON type also varies between events. These are wire-fixed across all delivered events; partners must handle both shapes.

Event types
Field name
JSON type

transaction, transaction_v2, card_spending_failed

urId

string

allowance

urId

int64 (number, no quotes)

fma.account.result, fma.penny_drop.result, fma.kyc.reuse_check.result, fma.kyc.result, fma.additional_kyc.required, fma.additional_kyc.completed

urId

int64 (number, no quotes)

kyc_status, sumsub_kyc_result, monthly_limit_exceeded

tokenId

string

All four shapes refer to the same UR NFT token id. The split is legacy: the tokenId family predates the unification on urId, and the int64/string split predates the unification on string. The wire shapes are frozen for backwards compatibility; do not assume future events will pick one canonical form.

Other quirks worth flagging up-front:

  • Subscription changes (enable / disable an event type, rotate webhook URL) are coordinated with your UR technical liaison; there is no self-service API.

  • Token symbols referenced in event bodies (USD24, EUR24, CHF24, JPY24, CNH24, SGD24, HKD24, USDC) are the UR-issued tokenized fiat assets; see Tokenized deposits for the catalogue and contract addresses.

  • Wire-fixed JSON keys such as deadLine (capital L, in card_spending_failed) are preserved verbatim from legacy schemas. Where they appear, the field reference table for that event notes the quirk explicitly.

6.1 Money movement & on-chain events

transaction (legacy transaction event)

Emitted when a tracked transaction is confirmed on-chain (deposit, withdrawal, transfer, FX, card spend).

Bidirectional fan-out: for a P2P transfer between two of your customers (both bound to your partner_id) you receive two webhooks: one with direction=out and the sender's urId, and one with direction=in and the receiver's urId. For a self-transfer (same urId on both sides) business-key dedup collapses to a single webhook.

Field reference (see also TransactionData in the API reference):

Field
Type
Description

urId

string

Token ID of the customer this event belongs to

title, subtitle, image

string

UI strings; partner can ignore or display

amount, currency

string

Display amount (decimal string) and ticker; partner-visible

type

string

Business type (e.g. transfer_in, transfer_out, card_spend, fx)

direction

string

in or out from this customer's perspective

txHash, chainId

string

On-chain identifiers; chainId is CAIP-2 (e.g. eip155:5000)

inputToken, inputAmount, inputTokenAddress

string

Source asset

outputAmount

string (omitempty)

FX leg

mcc

uint64 (omitempty)

Merchant Category Code (card spend)

reference, bankAccount, crdMultiToken, partnerRefId, …

various (omitempty)

Channel-specific extras

transaction_v2 (current transaction event)

Same business event as transaction, but a normalized schema and lifecycle (broadcastTimeE9, finalTimeE9). New partners should subscribe to transaction_v2 and ignore transaction.

Field
Type
Description

refundType

string

Refund discriminator. "" (not a refund), BANK_REFUND (bank transfer refund), CARD_REFUND (card spending refund), or CARD_REVERSAL (card authorization reversal). See §12.4 Identifying refund transactions for details.

detailsJson is a stringified JSON blob whose structure depends on data.type. See detailsJson reference by transaction type for the full schema of each type.

allowance (ERC-20 allowance change)

Emitted when one of your customer's allowance flags flips (approve / revoke / transferFrom drains it).

Field
Type
Description

data.urId

int64

Customer UR id (note: int64 here, not string; legacy quirk)

data.allowances[]

array

Updated allowance rows

…tokenSymbol

string

One of USD24, EUR24, CHF24, JPY24, CNH24, SGD24, HKD24, USDC

…hasAllowance

bool

True iff on-chain allowance > 0 for the UR contracts

…chainId

string

CAIP-2 chain id

Only the rows that changed are present; never a full snapshot. Treat allowances as a patch, not a state dump.

card_spending_failed (card authorization rejected for user-caused reason)

Emitted when a customer's card spend fails because of insufficient balance / missing allowance / similar customer-actionable reason. Not for network or processor failures.

Field
Type
Description

urId

string

Customer UR id

status

string

Always FAILED for this event

reason

string (omitempty)

Machine-readable reason code

transactionCurrency, transactionAmount

string (omitempty)

Original authorization details

cardCurrency

string (omitempty)

Card's billing currency

deadLine

string (omitempty)

Customer-action deadline (ISO 8601). Wire-fixed: the JSON key is literally deadLine (capital L), not the more conventional deadline. Preserved verbatim for backwards-compat and will not change.

authorizationToken

string (omitempty)

Reference for the rejected authorization

monthly_limit_exceeded (monthly spending limit hit)

Emitted when a customer's monthly spend in CHF would cross their configured limit.

Field
Type
Description

tokenId

string

Customer UR id

txAmount, txCurrency

string

Transaction that triggered the breach

availableCHF, usedCHF, maxCHF

string

Limit accounting in CHF

6.2 KYC (generic)

kyc_status (KYC status change)

Emitted when UR returns an updated compliance status for a customer.

Field
Type
Description

tokenId

string

Customer UR id

status

string

One of: Pending, Pass, Rejected, ManualReview, Error

sumsub_kyc_result (Sumsub review result)

Emitted when a Sumsub KYC review completes for one of your customers.

Field
Type
Description

tokenId

string

Customer UR id

kycResult

string

Pass or Rejected

6.3 Partner-managed (FMA) KYC

These events fire only for partners using the Fiat-Mediated Account (FMA) integration model: partners who own the KYC user-experience and let UR run downstream compliance. They are not sent to non-FMA partners.

The FMA events share a common envelope shape (per-event exceptions are noted in each entry):

  • urId (int64): UR token id (issued by UR-side mint after create-account)

  • sessionId (string, UUID): the partner-KYC session this event belongs to (present on fma.account.result, fma.penny_drop.result, fma.kyc.reuse_check.result, fma.additional_kyc.completed; absent on fma.kyc.result and on fma.additional_kyc.required, which fires before the retry session exists)

  • partnerId (string): your partner id (same string you sign requests with)

  • status (string): a state discriminator inside one logical event family (the fma.additional_kyc.* pair does not carry it: the event name itself is the discriminator)

  • occurredAt (int64): Unix seconds at emission (the fma.additional_kyc.* pair uses createdAt and completedAt instead)

fma.account.result (account onboarding outcome)

Single event that subsumes "account activated" and "session rejected" outcomes. Branch on status:

status

Meaning

Extra fields

activated

KYC passed end-to-end. Customer is now Live on-chain with their CHF spending limit set.

(none)

rejected

The session ended in failure (compliance hard-rejected, KYC-rejected, draft expired, or retry expired).

rejectCode, rejectReason

rejectCode enumeration (the values you will see in production, grouped by what caused the rejection):

rejectCode

Meaning

Origin

KYC_REJECTED

Compliance review returned a hard reject. Partner should NOT retry.

KYC pipeline

UPSTREAM_RETRY_EXHAUSTED

We could not reach an upstream (Sumsub or the compliance backend, etc.) within the 48-hour retry budget. Partner may request ops re-queue.

KYC pipeline

INTERNAL_ERROR

Defensive catch-all for a bug on our side. Always paired with an incident on UR's side; contact ops.

KYC pipeline

SUMSUB_REJECTED

Sumsub returned a hard reject during identity verification.

KYC pipeline

FORMA_NOT_SIGNED

Customer never signed Form A within the session lifetime.

KYC pipeline

SESSION_EXPIRED

Draft session expired (never made it past the data-ingestion step within its TTL).

Maintenance job

PENNY_TIMEOUT

Session reached IdentityVerification but neither penny-drop nor NFC completed within 30 days.

Maintenance job

STALE_ORPHAN_GC

Garbage-collection of orphaned legacy session rows; should not appear for newly-onboarded partners.

Maintenance job

Additional Reject* codes from the API-level error catalogue may appear if a partner-side API call triggers a session termination (rare). Treat any unknown code as "session ended; surface rejectReason to your support tooling and ask UR ops for translation". The set above covers >99 % of real production rejections.

When you receive status=activated you can trust:

  • The customer's UR NFT is Live on-chain

  • Their setClientLimit transaction has confirmed

  • It is safe to allow card spends / withdrawals / etc.

fma.penny_drop.result (penny-drop verification)

Per-attempt verification result for the penny-drop flow (applies to partners whose KYC variant requires a micro-transfer match step; subscription is per partner). Branch on status:

status

Meaning

verified

The customer's micro-transfer matched our expected amount; KYC advances to IdentityVerification → SignFormA.

failed

The most recent attempt did not match. The customer can retry up to a configured cap. When the last allowed attempt fails, the payload includes exhausted: true and no further attempts will be accepted.

Field reference (every field below is present on every delivery of this event type unless marked optional):

Field
Type
Present when
Description

urId

int64

always

UR token id

sessionId

string (UUID)

always

Partner-KYC session id

partnerId

string

always

Your partner id

status

string

always

verified or failed

amount

object {value: string, currency: string}

always

The micro-transfer amount we observed (decimal string + 3-letter ISO currency)

remitterCountryISO3

string

always

The remitter person's country, ISO 3166-1 alpha-3. Used by partner UI for compliance display.

failReason

string

status=failed only

Machine-readable reason. Common values: AMOUNT_MISMATCH, AMOUNT_BELOW_MIN, CURRENCY_MISMATCH, IBAN_MISMATCH. Treat unknown values as "failed; ask UR ops for translation".

attemptsRemaining

int

status=failed only

How many additional attempts the customer has after this one. 0 means no more attempts will be accepted by /kyc/penny-transfer.

exhausted

bool

status=failed only

true iff attemptsRemaining=0. When true, the customer must switch to the NFC fallback path; the penny-drop endpoint will return PENNY_DROP_MAX_ATTEMPTS_EXCEEDED for any further calls. The session itself is not terminated; an fma.account.result may still follow after the NFC path completes.

transactionRecordId

int64

always

ur_transaction_records.id of the evaluated on-chain fiat-deposit mint. Stable identity for the deposit this result refers to.

txHash

string

always

On-chain tx hash of the evaluated fiat-deposit mint.

txLogIndex

int64

optional

Event ordinal inside the tx; disambiguates when a single tx hash carries multiple mint events. Omitted when not applicable.

occurredAt

int64

always

Unix seconds at emission

fma.kyc.reuse_check.result (shared-token handoff verdict)

Fires only for partners using the shared-token (Copy Applicant) KYC reuse model. It is the async mirror of a handoff attempt's conclusive verdict: emitted once per conclusive attempt for status ∈ {passed, incomplete}. Terminal (non-remediable) verdicts are deliberately NOT webhooked; the partner learns those from the synchronous handoff response / POST /api/fma/v1/kyc/check and UR ops. A lost delivery degrades to polling /kyc/check, so this event is fire-and-forget on our side.

Field
Type
Present when
Description

urId

int64

always

UR token id

partnerId

string

always

Your partner id

sessionId

string (UUID)

always

The handoff session this verdict belongs to

attempt

int

always

Session-scoped monotonic attempt counter (part of the business key, so redeliveries of the same attempt dedupe)

status

string

always

passed (import accepted) or incomplete (remediable; resubmit the missing data and re-handoff)

correlationId

string

always

Trace id for cross-referencing with UR ops

missingFields

string[]

status=incomplete only

Dot-path identifiers of the data still required

requiredLevels

string[]

optional

Cloned-workflow level names mapped from missingFields, when resolvable

occurredAt

int64

always

Unix seconds at emission

fma.kyc.result (Sumsub review outcome for shared-token flows)

Fires for shared-token / pull / SDK FMA flows when the underlying Sumsub review reaches an outcome. This is the Sumsub-layer KYC verdict, distinct from fma.account.result (which is the account-activation outcome). Branch on status.

status

Meaning

pass

Identity data accepted; the flow has advanced past all Sumsub levels to SignFormA.

rejected

Sumsub returned a hard reject (RED).

step_up

Additional documents requested. (Reserved; not emitted by this path yet.)

Field
Type
Present when
Description

urId

int64

always

UR token id

partnerId

string

always

Your partner id

status

string

always

pass, rejected, or step_up

rejectCode

string

status=rejected only

Machine-readable reject reason

rejectReason

string

optional

Free-form reason text for support tooling

occurredAt

int64

always

Unix seconds at emission

Note: fma.kyc.result carries no sessionId; correlate on urId + occurredAt.

fma.additional_kyc.required (ops triggered a partner-driven retry)

Emitted when UR operations decide an already-onboarded customer must resubmit part or all of their KYC. UR seals the customer's current session as it emits this event, so any sessionId you still hold stops working. The new session is not in the payload: you create it by calling POST /api/fma/v1/kyc/session/create, which returns its id.

Field reference:

Field
Type
Description

directiveId

string (UUID)

Identifies this retry request. Correlates with the completed event and with UR support tickets.

type

string

Always retry today; other values are reserved.

taskType

string

What the customer must redo, and the field to branch on: full, passport, address_recheck, or redo_form_a.

retryLevel

int

The retry category ops selected. Advisory; taskType is authoritative. Note this is a different quantity from the retryLevel returned by /kyc/session/current, which counts sessions.

fiat24Mode

string

auto_register or ops_offline. Decides whether you finish the session with POST /kyc/submit.

dataChannel

string

push, sdk, or shared-token. Matches your integration path.

partnerId

string

Your partner id

externalUserId

string

The partner-side user id you bound to this urId at create-account time

urId

int64

UR token id (unchanged between the original and retry sessions)

retryOfSessionId

string (UUID)

The session this retry replaces. It is already sealed; calls against it return SESSION_NOT_FOUND (30006).

retryReason

string

Free-form reason text suitable for partner-side support tooling (not for direct end-user display)

requiredFields

string[]

Push channel only: dot-path identifiers to correct, for example registerRequest.address.street. Empty on other channels.

deadlineAt

int64

Unix seconds, or 0 when unset. Informational only: UR does not expire the directive or session when it passes, and no event fires at the deadline.

createdAt

int64

Unix seconds when the retry was triggered

Action on receipt: call POST /api/fma/v1/kyc/session/create to claim the new session, then drive the customer through the steps taskType names. See Retry KYC for the per-channel walkthrough.

fma.additional_kyc.completed (retry session finished)

Emitted when the retry session reaches its last step. No further call is required from you.

directiveId, taskType, retryLevel, and fiat24Mode repeat the values from the required event, so the pair matches without a lookup. Two fields differ:

Field
Type
Description

sessionId

string (UUID)

The retry session that just completed, the same id /kyc/session/create returned to you

completedAt

int64

Unix seconds when the session completed

Subscribe to both fma.additional_kyc.required and fma.additional_kyc.completed. An unsubscribed event is dropped, so a partner subscribed to only one of the pair silently sees half the flow.


7. Idempotency and ordering

7.1 What to dedupe on

Dedupe on X-Webhook-Request-Id. That UUID is fixed for the lifetime of a message: every retry sends the same id, and our database uniqueness constraint on (event_type, partner_id, business_key) guarantees the same logical event never gets two request ids.

Concretely on your side:

7.2 What NOT to dedupe on

  • The HTTP body, or any subset of fields inside it. Two different business events (e.g. status activated and a later status rejected for the same session id after a retry flow) can share field values, and you do want to receive both.

  • X-Webhook-Timestamp: that changes on every retry.

7.3 Ordering guarantees

  • Per-partner first-delivery order: the first delivery attempts for messages belonging to one partner go out in the same order they were produced. The internal Kafka topic is partitioned by partner_id and a single consumer goroutine per partition processes strictly sequentially, advancing the Kafka offset as soon as each delivery attempt completes (whether success or failure).

  • Retries are out-of-band: a failed message moves to PENDING_RETRY and is re-published to the same partition by a separate retry dispatcher after its backoff. The consumer does not block on retries; it proceeds to the next fresh message immediately. As a consequence, a retry of message A may arrive after a later message B if B's first delivery happens to succeed while A is still cycling. Build your handler so that re-applying message A after message B's effects is safe.

  • Across partners: no ordering guarantee.

  • Within an FMA session: the fma.account.result event for a sessionId is the final word for that session; once you've seen it, no further events for the same sessionId will arrive. Earlier fma.penny_drop.result events for the same sessionId are produced first, but the retry caveat above still applies (a delayed retry could theoretically arrive after account.result). If you order on the partner side by occurredAt, you'll get the intended chronology even on a delayed retry.


8. Operations

8.1 Rate limits

Per-partner default: 20 requests per second, burst 40. If you can sustain higher and need it, raise it with your UR technical liaison.

8.2 IP allowlists

Our outbound IP is stable per Kubernetes deployment but may shift on node rotation. Allowlist by domain, not by IP. If you must allowlist by IP, ask for the current allocation in writing and treat it as advisory; we won't notify before rotations.

8.3 TLS

Standard CA chain. We don't pin certificates on our side; you don't need to share anything for handshake.

8.4 Recovery probing

During incident response our system may re-drive delivery to your endpoint, but it does not send a synthetic probe event; there is no __probe__ event type. Recovery reuses the actual business messages (replayed from the internal Kafka topic), so anything you receive is a real, dedupable event carrying its normal X-Webhook-Request-Id. Keep handling every delivery idempotently (see §7); there is no special probe body to special-case.

8.5 Cancellation

UR operations can cancel a PENDING_RETRY message via internal admin tooling (for example, if a refund / reversal makes an earlier event obsolete). Messages already in flight may still arrive at your endpoint for a few seconds after cancellation. Treat late arrivals idempotently; your business-side reconciliation should already handle this.


9. Troubleshooting

Symptom
Likely cause
Fix

Signature verification fails on every webhook

You're re-serializing the body after JSON-parse

Read req.rawBody (or framework equivalent); sign over raw bytes, not re-encoded

Some webhooks verify, others don't

Two different keys delivered (V1 vs V2 with different message composition); you're using one verifier for both

Implement both verifiers (see §4.6)

Webhooks for one customer suddenly stop

Your endpoint is returning non-2xx and we hit the 20-attempt cap → DEAD

Check your logs at X-Webhook-Request-Id; ask UR ops to re-queue

Webhooks for ALL customers stop briefly, then resume

Circuit breaker opened on our side after 20 consecutive failures from your endpoint

Look at the 60 s before resumption for the failure mode

Same X-Webhook-Request-Id arrives 5× in a minute

Your endpoint is timing out (we keep retrying) or returning 5xx

Acknowledge fast; process asynchronously

Webhook count is roughly half what you expected

Subscription not enabled for the missing event type

Check your partner contract subscriptions

FMA activated arrives but customer is not Live on-chain

Should never happen; activated is only fired after the on-chain status flip and limit-set both confirm

Capture the X-Webhook-Request-Id and sessionId, contact UR ops

For unresolved incidents, open a ticket via your UR technical liaison with at minimum:

  • X-Webhook-Request-Id

  • X-Webhook-Timestamp

  • X-Webhook-Event-Type

  • The full request body (raw bytes)

  • The HTTP status you returned and the response body


10. Quick reference card

Verification message for V2:

Return 200 OK to ack. Return 4xx to fail permanently. Return 5xx / time out to retry.


See also

  • Signature and verify: generic UR request-signing rules and the signature scheme for the partner → UR direction. Note: that doc currently documents only the V1 webhook signature (UR → partner direction); the V2 scheme defined in §4.2 of this page is the authoritative webhook reference. The EIP-191 primitive is shared across both directions, but the message composition differs (request-body + deadline for partner → UR; timestamp + request-id + body for V2 webhooks).

  • OpenAPIs: full reference for the partner REST API; TransactionData and friends are documented there.

  • Partner integration flow concepts: when each event fires in the customer lifecycle.

  • The user lifecycle: end-to-end customer journey, with webhook touchpoints.

Last updated