Webhooks
Blocklive tells your server when something happens at the events your API key reads: a ticket sold, an order refunded, a guest checked in. Use it to create the buyer's account in your app and send your own follow-up, without polling.
Set one up
On the key's Manage sheet in Blocklive's admin (whoever manages the key: the organizer's admins, or a developer they added):
- Tick the events to send. Each needs the matching attendee detail on the key: ticket sales and refunds need Purchase status, check-ins need Venue check-in.
- Enter your endpoint: an
https://address on port 443, on the public internet. - Save. Blocklive shows the key's signing secret once (
blwh_…). Keep it with the key, in your server's secret settings. A new address gets a new secret. - Blocklive checks the endpoint at once (below). Nothing is sent until it passes.
An organizer can limit which events developers' keys may send, and switch webhooks off for one event.
The check
Saving an address sends it a signed webhook.challenge:
{
"id": "whc_…",
"type": "webhook.challenge",
"challenge": "5c0f…",
"created_at": "2026-11-17T18:00:00.000Z"
}Answer 2xx with the challenge value in a Blocklive-Verification response header. Check again on the sheet repeats it.
What you receive
POST /blocklive HTTP/1.1
Content-Type: application/json
Blocklive-Id: whe_9a1c…
Blocklive-Timestamp: 1763402400
Blocklive-Signature: v1=5257a869e7…
{
"id": "whe_9a1c…",
"type": "purchase.completed",
"created_at": "2026-11-17T18:00:00.000Z",
"event_id": "3b1f…",
"attendee_id": "att_…",
"order_id": "pur_…"
}type | When |
|---|---|
purchase.completed | A ticket is sold or claimed and its order is confirmed. |
order.refunded | An order is refunded or charged back: one per refund, however many tickets it voids. |
ticket.checked_in | A ticket is checked in at the door (order_id is null). |
webhook.test | Send a test event on the sheet. About nobody. |
Webhooks follow what happened within a few seconds. created_at is when Blocklive recorded the event.
A webhook carries no email, name or other personal detail. Read the attendee with your key for those:
curl -H "Authorization: Bearer $BLOCKLIVE_API_KEY" \
https://blocklive.io/api/v1/events/$EVENT_ID/attendees/$ATTENDEE_IDIt returns what the key may read at that moment. IDs are your key's own, the same as its reads return.
Check the signature
Every webhook is signed with HMAC-SHA256 over <Blocklive-Id>.<Blocklive-Timestamp>.<raw body>. The HMAC key is the secret's 32 bytes: the hex after blwh_, decoded. Check it before you parse the body, refuse anything more than 5 minutes old, and remember IDs you have handled so a repeat is ignored.
import { createHmac, timingSafeEqual } from 'node:crypto'
// secrets: your current signing secret, plus the old one while you switch.
function fromBlocklive(rawBody, headers, secrets) {
const id = headers['blocklive-id']
const timestamp = headers['blocklive-timestamp']
const signatures = (headers['blocklive-signature'] ?? '').split(' ')
if (
!id ||
!timestamp ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300
)
return false
return secrets.some((secret) => {
const expected = createHmac(
'sha256',
Buffer.from(secret.slice(5), 'hex')
)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest()
return signatures.some((s) => {
const given = Buffer.from(s.replace(/^v1=/, ''), 'hex')
return (
given.length === expected.length &&
timingSafeEqual(given, expected)
)
})
})
}In Python:
import hashlib, hmac, time
def from_blocklive(raw_body: bytes, headers, secrets) -> bool:
event_id = headers.get('Blocklive-Id', '')
timestamp = headers.get('Blocklive-Timestamp', '')
signatures = headers.get('Blocklive-Signature', '').split(' ')
if not event_id or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
for secret in secrets:
key = bytes.fromhex(secret.removeprefix('blwh_'))
expected = hmac.new(key, f'{event_id}.{timestamp}.'.encode() + raw_body, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(s.removeprefix('v1='), expected) for s in signatures):
return True
return FalseNew signing secret on the sheet makes a new one. For 24 hours each webhook carries two signatures, one per secret, so your server keeps verifying while you switch over.
Answer, and retries
- Answer
2xxwithin 5 seconds; do the work afterwards. Blocklive doesn't read the body. - Anything else, or no answer, is retried after 1 s, 5 s, 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h and 6 h, for at most a day. A
Retry-Afterheader is honoured, up to an hour. Then the event is given up and the next one goes. - Events go to each key in order, one at a time: a webhook you keep failing holds up the ones after it until it is given up.
410 Goneswitches the webhook off until it is checked again.- Redirects are not followed.
The sheet shows how many were delivered and given up, the last problem, and the last 25 attempts: when, which type, which try and what your server answered. Send again replays everything from the last hour, day or 7 days, with the same IDs: if you drop repeated IDs, forget the ones you want processed again. Check again and Send a test event (once the address is checked) work up to 5 times a minute.
Catch up
The same events are kept for 7 days. Read them with the key, in order, to catch up after downtime, or instead of webhooks if you'd rather poll:
GET https://blocklive.io/api/v1/webhook-events?cursor=<next_cursor>{
"data": [{ "id": "whe_…", "type": "purchase.completed", "…": "…" }],
"next_cursor": "…",
"complete": true,
"as_of": "2026-11-17T18:00:05.000Z"
}Keep the last next_cursor and send it next time. Without a cursor you get the last 7 days. A cursor older than that gets 410 resync_required: read the attendees in full. It counts against the key's 120 requests a minute.