QR codes are everywhere because they solve one problem well: getting a string from paper or a screen into a phone without anyone typing it. The string is usually a URL, and the useful projects are the ones where that URL does something: checks a guest in, opens a menu, joins a wifi network.
You will build the ticketing half of an event check-in system. Each ticket gets a URL that is signed so it cannot be guessed or forged, each URL is turned into a QR code image, the image is saved to disk, and a page displays it. At the door, scanning the code hits your server, which verifies the signature and marks the guest as arrived. Along the way you will deal with the one thing about this API that catches people out: the image link it gives you does not last.
What the API returns
QR Code Generator (qrcodegenerator) is a POST endpoint that takes a JSON body. One parameter is required: value, the text to encode. It does not return image bytes or a base64 string. It returns metadata and a link:
| Field | What it is |
|---|---|
id | Identifier for this image, also used as the download filename |
format | Image format, png by default |
type | The kind of data encoded, text by default |
size, margin | Dimensions in pixels |
expires | Unix timestamp in milliseconds when the download link stops working |
downloadURL | A signed URL for the image file |
The reference also lists a correction field for the error-correction level. Treat it as optional; not every response includes it.
curl -X POST "https://api.apiverve.com/v1/qrcodegenerator" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value":"https://example.com/checkin?t=T-10482","margin":10}'Compare expires with the current time: the link is temporary, so save the image.
When we called it, expires was almost exactly 48 hours after the request. Do not hardcode that; read the field. The point is the shape of the contract: the API generates the image and hosts it briefly; keeping it is your job.
Most styling options are premium. On plans that include them you can set size (50 to 2048 pixels), format (png and jpeg, with svg and webp marked as premium vector formats), color and backgroundColor, dot and corner styles, a gradient, and a logo in the centre. The type parameter is premium too, and its advanced values wifi and vcard are called out specifically. On every plan you get value and margin. Premium parameters sent on a lower plan are ignored rather than rejected, so code written against a paid plan still runs; it just produces the default black-on-white PNG.
The catalog lists this API at 10 credits per call.
Step 1: generate and save, in Node and Python
Generation is one request. Saving is a second, plain GET to downloadURL. That second request goes to a storage host, not to APIVerve, so do not send your API key with it. The URL already carries its own signature, and your key has no business leaving your server for anywhere else.
import { writeFile } from 'node:fs/promises';
const BASE = 'https://api.apiverve.com/v1';
export async function createQr(value, options = {}) {
const res = await fetch(`${BASE}/qrcodegenerator`, {
method: 'POST',
headers: { 'x-api-key': process.env.APIVERVE_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ value, ...options }),
signal: AbortSignal.timeout(15000),
});
const body = await res.json().catch(() => null);
if (!res.ok || body?.status !== 'ok') {
const outOfCredits = res.status === 429 && res.headers.get('x-api-remaining-credits') === '0';
throw new Error(outOfCredits ? 'Out of credits' : body?.error || `HTTP ${res.status}`);
}
return body.data;
}
export async function saveQr(qr, path) {
if (Date.now() >= qr.expires) throw new Error(`Download link for ${qr.id} has expired`);
const res = await fetch(qr.downloadURL, { signal: AbortSignal.timeout(15000) }); // no API key here
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
await writeFile(path, Buffer.from(await res.arrayBuffer()));
return path;
}import os, time
import requests
BASE = "https://api.apiverve.com/v1"
def create_qr(value, **options):
res = requests.post(
f"{BASE}/qrcodegenerator",
json={"value": value, **options},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=15,
)
body = res.json()
if res.status_code != 200 or body.get("status") != "ok":
raise RuntimeError(body.get("error") or f"HTTP {res.status_code}")
return body["data"]
def save_qr(qr, path):
if time.time() * 1000 >= qr["expires"]:
raise RuntimeError(f"Download link for {qr['id']} has expired")
img = requests.get(qr["downloadURL"], timeout=15) # no API key here
img.raise_for_status()
with open(path, "wb") as f:
f.write(img.content)
return path
if __name__ == "__main__":
qr = create_qr("https://example.com/checkin?t=T-10482")
print(save_qr(qr, f"{qr['id']}.{qr['format']}"))In Python, json= serialises the body and sets the Content-Type header for you. In Node you set both yourself. Note the units in the expiry check: expires is milliseconds, which matches JavaScript's Date.now() but not Python's time.time(), which is seconds. Mixing those up gives you a check that is wrong by a factor of a thousand, and in this case one that would never fire.
You store the downloadURL in your database and use it as the img src on ticket pages. What goes wrong?
Answer the check above to continue.
Encoding wifi credentials
The other classic use is a code on the wall that joins guests to your wifi. Phones recognise a standard text format for this:
WIFI:T:WPA;S:<network name>;P:<password>;;Setting the API's type to wifi is premium, but the image a QR code holds is just a string. Building the string yourself and sending it as value with the default type produces a code that phone cameras offer to join. Escape the characters the format treats as special, which are backslash, semicolon, comma, colon and double quote, by putting a backslash in front of each.
import { createQr, saveQr } from './qr.js';
const esc = (s) => s.replace(/([\\;,:"])/g, '\\$1');
const value = `WIFI:T:WPA;S:${esc('Cafe Guest')};P:${esc('espresso;2026')};;`;
const qr = await createQr(value);
console.log(await saveQr(qr, 'qr/wifi.png'));Run it with node wifi.js. Print the result, test it with two different phones before you laminate it, and remember that anyone who photographs the poster has your password, which is exactly what a guest network is for.
Harden it
Generate once, keep forever. The ticket server checks for an existing file before calling the API. A ticket's code never changes, so there is no reason to pay for it twice. If you run on a platform without a persistent disk, write the bytes to object storage instead and serve them from there.
Save immediately. Download the image in the same request that generates it. A queue that generates today and downloads next week will find the link dead.
Embed without a file when you need to. For an email, you can read the saved PNG and inline it as a data URI: data:image/png;base64,${buffer.toString('base64')}. Some email clients block data URIs, so a hosted copy on your own domain is the more reliable choice.
Keep the secret out of the code. TICKET_SECRET signs every ticket you have ever issued. Rotate it and every printed ticket stops verifying, so treat it like a password and plan rotation before the event, not during it.
Limit what anonymous users can trigger. In this demo anyone can request /tickets/T-anything and spend a call. In a real system, tickets are issued when an order is paid, and the page only displays codes that already exist.
Making requests covers POST bodies and timeouts across the catalog.
Ideas to extend it
- Batch issue. Generate codes for an entire guest list ahead of time with a small concurrency limit, so the door never waits on a network call.
- Branded codes. On a plan with styling, add your
colorand alogo, then test that the code still scans from a distance. - Offline door app. Since the signature can be checked with only the secret, a door laptop can verify tickets without internet and sync arrivals afterwards.
- Read the code back. The QR Code Reader decodes an uploaded image, which makes a good automated test that each saved file contains the URL you meant.
So far each project has used one or two APIs side by side. The last one chains several, where each step's output is the next step's input. Continue to Chain APIs into a pipeline.