> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prizerail.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Entries

In a campaign where **Prizerail draws from entries**, your backend sends entrants to Prizerail as they enter, each with their number of entries. At each cutoff Prizerail locks the period's list, commits its fingerprint, and draws the winners from it with the random value, each entrant in proportion to their entries. You don't pick winners.

```http theme={null}
POST /api/v1/campaigns/{slug}/entries
Authorization: Bearer pr_…
Content-Type: application/json
```

```json theme={null}
{
  "entrants": [
    "a1f9c2…",
    { "entrantId": "7be04d…", "entries": 3 },
    {
      "entrantId": "c55e18…",
      "displayName": "Ada",
      "avatarUrl": "https://…/ada.png"
    }
  ]
}
```

Each entrant is either:

* a string, the entrant's id, with one entry, or
* an object with `entrantId` plus an optional `entries`, `displayName` (up to 40 characters) and `avatarUrl` (https, up to 2,048 characters). The name and avatar show on the campaign page if they win.

Send up to 1,000 entrants per request, and as many requests as you like. A drawing takes up to 100,000 entrants.

## Which draw an entrant goes to

An entrant goes to the drawing whose cutoff is next when they arrive. After a cutoff, new entrants go to the following drawing. The response says which drawing they went to.

## Entries

`entries` is a whole number from 1 to 4,294,967,295, the entrant's number of entries. Leave it out and it's 1, so if you never send it, every entrant has the same chance. An entrant with 3 entries is three times as likely to win as one with 1. Nobody wins twice in one draw, unless the campaign allows [repeat winners](https://docs.prizerail.com/getting-started/faq.md#can-one-user-win-more-than-once-in-the-same-draw). The [proofs](https://docs.prizerail.com/verification/how-draws-work.md) call this number the entrant's weight.

## Sending again

Sending an entrant again before the cutoff sets their entries, name and avatar to what you sent, so a retry before the cutoff is safe and you can send a running total. After the cutoff, a retry goes to the next drawing instead. Sending them **without** `entries` sets it back to 1, so if you use it, always send it.

The response counts what happened:

```json theme={null}
{
  "drawing": {
    "id": "d7k2m9q4x1wz",
    "drawAt": "2026-10-17T01:00:00.000Z",
    "cutoffAt": "2026-10-17T00:45:00.000Z"
  },
  "added": 998,
  "updated": 1,
  "unchanged": 1,
  "entrantCount": 41203,
  "totalEntries": 58710
}
```

`added`, `updated` and `unchanged` count the entrants in this request. `entrantCount` and `totalEntries` are the drawing's totals so far.

## Adding instead of setting

If you'd rather add entries as users earn them than keep a running total, send `"mode": "increment"`. Each entrant is then one event, with the entries it earns and an `eventId`:

```json theme={null}
{
  "mode": "increment",
  "entrants": [
    { "entrantId": "7be04d…", "entries": 10, "eventId": "referral_8812" },
    { "entrantId": "7be04d…", "entries": 1, "eventId": "order_55120" }
  ]
}
```

Prizerail adds each event's entries to what the entrant has in the drawing taking entries, so 1,104 entries plus 10 makes 1,114. An entrant new to the drawing starts from 0.

**The `eventId` makes this safe to repeat.** Prizerail records each one for the campaign and ignores it if it comes again, so a retry after a lost response, or a queue that delivers the same event twice, never adds twice. That holds across cutoffs too: an event already counted in one drawing is ignored in the next. So:

* **Use the id of the thing that happened:** an order id, a referral id, a transaction hash. With none to hand, make a UUID when the event happens and store it with the event.
* **Set it once, and send the same one every time.** An id made fresh on each attempt, such as the time of sending, would add the entries again.
* **Never reuse an id for a different event.** The same `eventId` with another entrant or number of entries is a `409 event_conflict`, and nothing in that request is stored.

The response has `duplicates` too, the events Prizerail had already seen, in an earlier request or earlier in this one, and ignored. A name or avatar sent with an event replaces the one stored, and leaving them out, or sending them empty, keeps it. A repeated event changes nothing, its name and avatar included. To change a name with no new event, or to clear one, send the entrant in set mode with their total. An event that arrives after a cutoff goes to the next drawing, like any entry.

## Checking what Prizerail has

```http theme={null}
GET /api/v1/campaigns/{slug}/entries?drawing={id}&limit=1000&after={next}
```

It returns the drawing's `entrantCount`, `totalEntries` and its `entrants` in arrival order, the order the list locks in, each with its `entries`. Without `drawing`, it's the drawing taking entries now. Pass the response's `next` as `after` for the following page, until `next` is `null`. Each entrant shows its `position` in the list once it has locked.

## Code

Each example reads your API key from `PRIZERAIL_API_KEY` and the campaign's slug from `PRIZERAIL_CAMPAIGN`. The **SDK** tab uses the [`prizerail`](https://www.npmjs.com/package/prizerail) package (`npm install prizerail`), with batching, retries and the cutoff rule built in. The others use plain HTTP: `fetch` in TypeScript (Node 18 or later) and `requests` in Python, with a 30-second timeout on every request. Make entrant ids as in [Make entrant ids with an HMAC](https://docs.prizerail.com/getting-started/concepts.md#make-entrant-ids-with-an-hmac).

### Send a list

Splits the list into requests of 1,000 and retries a `429`, a `5xx`, a timeout or a network error with a growing, jittered wait, honoring `Retry-After`. Before the first batch it reads which drawing is taking entries and when its cutoff is, and every batch must land in that drawing.

A request is safe to repeat only **until that cutoff**, since sending an entrant again just sets their entries. After the cutoff, a repeat would go to the next drawing, and a request whose answer was lost may already have been stored. So the helper sends no batch and starts no retry within 30 seconds of the cutoff, its request timeout: it stops and says so, and you check where the entries went with `GET /entries?drawing=<id>`. Send well before the cutoff. Other errors, like a `409` for an ended campaign, stop the run.

```ts SDK theme={null}
import { Prizerail, PrizerailCutoffError, type Entrant } from "prizerail";

const campaign = new Prizerail({ apiKey: process.env.PRIZERAIL_API_KEY }).campaign(
process.env.PRIZERAIL_CAMPAIGN!
);

// Batches of 1,000, retries, and the cutoff rule above are built in.
export async function sendEntries(entrants: Entrant[]) {
try {
const res = await campaign.entries.send(entrants);
return {
drawingId: res.drawing.id,
sent: res.sent,
entrantCount: res.entrantCount,
totalEntries: res.totalEntries,
};
} catch (error) {
if (error instanceof PrizerailCutoffError) {
// error.progress.sent entries were confirmed. Check the rest with entries.list().
throw new Error(`${error.progress.sent} sent to ${error.progress.drawing.id}: ${error.detail}`);
}
throw error;
}
}

```

```ts TypeScript theme={null}
const API = "https://www.prizerail.com/api/v1";
const BATCH = 1000;
// Nothing is sent this close to the cutoff, the request timeout, so no
// request can arrive after it and land in the next drawing.
const MARGIN_MS = 30_000;

type Entrant = { entrantId: string; entries?: number };
type Target = { id: string; cutoffAt: number };

const auth = () => ({ Authorization: `Bearer ${process.env.PRIZERAIL_API_KEY}` });

/** Seconds from a Retry-After header, or null if it isn't a number. */
function retryAfter(res: Response | null): number | null {
  const seconds = Number(res?.headers.get("retry-after"));
  return Number.isFinite(seconds) && seconds > 0 ? seconds : null;
}

/** The drawing taking entries now, and its cutoff. */
async function targetDrawing(url: string): Promise<Target> {
  const res = await fetch(`${url}?limit=1`, {
    headers: auth(),
    signal: AbortSignal.timeout(30_000),
  });
  const body = await res.json().catch(() => null);
  if (!res.ok) {
    throw new Error(`${res.status} ${body?.error ?? ""}: ${body?.detail ?? res.statusText}`);
  }
  return { id: body.drawing.id, cutoffAt: Date.parse(body.drawing.cutoffAt) };
}

async function postWithRetry(url: string, body: unknown, target: Target) {
  for (let attempt = 0; ; attempt++) {
    let res: Response | null = null;
    try {
      res = await fetch(url, {
        method: "POST",
        headers: { ...auth(), "Content-Type": "application/json" },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30_000),
      });
    } catch {
      // A timeout or network error: retried below like a 5xx.
    }
    if (res?.ok) return res.json();
    const retryable = !res || res.status === 429 || res.status >= 500;
    if (!retryable || attempt === 6) {
      const detail = res ? await res.text() : "network error";
      throw new Error(`entries failed: ${res?.status ?? "-"} ${detail}`);
    }
    // Retry-After when given, otherwise about 2, 4, 8 … seconds, jittered.
    const wait = retryAfter(res) ?? 2 ** (attempt + 1) * (0.5 + Math.random());
    // A retry after the cutoff would go to the next drawing, and this
    // request may already be stored. Stop rather than enter people twice.
    if (Date.now() + wait * 1000 + MARGIN_MS >= target.cutoffAt) {
      throw new Error(
        `Drawing ${target.id}'s cutoff is too close to retry. Check which entries it has with GET /entries?drawing=${target.id}.`
      );
    }
    await new Promise(r => setTimeout(r, wait * 1000));
    // A timer can fire late, after a sleep or a busy loop, so check again.
    if (Date.now() + MARGIN_MS >= target.cutoffAt) {
      throw new Error(
        `Drawing ${target.id}'s cutoff passed while waiting to retry. Check which entries it has with GET /entries?drawing=${target.id}.`
      );
    }
  }
}

export async function sendEntries(entrants: Entrant[]) {
  const url = `${API}/campaigns/${process.env.PRIZERAIL_CAMPAIGN}/entries`;
  const target = await targetDrawing(url);
  // The drawing's own totals, from the last response. Counting added and
  // unchanged per batch would go wrong after a retry, since a batch stored
  // before its answer was lost comes back as unchanged.
  const totals = { drawingId: target.id, sent: 0, entrantCount: 0, totalEntries: 0 };
  for (let i = 0; i < entrants.length; i += BATCH) {
    if (Date.now() + MARGIN_MS >= target.cutoffAt) {
      throw new Error(
        `Drawing ${target.id}'s cutoff is too close: ${i} entrants sent, the rest not.`
      );
    }
    const batch = entrants.slice(i, i + BATCH);
    const res = await postWithRetry(url, { entrants: batch }, target);
    // Every batch must land in the drawing read before the first one.
    if (res.drawing.id !== target.id) {
      throw new Error(
        `The cutoff passed after ${i} entrants: the rest went to drawing ${res.drawing.id}, not ${target.id}.`
      );
    }
    totals.sent += batch.length;
    totals.entrantCount = res.entrantCount;
    totals.totalEntries = res.totalEntries;
  }
  return totals;
}
```

```python Python theme={null}
import os
import random
import time
from datetime import datetime
import requests

API = "https://www.prizerail.com/api/v1"
BATCH = 1000
# Nothing is sent this close to the cutoff, the request timeout, so no
# request can arrive after it and land in the next drawing.
MARGIN = 30

def auth() -> dict:
    return {"Authorization": f"Bearer {os.environ['PRIZERAIL_API_KEY']}"}

def retry_after(res) -> float | None:
    """Seconds from a Retry-After header, or None if it isn't a number."""
    try:
        seconds = float(res.headers.get("Retry-After", ""))
        return seconds if seconds > 0 else None
    except (AttributeError, ValueError):
        return None

def target_drawing(url: str) -> dict:
    """The drawing taking entries now, and its cutoff as a timestamp."""
    res = requests.get(url, headers=auth(), params={"limit": 1}, timeout=30)
    if not res.ok:
        raise RuntimeError(f"{res.status_code}: {res.text}")
    drawing = res.json()["drawing"]
    cutoff = datetime.fromisoformat(drawing["cutoffAt"].replace("Z", "+00:00"))
    return {"id": drawing["id"], "cutoff": cutoff.timestamp()}

def post_with_retry(url: str, body: dict, target: dict) -> dict:
    for attempt in range(7):
        try:
            res = requests.post(url, headers=auth(), json=body, timeout=30)
        except requests.RequestException:
            res = None  # A timeout or network error: retried like a 5xx.
        if res is not None and res.ok:
            return res.json()
        retryable = res is None or res.status_code == 429 or res.status_code >= 500
        if not retryable or attempt == 6:
            detail = f"{res.status_code} {res.text}" if res is not None else "network error"
            raise RuntimeError(f"entries failed: {detail}")
        # Retry-After when given, otherwise about 2, 4, 8 … seconds, jittered.
        wait = retry_after(res) or 2 ** (attempt + 1) * (0.5 + random.random())
        # A retry after the cutoff would go to the next drawing, and this
        # request may already be stored. Stop rather than enter people twice.
        if time.time() + wait + MARGIN >= target["cutoff"]:
            raise RuntimeError(
                f"Drawing {target['id']}'s cutoff is too close to retry. "
                f"Check which entries it has with GET /entries?drawing={target['id']}."
            )
        time.sleep(wait)
        # Sleep can overrun, for example on a suspended machine, so check again.
        if time.time() + MARGIN >= target["cutoff"]:
            raise RuntimeError(
                f"Drawing {target['id']}'s cutoff passed while waiting to retry. "
                f"Check which entries it has with GET /entries?drawing={target['id']}."
            )

def send_entries(entrants: list[dict]) -> dict:
    url = f"{API}/campaigns/{os.environ['PRIZERAIL_CAMPAIGN']}/entries"
    target = target_drawing(url)
    # The drawing's own totals, from the last response. Counting added and
    # unchanged per batch would go wrong after a retry, since a batch stored
    # before its answer was lost comes back as unchanged.
    totals = {"drawingId": target["id"], "sent": 0, "entrantCount": 0, "totalEntries": 0}
    for i in range(0, len(entrants), BATCH):
        if time.time() + MARGIN >= target["cutoff"]:
            raise RuntimeError(
                f"Drawing {target['id']}'s cutoff is too close: {i} entrants sent, the rest not."
            )
        batch = entrants[i : i + BATCH]
        res = post_with_retry(url, {"entrants": batch}, target)
        # Every batch must land in the drawing read before the first one.
        if res["drawing"]["id"] != target["id"]:
            raise RuntimeError(
                f"The cutoff passed after {i} entrants: the rest went to drawing "
                f"{res['drawing']['id']}, not {target['id']}."
            )
        totals["sent"] += len(batch)
        totals["entrantCount"] = res["entrantCount"]
        totals["totalEntries"] = res["totalEntries"]
    return totals
```

### Add entries to an entrant

Adds entries when a user earns them, with the event's id, as in [Adding instead of setting](#adding-instead-of-setting). Every retry sends the same `eventId`, so it's always safe to retry.

```ts SDK theme={null}
import { Prizerail } from "prizerail";

const campaign = new Prizerail({ apiKey: process.env.PRIZERAIL_API_KEY }).campaign(
process.env.PRIZERAIL_CAMPAIGN!
);

// Call it when the event happens, with an id stored with that event.
export async function addEntries(entrantId: string, entries: number, eventId: string) {
// { added, updated, duplicates, sent, batches, drawings }
return campaign.entries.increment([{ entrantId, entries, eventId }]);
}

```

```bash curl theme={null}
curl -s -X POST "https://www.prizerail.com/api/v1/campaigns/$PRIZERAIL_CAMPAIGN/entries" \
  -H "Authorization: Bearer $PRIZERAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "increment", "entrants": [{"entrantId": "9f2c41e7a0", "entries": 10, "eventId": "referral_8812"}]}'
```

```ts TypeScript theme={null}
const API = "https://www.prizerail.com/api/v1";

// Call it when the event happens, with an id stored with that event.
export async function addEntries(
  entrantId: string,
  entries: number,
  eventId: string,
) {
  const body = JSON.stringify({
    mode: "increment",
    entrants: [{ entrantId, entries, eventId }],
  });
  for (let attempt = 0; ; attempt++) {
    let res: Response | null = null;
    try {
      res = await fetch(
        `${API}/campaigns/${process.env.PRIZERAIL_CAMPAIGN}/entries`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.PRIZERAIL_API_KEY}`,
            "Content-Type": "application/json",
          },
          body,
          signal: AbortSignal.timeout(30_000),
        },
      );
    } catch {
      // A timeout or network error. Retrying is safe: the eventId is the same.
    }
    // { drawing, added, updated, unchanged, duplicates, entrantCount, totalEntries }
    if (res?.ok) return res.json();
    const retryable = !res || res.status === 429 || res.status >= 500;
    if (!retryable || attempt === 4) {
      const detail = res ? await res.text() : "network error";
      throw new Error(`entries failed: ${res?.status ?? "-"} ${detail}`);
    }
    await new Promise((r) =>
      setTimeout(r, 2 ** attempt * 1000 * (0.5 + Math.random())),
    );
  }
}
```

```python Python theme={null}
import os
import random
import time
import requests

API = "https://www.prizerail.com/api/v1"

def add_entries(entrant_id: str, entries: int, event_id: str) -> dict:
    """Call it when the event happens, with an id stored with that event."""
    body = {"mode": "increment", "entrants": [{"entrantId": entrant_id, "entries": entries, "eventId": event_id}]}
    for attempt in range(5):
        try:
            res = requests.post(
                f"{API}/campaigns/{os.environ['PRIZERAIL_CAMPAIGN']}/entries",
                headers={"Authorization": f"Bearer {os.environ['PRIZERAIL_API_KEY']}"},
                json=body,
                timeout=30,
            )
        except requests.RequestException:
            res = None  # A timeout or network error. Retrying is safe: same eventId.
        if res is not None and res.ok:
            return res.json()  # drawing, added, updated, unchanged, duplicates, entrantCount, totalEntries
        retryable = res is None or res.status_code == 429 or res.status_code >= 500
        if not retryable or attempt == 4:
            detail = f"{res.status_code} {res.text}" if res is not None else "network error"
            raise RuntimeError(f"entries failed: {detail}")
        time.sleep(2 ** attempt * (0.5 + random.random()))
```

### Set one entrant's entries

Sends a single entrant with their total, for example when one user's count changes between your regular sends. It sets the entrant's entries to the number you give, so send their new total, not the change. To add instead, see [Add entries to an entrant](#add-entries-to-an-entrant).

```ts SDK theme={null}
import { Prizerail } from "prizerail";

const campaign = new Prizerail({ apiKey: process.env.PRIZERAIL_API_KEY }).campaign(
process.env.PRIZERAIL_CAMPAIGN!
);

export async function sendEntry(entrantId: string, entries = 1) {
  // { drawing, added, updated, unchanged, entrantCount, totalEntries, sent, batches }
  return campaign.entries.send([{ entrantId, entries }]);
}
```

```bash curl theme={null}
curl -s -X POST "https://www.prizerail.com/api/v1/campaigns/$PRIZERAIL_CAMPAIGN/entries" \
  -H "Authorization: Bearer $PRIZERAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"entrants": [{"entrantId": "9f2c41e7a0", "entries": 2}]}'
```

```ts TypeScript theme={null}
const API = "https://www.prizerail.com/api/v1";

export async function sendEntry(entrantId: string, entries = 1) {
  const res = await fetch(
    `${API}/campaigns/${process.env.PRIZERAIL_CAMPAIGN}/entries`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.PRIZERAIL_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ entrants: [{ entrantId, entries }] }),
      signal: AbortSignal.timeout(30_000),
    },
  );
  const body = await res.json().catch(() => null);
  if (!res.ok) {
    throw new Error(
      `${res.status} ${body?.error ?? ""}: ${body?.detail ?? res.statusText}`,
    );
  }
  return body; // { drawing, added, updated, unchanged, entrantCount, totalEntries }
}
```

```python Python theme={null}
import os
import requests

API = "https://www.prizerail.com/api/v1"

def send_entry(entrant_id: str, entries: int = 1) -> dict:
    res = requests.post(
        f"{API}/campaigns/{os.environ['PRIZERAIL_CAMPAIGN']}/entries",
        headers={"Authorization": f"Bearer {os.environ['PRIZERAIL_API_KEY']}"},
        json={"entrants": [{"entrantId": entrant_id, "entries": entries}]},
        timeout=30,
    )
    if not res.ok:
        raise RuntimeError(f"{res.status_code}: {res.text}")
    return res.json()  # drawing, added, updated, unchanged, entrantCount, totalEntries
```

### Check what Prizerail has

Pages through the drawing taking entries now, to reconcile its entrants and total entries with your own records. Every page after the first asks for that same drawing, so a cutoff mid-way can't mix two lists. Entrants sent while it pages can change the totals, so it also returns `consistent`: whether the entrants it read add up to `entrantCount` and `totalEntries`. If that's `false`, page again when you've stopped sending.

```ts SDK theme={null}
import { Prizerail } from "prizerail";

const campaign = new Prizerail({ apiKey: process.env.PRIZERAIL_API_KEY }).campaign(
process.env.PRIZERAIL_CAMPAIGN!
);

export async function currentEntries() {
  // Pin the drawing taking entries now, so a cutoff mid-way can't mix two lists.
  const { id } = await campaign.entries.openDrawing();
  const entrants = [];
  for await (const entrant of campaign.entries.all({ drawing: id })) entrants.push(entrant);
  const { entrantCount, totalEntries } = await campaign.entries.list({ drawing: id, limit: 1 });
  const entries = entrants.reduce((sum, e) => sum + e.entries, 0);
  const consistent = entrants.length === entrantCount && entries === totalEntries;
  return { drawingId: id, entrantCount, totalEntries, consistent, entrants };
}
```

```bash curl theme={null}
curl -s "https://www.prizerail.com/api/v1/campaigns/$PRIZERAIL_CAMPAIGN/entries?limit=1000" \
  -H "Authorization: Bearer $PRIZERAIL_API_KEY"
# Repeat with &drawing=<drawing.id>&after=<next> until next is null.
```

```ts TypeScript theme={null}
const API = "https://www.prizerail.com/api/v1";

export async function currentEntries() {
  const entrants: { entrantId: string; entries: number }[] = [];
  let drawingId: string | null = null;
  let after: string | null = null;
  let totals = { entrantCount: 0, totalEntries: 0 };
  do {
    const query = new URLSearchParams({ limit: "1000" });
    if (drawingId) query.set("drawing", drawingId);
    if (after) query.set("after", after);
    const res = await fetch(
      `${API}/campaigns/${process.env.PRIZERAIL_CAMPAIGN}/entries?${query}`,
      {
        headers: { Authorization: `Bearer ${process.env.PRIZERAIL_API_KEY}` },
        signal: AbortSignal.timeout(30_000),
      },
    );
    const body = await res.json().catch(() => null);
    if (!res.ok) {
      throw new Error(
        `${res.status} ${body?.error ?? ""}: ${body?.detail ?? res.statusText}`,
      );
    }
    drawingId ??= body.drawing.id;
    entrants.push(...body.entrants);
    totals = {
      entrantCount: body.entrantCount,
      totalEntries: body.totalEntries,
    };
    after = body.next;
  } while (after);
  // Entrants sent while this pages can move the totals, so say whether the
  // list read adds up to them. If not, page again once sending stops.
  const entries = entrants.reduce((sum, e) => sum + e.entries, 0);
  const consistent =
    entrants.length === totals.entrantCount && entries === totals.totalEntries;
  return { drawingId, ...totals, consistent, entrants };
}
```

```python Python theme={null}
import os
import requests

API = "https://www.prizerail.com/api/v1"

def current_entries() -> dict:
    entrants, drawing_id, after, body = [], None, None, {}
    while True:
        params = {"limit": 1000}
        if drawing_id:
            params["drawing"] = drawing_id
        if after:
            params["after"] = after
        res = requests.get(
            f"{API}/campaigns/{os.environ['PRIZERAIL_CAMPAIGN']}/entries",
            headers={"Authorization": f"Bearer {os.environ['PRIZERAIL_API_KEY']}"},
            params=params,
            timeout=30,
        )
        if not res.ok:
            raise RuntimeError(f"{res.status_code}: {res.text}")
        body = res.json()
        drawing_id = drawing_id or body["drawing"]["id"]
        entrants += body["entrants"]
        after = body["next"]
        if not after:
            break
    # Entrants sent while this pages can move the totals, so say whether the
    # list read adds up to them. If not, page again once sending stops.
    entries = sum(e["entries"] for e in entrants)
    consistent = len(entrants) == body["entrantCount"] and entries == body["totalEntries"]
    return {
        "drawingId": drawing_id,
        "entrantCount": body["entrantCount"],
        "totalEntries": body["totalEntries"],
        "consistent": consistent,
        "entrants": entrants,
    }
```

## Entrant ids

> [!WARNING]
>
> Every locked list is published, hashed, so anyone can check a draw. Use an entrant id that can't be guessed or linked back to a person: an HMAC (A keyed hash. The same user id and key always give the same entrant id, and without the key nobody can work back to the user or guess the next one.) of your user id with a secret key, or a random id you store. A plain hash of a sequential or public user id can be reversed by trying ids. Keep the key fixed for the life of the campaign, and never use emails or usernames.
>
> **The same person must have the same entrant id in every draw.** The [guarantee draw](https://docs.prizerail.com/api/guarantees.md) counts each person once by their id across a whole period. Never rotate the key mid-campaign, and never use an id made per draw, per session or per device.

## Errors

| Status | Code | When |
| - | - | - |
| 400 | `bad_request` | Not JSON, no entrants or more than 1,000, a bad id, number of entries, name or avatar, the same entrant twice in one request in set mode, both the current and the older body shape at once, or in increment mode a missing `eventId` or a total that would pass 4,294,967,295 entries. An `eventId` repeated with the same entrant and entries isn't an error: it's counted in `duplicates`. |
| 404 | `not_found` | No such campaign under your account. |
| 409 | `campaign_ended` | The campaign has ended. |
| 409 | `no_open_drawing` | No drawing is taking entries. `detail` says why: the campaign is a draft, it's paused, or the schedule hasn't opened the next one yet. |
| 409 | `not_taking_entries` | The campaign stopped taking entries while the request was in flight. |
| 409 | `too_many_entrants` | This request would take the drawing past 100,000 entrants. Nothing in it was stored. |
| 409 | `event_conflict` | Increment mode: an `eventId` already used, in an earlier request or earlier in this one, for a different entrant or number of entries. Nothing in the request was stored. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.