Concepts
Idempotency
Send an Idempotency-Key with a POST, and a retry after a lost connection is answered with the first answer instead of registering somebody twice.
Why
A connection can drop after AttendBeacon registered somebody but before your code heard so. Sent again, the same POST would issue a second ticket. With an Idempotency-Key it does not: the second request gets the first one's answer.
How
Make a key of your own for each request (a UUID is ideal) and send it with the POST. Keep it with the request until you have an answer, and send the same key with every retry.
curl -X POST "https://attendbeacon.com/api/v1/events/b3ac0000-0000-4000-8000-000000000001/registrations" \
-H "Authorization: Bearer $ATTENDBEACON_KEY" \
-H "Idempotency-Key: 6f1c2a9e-4b7d-4c0f-8e3a-5b9d1c7e0f42" \
-H "Content-Type: application/json" \
-d '{ "ticketTypeId": "b3ac0006-0000-4000-8000-000000000001", "firstName": "Anna", "lastName": "de Vries", "email": "anna.devries@example.com" }'const idempotencyKey = crypto.randomUUID(); // once per registration, kept for its retries
async function register(person) {
for (let attempt = 1; ; attempt++) {
try {
const response = await fetch(`${base}/events/${eventId}/registrations`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Idempotency-Key": idempotencyKey, "Content-Type": "application/json" },
body: JSON.stringify(person)
});
if (response.status < 500) return response; // an answer: the first, or the first again
} catch { /* the connection dropped: try again with the same key */ }
if (attempt === 4) throw new Error("AttendBeacon did not answer.");
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}var idempotencyKey = Guid.NewGuid().ToString(); // once per registration, kept for its retries
for (var attempt = 1; ; attempt++)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"events/{eventId}/registrations") { Content = JsonContent.Create(person) };
request.Headers.Add("Idempotency-Key", idempotencyKey);
try
{
using var response = await client.SendAsync(request);
if ((int)response.StatusCode < 500) return await response.Content.ReadFromJsonAsync<JsonElement>();
}
catch (HttpRequestException) { /* try again with the same key */ }
if (attempt == 4) throw new InvalidOperationException("AttendBeacon did not answer.");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}idempotency_key = str(uuid.uuid4()) # once per registration, kept for its retries
for attempt in range(1, 5):
try:
response = requests.post(
f"{base}/events/{event_id}/registrations",
headers={"Authorization": f"Bearer {key}", "Idempotency-Key": idempotency_key},
json=person,
timeout=30,
)
if response.status_code < 500:
break # an answer: the first, or the first again
except requests.ConnectionError:
pass # try again with the same key
time.sleep(2 ** attempt)$idempotencyKey = [guid]::NewGuid().ToString() # once per registration, kept for its retries
$headers["Idempotency-Key"] = $idempotencyKey
$body = $person | ConvertTo-Json
foreach ($attempt in 1..4) {
try {
$attendee = Invoke-RestMethod -Method Post -Uri "$base/events/$eventId/registrations" -Headers $headers -ContentType "application/json" -Body $body
break
} catch {
if ($_.Exception.Response -and [int]$_.Exception.Response.StatusCode -lt 500) { throw }
Start-Sleep -Seconds ([math]::Pow(2, $attempt))
}
}What is kept, and for how long
The first answer to a key (its status, Location and body) is kept for 24 hours and given again to any request with the same key, method, path and body. A replayed answer carries Idempotent-Replayed: true.
| Situation | Answer |
|---|---|
| A retry of a request that was answered | The first answer, with Idempotent-Replayed: true |
| A retry while the first is still being handled | 409 idempotencyKeyInUse, with Retry-After: 1 |
| The same key with another method, path or body | 422 idempotencyKeyReused: use a new key for a new request |
| A key longer than 255 characters, or with spaces | 400 invalidIdempotencyKey |
| The first request failed on AttendBeacon's side (5xx) | Nothing is kept, so the retry is handled afresh |
A key belongs to the API key that sent it: two integrations cannot collide. The answers kept may name a person; when that person deletes their data, the answers naming them are deleted too.
Which calls
Every POST takes an Idempotency-Key: registering, issuing tickets, cancelling, checking in, creating events, sessions, discount codes and webhooks. PATCH, PUT and DELETE are safe to repeat by their nature; PATCH takes If-Match instead, so a change made in between is not overwritten (Errors, preconditionFailed).