Cloud API

Send an image, get the barcode back. One endpoint, one header. Everything below works with the key we emailed you.

Quickstart

Put your key in the box — every example on this page updates to use it. Nothing is sent anywhere until you press a button.

Kept in this browser tab only. Lost it? We can re-issue — contact us.

The whole integration is this:

# the image goes in the body, the key goes in a header
curl -X POST "https://api.yourdomain.com/v1/decode" \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@barcode.png"
The one thing people get wrong: the key is a header, not part of the body. If you paste a curl command into Postman's body box you will get 401 Missing X-API-Key header — in Postman use File → Import → Raw text, or set the header yourself under the Headers tab.

Try it live

Pick an image and call the real API from this page. This consumes one scan from your quota, exactly like your own code would.

Decode an image

POST /v1/decode

Send the image either as a multipart file field or as the raw request body. Both are identical to us.

Request

PartValue
X-API-Key headerYour key. Required.
BodyMultipart field file, or the image bytes directly.
?format=Optional. DotCode, QRCode, DataMatrix, PDF417, Aztec, MaxiCode. Restricting it is faster and slightly more reliable.

Response

{
  "ok": true,
  "text": "01095011015300031725013110ABC123",  // raw payload
  "format": "DotCode",
  "via": "gray",                          // which pass read it
  "elapsed_ms": 14.2,
  "error": null,
  "gs1": { /* structured fields, or null -- see below */ }
}

Raw text and GS1 fields

Most DotCodes in tobacco and pharma carry a GS1 element string rather than free text. text is always the raw payload; gs1 is the same data parsed, or null when the code is plain text.

"gs1": {
  "is_gs1": true,
  "fields": { "01": "09501101530003", "17": "250131", "10": "ABC123" },
  "elements": [
    { "ai": "01", "label": "GTIN",        "value": "09501101530003" },
    { "ai": "17", "label": "Expiry date", "value": "250131",
      "formatted": "2025-01-31" },
    { "ai": "10", "label": "Batch/Lot",   "value": "ABC123" }
  ],
  "errors": []
}

Use ?gs1=auto (default), always, or off. Dates gain an ISO formatted value, and GTIN check digits are verified so a misread is visible rather than silent.

ok: false with an error means the call was fine but no symbol was found in that picture. That is a normal answer, not a failure — see image quality.

/v2/decode is the same decoder and takes the same key; it also returns your remaining quota in the response. Handy while you are building, unnecessary in production.

Code samples

curl -X POST "https://api.yourdomain.com/v1/decode?format=DotCode" \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@barcode.png"
import requests

# pip install requests
r = requests.post(
    "https://api.yourdomain.com/v1/decode",
    headers={"X-API-Key": "YOUR_KEY"},
    files={"file": open("barcode.png", "rb")},
    params={"format": "DotCode"},   # optional, but faster
    timeout=30,
)
r.raise_for_status()
data = r.json()

if data["ok"]:
    print("read:", data["text"])
else:
    print("no symbol found:", data["error"])
// Node 18+ has fetch and FormData built in
import { openAsBlob } from "node:fs";

const form = new FormData();
form.append("file", await openAsBlob("barcode.png"), "barcode.png");

const res = await fetch(
  "https://api.yourdomain.com/v1/decode",
  { method: "POST", headers: { "X-API-Key": "YOUR_KEY" }, body: form }
);
const data = await res.json();
console.log(data.ok ? data.text : "no symbol: " + data.error);
<?php
$ch = curl_init("https://api.yourdomain.com/v1/decode");
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => ["X-API-Key: YOUR_KEY"],
  CURLOPT_POSTFIELDS     => ["file" => new CURLFile("barcode.png")],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);

echo $data["ok"] ? $data["text"] : "no symbol: " . $data["error"];
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-API-Key", "YOUR_KEY");

using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(File.ReadAllBytes("barcode.png")),
         "file", "barcode.png");

var res  = await http.PostAsync(
    "https://api.yourdomain.com/v1/decode", form);
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine(json);
// Java 11+ — multipart via a small boundary body
var path  = Path.of("barcode.png");
var bound = "----voltionx";
var head  = ("--" + bound + "\r\nContent-Disposition: form-data; " +
             "name=\"file\"; filename=\"barcode.png\"\r\n" +
             "Content-Type: image/png\r\n\r\n").getBytes();
var tail  = ("\r\n--" + bound + "--\r\n").getBytes();
var body  = new ByteArrayOutputStream();
body.write(head); body.write(Files.readAllBytes(path)); body.write(tail);

var req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.yourdomain.com/v1/decode"))
    .header("X-API-Key", "YOUR_KEY")
    .header("Content-Type", "multipart/form-data; boundary=" + bound)
    .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
    .build();

System.out.println(HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString()).body());

Check your usage

GET /v1/usage — your plan, quota and what you have used this month.

curl "https://api.yourdomain.com/v1/usage" \
  -H "X-API-Key: YOUR_KEY"
{
  "plan": "pro",
  "quota": 100000,
  "used": 2380,
  "remaining": 97620,
  "resets_on": "2026-08-01"
}

Prefer a screen? Sign in at your usage portal with the same key.

Live camera scanning

For a continuous camera stream there is a WebSocket that returns a result per frame. Browsers cannot set headers on a WebSocket, so the key goes in the query string:

wss://api.yourdomain.com/v1/live?api_key=YOUR_KEY&format=DotCode

Send JPEG frames as binary messages; each reply is the same JSON shape as /v1/decode. If you just want a working scanner for staff, use our hosted Live Scanner instead — it is a separate product with its own licence.

Errors

StatusMeaningWhat to do
200 ok:falseNo symbol found in the image Normal answer. Check image quality.
401Missing or wrong key Send X-API-Key as a header. Watch for a copied space or newline.
402Quota exhausted Upgrade, or wait for resets_on.
400Body was not a readable image Empty field, or not a PNG/JPEG. Does not use quota.
413Image over 8 MBResize before sending.
429Too many requests too quickly Slow down and retry after a moment.
5xxOur side Retry once after a second; tell us if it persists.

Quota & limits

  • One successful or unsuccessful decode attempt = one scan. A 400 (bad upload) or 401 costs you nothing.
  • Quota resets monthly, anchored to your signup date — see resets_on in /v1/usage.
  • Maximum image size 8 MB. Images beyond roughly 40 megapixels are rejected.
  • Keys are per customer, not per machine — use the same key from as many servers as you like.
Treat the key like a password: keep it on your server, never in a mobile app or front-end JavaScript, where anyone could read it and spend your quota.

Image quality

If a picture will not read, it is almost always one of these — in the order we actually see them:

  • Too small. The dots must be individually resolved. Move closer or raise the resolution; enlarging afterwards does not add detail.
  • Motion blur. Shorter exposure, or steady the camera.
  • Glare straight across the symbol from a light source.
  • Severe angle. We deskew, but there is a limit.
  • No quiet zone. Leave a clear margin around the code.

Passing ?format= often reads a marginal image that the try-everything path gives up on.

Need help?

Reply to the email your key came in, and include the image that failed plus the JSON you got back — that pair is usually enough for us to tell you exactly what happened.