Appearance
Webhook Integration
Webhooks push round events to your server instead of requiring polling. Configure your webhook URL via PUT /api/operator/webhook.
Event Types
| Event | Description |
|---|---|
round.opened | Round is open for manifest submission |
round.sealing | No more manifests accepted; submit before deadline |
round.sealing_deadline | Sealing deadline passed; some operators may have missed |
round.escrowed | All operators deposited escrow |
round.escrow_deadline | Escrow deadline passed; exclusions applied |
round.drawn | Draw executed; winner determined |
round.settled | Settlement finalized; funds released |
round.closed | Round complete |
round.failed | Round failed (no manifests, no escrow, etc.) |
operator.won | Your operator sold the winning ticket |
operator.excluded | Your operator was excluded (e.g., missed escrow) |
operator.heartbeat_missed | Heartbeat timeout; send recovery heartbeat |
Setting Up a Webhook Receiver
1. Create an HTTP endpoint
typescript
import express from "express";
import { createHmac } from "node:crypto";
const app = express();
// Use raw body for signature verification
app.use("/webhook", express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const signature = req.headers["x-ultima-signature"] as string;
const rawBody = req.body.toString();
const expected = createHmac("sha256", process.env.ULTIMA_API_KEY!)
.update(rawBody)
.digest("hex");
if (signature !== expected) {
res.status(401).send("Invalid signature");
return;
}
const payload = JSON.parse(rawBody);
const { event, roundId, timestamp, data } = payload;
console.log(`Received ${event} for round ${roundId}`);
// Handle event...
res.status(200).send("OK");
});2. Register your URL
typescript
await client.setWebhookUrl("https://your-domain.com/webhook");3. Respond quickly
Return 200 within 30 seconds. The coordinator retries failed deliveries with exponential backoff (3 attempts).
Test Webhook Tool
Use the sandbox to verify your integration:
bash
curl -X POST https://sandbox.ultimalotto.com/api/operator/test-webhook \
-H "X-Ultima-Key: ulk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"eventType": "round.opened"}'Response:
json
{
"success": true,
"delivery": {
"event": "round.opened",
"status": "success",
"statusCode": 200
},
"hint": "Webhook delivered successfully! Check your server logs."
}Send all event types at once:
bash
curl -X POST https://sandbox.ultimalotto.com/api/operator/test-webhook/all \
-H "X-Ultima-Key: ulk_your_api_key"Event Payloads
round.opened
typescript
{
roundId: number;
ticketPriceUsdc: number;
operatorCount: number;
scheduledDrawTime: string; // ISO 8601
}round.sealing
typescript
{
roundId: number;
sealingDeadline: string; // ISO 8601
manifestsSubmitted: number;
}round.sealing_deadline
typescript
{
roundId: number;
operatorsSealed: number;
operatorsMissed: number;
}round.escrowed
typescript
{
roundId: number;
totalEscrowed: number;
operatorsDeposited: number;
}round.escrow_deadline
typescript
{
roundId: number;
operatorsExcluded: string[]; // operator IDs
}round.drawn
typescript
{
roundId: number;
drawSeed: string;
winnerIndex: number;
winningOperatorId: string;
totalTickets: number;
}round.settled
typescript
{
roundId: number;
winningOperatorId: string;
totalPotUsdc: number;
ultimaFeeUsdc: number;
operatorPoolUsdc: number;
winnerBonusUsdc: number;
winnerPayoutUsdc: number;
}round.closed
typescript
{
roundId: number;
totalTickets: number;
totalOperators: number;
}round.failed
typescript
{
roundId: number;
reason: string;
}operator.won
typescript
{
roundId: number;
operatorId: string;
winnerIndex: number;
bonusUsdc: number;
payoutUsdc: number;
}operator.excluded
typescript
{
roundId: number;
operatorId: string;
reason: string;
}operator.heartbeat_missed
typescript
{
operatorId: string;
lastHeartbeat: string; // ISO 8601
}