Linger API
Read your brand's offers, codes and followers, and report redemptions from any shop or till. Every call is scoped to the brand that owns the API key, and followers are only people who consented to that brand.
Base URL https://api.uselinger.app/v1. Machine-readable description: OpenAPI 3.1, for Postman or a code generator. Questions: hello@uselinger.app.
Authentication
Send your key in the Authorization header: Bearer lk_live_…. Brand owners create keys in the brand portal under Settings → API keys, choose what each key can do, and can revoke a key at any time. A key is shown once, when it’s created. Keep it on your server, never in a web page or app.
offers:read: Read offerscodes:read: Read offer codesfollows:read: Read followers (only people who consented to your brand)redemptions:write: Report redemptions
Pages, limits and errors
- Lists return
data,has_moreandnext_cursor. Passnext_cursorasstarting_afterto get the next page. Up to 500 per page. - Up to 120 requests a minute per key; more get a 429.
- Errors are JSON:
{"error": "missing_scope", "detail": …}. Times are ISO 8601 with a time zone. Money is in pounds. - Followers are only people who consented to your brand and haven’t unsubscribed. Treat their emails as personal data.
List offers GET /offers
Your brand's offers at every show.
Scope: offers:read.
Query parameters
| Field | Type | Notes |
|---|---|---|
show_id | string (uuid) | Only this show. |
limit | integer | How many to return, 1 to 500. Default 100. |
starting_after | string (uuid) | The next_cursor from the previous page. |
Example
curl https://api.uselinger.app/v1/offers?limit=100 \
-H "Authorization: Bearer $LINGER_API_KEY"Responses
200One page, oldest id first.400The query or body didn't validate;detailnames the fields.401Missing, unknown or revoked API key.403The key doesn't have the scope this call needs.429More than 120 requests a minute with this key.
List codes GET /codes
Offer codes from your pools, with whether each was given out or used.
Scope: codes:read.
Query parameters
| Field | Type | Notes |
|---|---|---|
offer_id | string (uuid) | Only this offer. |
status | "unclaimed" | "claimed" | "redeemed" | unclaimed (still in the pool), claimed (given to a follower) or redeemed. |
limit | integer | How many to return, 1 to 500. Default 100. |
starting_after | string (uuid) | The next_cursor from the previous page. |
Example
curl https://api.uselinger.app/v1/codes?limit=100 \
-H "Authorization: Bearer $LINGER_API_KEY"Responses
200One page, oldest id first.400The query or body didn't validate;detailnames the fields.401Missing, unknown or revoked API key.403The key doesn't have the scope this call needs.429More than 120 requests a minute with this key.
List followers GET /follows
People who followed your brand and haven't withdrawn consent, with the code they were given. Anyone who unsubscribes drops out.
Scope: follows:read.
Query parameters
| Field | Type | Notes |
|---|---|---|
show_id | string (uuid) | Only this show. |
since | string (date-time) | Only follows from this time on (ISO 8601). |
limit | integer | How many to return, 1 to 500. Default 100. |
starting_after | string (uuid) | The next_cursor from the previous page. |
Example
curl https://api.uselinger.app/v1/follows?limit=100 \
-H "Authorization: Bearer $LINGER_API_KEY"Responses
200One page, oldest id first.400The query or body didn't validate;detailnames the fields.401Missing, unknown or revoked API key.403The key doesn't have the scope this call needs.429More than 120 requests a minute with this key.
Report a redemption POST /redemptions
Tell Linger a code was used, from your shop, till or agency. Safe to retry: the same order_id and code are recorded once.
Scope: redemptions:write.
Body
| Field | Type | Notes |
|---|---|---|
code | string | |
channel | "online" | "in_store" | Default "online". |
order_id | string | Your order reference. Repeating a call with the same order_id and code is safe. |
order_value | number | |
currency | string | |
redeemed_at | string (date-time) | Defaults to now. |
Example
curl -X POST https://api.uselinger.app/v1/redemptions \
-H "Authorization: Bearer $LINGER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"LNG-7F3KQ9","channel":"online","order_id":"1001","order_value":18.5}'Responses
200Already recorded; nothing changed.201Recorded.400The query or body didn't validate;detailnames the fields.401Missing, unknown or revoked API key.403The key doesn't have the scope this call needs.404No such code for your brand.409The code exists but was never given to a follower.429More than 120 requests a minute with this key.
Webhooks
Instead of asking the API for changes, add an endpoint in the brand portal (Settings → Webhooks) and Linger sends a POST the moment something happens. Each endpoint chooses its events:
follow.created: Someone follows you (with their email and consent).code.issued: A follower is given an offer code.code.redeemed: A code is used, from any shop, till or the staff page.consent.withdrawn: Someone unsubscribes: stop emailing them.
The body is JSON: id (unique per event; use it to ignore repeats), type, created_at, brand_id and data. In follow.created, data is a Follow (below); code.issued adds the code and offer; code.redeemed has the code, offer, channel, source and order value; consent.withdrawn has the email to stop contacting. Followers are only people who consented to your brand.
{
"id": "evt_3f1c0d9b7a2e4c55b0e1f2a3b4c5d6e7",
"type": "follow.created",
"created_at": "2027-05-14T10:21:07.312Z",
"brand_id": "8b0c…",
"data": {
"id": "c2d1…",
"email": "sam@example.com",
"show": { "id": "4a9e…", "name": "Foodies Bristol" },
"stand_code": "CKC",
"followed_at": "2027-05-14T10:21:07.120Z",
"consent": { "granted_at": "2027-05-14T10:21:07.101Z", "wording_version": "checkbox-2026-10-draft2" }
}
}Checking the signature
Every request carries Linger-Signature: t=…,v1=…: an HMAC-SHA256 of <t>.<raw body> with your endpoint’s signing secret (shown once, when you add the endpoint). Recompute it from the raw body, compare in constant time, and reject timestamps more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before JSON.parse.
export function fromLinger(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Retries
Answer with any 2xx within 5 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then marked failed; the portal shows each endpoint’s recent deliveries. Events can arrive more than once or out of order, so key on id. Use “Send test event” in the portal to try your endpoint with a webhook.test event.
Objects
Offer
What a stand gives people who follow your brand. The newest one that hasn't expired is live.
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
show | Show | |
title | string | |
terms | string | null | |
value_type | "fixed" | "percent" | "free_item" | "other" | |
value | number | null | Pounds for fixed, percent for percent, otherwise null. |
expires_at | string (date-time) | null | |
created_at | string (date-time) |
Show
A food or drink show your brand has a stand at.
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
name | string | |
starts_on | string (date) | |
ends_on | string (date) |
Code
A single-use offer code from the pool Linger generates for each offer.
| Field | Type | Notes |
|---|---|---|
code | string | |
offer_id | string (uuid) | |
status | "unclaimed" | "claimed" | "redeemed" | |
claimed_at | string (date-time) | null | When it was given to a follower. |
redeemed_at | string (date-time) | null |
Follow
Someone who followed your brand and hasn't withdrawn consent. People who unsubscribe drop out of this list.
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
email | string (email) | |
show | object | |
stand_code | string | |
followed_at | string (date-time) | |
consent | object | |
code | string | null | The offer code this follower was given, if any. |
RedemptionResult
| Field | Type | Notes |
|---|---|---|
status | "recorded" | "duplicate" | |
id | string (uuid) |