Enable
POST /v1/platform-users/inbound/enable
Creates the secret URL and returns it. It is shown only once. Calling it again replaces the URL.
Response
Status
GET /v1/platform-users/inbound
Response
last_event_at is the time of the last event received.
Disable
POST /v1/platform-users/inbound/disable
Switches the URL off. Returns {"enabled": false}. Events posted to it afterwards get 404 invalid_inbound_url.
Send events
POST https://api-id.cabalspy.xyz/v1/inbound/csin_...
Post one event per request as a JSON object. The event is read like this:
- User id: the first of
external_id,user.id,data.userId,userId,data.user_id,user_idanddata.idthat is set. It becomes theexternal_id. - Event name:
event,typeoreventName. - Wallets: the
walletsarray if the body has one (CabalSpy ID format, withchainsandsource). Otherwise every Solana or EVM address found anywhere in the body in a field namedaddress,wallet_address,walletAddress,publicKeyorpublic_key. Other values, such as e-mail addresses, are ignored. - Profile:
display_nameandavatar_urlat the top level, if present.
Wallet removals from auth providers are not applied, because their payloads usually also carry the wallets the user keeps. To remove a wallet, send
{"event": "wallet.removed", "external_id": "...", "wallets": ["<address>"]} to the inbound URL, or call POST /v1/platform-users/wallets/delete.CabalSpy ID format
Use this from your own backend.event is one of user.created, user.updated, wallet.added and user.deleted. wallets takes the same items as Create or update a user.
Privy
Add the inbound URL as a webhook endpoint in the Privy dashboard and subscribe to the user created and wallet created events. The Privy user id (user.id, did:privy:...) becomes the external_id, and the wallet addresses in linked_accounts and wallet are added.
Privy user.created
Dynamic
Add the inbound URL as a webhook in the Dynamic dashboard and subscribe to user creation and wallet linking. The top-leveluserId becomes the external_id; every address or publicKey is added.
Dynamic user.created
Any other provider
Any JSON object works as long as it carries the user id in one of the fields above and the wallets in address-like fields.Generic
Example
Response
Every event that reaches your instance is answered with 200, also when it is rejected for a business reason, such as a missing user id, an invalid address or your plan’s user limit. Providers retry deliveries that are not answered with 2xx, so this keeps them from retrying events that would fail again. Checkaccepted.
boolean
Whether the event was processed.
string
created, updated, deleted, wallets_removed, ignored (a wallet removal from an auth provider), unknown_user (deletion or wallet removal for a user you do not have) or waiting_for_wallet.string
The user id read from the event.
string
created and updated: the user’s CabalSpy ID account id.
array
created and updated: the wallets and chains that are new.
array
wallets_removed: the addresses removed. wallets_not_removed:
{address, code} for each one kept, for example last_wallet or wallet_not_found.boolean
deleted: as in Delete a user.
object
When accepted is false: code and message, the same codes as Create or update a user.
Rate limit and retries
The inbound URL accepts 600 events per minute per instance, counted in fixed one-minute windows. Above that, events are not processed and answered with 429rate_limited. Your provider retries them on its own schedule (Privy and Dynamic retry failed deliveries with a backoff). Retries are safe: an event creates or updates the user, so a repeated event changes nothing.
When you send events from your own backend, retry 429 answers after the next full minute.
Other non-2xx answers:
The inbound URL does not count towards your API key’s request rate. To import an existing user base, use Import instead.

