On this page

Documentation

Everything you need to integrate with the Pinbloom API: creating a key, generating QR codes, testing the whole flow, and receiving notifications via webhook.

For the full reference of every endpoint — every field, response schemas, and the option to try them directly from your browser — see the Swagger documentation.

1. API keys

An API key authenticates every request you make to the Pinbloom API. You can create two kinds of key, and every endpoint only accepts one of them:

  • Sandbox — meant for development. Every endpoint simulates the real process without saving anything and without consuming your plan's quota.
  • Live — generates real QR codes, each one counted against your plan's monthly quota.

A sandbox key only works on sandbox endpoints, a live key only works on the production one — using the wrong one gets you a 401.

Create a key

  1. Go to Settings → Developers.
  2. Open the Sandbox or Live tab, depending on which kind you need.
  3. Click New key, give it a name that reminds you where it's used (e.g. "Production server", "Local dev"), then Create.
  4. The key appears in the table, partially hidden — click the eye icon to reveal it, or the copy icon to copy it straight to your clipboard.

Your plan allows a limited number of keys of each kind — the count above the table ("x/y used") shows how many you have left; delete one you no longer use to free up a slot.

Authenticating requests

Send the key as a bearer token on every request:

Authorization: Bearer qrv_live_xxxxxxxxxxxxxxxx

Sandbox keys start with qrv_sandbox_, live keys with qrv_live_ — matching whichever endpoint you're calling.

2. Generating a QR code

POST /api/qr/generate — requires a live key. Persists the QR code, generates its image, and consumes 1 from your monthly quota. Every field in the body is optional:

Field Type What it's for
tag string Your own label, returned as-is. Max 255 characters.
metadata object Freeform. Returned in webhooks and in the QR validation response — never shown to the operator scanning it. Max 20KB serialized.
displayFields array of {label, value} Human-readable info shown to the operator when they scan the QR (e.g. seat number, order id) — not included in webhooks. Max 20 items, 20KB serialized.
starts_at date-time From when the QR code is valid: scanned before it, it answers not_started and stays unused. Omit it for a QR code valid right away. When expires_at is also given, it must come first.
expires_at date-time Must be in the future. Omit it for a QR code that never expires.
format qrcode | barcode What to generate. Omit it and you get a qrcode. See below for what barcode changes.
curl -X POST https://pinbloom.eu/api/qr/generate \
  -H "Authorization: Bearer qrv_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "tag": "promo-2026",
        "displayFields": [{ "label": "Seats", "value": "4" }],
        "expires_at": "2026-12-31T23:59:59Z"
      }'

Response (201):

{
  "uuid": "019f2b3c-4a11-7c3e-9c2a-1b8e6f0a2d4c",
  "tag": "promo-2026",
  "expiresAt": "2026-12-31T23:59:59+00:00",
  "format": "qrcode",
  "shortCode": null,
  "qrImage": "PHN2ZyB4bWxucz0i...",
  "imageUrl": "https://pinbloom.eu/c/k7Qm2...png"
}

qrImage is the QR code itself — an SVG image, base64-encoded. Decode it to get the actual image file.

imageUrl is the public address of a PNG of the code: put it in an <img> of the emails you send your customers, or on the order page. Mail clients do not show SVG, and Gmail blocks base64 images inside an email. The address needs no API key, never expires and keeps working after the code is used or revoked. Some mail clients, Outlook desktop for example, block remote images until the reader allows them: put the code in text, or a link to the ticket, in the email too.

No QR codes left on your plan? You get a 422 instead, and nothing is created or deducted.

Subscription ended? You get a 403: after the end date no new codes are generated, here or in the sandbox, even with extra credits. Codes already created stay valid for 6 months.

Barcodes

With "format": "barcode" the qrImage field holds a Code128 instead of a QR. It is still a base64-encoded SVG.

Nothing else changes. tag, metadata and displayFields behave the same way, webhooks arrive identical, and checking a code at the door works without you doing anything differently.

The only difference in the response is shortCode: the 14-digit number the barcode holds, in case you want it printed in plain figures under the image. For a QR it is null.

Several codes in one call

POST /api/qr/generate-batch — requires a live key. The body has one field, codes: an array where every item takes the same fields as /api/qr/generate. So every ticket of an order can have its own tag, displayFields and dates. The response (201) is { "codes": [ … ] }, in the order of the request, with the same fields as /api/qr/generate.

At most 100 codes per call. With 101 or more you get a 422 and no code is created. For a bigger order, split it into several calls.

All or nothing. If the quota does not cover every code, or one item is not valid, no code is created and you get a 422. The error of an item gives its position, for example codes.2.expires_at. An order never ends up with half of its tickets.

curl -X POST https://pinbloom.eu/api/qr/generate-batch \
  -H "Authorization: Bearer qrv_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
        "codes": [
          { "tag": "order-1042", "displayFields": [{ "label": "Seat", "value": "A1" }] },
          { "tag": "order-1042", "displayFields": [{ "label": "Seat", "value": "A2" }] }
        ]
      }'

Repeating a call without duplicates

When a call to /api/qr/generate or /api/qr/generate-batch times out, you do not know whether the codes were created. Send the Idempotency-Key header with a string of your own, for example the order id, and repeat the call safely:
  • same key, same request: you get the same response as the first time, with the Idempotent-Replayed: true header, and nothing is created;
  • same key, different request: 422;
  • same key while the first call is still running: 409. Try again shortly.
The key lasts 24 hours and belongs to the API key that sends it. From 1 to 255 characters. A call that fails, for example with no quota left, does not keep the key: fix it and repeat with the same one. Without the header every call creates new codes.

3. Managing codes

Three endpoints, with a live key, for the codes created through the API by the key's account, with any of its live keys. Collection codes are separate: these endpoints answer 404 for them, like for a code that does not exist.

Reading the state

GET /api/qr/{uuid} — reading always works, even after the subscription has ended. Response (200):

{
  "uuid": "019f2b3c-4a11-7c3e-9c2a-1b8e6f0a2d4c",
  "status": "unused",
  "tag": "promo-2026",
  "format": "qrcode",
  "shortCode": null,
  "usedAt": null,
  "revokedAt": null,
  "startsAt": null,
  "expiresAt": "2026-12-31T23:59:59+00:00"
}
status Meaning
unused Not used: a scan finds it valid.
used Already used. usedAt says when.
revoked Revoked. revokedAt says when.
expired Not used, but past expiresAt.
not_started Not used, but before startsAt.

Revoking and restoring

curl -X POST https://pinbloom.eu/api/qr/019f2b3c-4a11-7c3e-9c2a-1b8e6f0a2d4c/revoke \
  -H "Authorization: Bearer qrv_live_xxxxxxxxxxxxxxxx"

POST /api/qr/{uuid}/revoke revokes an unused code: from then on it fails validation. POST /api/qr/{uuid}/restore makes a revoked code unused again. Both answer (200) with the state of the code, like the reading.

Revoking a code already revoked, or restoring a code that is not revoked, changes nothing and answers 200: you can repeat the call safely. A used code can be neither revoked nor restored: 409.

Subscription ended? During the 6 months the codes stay valid you can still revoke. Restoring needs an active subscription: 403.

4. Testing without going live

Generating a test code

POST /api/sandbox/qr/generate requires a sandbox key and consumes no quota. It does not read the request body: it always creates a QR code, never a barcode, and answers only with uuid and qrImage. To try the fields, the format and the full response of the live endpoint, use POST /api/sandbox/qr/generate-batch, even with a single code.

curl -X POST https://pinbloom.eu/api/sandbox/qr/generate \
  -H "Authorization: Bearer qrv_sandbox_xxxxxxxxxxxxxxxx"

A QR code generated this way exists only in the response you get back. It cannot be "scanned" by the staff app.

POST /api/sandbox/qr/generate-batch does the same for several codes: same body, same limit of 100 codes, same fields in the response. It stores nothing, so imageUrl is null.

Simulating a scan

POST /api/sandbox/simulate — use this endpoint to test the data your server receives via webhook in sandbox:

Field What to send
qr_code Any string you like — echoed back as-is, doesn't need to be a real UUID.
simulate One of: valid, already_used, expired, revoked, not_started, not_found.
curl -X POST https://pinbloom.eu/api/sandbox/simulate \
  -H "Authorization: Bearer qrv_sandbox_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "qr_code": "abc123", "simulate": "valid" }'

Every outcome except not_found fires the matching event (section 5) to the webhooks registered on this sandbox key and subscribed to it. Delivery is identical to production: same signature, same five attempts. It is the only way to test a webhook integration before going live.

5. Webhooks

Available on Pro and Business plans. A webhook notifies your own server whenever a QR changes state (API-generated QR codes only, not events). A single API key can have several webhooks (e.g. one for staging, one for a local ngrok tunnel while you're building the integration), and each one is independently subscribed to just the events you care about.

Create a webhook

  1. Go to Settings → Developers and open the Sandbox or Live tab — whichever key you want to attach it to.
  2. In that key's row, click the Webhooks pill ("No webhooks" or a count) to open its panel.
  3. Click New webhook.
  4. Give it a name, a public https:// URL, and pick the events it should receive — all of them are selected by default.
  5. Click Create. Its signing secret is shown right there on the card: click the eye icon to reveal it, the copy icon to copy it.

Use that secret in your own project to verify a request genuinely came from Pinbloom (see "Verifying a request is genuine" below).

Event Fires when
qr.used The QR was valid and just got marked used.
qr.already_used Someone scanned a QR that was already used.
qr.revoked Someone scanned a QR you'd revoked.
qr.expired Someone scanned a QR past its expiry date.
qr.not_started Someone scanned a QR code before its validity started.

A webhook only receives the events it's subscribed to — pick them when you create it, or change them later from the same panel. Every event has the same payload shape:

{
  "event": "qr.used",
  "data": {
    "uuid": "019f...",
    "tag": "promo-2026",
    "metadata": { "campaign": "summer" },
    "usedAt": "2026-08-10T14:30:00+00:00",
    "revokedAt": null,
    "startsAt": null,
    "expiresAt": null,
    "scannedAt": "2026-08-10T14:30:00+00:00",
    "offline": false
  }
}

scannedAt is when the code was scanned. offline is true when the operator's app had no network: the notification goes out when the scan reaches the server, so it can come well after scannedAt.

Verifying a request is genuine

Every request carries an X-Pinbloom-Signature header: the HMAC-SHA256 of the exact request body, keyed with the webhook's own secret. Verify it before trusting anything in the payload:

$expected = hash_hmac('sha256', $requestBody, $yourStoredSecret);

if (! hash_equals($expected, $request->header('X-Pinbloom-Signature'))) {
    abort(401); // not authentic, discard
}
// verified — safe to act on the payload

The same HMAC-SHA256 scheme works in any language — compute it over the raw request body and compare with a constant-time check.

Requirements & delivery

  • The URL must be a public https:// address — no localhost, no private network. For local development, tunnel through something like ngrok.
  • If your endpoint doesn't respond in time or errors, delivery is retried automatically — up to 5 attempts over roughly 40 minutes, with increasing delay between them — before it's given up on.
  • Each webhook is delivered independently: a slow or dead endpoint only ever affects its own retries, never your other webhooks.
  • After the subscription ends, webhooks keep being sent for 6 months. Then the account is suspended: every scan answers "Service expired" and no webhook is sent.