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 todeposit.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.sideshiftAccountIdis the depositing user. - Hosted checkout. A payer completes a session from
POST /checkout/sessions.deposit.confirmedcarriesmetadata.source: "connect_hosted_checkout"andmetadata.checkoutSessionId, plus the metadata you attached to the session.depositIdis the session id. - Your own wallet top-up. You fund your own SideShift wallet through the SideShift
app.
data.sideshiftAccountIdis your own company id andmetadata.source: "wallet_topup".
amountCents is the gross amount charged to the payer, feeCents is the processing fee,
and netAmountCents is what was credited to the wallet.
deposit.pending
deposit.pending
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.
deposit.confirmed
deposit.confirmed
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.deposit.failed
deposit.failed
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 throughPOST /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.
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:
withdrawal.created
withdrawal.created
Sent once when the user initiates the withdrawal.
status is typically requested.withdrawal.updated
withdrawal.updated
Sent each time
status changes. Pure metadata refreshes with no status change are not
relayed. canceled is delivered on this event only.withdrawal.completed
withdrawal.completed
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.withdrawal.failed
withdrawal.failed
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 isduplicate_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: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 onX-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.
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.