Skip to content
API key

Guide

Going to production

What to put in place between a script that works on your laptop and an integration other people depend on.

Keep the key secret

An API key can create, change and delete everything its scopes cover. Treat it like a password.

  • Store it in an environment variable or a secrets manager, never in source code. Every example in these docs reads QRBOLD_API_KEY from the environment.
  • Keep it out of version control. Add .env to .gitignore, and turn on secret scanning with the pattern qrb_live_.
  • Keep it out of logs, error reports and support tickets. Log the key’s name or id, never the Authorization header.
  • Send it only in a header, over HTTPS. A key in a URL ends up in browser history and server logs, and the API ignores it there anyway.
  • Use one key per system. When something leaks or is retired, you revoke one key and nothing else stops.
  • Rotate on a schedule and whenever someone with access leaves. See rotate and revoke.

Browser or server

The correct shape has your own server in the middle:

  1. Your front end

    Browser or app. No key.

  2. Your server

    Holds the key. Checks who is asking.

  3. QRBold API

    Called only by your server

  • Your front end calls your server, authenticated however your product authenticates users.
  • Your server decides whether that user may do the thing, then calls QRBold.
  • Your server returns only what the front end needs, such as a card’s url or a QR image.

The API does not offer publishable or client-side keys, and it does not allow cross-origin requests from other websites’ pages. The API explorer in these docs is the one deliberate exception: it runs in your own browser with a key you paste for that session.

Least privilege

Give each key only what its job needs.

Suggested scopes by job
The integrationScopes
Creates cards from HR datacards:write, cards:read, templates:read
Shows scan counts on a dashboardanalytics:read
Prints QR codes for existing cardscards:read
Manages campaign QR codesqr:write, qr:read
An outside vendor creating cards for one groupA campaign-scoped key

Give keys an expiry when the work has an end date. Review the list of keys in the dashboard now and then, and revoke the ones whose Last used is long ago.

Timeouts and retries

  • Set a timeout on every request. Thirty seconds is generous. Without one, a stalled connection stalls your job.
  • Retry network errors, timeouts, 429 and 5xx. Wait longer each time (for example 1, 2 and 4 seconds) and give up after a few attempts.
  • Do not retry other 4xx responses. The request is wrong or not allowed and will fail the same way. Fix it first.
  • Only retry a write that carries an idempotency key. A timed-out POST may have succeeded. Without a key, a retry can create a duplicate.
qrbold.mjs
// One request with a timeout, safe retries and rate-limit handling.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function qrbold(method, path, { body, idempotencyKey } = {}) {
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch('https://api.qrbold.com/api/public/v1' + path, {
        method,
        headers: {
          Authorization: `Bearer ${process.env.QRBOLD_API_KEY}`,
          ...(body ? { 'Content-Type': 'application/json' } : {}),
          ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(30_000),
      });
    } catch (networkError) {
      // A write without an idempotency key may have succeeded: do not repeat it blindly.
      const safe = method === 'GET' || Boolean(idempotencyKey);
      if (!safe || attempt >= 4) throw networkError;
      await sleep(500 * 2 ** attempt);
      continue;
    }

    if (res.status === 429 && attempt < 6) {
      await sleep((Number(res.headers.get('RateLimit-Reset') ?? 60) + 1) * 1000);
      continue;
    }
    if (res.status >= 500 && attempt < 4 && (method === 'GET' || idempotencyKey)) {
      await sleep(500 * 2 ** attempt);
      continue;
    }
    return res; // 2xx, or a 4xx that retrying will not fix
  }
}

Idempotency

Send an Idempotency-Key header on every POST and PATCH. If the same request arrives again with the same key within 24 hours, QRBold returns the original response, marked with the header Idempotent-Replay: true, and does nothing twice.

  • Use a UUID. Generate it once per operation, before the first attempt.
  • Store it with the thing you are creating, so a retry after a crash can reuse it.
  • Never reuse a key for a different request. That answers 409 idempotency_key_reused.
  • Failures on QRBold’s side (5xx) are not remembered, so retrying after one really does try again.

An idempotency key protects retries for a day. It does not stop the same person being created again next month. For that, keep your own record of who has a card, or use a campaign endpoint. Full rules are under idempotency.

Pacing under the rate limit

Each key has its own per-minute allowance: 120 requests on the plans that include the REST API. Going over it answers 429 until the minute ends.

  • Pace bulk jobs deliberately rather than sending as fast as you can and reacting to 429s.
  • Read the RateLimit-Remaining and RateLimit-Reset response headers to see where you stand.
  • Do not run several workers on one key without sharing a limiter between them. Or give each worker its own key.
  • Cache what does not change. A card’s url and its QR image are stable; fetch them once and store them.

Validate before you send

  • Card creation ignores unknown keys. A misspelled field name is dropped silently. Build the fields object from a fixed list of names, and compare the response with what you sent.
  • Check lengths and formats yourself where you can: 200 characters for short text, slugs of letters, numbers and hyphens, destinations starting with http:// or https://.
  • Check image links resolve and are public. QRBold stores the link you send; a broken link gives a card with a missing photo, not an error.
  • Read the template’s policy once and do not send locked or hidden fields.
  • Remember that a card update replaces. Read, modify, then write the whole fields object.

Logging and monitoring

For every request, log:

  • The time, method and path.
  • The HTTP status, and on failure error.code and error.message.
  • The idempotency key you sent.
  • The id of the card or code affected.
  • The Request-Id response header, where one is present.

Never log the key. Alert on a rise in 401s (a key has expired or been revoked), 403s (the plan changed or its limit was reached) and 5xx.

QRBold does not send webhooks, so there is no push signal for “a card was changed in the dashboard”. If you mirror cards in your own database, re-read them on a schedule.

Handling public URLs

  • Store id and url together for every card and code. The id is for the API, the url is for people.
  • Use url exactly as returned. Do not rebuild it from the short code; the domain can be a custom one.
  • Never show an API endpoint to an end user, and never encode one in a QR code.
  • A QR image is yours to host. Download it once, store it, and serve it from your own storage. The image endpoint needs a key and cannot be used as an image address.
  • Deleting breaks links. A deleted or paused card’s QR code stops opening it. Check before you delete anything that may be in print.

Before printing at scale

Print is the one step you cannot undo with an API call. There is no test environment, so the rehearsal happens in production with a small real batch.

  • Create two or three real cards with the exact code and template you will use for the full run.
  • Confirm each has status: "published" and paused: false.
  • Download their QR images in the final format and size.
  • Print a proof on the real material and scan it with at least two different phones.
  • Confirm the plan has enough card seats or QR codes for the whole run, with margin.
  • Run the whole job, then spot-check a sample of the results before sending files to the printer.
  • Keep a file mapping each person to their card id and url. You will need it for reprints.

For campaign endpoints, use the dry-run endpoint to check payloads without creating anything.

Launch checklist

  • The key is in a secrets manager, not in code or a repository.
  • All QRBold calls are made from a server.
  • The key has only the scopes it needs, and its own name.
  • Every request has a timeout.
  • Writes send an Idempotency-Key that is stored and reused on retry.
  • Retries cover network errors, 429 and 5xx only, with a growing wait.
  • 429 is detected by status code, because its body is not the standard error shape.
  • Bulk jobs are paced under the rate limit.
  • Ids and url values are stored; paused is checked.
  • Errors are logged with their code, without the key.
  • Someone owns key rotation, and knows where the key is used.
  • A real QR code has been printed and scanned.