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"
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
| Part | Value |
|---|---|
X-API-Key header | Your key. Required. |
| Body | Multipart 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
| Status | Meaning | What to do |
|---|---|---|
200 ok:false | No symbol found in the image | Normal answer. Check image quality. |
401 | Missing or wrong key | Send X-API-Key as a header. Watch for a copied space or newline. |
402 | Quota exhausted | Upgrade, or wait for resets_on. |
400 | Body was not a readable image | Empty field, or not a PNG/JPEG. Does not use quota. |
413 | Image over 8 MB | Resize before sending. |
429 | Too many requests too quickly | Slow down and retry after a moment. |
5xx | Our 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) or401costs you nothing. - Quota resets monthly, anchored to your signup date — see
resets_onin/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.
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.