Webhook Event Reference
Full payload schemas for every webhook event type
All webhook events share the same envelope format:
{
"id": "evt_accounts.created:usr_abc123",
"type": "accounts.created",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": { ... }
}| Field | Type | Description |
|---|---|---|
id | string | Event id |
type | string | Event type (dot notation) |
createdAt | string | ISO 8601 timestamp |
data | object | Event-specific payload |
accounts.created
Fired when a new user is created in your app (first authentication).
{
"id": "evt_accounts.created:usr_abc123",
"type": "accounts.created",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"userId": "usr_abc123",
"appId": "app_xyz",
"createdAt": "2026-04-09T18:15:03.000Z",
"email": "user@example.com"
}
}| Field | Type | Description |
|---|---|---|
userId | string | The new user's ID |
appId | string | Your app's ID |
createdAt | string | When the user was created |
email | string | null | The user's email address when sufwan-local-test has one from magic link or OAuth, otherwise null |
accounts.linked
Fired when a user is linked to a partner external ID via the identity assertion flow.
{
"id": "evt_accounts.linked:usr_abc123:partner_42",
"type": "accounts.linked",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"userId": "usr_abc123",
"externalId": "partner_42",
"linkedAt": "2026-04-09T18:15:03.000Z"
}
}| Field | Type | Description |
|---|---|---|
userId | string | The user's sufwan-local-test ID |
externalId | string | Partner id (the HMAC externalId), not the venue externalIdentifier. See Ids |
linkedAt | string | When the link was created |
wallets.ready
Fired when a server-managed wallet is provisioned for a user (both EVM and Solana addresses are available).
{
"id": "evt_wallets.ready:usr_abc123",
"type": "wallets.ready",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"userId": "usr_abc123",
"evmAddress": "0x1234...abcd",
"svmAddress": "ABC123...xyz",
"createdAt": "2026-04-09T18:15:03.000Z"
}
}| Field | Type | Description |
|---|---|---|
userId | string | The user's ID |
evmAddress | string | EVM (Ethereum/Polygon) wallet address |
svmAddress | string | Solana wallet address |
createdAt | string | When the wallet was provisioned |
trades.placed
Fired when a user submits a trade. orderId in this event is the quote id (quote_…). Query the API for the breakdown.
{
"id": "evt_trades.placed:quote_abc123",
"type": "trades.placed",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"orderId": "quote_abc123",
"userId": "usr_abc123",
"side": "yes",
"timestamp": "2026-04-09T18:15:03.000Z",
"externalId": "partner-abc"
}
}| Field | Type | Description |
|---|---|---|
orderId | string | Quote id. This example is quote_…, not a venue order id |
userId | string | The user who placed the trade |
side | string | Outcome side: "yes" or "no" |
timestamp | string | When the order was placed |
externalId | string | null | Partner id (the HMAC externalId), not the venue externalIdentifier. See Ids. null when the trade did not carry one |
trades.filled
Fired when a trade order reaches a terminal fill state. One event per venue-level order fill.
{
"id": "evt_trades.filled:ord_abc123",
"type": "trades.filled",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"orderId": "ord_abc123",
"userId": "usr_abc123",
"status": "filled",
"timestamp": "2026-04-09T18:15:03.000Z",
"externalId": "partner-abc",
"clientOrderId": null
}
}| Field | Type | Description |
|---|---|---|
orderId | string | The individual order ID (query API for fill details) |
userId | string | The user who placed the trade |
status | string | "filled" or "partial_fill" |
timestamp | string | When the fill was recorded |
externalId | string | null | Partner id (the HMAC externalId), not the venue externalIdentifier. See Ids. null when the trade did not carry one |
clientOrderId | string | null | The clientOrderId a limit order was placed with. null on market orders and on limit orders placed without one. Limit orders carry no externalId |
trades.fee_reconciled
Fired once per self-custody trade when its app fee reaches a final state. Self-custody fees need
the user's second signature and an on-chain confirmation after the fill, so this lands a few
seconds after trades.filled. Wait for this event instead of polling for the fee. It is sent for
every self-custody trade, with status: "none" when no fee applied.
{
"id": "evt_trades.fee_reconciled:dag_abc123",
"type": "trades.fee_reconciled",
"createdAt": "2026-09-29T18:15:07.000Z",
"data": {
"dagRunId": "dag_abc123",
"quoteId": "qt_abc123",
"userId": "usr_abc123",
"orders": [{ "orderId": "ord_abc123", "externalId": "partner-abc" }],
"status": "confirmed",
"feeDueRaw": "4693",
"feeCollectedRaw": "4693",
"appShareRaw": "3754",
"platformShareRaw": "939",
"chainId": "137",
"tokenSymbol": "pUSD",
"tokenAddress": "0xpusd",
"appTxHash": "0xabc",
"platformTxHash": null,
"timestamp": "2026-09-29T18:15:07.000Z"
}
}| Field | Type | Description |
|---|---|---|
dagRunId | string | The trade execution run. The event id is keyed on it, so it arrives once per trade |
quoteId | string | The quote the trade executed |
userId | string | The user who placed the trade |
orders | array | Every order in the trade. externalId on each order is the partner id (the HMAC externalId), not the venue externalIdentifier. See Ids. The fee is charged once across all of them |
status | string | "confirmed", "under_collected", "failed", "skipped_zero", or "none" when no fee applied |
feeDueRaw | string | Fee owed, in token-native atomic units |
feeCollectedRaw | string | Fee actually collected, in token-native atomic units |
appShareRaw | string | Your share of the fee |
platformShareRaw | string | sufwan-local-test's share of the fee |
chainId | string | null | Chain the fee was paid on ("137" Polygon, "1337" HyperCore) |
tokenSymbol | string | null | Fee token symbol |
tokenAddress | string | null | Fee token address |
appTxHash | string | null | Transaction that paid your share |
platformTxHash | string | null | Transaction that paid sufwan-local-test's share, when paid separately |
timestamp | string | When the event was emitted |
deposits.confirmed
Fired when an on-chain token transfer into a user's managed wallet is confirmed. One event per (chain, transaction, user). Bridge legs and venue settlement payouts are excluded, so this only reflects money the user (or a third party) sent in.
{
"id": "evt_deposits.confirmed:137:0xabc123:usr_abc123",
"type": "deposits.confirmed",
"createdAt": "2026-09-24T18:15:03.000Z",
"data": {
"userId": "usr_abc123",
"chainId": 137,
"tokenSymbol": "USDC",
"tokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
"amountRaw": "100000000",
"txHash": "0xabc123",
"fromAddress": "0xsender",
"timestamp": "2026-09-24T18:15:03.000Z"
}
}| Field | Type | Description |
|---|---|---|
userId | string | The user whose wallet received the deposit |
chainId | number | EVM chain ID, or the internal ID for Solana |
tokenSymbol | string | Token symbol, e.g. "USDC" |
tokenAddress | string | Token contract address or mint |
amountRaw | string | Amount in the token's atomic units (6 decimals for USDC) |
txHash | string | On-chain transaction hash |
fromAddress | string | Sending address, or empty when unknown |
timestamp | string | When the deposit was confirmed |
markets.resolved
Fired once per app when a prediction market resolves. Contains a summary — use the pull endpoint for the full list of affected users.
{
"id": "evt_markets.resolved:vm_123:app_xyz",
"type": "markets.resolved",
"createdAt": "2026-04-09T18:15:03.000Z",
"data": {
"venueMarketId": "vm_123",
"externalIdentifier": "KXBTC-26APR09",
"status": "resolved",
"outcomes": [
{ "label": "Yes", "winner": true, "payoutPerShare": 1, "refundAtCost": false },
{ "label": "No", "winner": false, "payoutPerShare": 0, "refundAtCost": false }
],
"affectedUsersCount": 1842,
"detailsPath": "/apps/app_xyz/market-resolutions/vm_123/users"
}
}| Field | Type | Description |
|---|---|---|
venueMarketId | string | The resolved market ID |
externalIdentifier | string | Venue externalIdentifier, not the partner HMAC externalId. See Ids |
status | string | "resolved" or "closed" |
outcomes | array | Resolution outcome for each option: label, winner, externalIdentifier, payoutPerShare, refundAtCost, winnerSource |
outcomes[].payoutPerShare | number? | USDC paid per share, 0 to 1. Absent when the venue has not reported it. A voided or split market pays less than 1 on every side (e.g. 0.5), so check this before treating winner: true as a $1 win |
outcomes[].refundAtCost | boolean | The venue voided the market and refunded each holder their stake at entry price |
outcomes[].winnerSource | string? | "venue" (the venue's own result) or "matched" (inferred by sufwan-local-test because the venue named no winner yet; can still change). Absent when winner is absent or predates this field. See Inferred winners |
affectedUsersCount | number | Users in your app with open positions |
detailsPath | string | API path to paginate affected user IDs |
More than one winner: true means a split or voided market. Read payoutPerShare and
refundAtCost before showing the user an amount. See
Market resolution & voids.
Querying affected users
Use the SDK to paginate through affected users. Pass includeEmails: true to get email addresses
where available.
import { AggAdminClient } from "@agg-build/sdk/server";
const admin = new AggAdminClient({
baseUrl: "https://api.t.sufwankhalid.com",
apiKey: process.env.AGG_APP_API_KEY!,
});
// Paginate through all affected users
let cursor: string | undefined;
do {
const page = await admin.listMarketResolutionUsers(
appId,
event.data.venueMarketId,
{ limit: 100, cursor, includeEmails: true },
);
for (const user of page.data) {
console.log(user.userId, user.email);
}
cursor = page.nextCursor ?? undefined;
} while (cursor);See the List users affected by market resolution API reference for full details.
This endpoint returns the current set of users with non-zero positions, not an immutable
snapshot at resolution time. If positions are modified after resolution, the results may differ
from the affectedUsersCount in the webhook.