Overview
Webhooks deliver real-time HTTP callbacks when events happen in your Conduit account. Instead of polling the API, register an endpoint and Conduit pushes events to you. Conduit guarantees at-least-once delivery — your endpoint may receive the same event more than once. Clients SHOULD dedup byid and order by createdAt. We do not guarantee strict transport ordering.
Setting Up
1. Create an endpoint
secret. Save it securely, it is only shown once and cannot be retrieved later.
subscription is a tagged union:
{ "mode": "all" }(the default ifsubscriptionis omitted) subscribes the endpoint to every event type.{ "mode": "selected", "eventTypes": ["..."] }subscribes only to the listed event types.eventTypesmust be non-empty.
PATCH /v2/webhooks/endpoints/:id with the same subscription shape.
2. Verify signatures
Every webhook request includes aX-Conduit-Signature header in the format:
v1 is HMAC-SHA256(<unix-timestamp>.<raw-body>, secret), computed over the raw request bytes before any JSON parsing.
A delivery normally carries a single v1. While you are rotating an endpoint’s signing secret, deliveries carry two v1 values for a grace period — one signed with your new secret and one with the previous one — so deliveries keep verifying while you roll your secret over. Verify by recomputing the digest for your secret and accepting the delivery if it matches any v1 value. Once the grace period ends, only the current secret is used.
We recommend rejecting deliveries where t is older than 300 seconds to guard against replay attacks.
Node.js / Bun
Python 3
Common verification mistakes
3. Rotate your signing secret
If your signing secret is leaked — or you rotate secrets on a schedule — call:{ ...endpoint, "secret": "whsec_...", "signature": { ... } }). Store it immediately; it is never shown again.
Rotation does not cut over instantly. For a grace period (about 48 hours) every delivery is signed with both the new and the previous secret — two v1 values in X-Conduit-Signature (see Verify signatures). This lets you roll your verification over without dropping events:
- Call rotate and store the new secret.
- Deploy the new secret to your verifier. Because you accept any matching
v1, deliveries keep verifying throughout — under the old secret before you deploy, under the new one after. - Once the grace period ends, only the new secret signs. Any verifier still on the old secret stops verifying — which is the forcing function that completes the rollover.
Webhook Headers
Every webhook request includes these headers:Payload Format
data object varies by event type. Use the type field to determine how to process the payload.
Tracking transaction progress via webhooks
transaction.created carries a stage field — the same progress signal as GET /v2/transactions/:id’s
stage (see Progress: the stage field). Narrower
events that fire between creation and a terminal outcome (transaction.processing,
transaction.awaiting_signature, transaction.signature_collected, transaction.quorum_met,
transaction.awaiting_sender_information) do not carry stage — the event itself is a more
specific progress signal than stage would add.
Terminal events (transaction.completed, .cancelled, .failed) don’t carry it either since status
already conveys the outcome. If you need the current stage between those events, poll
GET /v2/transactions/:id or GET /v2/payouts/:id.
For a non-custodial payout,
transaction.awaiting_signature is discriminated by signingMode. A
wallet in the passkey_required mode carries the signer’s verificationUrl (use the latest
attempt’s URL and discard earlier ones); for how to obtain that link, re-fetch it if you missed
the delivery, and the one request pattern to avoid, see
Getting the signing link. A
wallet in a programmatic mode carries a signingRequestId — see
Machine-signer stamping — plus an optional verificationUrl
when its roster has a human passkey signer who may also approve on the verify page (a machine-only
roster omits it).Event Reference
Paired events
A single business transition can emit more than one event. See the per-event descriptions inGET /v2/webhooks/event-types for the canonical dedupe rule on each pair.
Delivery Lifecycle
Each webhook delivery goes through these statuses:Retries
Failed deliveries are retried with increasing delays:
You can also manually retry a failed delivery:
Managing Endpoints
The
status query parameter accepts pending, processing, succeeded, or failed (case-insensitive); unknown values return 400. Filters compose with endpointId.
The eventType query parameter accepts an exact lowercase match against the event type (e.g. transaction.failed or order.failed). Unknown event types return an empty page. All three filters (endpointId, status, eventType) are optional and may be combined freely.
failureMessage symmetry contract
The failureMessage value is consistent across three surfaces: the database row, the polled GET /v2/transactions/{id} response, and the transaction.failed webhook payload. Applies equally to order.failed.
The polled GET and the webhook payload never diverge, so an integrator can rely on either as the source of truth. On the last row the two agree with each other and carry the same fixed text however such a transfer ends, which is what makes them safe to branch on: there is no code to switch on, so treat the transfer as not completed and read
GET /v2/transactions?type=deposit_return to see whether the funds went back.
Pausing an Endpoint
Setstatus: "disabled" (via PATCH /v2/webhooks/endpoints/:id) to stop receiving new deliveries on an endpoint. The endpoint is excluded from event fan-out — no new deliveries are enqueued. In-flight deliveries already queued at the moment of the flip continue to retry per the retry schedule and are not cancelled. Set it back to status: "active" to resume receiving new deliveries.
Best Practices
- Respond quickly. Return a 2xx status within 5 seconds. Process the event asynchronously after acknowledging receipt.
- Deduplicate. Use the event
idto detect and skip duplicate deliveries. - Verify signatures. Always validate
X-Conduit-Signaturebefore processing the payload. - Handle unknown events. Your endpoint may receive new event types as the API evolves. Return 2xx for events you don’t recognize — don’t reject them.
- Use HTTPS. Webhook endpoint URLs must use HTTPS.
application.approved
Fired when an application is approved.applicationType discriminates the variant — route on it: customer_onboarding — customer is now active; paired with customer.created (same applicationId, customerId, clientReferenceId); dedupe on (applicationId, customerId) if your handler reacts to either; idempotent re-approval does not re-emit the pair. virtual_account — virtual account has been created; asset carries the asset code/chain; the VA activates asynchronously and fires virtual_account.activated when ready. crypto_wallet — customer is now eligible for POST /v2/customers/:customerId/wallets/claim-non-custodial; no wallets have been provisioned yet. customer_update — the customer data change was accepted. organization_onboarding — organization is approved; no customerId is present.
application.rejected
Fired when an application is rejected.applicationType discriminates the variant. failureCode (machine-readable) and failureMessage (human-readable) are present when a specific reason is available. customerId is present for virtual_account, crypto_wallet, and customer_update rejections (customer exists by rejection time); absent for organization_onboarding; optional for customer_onboarding (absent when rejection occurs before customer creation). For crypto_wallet: jurisdiction-ineligible requests are refused synchronously with 422 at request time and do not create an application row, so no webhook fires for that case.
claim.completed
Fired when a non-custodial claim resolves: every roster signer has enrolled and the wallets are activated. Carries the claimId returned by POST /v2/customers/:id/wallets/claim-non-custodial plus the activated wallet IDs, so you can close the loop on a claim you were polling.claim.failed
Fired when a non-custodial claim fails during provisioning — after the claim was accepted (202). Carries the claimId and a human-readable failure reason to surface to your user. (Synchronous validation failures — roster/threshold/jurisdiction — are returned inline on the POST as a 4xx and do NOT fire this webhook.)crypto_wallet.completed
Fired when the customer’s end user has finished onboarding their non-custodial wallets and the wallets are ready to receive funds. Carries the customerId; fetch the wallets with GET /v2/customers/:id/wallets. To close the loop on a specific claim with the activated wallet IDs, use claim.completed.customer.created
Fired when a customer is created after onboarding approval. Paired with application.approved (applicationType=customer_onboarding) on the first approval (same applicationId, customerId, clientReferenceId); on idempotent re-approval the customer already exists so the pair is not re-emitted. Dedupe on (applicationId, customerId) if your handler reacts to either.customer.restricted
Fired when a restriction is placed on a customer. The customer is blocked from initiating the affected money-movement capabilities. The restriction reason and type are internal-only and are never included.idv_link.created
Fired when a hosted identity-verification link is created for a person on an application — one event per person. Subscribe to this event to deliver the link through your own channels. It is emitted regardless of whether Conduit also emails the person directly; that is a separate account setting and does not affect this event.url is a one-time credential: treat it as a secret and do not log it. Person referenceIds are listed on the application (persons[]), and a fresh link can be fetched at any time via POST /v2/applications//persons//idv-link.
order.cancelled
Fired when a pending order is cancelled — either by the sweep job (reason=expired, when lock_expires_at elapses) or by the client (reason=client_cancelled, via POST /v2/orders/:id/cancel).order.created
Fired when an order is created via POST/v2/orders. The order is in pending status with the rate locked until lockExpiresAt. The order will not move funds until it is executed — either explicitly via POST /v2/orders/:id/execute, or automatically by the platform when source funds land (if autoExecute is true). If the lock expires while the order is still pending and unclaimed, the expiry sweep cancels it and order.cancelled fires with reason: expired. When the order was created without an explicit source, depositInstructions carries the address to fund it at; that order auto-executes once the deposit lands, and the lock expiry is the funding deadline instead of a rate lock.
order.failed
Fired when an order cannot execute or execution reaches a terminal failure. This includes pending auto-execute ONRAMP orders whose fiat source deposit terminates without crediting the customer — frozen, returned, or terminated before credit (e.g. sender-info timeout).reasonCode identifies the customer-facing failure category: insufficient_funds (source funds insufficient at execution time), provider_unavailable (transient rail/provider unavailability — retry may succeed), provider_rejected (provider or screening declined the leg, including source-deposit rejection on auto-execute ONRAMP orders; retry will not help — submit with a different recipient or funding source), internal_error (Conduit-side failure — contact support), cancelled (execution cancelled mid-flight).
order.succeeded
Fired when an order completes successfully — the source amount has been debited from the customer and the destination amount has been credited and is available to spend. Carries the spawnedtransactionId (and txHash when a chain leg ran) so integrators can reconcile and link back to the underlying transaction without a GET round-trip.
organization.activated
Fired when an organization is activatedorganization.restricted
Fired when a restriction is placed on an organization. Every customer under the org is blocked from the affected capabilities. The reason and type are internal-only.rfi.cancelled
Fired when a published request for information is cancelled. Fetch details via GET /v2/rfis/.rfi.deadline_extended
Fired when compliance extends the response deadline on an open request for information. Fetch the new due date via GET /v2/rfis/.rfi.more_info_requested
Fired when compliance requests more information on an already-responded request for information, opening a new round. Fetch details via GET /v2/rfis/.rfi.published
Fired when a request for information is published to your organization. Fetch details via GET /v2/rfis/.rfi.resolved
Fired when a request for information is resolved. Fetch details via GET /v2/rfis/.rfi.response_submitted
Fired when a client response to a request for information is stored. Fetch details via GET /v2/rfis/.transaction.awaiting_sender_information
Fired when a deposit is parked waiting for sender information for the source address. Payload carries the sourceAddress (null when no on-chain sender could be attributed) and an expiresAt deadline — after which the deposit auto-rejects. Chain is on assetAmount.transaction.awaiting_signature
Fired when the customer’s signing roster must approve before broadcast — on a payout, or on the source transfer of a conversion from a non-custodial wallet. The payload is discriminated bysigningMode. passkey_required carries the shared verificationUrl (Conduit-hosted approval page the fintech distributes to its human signers). programmatic / programmatic_unattended carry a signingRequestId — a machine integration discovers the signing details with GET /v2/signing-requests/{id} and approves via POST /v2/signing-requests/{id}/approve or rejects via POST /v2/signing-requests/{id}/reject. A programmatic payload ALSO carries an optional verificationUrl when the wallet’s roster has an active passkey signer: that human signer is a member of the signing quorum and may approve the payout on the verify page alongside the machine signers. The verificationUrl is omitted for a machine-only roster. If a signing window expires before quorum, Conduit rebuilds the request (fresh nonce/fees) and re-fires this event with an incremented attempt; treat the latest attempt as authoritative. After the final attempt expires the payout fails with user_signature_expired (a conversion fails via order.failed).
transaction.awaiting_user_signature
Deprecated — superseded bytransaction.awaiting_signature; still delivered during a compatibility window and removed in a future release. Fired when the customer’s signing roster must approve before broadcast — on a payout, or on the source transfer of a conversion from a non-custodial wallet. Payload carries the single shared verificationUrl (Conduit-hosted approval page distributed by the fintech to its signers), expiresAt, and attempt. If a signing window expires before quorum, Conduit rebuilds the request (fresh nonce/fees, a new verificationUrl) and re-fires this event with an incremented attempt; clients should always treat the latest verificationUrl as authoritative and discard prior links. After the final attempt expires the payout fails with user_signature_expired (a conversion fails via order.failed).
transaction.cancelled
Fired when a transaction is cancelled before reaching its terminal-completed state. Distinct fromtransaction.failed: a cancelled transaction is not a failure; the client (or, in the future, an expiry sweep) terminated it intentionally. cancellationReason is client_cancelled when the client called POST /v2/payouts/:id/cancel. Reserved value expired is published when the underlying lock window elapses (future). The payload intentionally has no failureCode/failureMessage. Shares the cancellation semantics and cancellationReason vocabulary with order.cancelled (the payload itself is transaction-shaped: transactionId + nested source/destination).
transaction.completed
Fired when a transaction completes successfully.source and destination carry the same nested shape as GET /v2/transactions/:id. The settlement reference lives inside the relevant side variant: external_crypto.txHash for crypto rails, and on external_bank the real wire references as typed fields — swiftUetr, fedwireImad, fedwireOmad, achTraceNumber, rtpTransactionId, fedNowMessageId — each present only when the network exposes it (on-us transfers carry none). Fiat payout events include the selected rail for reconciliation on payout.rail.
transaction.created
Fired when a new transaction is initiated.type identifies the direction: deposit, onramp, offramp, withdrawal, conversion (a crypto-to-crypto swap or bridge between two of the customer’s wallets), or deposit_return (funds sent back from an order’s funding address to the address they came from). source and destination carry the same nested shape as GET /v2/transactions/:id — discriminated by type (wallet, deposit_address, virtual_account, external_crypto, external_bank, external_bank_inbound, internal_transfer, external_unknown) and each variant carries its own assetAmount. stage is the same client-safe progress signal as the GET response — see that endpoint’s field docs for the vocabulary and lifecycle. Transactions belonging to an order carry linkedOrderId referencing it — both the conversion the order executes and a withdrawal chained from its autoPayout; absent on transactions with no order.
transaction.failed
Fired when a transaction reaches a terminal failed state.source and destination carry the same nested shape as GET /v2/transactions/:id. The payload carries a failureCode your integration can branch on:
user_signature_*— recoverable: submit a new transaction.chain_broadcast_failed— the on-chain broadcast or signing did not reach finality and no funds left the wallet; recoverable, submit a new payout.crypto_wallet_misconfigured— the wallet’s signing configuration prevents Conduit from moving funds and the wallet has been frozen; NOT recoverable by retrying, contact support.provider_rejected— chain RPC or sandbox-scenario declined the broadcast;failureMessagecarries the operator/scenario-supplied reason when the underlying message was marked for public surfacing (sandbox/scenario paths). Live provider diagnostics are gated off the public surface. Adjust inputs (e.g. destination address, amount) and retry.travel_rule_rejected— counterparty VASP rejected the travel-rule transfer;failureMessagecarries the counterparty’s reason when supplied. Not retryable without coordinating with the receiving institution.compliance_hold/compliance_review_rejected— compliance review required; not retryable without investigation.returned_by_sender— fiat sender reversed the inbound transfer, or compliance marked the deposit returned before credit. A transfer sent back from a funding address carries nofailureCodeat all — see the RETURNED_BY_SENDER page for how to reconcile that case.rail_policy_rejected/insufficient_funds_at_settle/rail_unavailable— payment-rail failure; adjust amount, recipient, or rail and retry.sender_info_timeout— sender-info gate timed out; submit with sender details included. WhenfailureCodeis absent the failure has no actionable code — contact support. Order-level failures (including conversion provider unavailability) surface onorder.failedwith areasonCode, not here.
transaction.processing
Fired when a payout clears compliance and moves into active processing (signature collection, co-stamp, broadcast). The public status transitions frompending to processing at the same moment. On a multi-signer non-custodial payout this fires once, before transaction.awaiting_signature; the payout may still carry a queuePosition while it waits for its per-wallet signing turn.
transaction.quorum_met
Fired when all required signer stamps are in (collected >= required). Compliance already cleared before signing began, so Conduit now co-signs and broadcasts — no further review gate stands between quorum and broadcast.
transaction.rejected
Fired when a compliance reviewer rejects the supporting document on an accepted payout, before execution. Terminal: funds are returned to the available balance. Resubmit a new payout with an acceptable document (see acceptedDocumentTypes) and a fresh idempotency key.transaction.signature_collected
Fired once per signer stamp collected on a multi-signer payout. Trackcollected / required to drive a progress UI; once collected >= required, transaction.quorum_met follows.
virtual_account.activated
Fired when a virtual account is activatedwallet_ceremony.awaiting_admin_approval
Fired when a non-custodial wallet ceremony (roster add/remove, quorum change) is waiting for a customer admin’s passkey co-stamp to reach quorum. CarriesadminVerificationUrl — the Conduit-hosted approval page to route the admin to — and expiresAt, after which the ceremony auto-fails and must be resubmitted. For promote/remove ceremonies this webhook is the only channel that delivers the approval URL. (A passkey signer added to a live roster parks on a separate wallet_signer.awaiting_admin_approval event instead.)
wallet_ceremony.completed
Fired when a non-custodial wallet ceremony completes successfully. All roster or quorum changes requested by the ceremony are now in effect.wallet_ceremony.failed
Fired when a non-custodial wallet ceremony expires or fails. Any reserved resources have been released.status is failed on expiry or unrecoverable error; cancelled when the ceremony was explicitly cancelled.
wallet_signer.added
Fired when a wallet signer row is created on the roster (immediately at invite time, status pending_activation) — co-emitted withwallet_signer.invited from the same outbox transaction. Distinct from wallet_signer.invited (which carries the verification URL) and wallet_signer.enrolled (sent later when the signer completes credential enrollment).
wallet_signer.awaiting_admin_approval
Fired when a passkey signer added to a live roster has enrolled their passkey and now parks for a customer admin’s co-stamp. CarriessignerId + customerId (the signer’s identity — no walletId, since a signer’s roster can back several wallets), adminVerificationUrl — the Conduit-hosted approval page to route an admin to — and expiresAt, after which the pending add auto-fails. This is the only channel that delivers this admin link; if it is lost, recover it with POST /v2/customers/:customerId/wallet-signers/:signerId/reissue-admin-approval. (A ceremony-backed roster/quorum change instead uses wallet_ceremony.awaiting_admin_approval.)
wallet_signer.demoted
Fired when an admin is demoted to signer via the two-step demote ceremony (root-quorum update + tag update). Validates min-2-admins floor before starting.wallet_signer.enrolled
Fired when a wallet signer completes credential enrollment via the verification URL.passkeyCount reflects the number of passkeys registered for passkey signers.
wallet_signer.enrollment_approved
Fired when a customer admin’s co-stamp approves a signer’s credential ceremony — a newcomer’s first passkey, or an existing signer adding a device. The post-approval counterpart towallet_ceremony.awaiting_admin_approval. Carries approvedByWalletSignerId (the approving admin) and approvedAt. Distinct from wallet_signer.enrolled, which fires when the signer submits their credential; this fires only once the admin approves it.
wallet_signer.invited
Fired when a wallet signer is invited to enroll their credential. Payload carries the per-signer enrollment verificationUrl the fintech distributes out-of-band, plus expiresAt — after which the invitation auto-expires.wallet_signer.promoted
Fired when a wallet signer is promoted to admin via the two-step promote ceremony (tag update + root-quorum update). A passkey admin must have at least 2 passkeys enrolled; a machine (api_key) admin needs none and is allowed only in the fully-automated signing mode.wallet_signer.removed
Fired when a wallet signer is removed from the roster.reason discriminates between customer-initiated removal (customer_removed) and internal ops removal (ops_removed). Pending payouts carrying the removed signer’s stamp are voided with failureCode roster_changed.
wallet.created
Fired when a crypto wallet is created. Not delivered for the Conduit-managed funding address behind a deposit-funded order — that address is not a wallet resource, and the order’sdepositInstructions publishes it instead.