Skip to main content
Instead of calling the API on every sign-up, you can give your auth provider or your backend a secret inbound URL. Every sign-up or wallet event posted to it creates or updates the platform user, exactly like Create or update a user. It works with the webhooks of Privy, Dynamic and any other provider that sends JSON, and with your own format. No API key is needed: the secret in the URL identifies your instance. In the dashboard the inbound URL is the Sign-up URL on the instance’s White-label tab: Switch on creates it, Create a new URL replaces it and Switch off disables it, like the calls below.
Keep the URL secret. Whoever knows it can create users in your instance. If it leaks, enable it again to get a new URL, the old one stops working at once.

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
The URL itself is never shown again. 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_id and data.id that is set. It becomes the external_id.
  • Event name: event, type or eventName.
  • Wallets: the wallets array if the body has one (CabalSpy ID format, with chains and source). Otherwise every Solana or EVM address found anywhere in the body in a field named address, wallet_address, walletAddress, publicKey or public_key. Other values, such as e-mail addresses, are ignored.
  • Profile: display_name and avatar_url at the top level, if present.
Then:
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-level userId 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. Check accepted.
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 429 rate_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.