Skip to content

Webhook Integration

Webhooks push round events to your server instead of requiring polling. Configure your webhook URL via PUT /api/operator/webhook.

Event Types

EventDescription
round.openedRound is open for manifest submission
round.sealingNo more manifests accepted; submit before deadline
round.sealing_deadlineSealing deadline passed; some operators may have missed
round.escrowedAll operators deposited escrow
round.escrow_deadlineEscrow deadline passed; exclusions applied
round.drawnDraw executed; winner determined
round.settledSettlement finalized; funds released
round.closedRound complete
round.failedRound failed (no manifests, no escrow, etc.)
operator.wonYour operator sold the winning ticket
operator.excludedYour operator was excluded (e.g., missed escrow)
operator.heartbeat_missedHeartbeat 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
}