Skip to main content
Connect notifies your backend about deposits, completed transfers, withdrawal status changes and risk flags by POSTing a signed JSON event to a URL you configure. This page is the complete contract: the events, their exact payloads, the headers, the retry policy, and the endpoints for testing, inspecting and replaying deliveries.

Setup

Configure webhooks in the Connect console under Embed Widgets. There are two independent endpoints: Each has its own URL, its own signing secret and its own subscription list. A secret is generated on first save and shown as whsec_ followed by 64 hex characters. The URL must be http or https. Saving preserves what you do not change: a save that only changes the URL keeps the existing secret and subscription list. Sandbox events are delivered only to the sandbox webhook and signed only with the sandbox secret. If no sandbox webhook is configured, sandbox events are not delivered; they never fall back to the live URL or secret.

Subscriptions

An endpoint registered without an explicit event list is subscribed to deposit.pending, deposit.confirmed, deposit.failed, withdrawal.created, withdrawal.completed, withdrawal.failed and transfer.completed. withdrawal.updated is left out of that default because its intermediate transitions are noise for anyone who has not asked for them, and account.risk_flagged is opt-in. A withdrawal.completed subscription implies withdrawal.failed: if you asked to hear that a payout landed, you hear about the bounce too. Handle unknown type values by acknowledging and ignoring them.

Events

Nine events are available.

Envelope

Every delivery is a POST with this JSON body:
id is unique per event and is the value to deduplicate on; it is also sent as the X-Sideshift-Event-Id header. timestamp is when this delivery was built, so a replay carries the same id with a newer timestamp. data always contains sideshiftAccountId, currency ("usd") and status. It contains externalId whenever the account has one: the value you set on POST /accounts/create or PATCH /accounts/{id}. This holds for every event, including the withdrawal.* family. Money fields (amountCents, feeCents, netAmountCents, paymentId) are present on every event where money moved and absent on account.risk_flagged. Integrations in legacy mode additionally receive whopPaymentId as an alias of paymentId.

Deposit events

Deposit events describe money arriving in a wallet you own. They fire on three funding paths:
  • Escrow pay-in. One of your users deposits through a pay-in widget whose token was minted with escrowMode: true. The money is credited to your company wallet (or the escrow destination you named). data.sideshiftAccountId is the depositing user.
  • Hosted checkout. A payer completes a session from POST /checkout/sessions. deposit.confirmed carries metadata.source: "connect_hosted_checkout" and metadata.checkoutSessionId, plus the metadata you attached to the session. depositId is the session id.
  • Your own wallet top-up. You fund your own SideShift wallet through the SideShift app. data.sideshiftAccountId is your own company id and metadata.source: "wallet_topup".
A user’s ordinary pay-in into their own wallet (a non-escrow pay-in widget) is that user’s money, not yours, and produces no deposit event to you. amountCents is the gross amount charged to the payer, feeCents is the processing fee, and netAmountCents is what was credited to the wallet.
Sent the moment a deposit is initiated, before the underlying payment settles. Fires for escrow pay-ins and your own wallet top-ups. Use it to mark the deposit as in-flight; nothing has been credited yet.
Sent when the payment settles and the wallet is credited. Card payments usually confirm within seconds; bank rails can take business days after deposit.pending. This is the event to credit on.
Sent when an escrow deposit is declined or fails. feeCents and netAmountCents are 0, and the reason is in metadata.failureReason ("Unknown" when the provider gave none).

transfer.completed

Sent when a transfer you initiated through POST /accounts/transfer or the batch endpoint completes, in any direction. sideshiftAccountId is the destination account; externalId is the destination’s external id, and is absent when the destination is your own company. feeCents is always 0 and netAmountCents equals amountCents. paymentId is the payment provider’s transfer id when there was one (withdrawal-destination transfers), otherwise the transferId. metadata is your transfer metadata plus transferId, direction, destinationBalance, fromAccountId and toAccountId.
For live transfers that settle to the withdrawal-ready balance, the event id is deterministic (embed-transfer: followed by the transferId), so a transfer recovered by SideShift’s reconciliation after a lost response delivers with the same id and your deduplication holds. Other transfers use a random id.

Withdrawal events

Withdrawal events describe a user moving money out of their SideShift account to an external payout method through the payout widget. They are sourced from the payment provider’s own withdrawal events and are live-only; there are no withdrawals in sandbox. Routing. A withdrawal event fans out to every integration that has previously settled a completed transfer to that user, because each of them has a ledger-reconciliation interest in the outcome. The set is resolved once per withdrawal and reused for its later status updates. Status enum. status is passed through from the payment provider:
completed, failed, canceled and denied are terminal. There is no per-transition history on the provider side, so metadata.previousStatus on withdrawal.updated is how you reconstruct the path. Shared payload. All four events carry the same data shape:
Sent once when the user initiates the withdrawal. status is typically requested.
Sent each time status changes. Pure metadata refreshes with no status change are not relayed. canceled is delivered on this event only.
A derived alias of withdrawal.updated filtered to status: "completed". The two events fire together when a withdrawal completes. Subscribe to this if you only want the terminal success.
A derived alias of withdrawal.updated for failed (the payout was returned, usually by the receiving bank) and denied (SideShift or the provider refused it). status is not normalised, so read it to tell the two apart. A canceled withdrawal is not a failure and does not produce this event.

account.risk_flagged

Sent once per risk flag when SideShift detects an account-level risk signal on one of your accounts. Today the only signal is duplicate_identity_profile: the same identity verification profile was observed on more than one Connect account under your integration. Use it as a stop-and-review signal before sending further transfers. Bank-account duplicate detection is not emitted, because SideShift does not receive raw bank details or a stable fingerprint from the payout provider; bankAccountFingerprintAvailable is always false today.

Delivery

Headers

Signature

The signed payload is the timestamp, a literal dot, and the raw request body:
Sign the bytes you received, not a re-serialised object; JSON.stringify on a parsed body changes whitespace and the signature will not match. Compare with a constant-time function and reject stale timestamps, since a signature on its own stays valid forever. Reference implementations in Node.js, Python and Go are on Testing.

Retries

A delivery is a short series of attempts inside a single dispatch: There is no long-running retry queue behind these attempts. If all of them fail, the delivery is logged as failed and you recover it with the replay endpoint below. Return 200 quickly and process asynchronously; the timeout runs on your handler, so a slow database write is indistinguishable from an outage.

Deduplication

Deliveries can arrive more than once: a retry after a timeout your handler actually served, a replay you triggered, or a reconciled transfer redelivered under its deterministic id. Deduplicate on X-Sideshift-Event-Id and treat a repeat as a no-op.
Webhooks are a notification, not a ledger. Reconcile anything that must be exactly right against the API: read GET /accounts/balance and GET /transfers rather than summing the amounts you were told about.

Managing deliveries

Delivery logs

GET /api/embed/webhook-logs returns delivery attempts for your integration and mode, newest first.
payload is the data object that was sent. A replayed delivery is logged as a new entry.

Replay

POST /api/embed/webhooks/{eventId}/replay re-sends a previously dispatched event to your current endpoint. It is how you recover from an outage without asking anyone to reproduce the original activity.
The body is optional. Without logId the most recent delivery of that event id in your mode is replayed. The payload is re-signed with your current secret and a fresh timestamp, the event id is unchanged, and the attempt is logged with replay: true.

Test delivery

POST /api/embed/webhooks/test signs and delivers one synthetic event of any of the nine types to your sandbox webhook, shaped like the real event and logged like a real delivery. It requires a sandbox key. Pass eventType (default deposit.confirmed) and, for withdrawal.updated, a status. The full request body, response and failure modes are on Testing.