REST API & Docker

stable

Scan, generate and parse barcodes from any language over HTTP with qrgen serve or the Docker image: endpoints, curl examples, errors, CORS, framework mounting and client code.

Edit on GitHub

QRGen includes a small HTTP server that exposes scanning, generation and parsing as a REST API. Run it with qrgen serve, with Docker, or mount it inside your own Node.js app. Any language that can send an HTTP request can then decode barcodes in images and create barcode images.

The server processes images on the machine it runs on and stores nothing.

Run the server

Docker

docker run --rm -p 8080:8080 ghcr.io/rockyljewell/qr-gen:latest
curl http://localhost:8080/health
{ "ok": true, "name": "qrgen", "version": "1.0.0", "docs": "https://rockyljewell.github.io/QR-GEN/docs/rest-api/" }

The image is built from the repository’s root Dockerfile and runs qrgen serve on port 8080. To build it yourself:

git clone https://github.com/Rockyljewell/QR-GEN.git
cd QR-GEN
docker build -t qrgen .
docker run --rm -p 8080:8080 qrgen

With Docker Compose:

# compose.yaml
services:
  qrgen:
    image: ghcr.io/rockyljewell/qr-gen:latest
    ports:
      - "8080:8080"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 3

Other services in the same Compose project reach it at http://qrgen:8080.

Node.js

npx qrgen-sdk serve --port 8080

Note: Until the first npm release is published, install the latest build with npm install https://rockyljewell.github.io/QR-GEN/downloads/qrgen-sdk.tgz and run npx qrgen serve.

--host sets the interface (default 0.0.0.0), and the PORT and HOST environment variables are used when the flags are absent. See CLI.

Warning: The API has no authentication or rate limiting. Keep it on a private network, or put it behind a reverse proxy or API gateway that adds them, before exposing it to the internet.

Endpoints

Method Path Request Response
GET /health (also /) { ok, name, version, docs }
GET /v1/symbologies { read: [...], write: [...], details: [{ id, name, kind, aliases }] }
POST /v1/scan Raw image bytes, multipart/form-data, or JSON with base64 { barcodes: [...] }
GET /v1/generate Options in the query string Image bytes
POST /v1/generate Options as a JSON body Image bytes
POST /v1/parse JSON { "data": "..." } or raw text ParsedContent

Scan

POST /v1/scan accepts the image in three ways. The server reads PNG, JPEG, GIF and BMP.

Raw bytes. Send the file as the body with any image content type:

curl --data-binary @photo.jpg -H 'content-type: image/jpeg' http://localhost:8080/v1/scan

Multipart. The first file part is used (or a field named image or file). An optional symbologies field limits the search:

curl -F image=@photo.jpg http://localhost:8080/v1/scan
curl -F image=@photo.jpg -F symbologies=qr,ean13 http://localhost:8080/v1/scan

JSON. image is base64 or a data URL. Build the body in a file, because base64 images quickly exceed the shell’s argument length limit:

printf '{"image":"%s","symbologies":["qr"]}' "$(base64 < photo.jpg | tr -d '\n')" > body.json
curl -H 'content-type: application/json' --data-binary @body.json http://localhost:8080/v1/scan

Options can go in the query string for every body type, in the JSON body, or in multipart fields. Body options override query options:

Option Default Description
symbologies all Comma-separated in the query (?symbologies=qr,ean13), an array or string in JSON.
tryHarder true false is faster on clean, high-contrast images.
maxResults 255 Maximum codes to return. Integer from 1 to 255.
parse true Add a parsed field (from parseContent()) to each result.

Booleans in the query string accept true/false, 1/0, yes/no and on/off.

Response:

{
  "barcodes": [
    {
      "data": "https://example.com",
      "symbology": "qr",
      "symbologyName": "QR Code",
      "rawBytes": "aHR0cHM6Ly9leGFtcGxlLmNvbQ==",
      "contentType": "text",
      "isGS1": false,
      "location": {
        "topLeft": { "x": 16, "y": 16 },
        "topRight": { "x": 116, "y": 16 },
        "bottomRight": { "x": 116, "y": 116 },
        "bottomLeft": { "x": 16, "y": 116 }
      },
      "frameSize": { "width": 132, "height": 132 },
      "orientation": 0,
      "ecLevel": "Q",
      "symbologyIdentifier": "]Q1",
      "timestamp": 1790467200000,
      "parsed": { "type": "url", "url": "https://example.com" }
    }
  ]
}

Each entry is a Barcode. An image without codes returns 200 with { "barcodes": [] }.

Generate

GET /v1/generate takes options in the query string, which makes it usable directly as an image URL:

curl -o qr.svg "http://localhost:8080/v1/generate?data=https%3A%2F%2Fexample.com&symbology=qr"
curl -o qr.png "http://localhost:8080/v1/generate?data=hello&format=png&scale=8&ecLevel=H"
<img src="http://localhost:8080/v1/generate?data=5901234123457&symbology=ean13&hrt=1" alt="EAN-13 barcode" />

POST /v1/generate takes the same options as JSON, which is easier for long or binary-looking data:

curl -o label.png -H 'content-type: application/json' \
  -d '{"data": "(01)09501101530003(17)250101(10)ABC123", "symbology": "code128", "gs1": true, "hrt": true, "format": "png", "scale": 3}' \
  http://localhost:8080/v1/generate
Option Default Description
data required The text to encode (text and value are accepted as aliases).
symbology qr Any writable symbology.
format svg svg (image/svg+xml), png (image/png) or txt (text/plain text rendering).
scale 4 Module size in pixels.
ecLevel encoder default Error correction (ec is accepted too): L, M, Q, H for QR.
gs1 false Encode as GS1; data in HRI form (01)....
hrt false Human-readable text under linear barcodes.
margin true Quiet zone.
foreground, background #000000, #ffffff Colors for SVG and PNG output (background may be transparent).
rotate 0 0, 90, 180 or 270.

Remember to URL-encode query values: # in colors becomes %23, ( and ) in GS1 data are safe.

Parse

curl -H 'content-type: application/json' -d '{"data": "WIFI:T:WPA;S:Office;P:secret;;"}' http://localhost:8080/v1/parse
{ "type": "wifi", "ssid": "Office", "password": "secret", "security": "WPA", "hidden": false }

A body that isn’t JSON is parsed as raw text, so curl --data-binary 'tel:+15551234567' http://localhost:8080/v1/parse works too. The response is a ParsedContent object.

Errors

Errors return a 4xx or 5xx status with a JSON body:

{ "error": { "code": "bad-request", "message": "Cannot encode as EAN-13: Invalid character at position 1 in input (digits and \"+\" or space only)" } }
Status code Cause
400 bad-request Invalid JSON, missing data or image, an empty image, an unknown symbology, or data the symbology can’t encode.
400 unsupported The requested operation isn’t available for that symbology.
404 not-found Unknown path or method.
413 payload-too-large The body is larger than maxBodyBytes (15 MiB by default).
500 engine-load-failed The engine couldn’t be loaded (for example, the wasm files are missing from the deployment).
500 unknown Anything else.

CORS

The server sends Access-Control-Allow-Origin: *, allows GET, POST and OPTIONS with a Content-Type header, and answers preflight OPTIONS requests with 204. Browser apps on any origin can call it. To restrict or disable CORS, create the server in code (below) with the cors option.

Mount in your own server

qrgen-sdk/server exports createServer(options), which returns a Node http.Server, and createHandler(options), which returns a (req, res) handler for any framework built on Node’s HTTP module.

Option Type Default Description
maxBodyBytes number 15728640 (15 MiB) Largest accepted request body.
cors string | false "*" Access-Control-Allow-Origin value, or false to send no CORS headers.
log boolean false Log one line per request to the console.

Plain Node.js

import { createServer } from "qrgen-sdk/server";

createServer({ cors: "https://app.example.com", maxBodyBytes: 5 * 1024 * 1024, log: true }).listen(8080);

Express

Mount the handler under a prefix. It reads the raw request body itself, and it also works after express.json() or express.raw() (it uses req.body when a parser already consumed the stream). Mount it before multipart parsers such as multer, which move the file out of the body.

import express from "express";
import { createHandler } from "qrgen-sdk/server";

const app = express();

app.use("/qrgen", createHandler({ maxBodyBytes: 10 * 1024 * 1024 }));

app.use(express.json());
app.post("/api/orders", (req, res) => res.json({ ok: true }));

app.listen(3000); // POST http://localhost:3000/qrgen/v1/scan

Fastify

Fastify parses bodies before handlers run. Register the handler in an encapsulated plugin that leaves bodies unread, then hand the raw request over:

import Fastify from "fastify";
import { createHandler } from "qrgen-sdk/server";

const app = Fastify();
const qrgen = createHandler();

app.register(
  async (scope) => {
    scope.removeAllContentTypeParsers();
    scope.addContentTypeParser("*", (_request, _payload, done) => done(null));
    scope.all("/*", (request, reply) => {
      reply.hijack();
      request.raw.url = request.raw.url.slice("/qrgen".length) || "/";
      qrgen(request.raw, reply.raw);
    });
  },
  { prefix: "/qrgen" }
);

await app.listen({ port: 3000 });

The content-type parser change only applies inside the plugin, so the rest of your Fastify routes keep their normal body parsing.

Hono on Node.js

With @hono/node-server, route the prefix to QRGen at the Node level and everything else to Hono:

import { createServer } from "node:http";
import { getRequestListener } from "@hono/node-server";
import { Hono } from "hono";
import { createHandler } from "qrgen-sdk/server";

const app = new Hono();
app.get("/", (c) => c.text("Hello"));

const hono = getRequestListener(app.fetch);
const qrgen = createHandler();

createServer((req, res) => {
  if (req.url === "/qrgen" || req.url?.startsWith("/qrgen/")) {
    req.url = req.url.slice("/qrgen".length) || "/";
    return qrgen(req, res);
  }
  return hono(req, res);
}).listen(3000);

The handler needs Node’s IncomingMessage and ServerResponse, so it doesn’t run on edge runtimes such as Cloudflare Workers.

Clients

Every example sends a JPEG as raw bytes to /v1/scan and prints each result.

Python

With requests:

import requests

BASE = "http://localhost:8080"

with open("photo.jpg", "rb") as f:
    r = requests.post(f"{BASE}/v1/scan", params={"symbologies": "qr,ean13"}, data=f, headers={"Content-Type": "image/jpeg"})
r.raise_for_status()
for b in r.json()["barcodes"]:
    print(b["symbology"], b["data"])

png = requests.get(f"{BASE}/v1/generate", params={"data": "https://example.com", "format": "png", "scale": 8})
png.raise_for_status()
with open("qr.png", "wb") as out:
    out.write(png.content)

To decode locally in Python without a server, use the Python SDK.

Go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type Barcode struct {
	Data          string `json:"data"`
	Symbology     string `json:"symbology"`
	SymbologyName string `json:"symbologyName"`
}

func main() {
	f, err := os.Open("photo.jpg")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	resp, err := http.Post("http://localhost:8080/v1/scan?symbologies=qr,ean13", "image/jpeg", f)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		log.Fatalf("scan failed: %s", resp.Status)
	}

	var result struct {
		Barcodes []Barcode `json:"barcodes"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		log.Fatal(err)
	}
	for _, b := range result.Barcodes {
		fmt.Printf("%s: %s\n", b.SymbologyName, b.Data)
	}
}

Java

With the built-in java.net.http.HttpClient (Java 11+). Parse the JSON with the library you already use, such as Jackson or Gson:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;

public class ScanExample {
  public static void main(String[] args) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8080/v1/scan?symbologies=qr,ean13"))
        .header("Content-Type", "image/jpeg")
        .POST(HttpRequest.BodyPublishers.ofFile(Path.of("photo.jpg")))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) throw new RuntimeException("Scan failed: " + response.body());
    System.out.println(response.body()); // {"barcodes":[...]}
  }
}

C#

With HttpClient and System.Net.Http.Json (.NET 6+):

using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:8080") };

using var content = new ByteArrayContent(await File.ReadAllBytesAsync("photo.jpg"));
content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");

using var response = await http.PostAsync("/v1/scan?symbologies=qr,ean13", content);
response.EnsureSuccessStatusCode();

var result = await response.Content.ReadFromJsonAsync<ScanResult>();
foreach (var b in result!.Barcodes)
    Console.WriteLine($"{b.SymbologyName}: {b.Data}");

record Barcode(string Data, string Symbology, string SymbologyName);
record ScanResult(List<Barcode> Barcodes);

For on-device scanning in .NET apps, see the .NET SDK.

PHP

With the curl extension, as a multipart upload:

<?php
$ch = curl_init("http://localhost:8080/v1/scan?symbologies=qr,ean13");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => ["image" => new CURLFile("photo.jpg", "image/jpeg")],
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($body === false || $status !== 200) {
    throw new RuntimeException("Scan failed ($status): $body");
}

foreach (json_decode($body, true)["barcodes"] as $b) {
    echo $b["symbologyName"], ": ", $b["data"], PHP_EOL;
}

Ruby

With Net::HTTP from the standard library:

require "json"
require "net/http"

uri = URI("http://localhost:8080/v1/scan?symbologies=qr,ean13")
request = Net::HTTP::Post.new(uri, "Content-Type" => "image/jpeg")
request.body = File.binread("photo.jpg")

response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
raise "Scan failed (#{response.code}): #{response.body}" unless response.is_a?(Net::HTTPSuccess)

JSON.parse(response.body)["barcodes"].each do |b|
  puts "#{b["symbologyName"]}: #{b["data"]}"
end

Deployment tips

  • Sizing. Decoding is CPU-bound and single-threaded per process. For more throughput, run several containers (or processes) behind a load balancer rather than a bigger single instance.
  • Image size. Phone photos decode faster when downscaled to about 2000 px on the long side before upload.
  • Timeouts. Most scans complete in well under a second. Set client timeouts of a few seconds to cover large images with tryHarder.
  • Health checks. Use GET /health.
Esc

Share this page

REST API & Docker · Docs | QRGen

Star QRGen on GitHub Add a "Powered by QRGen" badge