Webhooks

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": { ... }
}
FieldTypeDescription
idstringEvent id
typestringEvent type (dot notation)
createdAtstringISO 8601 timestamp
dataobjectEvent-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"
  }
}
FieldTypeDescription
userIdstringThe new user's ID
appIdstringYour app's ID
createdAtstringWhen the user was created
emailstring | nullThe 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"
  }
}
FieldTypeDescription
userIdstringThe user's sufwan-local-test ID
externalIdstringPartner id (the HMAC externalId), not the venue externalIdentifier. See Ids
linkedAtstringWhen 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"
  }
}
FieldTypeDescription
userIdstringThe user's ID
evmAddressstringEVM (Ethereum/Polygon) wallet address
svmAddressstringSolana wallet address
createdAtstringWhen 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"
  }
}
FieldTypeDescription
orderIdstringQuote id. This example is quote_…, not a venue order id
userIdstringThe user who placed the trade
sidestringOutcome side: "yes" or "no"
timestampstringWhen the order was placed
externalIdstring | nullPartner 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
  }
}
FieldTypeDescription
orderIdstringThe individual order ID (query API for fill details)
userIdstringThe user who placed the trade
statusstring"filled" or "partial_fill"
timestampstringWhen the fill was recorded
externalIdstring | nullPartner id (the HMAC externalId), not the venue externalIdentifier. See Ids. null when the trade did not carry one
clientOrderIdstring | nullThe 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"
  }
}
FieldTypeDescription
dagRunIdstringThe trade execution run. The event id is keyed on it, so it arrives once per trade
quoteIdstringThe quote the trade executed
userIdstringThe user who placed the trade
ordersarrayEvery 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
statusstring"confirmed", "under_collected", "failed", "skipped_zero", or "none" when no fee applied
feeDueRawstringFee owed, in token-native atomic units
feeCollectedRawstringFee actually collected, in token-native atomic units
appShareRawstringYour share of the fee
platformShareRawstringsufwan-local-test's share of the fee
chainIdstring | nullChain the fee was paid on ("137" Polygon, "1337" HyperCore)
tokenSymbolstring | nullFee token symbol
tokenAddressstring | nullFee token address
appTxHashstring | nullTransaction that paid your share
platformTxHashstring | nullTransaction that paid sufwan-local-test's share, when paid separately
timestampstringWhen 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"
  }
}
FieldTypeDescription
userIdstringThe user whose wallet received the deposit
chainIdnumberEVM chain ID, or the internal ID for Solana
tokenSymbolstringToken symbol, e.g. "USDC"
tokenAddressstringToken contract address or mint
amountRawstringAmount in the token's atomic units (6 decimals for USDC)
txHashstringOn-chain transaction hash
fromAddressstringSending address, or empty when unknown
timestampstringWhen 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"
  }
}
FieldTypeDescription
venueMarketIdstringThe resolved market ID
externalIdentifierstringVenue externalIdentifier, not the partner HMAC externalId. See Ids
statusstring"resolved" or "closed"
outcomesarrayResolution outcome for each option: label, winner, externalIdentifier, payoutPerShare, refundAtCost, winnerSource
outcomes[].payoutPerSharenumber?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[].refundAtCostbooleanThe venue voided the market and refunded each holder their stake at entry price
outcomes[].winnerSourcestring?"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
affectedUsersCountnumberUsers in your app with open positions
detailsPathstringAPI 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.

On this page