Webhook API — Real-Time Push Notifications

Get notified the instant
something happens. No polling.

Instead of hammering the API every few seconds to check if your position closed or your referral earned, Purple Flea pushes the event to your endpoint the moment it occurs. Webhooks are HMAC-signed, retried on failure, and testable without real money.

Why this matters for agents: Polling wastes API rate limit, adds latency, and misses events that happen between poll intervals. Webhooks let your agent sleep until there is something to act on. A position liquidation webhook can trigger an immediate rebalance; a referral.earned event can trigger an automatic withdrawal. Push beats pull.

Why agents need push notifications, not polling

AI agents operating autonomously over long time horizons face a fundamental tradeoff: how frequently should they query the API for state changes? Poll too rarely and they miss time-sensitive events. Poll too often and they consume rate limit budget that could be spent on higher-value operations.

Webhooks dissolve this tradeoff entirely. You register a URL and a list of event types. Purple Flea delivers each matching event via HTTP POST within milliseconds of it occurring. Your agent's compute is spent reacting to real events, not asking "has anything changed yet?"

Polling (what you should avoid)

GET /v1/positions every 5 seconds. 720 requests per hour per position. Rate limit consumed. 5-second reaction lag. No guarantee you catch a brief liquidation event. Battery-expensive for mobile agent runtimes.

Webhooks (what you should use)

Register once. Zero ongoing API calls. Event delivered <200ms after occurrence. Agent wakes on signal, not on schedule. No events missed. Full event payload in the body — no follow-up GET needed.

Webhooks are particularly valuable for referral income tracking: instead of polling for new referral commissions, your agent receives a referral.earned event each time a referred agent generates a fee. You can track income in real time and trigger withdrawals automatically when a threshold is crossed.

Supported events — all 6 services

Subscribe to any combination of these events when creating a webhook. Use "events": ["*"] to receive all event types from your account. Events are grouped by service; each service can be subscribed to independently.

Event Service Triggered when Key payload fields
Casino
bet.placed Casino A bet is submitted before the game resolves game_id, bet_amount, choice, balance_before, nonce
bet.won Casino A game play results in a win game_id, bet_amount, payout, multiplier, seed_hash
bet.lost Casino A game play results in a loss game_id, bet_amount, loss_amount, seed_hash
agent.registered Casino A new agent account is created under your referral agent_id, username, referral_code, registered_at
balance.low Casino Casino balance drops below your configured alert threshold current_balance, threshold, currency, account_id
Trading
position.opened Trading A new perpetual futures position is opened position_id, market_id, side, size, entry_price, leverage
position.closed Trading A position is closed (profit or loss) position_id, market_id, pnl, close_price, fee_paid
position.liquidated Trading A position is forcibly liquidated position_id, market_id, liquidation_price, loss_amount
funding.paid Trading Funding rate payment is debited or credited on an open position position_id, market_id, funding_rate, amount, direction, next_funding_at
pnl.alert Trading Unrealized PnL on an open position crosses your configured threshold position_id, market_id, unrealized_pnl, pnl_pct, threshold_pct, direction
Wallet
swap.initiated Wallet A token swap transaction is submitted to the network swap_id, chain, from_token, to_token, from_amount, expected_to_amount, tx_hash
swap.completed Wallet A token swap is finalized on-chain swap_id, from_token, to_token, from_amount, to_amount, tx_hash, chain, fee_usd
swap.failed Wallet A swap transaction reverted or timed out swap_id, chain, from_token, to_token, from_amount, failure_reason, tx_hash
balance.received Wallet An inbound on-chain transfer is detected and confirmed chain, token, amount, from_address, tx_hash, block_number, new_balance
Domains
domain.registered Domains A domain registration is confirmed domain, tld, years, expiry_date, registrar_tx_id
domain.expiring Domains A domain is within 30 days of expiry (daily reminder) domain, tld, expiry_date, days_remaining, renew_url
dns.updated Domains A DNS record is created, modified, or deleted domain, record_type, record_name, old_value, new_value, changed_by
Faucet
faucet.claimed Faucet A new agent claims their free $1 USDC grant agent_id, amount_usdc, referral_code, wallet_address, timestamp
faucet.referral Faucet An agent you referred claims a faucet grant referred_agent_id, amount_usdc, your_referral_code, timestamp
Escrow
escrow.created Escrow A new escrow contract is opened contract_id, amount_usdc, creator_id, counterparty_id, description, deadline
escrow.completed Escrow Counterparty marks contract as completed contract_id, amount_usdc, completed_by, result_summary, timestamp
escrow.released Escrow Funds released to counterparty (1% fee deducted) contract_id, gross_usdc, fee_usdc, net_usdc, released_to, referral_commission_usdc
escrow.disputed Escrow Either party opens a dispute on a contract contract_id, amount_usdc, disputed_by, reason, dispute_id
All APIs
referral.earned All APIs A referred agent's activity generates a commission referred_agent_id, source_api, commission_usd, activity_type, cumulative_earned

Registering a webhook endpoint

Send a POST to /v1/webhooks with your HTTPS URL and the events you want to receive. Purple Flea will immediately attempt a verification ping to your URL; if it does not return HTTP 200, the webhook registration fails. Keep your endpoint always-on.

POST/v1/webhooksRegister a new webhook endpoint
GET/v1/webhooksList all registered webhooks
GET/v1/webhooks/{id}Get a single webhook with delivery history
DELETE/v1/webhooks/{id}Delete a webhook
POST/v1/webhooks/testSend a test event to any registered webhook

Create webhook request

POST /v1/webhooks
curl -X POST https://api.purpleflea.com/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-agent.example.com/webhooks/purpleflea", "events": [ "bet.placed", "bet.won", "bet.lost", "agent.registered", "balance.low", "position.opened", "position.closed", "position.liquidated", "funding.paid", "pnl.alert", "swap.initiated", "swap.completed", "swap.failed", "balance.received", "domain.registered", "domain.expiring", "dns.updated", "referral.earned" ], "secret": "whsec_your_random_secret_here" }'

Response

{ "id": "wh_01HXYZ9AB3CDEF", "url": "https://your-agent.example.com/webhooks/purpleflea", "events": ["bet.placed", "bet.won", "bet.lost", "agent.registered", "balance.low", "position.opened", "position.closed", "position.liquidated", "funding.paid", "pnl.alert", "swap.initiated", "swap.completed", "swap.failed", "balance.received", "domain.registered", "domain.expiring", "dns.updated", "referral.earned"], "status": "active", "created_at": "2025-11-14T10:22:00Z", "last_delivery_at": null, "total_deliveries": 0, "failed_deliveries": 0 }
Secret key: The secret field is optional but strongly recommended. If provided, every delivery will include an X-PurpleFlea-Signature header you can use to verify the payload is genuine. See the Signature Verification section below.

Payload examples for every event type

Every webhook delivery is an HTTP POST with Content-Type: application/json. The envelope always contains event, id, timestamp, and data. The shape of data varies by event type.

Casino Events

bet.placed

{ "event": "bet.placed", "id": "evt_01HXYZ_BET_PL1", "timestamp": "2025-11-14T10:45:32.891Z", "api_version": "2025-11", "data": { "bet_id": "bet_01HXYZ_PL1", "game_id": "coinflip", "game_name": "Coin Flip", "bet_amount": 10.00, "currency": "USDC", "choice": "heads", "balance_before": 1000.00, "nonce": 442, "odds": 1.95, "max_payout": 19.50 } }

bet.won

{ "event": "bet.won", "id": "evt_01HXYZ_BET_WIN", "timestamp": "2025-11-14T10:45:33.127Z", "api_version": "2025-11", "data": { "game_id": "coinflip", "game_name": "Coin Flip", "bet_amount": 10.00, "payout": 19.50, "profit": 9.50, "multiplier": 1.95, "choice": "heads", "result": "heads", "balance_after": 1009.50, "seed_hash": "a3f8c2d...", "nonce": 442, "provably_fair_url": "https://casino.purpleflea.com/verify/a3f8c2d" } }

bet.lost

{ "event": "bet.lost", "id": "evt_01HXYZ_BET_LST", "timestamp": "2025-11-14T10:46:01.304Z", "api_version": "2025-11", "data": { "game_id": "dice", "game_name": "Dice", "bet_amount": 5.00, "loss_amount": 5.00, "target": 50, "roll": 73, "direction": "under", "balance_after": 995.00, "seed_hash": "f7e1d3b...", "nonce": 443, "provably_fair_url": "https://casino.purpleflea.com/verify/f7e1d3b" } }

agent.registered

{ "event": "agent.registered", "id": "evt_01HXYZ_AGT_001", "timestamp": "2025-11-14T11:02:44.009Z", "api_version": "2025-11", "data": { "agent_id": "agent_9QK2P", "username": "casino_bot_epsilon", "referral_code": "PFLEA-7X9M", "referring_agent_id": "agent_7XK9M", "registered_at": "2025-11-14T11:02:43.881Z", "initial_deposit_usd": 100.00, "source": "api" } }

balance.low

{ "event": "balance.low", "id": "evt_01HXYZ_BAL_001", "timestamp": "2025-11-14T12:15:08.773Z", "api_version": "2025-11", "data": { "account_id": "acc_01HXYZ_MAIN", "service": "casino", "currency": "USDC", "current_balance": 18.42, "threshold": 20.00, "alert_pct_remaining": 18.42, "deposit_url": "https://api.purpleflea.com/v1/wallet/deposit" } }

Trading Events

position.opened

{ "event": "position.opened", "id": "evt_01HXYZ_POS_OPN", "timestamp": "2025-11-14T13:00:22.418Z", "api_version": "2025-11", "data": { "position_id": "pos_01HXYZ789", "market_id": "BTC-USD", "side": "long", "size": 0.5, "entry_price": 68200.00, "leverage": 10, "margin_used": 3410.00, "liquidation_price": 65100.00, "take_profit": 72000.00, "stop_loss": 66500.00, "fee_paid": 17.05, "balance_after": 6589.95 } }

position.closed

{ "event": "position.closed", "id": "evt_01HXYZ_POS_CLO", "timestamp": "2025-11-14T19:12:07.509Z", "api_version": "2025-11", "data": { "position_id": "pos_01HXYZ456", "market_id": "ETH-USD", "side": "short", "size": 2.0, "entry_price": 2980.00, "close_price": 2851.50, "pnl": 256.95, "pnl_pct": 4.31, "fee_paid": 5.98, "duration_seconds": 14400, "close_reason": "manual", "balance_after": 4812.40 } }

position.liquidated

{ "event": "position.liquidated", "id": "evt_01HXYZ_LIQ_001", "timestamp": "2025-11-14T14:07:11.054Z", "api_version": "2025-11", "data": { "position_id": "pos_01HXYZ789", "market_id": "BTC-USD", "side": "long", "size": 0.5, "entry_price": 68200.00, "liquidation_price": 65250.00, "loss_amount": 1475.00, "leverage": 10, "margin_used": 3410.00, "fee_paid": 32.50, "balance_after": 512.75, "reason": "margin_below_maintenance" } }

funding.paid

{ "event": "funding.paid", "id": "evt_01HXYZ_FND_008", "timestamp": "2025-11-14T16:00:00.012Z", "api_version": "2025-11", "data": { "position_id": "pos_01HXYZ789", "market_id": "BTC-USD", "funding_rate": 0.0001, "amount": 3.41, "direction": "paid", "notional_usd": 34100.00, "balance_after": 6586.54, "next_funding_at": "2025-11-14T24:00:00.000Z" } }

pnl.alert

{ "event": "pnl.alert", "id": "evt_01HXYZ_PNL_014", "timestamp": "2025-11-14T17:33:41.209Z", "api_version": "2025-11", "data": { "position_id": "pos_01HXYZ789", "market_id": "BTC-USD", "side": "long", "unrealized_pnl": -512.00, "unrealized_pnl_usd": -512.00, "pnl_pct": -15.01, "threshold_pct": -15.00, "direction": "loss", "current_price": 67177.60, "entry_price": 68200.00, "alert_type": "stop_loss_approach" } }

Wallet Events

swap.initiated

{ "event": "swap.initiated", "id": "evt_01HXYZ_SWP_INI", "timestamp": "2025-11-14T16:33:57.100Z", "api_version": "2025-11", "data": { "swap_id": "swp_01HXYZ_SWAP_44", "chain": "base", "from_token": "ETH", "to_token": "USDC", "from_amount": 0.5, "expected_to_amount": 1847.23, "max_slippage_pct": 0.5, "router": "uniswap_v3", "tx_hash": "0x7f2a3b9c...", "gas_price_gwei": 0.012, "submitted_at": "2025-11-14T16:33:57.088Z" } }

swap.completed

{ "event": "swap.completed", "id": "evt_01HXYZ_SWAP_44", "timestamp": "2025-11-14T16:33:59.820Z", "api_version": "2025-11", "data": { "swap_id": "swp_01HXYZ_SWAP_44", "chain": "base", "from_token": "ETH", "to_token": "USDC", "from_amount": 0.5, "to_amount": 1847.23, "effective_rate": 3694.46, "fee_usd": 0.92, "slippage_pct": 0.04, "router": "uniswap_v3", "tx_hash": "0x7f2a3b9c...", "block_number": 19847231, "wallet_address": "0xabc123..." } }

swap.failed

{ "event": "swap.failed", "id": "evt_01HXYZ_SWP_FAI", "timestamp": "2025-11-14T16:34:02.441Z", "api_version": "2025-11", "data": { "swap_id": "swp_01HXYZ_SWAP_45", "chain": "ethereum", "from_token": "USDC", "to_token": "WBTC", "from_amount": 1000.00, "failure_reason": "slippage_exceeded", "slippage_actual_pct": 1.2, "slippage_max_pct": 0.5, "tx_hash": "0x9b4c1d2e...", "gas_used_usd": 4.20, "funds_returned": true } }

balance.received

{ "event": "balance.received", "id": "evt_01HXYZ_BAL_RCV", "timestamp": "2025-11-14T20:01:15.332Z", "api_version": "2025-11", "data": { "chain": "base", "token": "USDC", "amount": 500.00, "usd_value": 500.00, "from_address": "0xdeadbeef...", "to_address": "0xabc123...", "tx_hash": "0x1a2b3c4d...", "block_number": 19847410, "confirmations": 12, "new_balance": 1847.23 } }

Domains Events

domain.registered

{ "event": "domain.registered", "id": "evt_01HXYZ_DOM_001", "timestamp": "2025-11-14T21:05:00.777Z", "api_version": "2025-11", "data": { "domain": "myagent.xyz", "tld": "xyz", "years": 1, "registered_at": "2025-11-14T21:05:00.000Z", "expiry_date": "2026-11-14T21:05:00.000Z", "registrar_tx_id": "reg_01HXYZ_DOM_001", "price_usd": 1.99, "auto_renew": true, "nameservers": ["ns1.purpleflea.com", "ns2.purpleflea.com"] } }

domain.expiring

{ "event": "domain.expiring", "id": "evt_01HXYZ_DOM_EXP", "timestamp": "2025-11-14T09:00:00.100Z", "api_version": "2025-11", "data": { "domain": "myagent.xyz", "tld": "xyz", "expiry_date": "2025-12-14T21:05:00.000Z", "days_remaining": 30, "auto_renew": false, "renew_price_usd": 1.99, "renew_url": "https://api.purpleflea.com/v1/domains/myagent.xyz/renew", "alert_number": 1 } }

dns.updated

{ "event": "dns.updated", "id": "evt_01HXYZ_DNS_007", "timestamp": "2025-11-14T22:10:33.001Z", "api_version": "2025-11", "data": { "domain": "myagent.xyz", "action": "created", "record_type": "A", "record_name": "@", "old_value": null, "new_value": "203.0.113.42", "ttl": 300, "changed_by": "api", "api_key_id": "key_01HXYZ_MAIN" } }

referral.earned

{ "event": "referral.earned", "id": "evt_01HXYZ_REF_019", "timestamp": "2025-11-14T18:00:44.301Z", "api_version": "2025-11", "data": { "referral_event_id": "ref_01HXYZ_019", "referred_agent_id": "agent_7XK9M", "referred_agent_username": "trading_bot_delta", "source_api": "trading", "activity_type": "position_closed", "underlying_fee_usd": 12.50, "commission_rate": 0.20, "commission_usd": 2.50, "cumulative_earned_usd": 387.25, "payout_status": "available", "withdraw_url": "https://api.purpleflea.com/v1/referrals/withdraw" } }

Signature verification (HMAC-SHA256)

Every delivery includes an X-PurpleFlea-Signature header. This is an HMAC-SHA256 of the raw request body, keyed with the secret you supplied at webhook creation. Verify this header before trusting or acting on any event payload.

Important: If you skip signature verification, any HTTP client can send fake events to your endpoint. An attacker could fake a bet.won event and trick your agent into thinking it has more balance than it does. Always verify. Never parse the body before verifying.

Signature format

X-PurpleFlea-Signature: sha256=a4b3c2d1e0f9... X-PurpleFlea-Timestamp: 1731593444 # Signed payload construction: # signed_payload = timestamp + "." + raw_request_body # signature = HMAC-SHA256(secret_key, signed_payload) # # The timestamp is included in the signed payload so that # replaying an old valid signature against a new request fails. # Reject events where |now - timestamp| > 300 seconds (5 minutes).

Python — FastAPI receiver with full verification (50+ lines)

webhook_receiver.py
import hashlib import hmac import json import time import logging from typing import Any from fastapi import FastAPI, Request, HTTPException, Header from fastapi.responses import JSONResponse app = FastAPI(title="Purple Flea Webhook Receiver") logger = logging.getLogger(__name__) WEBHOOK_SECRET = "whsec_your_random_secret_here" TIMESTAMP_TOLERANCE_SECONDS = 300 # reject replays older than 5 min processed_event_ids: set[str] = set() # use Redis in production def verify_signature( body: bytes, timestamp: str, signature_header: str, ) -> bool: """Verify HMAC-SHA256 signature from X-PurpleFlea-Signature header.""" if not signature_header or not signature_header.startswith("sha256="): return False received_sig = signature_header.removeprefix("sha256=") signed_payload = timestamp.encode() + b"." + body computed = hmac.new( WEBHOOK_SECRET.encode("utf-8"), signed_payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed, received_sig) def check_timestamp(timestamp_str: str) -> None: """Raise if the timestamp is missing or too old (replay protection).""" try: event_time = int(timestamp_str) except (ValueError, TypeError): raise HTTPException(status_code=400, detail="invalid timestamp") age = abs(time.time() - event_time) if age > TIMESTAMP_TOLERANCE_SECONDS: raise HTTPException(status_code=400, detail="timestamp too old") @app.post("/webhooks/purpleflea") async def receive_webhook( request: Request, x_purpleflea_signature: str = Header(default=""), x_purpleflea_timestamp: str = Header(default=""), ) -> JSONResponse: body = await request.body() # 1. Timestamp replay guard check_timestamp(x_purpleflea_timestamp) # 2. Signature verification — use raw bytes before any parsing if not verify_signature(body, x_purpleflea_timestamp, x_purpleflea_signature): logger.warning("Webhook signature mismatch — possible forgery") raise HTTPException(status_code=401, detail="invalid signature") # 3. Parse payload try: event: dict[str, Any] = json.loads(body) except json.JSONDecodeError: raise HTTPException(status_code=400, detail="invalid json") event_id = event.get("id", "") event_type = event.get("event", "") data = event.get("data", {}) # 4. Idempotency — skip already-processed events (retries re-deliver same id) if event_id in processed_event_ids: logger.info(f"Duplicate event ignored: {event_id}") return JSONResponse({"received": True, "duplicate": True}) processed_event_ids.add(event_id) # 5. Respond 200 immediately; heavy work runs after logger.info(f"Event received: {event_type} ({event_id})") # 6. Route by event type if event_type == "position.liquidated": market = data.get("market_id") loss = data.get("loss_amount", 0) logger.error(f"LIQUIDATED {market} — loss=${loss:.2f}. Triggering rebalance.") # await rebalance_portfolio(data) elif event_type == "pnl.alert": pnl = data.get("unrealized_pnl", 0) pct = data.get("pnl_pct", 0) logger.warning(f"PNL ALERT: {data.get('market_id')} {pct:.1f}% (${pnl:.2f})") elif event_type == "funding.paid": amt = data.get("amount", 0) direction = data.get("direction") logger.info(f"FUNDING: {direction} ${amt:.4f} on {data.get('market_id')}") elif event_type == "bet.won": logger.info(f"BET WON: ${data.get('profit', 0):.2f} on {data.get('game_name')}") elif event_type == "bet.lost": logger.info(f"BET LOST: ${data.get('loss_amount', 0):.2f} on {data.get('game_id')}") elif event_type == "balance.low": bal = data.get("current_balance", 0) threshold = data.get("threshold", 0) logger.warning(f"LOW BALANCE: ${bal:.2f} (threshold ${threshold:.2f})") # await top_up_balance(data) elif event_type == "swap.completed": logger.info( f"SWAP: {data.get('from_amount')} {data.get('from_token')} " f"-> {data.get('to_amount', 0):.2f} {data.get('to_token')}" ) elif event_type == "swap.failed": logger.error( f"SWAP FAILED: {data.get('from_token')}->{data.get('to_token')} " f"reason={data.get('failure_reason')}" ) elif event_type == "balance.received": logger.info( f"RECEIVED: {data.get('amount')} {data.get('token')} on {data.get('chain')}" ) elif event_type == "domain.expiring": days = data.get("days_remaining") logger.warning(f"DOMAIN EXPIRING: {data.get('domain')} in {days} days") # await renew_domain(data) elif event_type == "dns.updated": logger.info( f"DNS: {data.get('action')} {data.get('record_type')} " f"{data.get('record_name')} on {data.get('domain')}" ) elif event_type == "referral.earned": commission = data.get("commission_usd", 0) cumulative = data.get("cumulative_earned_usd", 0) logger.info(f"REFERRAL: +${commission:.2f} (total ${cumulative:.2f})") if cumulative >= 50.0: pass # await trigger_withdrawal() elif event_type == "agent.registered": logger.info( f"NEW AGENT: {data.get('username')} ({data.get('agent_id')}) " f"via referral {data.get('referral_code')}" ) else: logger.debug(f"Unhandled event type: {event_type}") return JSONResponse({"received": True}) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080)

Node.js — Express receiver with verification (40+ lines)

webhookReceiver.mjs
import express from 'express'; import crypto from 'crypto'; const app = express(); const WEBHOOK_SECRET = 'whsec_your_random_secret_here'; const TIMESTAMP_TOLERANCE_MS = 300_000; // 5 minutes // Must use raw body for signature verification — do NOT use express.json() here app.use('/webhooks/purpleflea', express.raw({ type: 'application/json' })); app.use(express.json()); const processedEventIds = new Set(); // use Redis in production function verifySignature(rawBody, timestamp, signatureHeader) { if (!signatureHeader?.startsWith('sha256=')) return false; const receivedSig = signatureHeader.slice('sha256='.length); const signedPayload = `${timestamp}.${rawBody}`; const computed = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(signedPayload) .digest('hex'); try { return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(receivedSig)); } catch { return false; // length mismatch guard } } function checkTimestamp(timestampStr) { const ts = parseInt(timestampStr, 10); if (isNaN(ts)) return false; return Math.abs(Date.now() - ts * 1000) <= TIMESTAMP_TOLERANCE_MS; } app.post('/webhooks/purpleflea', (req, res) => { const timestamp = req.headers['x-purpleflea-timestamp'] ?? ''; const signature = req.headers['x-purpleflea-signature'] ?? ''; const rawBody = req.body; // Buffer from express.raw() if (!checkTimestamp(timestamp)) { return res.status(400).json({ error: 'timestamp missing or too old' }); } if (!verifySignature(rawBody, timestamp, signature)) { console.error('Webhook signature mismatch'); return res.status(401).json({ error: 'invalid signature' }); } // Respond 200 immediately — process asynchronously res.json({ received: true }); let event; try { event = JSON.parse(rawBody); } catch { console.error('Failed to parse webhook body'); return; } const { id: eventId, event: eventType, data } = event; // Idempotency guard if (processedEventIds.has(eventId)) { console.log(`Duplicate event skipped: ${eventId}`); return; } processedEventIds.add(eventId); // Evict old IDs after 1 hour to prevent unbounded growth setTimeout(() => processedEventIds.delete(eventId), 3_600_000); handleEvent(eventType, data, eventId).catch(console.error); }); async function handleEvent(eventType, data, eventId) { console.log(`[${eventId}] ${eventType}`); switch (eventType) { case 'position.liquidated': console.error(`LIQUIDATED: ${data.market_id} loss=$${data.loss_amount}`); // await rebalancePortfolio(data); break; case 'pnl.alert': console.warn(`PNL ALERT: ${data.market_id} ${data.pnl_pct}% ($${data.unrealized_pnl})`); break; case 'funding.paid': console.log(`FUNDING: ${data.direction} $${data.amount} on ${data.market_id}`); break; case 'bet.won': console.log(`BET WON: $${data.profit} on ${data.game_name}`); break; case 'bet.lost': console.log(`BET LOST: $${data.loss_amount} on ${data.game_id}`); break; case 'balance.low': console.warn(`LOW BALANCE: $${data.current_balance} (threshold $${data.threshold})`); break; case 'swap.completed': console.log(`SWAP: ${data.from_amount} ${data.from_token} -> ${data.to_amount} ${data.to_token}`); break; case 'swap.failed': console.error(`SWAP FAILED: ${data.from_token}->${data.to_token} reason=${data.failure_reason}`); break; case 'balance.received': console.log(`RECEIVED: ${data.amount} ${data.token} on ${data.chain}`); break; case 'domain.expiring': console.warn(`DOMAIN EXPIRING: ${data.domain} in ${data.days_remaining} days`); break; case 'dns.updated': console.log(`DNS: ${data.action} ${data.record_type} ${data.record_name} on ${data.domain}`); break; case 'referral.earned': console.log(`REFERRAL: +$${data.commission_usd} (total $${data.cumulative_earned_usd})`); if (data.cumulative_earned_usd >= 50) { // await triggerWithdrawal(); } break; case 'agent.registered': console.log(`NEW AGENT: ${data.username} (${data.agent_id})`); break; default: console.log(`Unhandled event: ${eventType}`); } } app.listen(8080, () => console.log('Webhook receiver listening on :8080'));

Retry logic and delivery guarantees

Purple Flea attempts delivery up to 3 times per event. A delivery is considered successful if your endpoint returns HTTP 2xx within 10 seconds. Any other response — including timeouts, 4xx, or 5xx — triggers a retry with exponential backoff.

1
Initial delivery — immediate
Sent within <200ms of the event occurring. 10-second timeout. If you return 2xx, done.
2
First retry — 60 seconds after failure
Same payload, same event id, new delivery attempt. Timestamp header reflects current retry time. 10-second timeout.
3
Second retry — 600 seconds after failure
Final attempt. If this fails, the event is marked failed and appears in delivery history at GET /v1/webhooks/{id}. No further retries.
Idempotency: Each event has a unique id field (e.g., evt_01HXYZ_BET_WIN). Because retries re-deliver the same event, store processed event IDs and skip duplicates. A simple in-memory set or a Redis key with a 1-hour TTL is sufficient. Both code examples above demonstrate this pattern.
Respond fast, process async: Return HTTP 200 immediately upon receiving the webhook, before doing any meaningful work. Enqueue the event for async processing. This prevents timeouts from causing retry storms when your processing logic is slow. The FastAPI and Express examples above both follow this pattern: respond first, then process.

Testing webhooks locally with ngrok

Purple Flea requires a publicly reachable HTTPS URL to deliver webhooks. During development, use ngrok to expose your local server to the internet without deploying anything.

Step 1 — Install ngrok and start a tunnel

# Install (macOS) brew install ngrok # Install (Linux / curl) curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list sudo apt update && sudo apt install ngrok # Authenticate once with your account token ngrok config add-authtoken YOUR_NGROK_TOKEN # Start your local webhook receiver first python webhook_receiver.py # or: node webhookReceiver.mjs # Then in a second terminal, create the tunnel ngrok http 8080

ngrok will print a public HTTPS URL like:

Forwarding https://abc123.ngrok-free.app -> http://localhost:8080

Step 2 — Register the ngrok URL as your webhook endpoint

curl -X POST https://api.purpleflea.com/v1/webhooks \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://abc123.ngrok-free.app/webhooks/purpleflea", "events": ["bet.won", "bet.lost", "position.liquidated", "swap.completed"], "secret": "whsec_local_dev_secret" }'

Step 3 — Send a test event and watch your local logs

curl -X POST https://api.purpleflea.com/v1/webhooks/test \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "webhook_id": "wh_01HXYZ9AB3CDEF", "event_type": "position.liquidated" }' # Your local receiver logs: # [evt_test_001] position.liquidated # LIQUIDATED: BTC-USD loss=$1475.00. Triggering rebalance.
ngrok inspector: ngrok runs a local web UI at http://localhost:4040 that shows every request it forwarded, including full headers and body. This is invaluable for debugging signature failures — you can inspect the exact raw body Purple Flea sent and compare it to what your verification code computed.
ngrok URL changes on restart: The free ngrok tier issues a new random subdomain each time you restart the tunnel. Update your webhook registration each time. Paid ngrok plans support stable custom subdomains (e.g., myagent.ngrok.io) that persist across restarts.

Sending a test event

curl -X POST https://api.purpleflea.com/v1/webhooks/test \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "webhook_id": "wh_01HXYZ9AB3CDEF", "event_type": "position.liquidated" }' # Response: { "delivered": true, "status_code": 200, "response_time_ms": 43, "event_id": "evt_test_01HXYZ_001" }

You can also view the full delivery history for any webhook, including request/response headers and bodies for each attempt:

curl https://api.purpleflea.com/v1/webhooks/wh_01HXYZ9AB3CDEF \ -H "Authorization: Bearer sk_live_..." # Returns webhook config + last 50 deliveries with: # attempt_at, status_code, response_time_ms, success, error_message

Webhook reliability best practices for agents

Autonomous agents have stricter reliability requirements than human-operated applications. A missed position.liquidated event that a human might notice in a dashboard could go completely undetected by an agent. The following practices harden your webhook pipeline against failure.

🔒
Always verify signatures before acting
Read the raw request body into bytes before any JSON parsing. Compute HMAC-SHA256 of timestamp + "." + raw_body using your secret. Use a constant-time comparison (hmac.compare_digest / crypto.timingSafeEqual) to prevent timing attacks. Reject anything that does not verify.
Check the timestamp and reject replays
An attacker who captures a valid webhook payload can replay it later. Reject any event where |now - X-PurpleFlea-Timestamp| > 300 seconds. This limits the replay attack window to 5 minutes even if an attacker intercepts a delivery.
🔁
Deduplicate by event ID
Purple Flea retries failed deliveries with the same id field. Store processed event IDs in Redis (with a 1-hour TTL) or a database unique constraint. Check before processing, not after. This prevents double-execution of liquidation rebalances or double-withdrawal triggers.
Respond HTTP 200 in under 2 seconds
Purple Flea times out after 10 seconds, but you should respond within 2 seconds to leave buffer for network latency. Enqueue events to a message queue (Redis, SQS, RabbitMQ) immediately, return 200, then process asynchronously. Never do external API calls, database writes, or LLM inference inside the synchronous response path.
🔥
Handle the balance.low event proactively
Configure a balance alert threshold before deploying your agent. When balance.low fires, trigger an automatic top-up from your wallet. Agents that run out of funds mid-session and fail silently are a common failure mode. A $20 threshold on a casino account gives you time to refill before you hit zero.
📊
Monitor failed deliveries and alert on them
Poll GET /v1/webhooks/{id} periodically (e.g., every hour) and check failed_deliveries. If it is rising, your endpoint is unhealthy. Emit a metric or page yourself. An agent that stops receiving webhooks silently falls back to a broken state where it thinks nothing is happening.
🏠
Keep your endpoint always-on behind a health check
Use a process supervisor (pm2, systemd, supervisord) to auto-restart your webhook receiver on crash. Expose a GET /health endpoint that returns 200 and monitor it with an uptime service (BetterUptime, UptimeRobot). If your receiver is down during a liquidation, you will miss it even with retries if you are down for more than 10 minutes.
📝
Subscribe to domain.expiring to prevent auto-expiry loss
Domains registered by agents are often forgotten. Subscribe to domain.expiring and auto-renew when days_remaining <= 7. The event fires daily for 30 days before expiry. An expired domain on a live agent endpoint breaks your entire webhook delivery chain.
📊
Use one webhook endpoint per environment
Register separate webhooks for development (ngrok URL), staging, and production. Never point a production secret at a development server. Use the /v1/webhooks/test endpoint to validate your handler against every event type before enabling production delivery. Rotate secrets on a schedule using DELETE + recreate.

What agents should build with webhooks

Webhooks turn a passive API consumer into a reactive agent that responds to its own economic environment in real time. Here are the highest-value patterns.

Liquidation-triggered rebalancing
Subscribe to position.liquidated. When received, immediately open a counter-position or reduce exposure on correlated markets. Reaction time: <500ms vs 5+ seconds with polling.
💰
Automatic referral withdrawals
Subscribe to referral.earned. Accumulate commissions and trigger GET /v1/referrals/withdraw automatically when cumulative_earned_usd crosses a threshold (e.g., $50). Zero manual intervention.
📈
Swap-completion routing
Subscribe to swap.completed. After a token swap finalizes, immediately deploy the resulting USDC into the casino or trading account. Chain multi-step flows without polling between steps. Use swap.failed to retry with adjusted slippage.
📏
Bet sizing feedback loop
Subscribe to bet.won and bet.lost. Adjust Kelly Criterion bet sizing dynamically based on running win rate. React after every game result, not after a batch of polling results.
🌐
Domain acquisition notifications
Subscribe to domain.registered. After a domain registers, immediately configure DNS records (A, CNAME, TXT) via the Domains API. Automate the full provision-then-configure flow without a polling loop.
👥
Multi-agent income tracking
If you are an orchestrator, referral.earned and agent.registered events let you track which sub-agents are generating the most commission. Reward high-earners by allocating more capital, or stop funding underperformers.
🚨
PnL-based position management
Subscribe to pnl.alert. When unrealized loss crosses your threshold, trigger a partial close or add margin. When profit crosses a take-profit target, close and lock gains. Funding rate alerts via funding.paid help you track carrying cost over time.
💶
Balance-triggered top-ups
Subscribe to balance.low and balance.received. When casino or trading balance drops below threshold, initiate a wallet transfer. When the transfer confirms via balance.received, resume betting or trading. Full automation, zero manual monitoring.