📘 Interactive reference: this API is now described by an OpenAPI 3.1 spec (
openapi.yaml), rendered with Scalar. SeeDEPLOY.mdfor hosting. The spec is the source of truth; this README is kept as a human-readable mirror.
Unitag provides a simple yet powerful REST API to integrate the generation of QR Codes in your application
Our services are hosted in Amazon Ireland (eu-west-1) and OVH France
A public sandbox is available for testing the generation of QR Codes. In this environment you are limited to 1000 calls per day, if additional operations are needed please contact our support. In addition, any QR Code created will be deleted after 6 months, upon scanning they will return an HTTP 404 from our resolvers.
Those limitations reset every day at midnight (00:00am) GMT
When testing your API connectivity make sure to use the following URLs:
REST API
https://api-v2.sandbox.unitag.io
The REST API endpoints are as follows:
REST API
All requests and responses of Content-Typeapplication/json and follow typical HTTP response status codes for success and failures
Unless otherwise stated, errors to bad requests will respond with HTTP 4xx or status codes. The body will also contain a message parameter indicating the cause.
| Status Code | Reason |
|---|---|
| 400 | Bad Request - Invalid request format |
| 401 | Unauthorized - Bad API key |
| 403 | Forbidden - Insufficient rights or credits to perform the operation |
| 404 | Not found - The resource cannot be found for the user |
| 500 | Internal error - Please contact our support |
A successful response is indicated by HTTP status code 200 and may contain an optional body. If the response has a body it will be documented under each resource below.
Most identifiers are UUID unless otherwise specified. When making a request which requires a UUID. Those UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security Services.
Example: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
When a rate limit is exceeded, a status of 429 Too Many Requests will be returned.
1000 calls a day
1000000 calls a day
Each of those counters are reset every day at midnight (00:00am GMT)
You can generate an API key from https://console.sandbox.unitag.io/settings section API. In case of issue, please contact our Support Team (via the support button in your dashboard) in order to gain access to it and receive your credentials.
You can then call Unitag API endpoints by adding your API key as a header to the request such as:
X-Unitag-ApiKey=<API-Key>
Upon creating your key you will have to choose the targeted environment accordingly. Environments are isolated, meaning that any asset created in one will not be duplicated or accessible to/from another environment.
Create a QR Code Preview for the profile of the API Key
HTTP request
POST /qrcodes/preview
Request fields
Example:
{
"data": {
"type": "URL",
"resolution": "dynamic",
"url": "https://unitag.io"
},
"settings": {
"template_id": "",
"redundancy": "M",
"eyes": {
"type": "curved",
"color_ext_top_left": "",
"color_ext_top_right": "",
"color_ext_bottom_left": "",
"color_int_bottom_left": "",
"color_int_top_right": "",
"color_int_top_left": ""
},
"layout": {
"gradient_type": "vertical",
"type": "gradient",
"color_background": "#ffffff",
"color_one": "#53c1a6",
"color_two": "#c9285a",
"color_shadow": "#bdbdbd",
"force_shadow": "none"
},
"logo": {
"excavate": false,
"url": "",
"width": 0,
"height": 0,
"x": 0,
"x_norm": 0,
"y": 0,
"y_norm": 0
},
"background": {
"url": "",
"contrast": 0,
"brightness": 0
},
"modules": {
"type": "simple"
},
"frame": {
"style": "simple",
"text_top": "",
"text_bottom": "SCAN ME",
"background_color": "#000000"
}
}
}Details
Data object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | true | Type of QR Code, one of "url, vcard" |
| resolution | string | true | Resolution of your QR Code, one of "dynamic, dynamic-pro, static" |
| url | string | true | Final URL to which your QR Code should redirect to upon scanning |
| domain_id | string | false | Mandatory if resolution type is "dynamic-pro" |
| preferred_short_url | string | false | Custom short URL segment (alphanumeric, no special characters) |
Settings object:
- Layout object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | true | One of "single_color, gradient, image_overlay" — controls how the QR modules are coloured/filled, not the logo (see note below) |
| color_one | string | false | Hexadecimal color for QR Code modules and eyes |
| color_two | string | false | Hexadecimal color for gradient end (only when type is "gradient") |
| color_background | string | false | Hexadecimal color for the background |
| gradient_type | string | false | One of "horizontal, vertical, diagonal, diagonal_inverted, radial" (only when type is "gradient") |
| color_shadow | string | false | Hexadecimal color for shadow |
| force_shadow | string | false | Shadow intensity, one of "S, M, L, none" |
Logo vs.
image_overlay— these are independent features.
- To add a logo (a small image in the centre of the QR Code), use the Logo object below. It works with any
layout.type— keeptypeassingle_colororgradient. You do not needimage_overlayfor a logo.image_overlayis a separate effect that fills the QR modules themselves with an image. It requires a Background object with aurl(orasset_uuid); sendingtype: "image_overlay"without a background image returns400.
- Eyes object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | false | Type of Eye (see table below) |
| color_ext_top_left | string | false | Hexadecimal color for the external top-left eye |
| color_ext_top_right | string | false | Hexadecimal color for the external top-right eye |
| color_ext_bottom_left | string | false | Hexadecimal color for the external bottom-left eye |
| color_int_top_left | string | false | Hexadecimal color for the internal top-left eye |
| color_int_top_right | string | false | Hexadecimal color for the internal top-right eye |
| color_int_bottom_left | string | false | Hexadecimal color for the internal bottom-left eye |
Please read the table below which illustrates the different options:
| Eye type | Representation |
|---|---|
| simple | ![]() |
| diamond | ![]() |
| rounded | ![]() |
| rounded_light | ![]() |
| rounded_strong | ![]() |
| eye_right | ![]() |
| eye_left | ![]() |
| shield | ![]() |
| pillow | ![]() |
| star | ![]() |
| leaf | ![]() |
| sieve | ![]() |
| dots | ![]() |
| wave | ![]() |
| sharp | ![]() |
| curved | ![]() |
| tik_tak_toe | ![]() |
| octagonal | ![]() |
| alien | ![]() |
| grid | ![]() |
| square_diamond | ![]() |
| thin_circle | ![]() |
- Modules object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | false | Type of Module (see table below) |
Please read the table below which illustrates the different options:
| Module type | Representation |
|---|---|
| simple | ![]() |
| rounded | ![]() |
| rounded_light | ![]() |
| rounded_strong | ![]() |
| sieve | ![]() |
| angular | ![]() |
| paint | ![]() |
| dots | ![]() |
| arrows | ![]() |
| styled | ![]() |
| rectangles | ![]() |
| tik_tak_toe | ![]() |
| connections | ![]() |
| shinny | ![]() |
| diamonds | ![]() |
| small_circles | ![]() |
- Background object:
Attention:
The overlay will apply an image on your QR Code, we recommend using a 300px by 300px image with a sufficient contrast against your background color or your QR Code my not be readable by native scannersThis Background object is only used when
layout.typeisimage_overlay, and is required in that case. It is not how you add a logo — for a logo, use the Logo object instead.
| field | type | mandatory | description |
|---|---|---|---|
| url | string | false | The URL of your remote asset to use as overlay |
| brightness | float | false | From 0.0 to 1.0 (default 0.5), brightness will be applied on your asset |
| contrast | float | false | From 0.0 to 1.0 (default 0.5), contrast that will be applied on your asset |
- Logo object:
Attention:
A logo is an image present in your QR Code that hides some information, thanks to our QR Code redundancy it should have no effect on the QR Code readability. But, a logo too big or misplaced can render your QR Code unreadable
The logo is added by including this Logo object. It is independent of
layout.type— keeptypeassingle_color(orgradient); you do not needimage_overlay. To simply place a logo in the centre, send theurlandwidthand omitx_norm/y_norm— the logo defaults to the QR centre.
| field | type | mandatory | description |
|---|---|---|---|
| excavate | boolean | false | (default: false) Controls how the logo sits on the QR Code. true: the QR modules under the logo are removed (excavated), so the logo shows inside a clear/white zone. false: the logo is drawn directly on top of the modules, with no excavation. |
| x | number | false | Absolute origin position in pixels on the x axis in the QR Code |
| y | number | false | Absolute origin position in pixels on the y axis in the QR Code |
| x_norm | number | false | Normalized x position offset (defaults to 0 = centred when omitted) |
| y_norm | number | false | Normalized y position offset (defaults to 0 = centred when omitted) |
| width | number | false | Width in pixels of your logo |
| height | number | false | Height in pixels of your logo |
| url | string | false | The URL of your remote asset to use as logo |
- Frame object (optional):
Frames add a decorative border with text around your QR Code. The frame is rendered server-side and included in the output image.
| field | type | mandatory | description |
|---|---|---|---|
| style | string | true | Frame style (see list below) |
| text_top | string | false | Text displayed above the QR Code (supported by some styles) |
| text_bottom | string | false | Text displayed below the QR Code |
| background_color | string | false | Hexadecimal color for the frame (default: black) |
Available frame styles: simple, beverage, banner, filled_card, callout_filled, callout_top, dashed_border, inline, scan, rounded, banner_ribbon, phone_scan, phone_screen
Styles that support independent text_top: simple, beverage, banner, filled_card, callout_top, dashed_border. Other styles render a single text using text_bottom.
Response
Content-Type: image/png
The preview image of the resulted QR Code
Response headers
| header | description |
|---|---|
| X-Unitag-Generator | "CONTRAST_WARNING" if the QR Code colors may cause scan issues, "OK" otherwise |
| X-Unitag-Logo-Too-Big | "true" if the logo exceeds recommended size for scan readability |
Create a QR Code for the profile of the API Key
HTTP request
POST /qrcode
Request fields
Example
{
"data": {
"type": "url",
"resolution": "dynamic",
"url": "https://example.com/my-page",
"label": "My first QR code"
},
"settings": {
"layout": {
"type": "single_color",
"color_one": "#000000",
"color_background": "#ffffff"
},
"eyes": {
"type": "simple"
},
"modules": {
"type": "simple"
},
"logo": {
"excavate": false,
"width": 60,
"url": "https://example.com/my-logo.png"
}
}
}The example above adds a centred logo on a single-colour QR Code (the most common case). The logo lives in the
logoobject and does not requireimage_overlay;x_norm/y_normare omitted so the logo is centred. See the Logo vs.image_overlaynote in the QR Code Preview section.
Example of data payload with Dynamic Pro
{
"data": {
"type": "url",
"resolution": "dynamic-pro",
"domain_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"url": "https://example.com/my-page",
"[...]": "[...]"
}
}Example of data payload with a Campaign associated to the QR code at its creation
{
"data": {
"type": "url",
"resolution": "dynamic",
"url": "https://example.com/my-page",
"label": "My first QR code",
"campaign": {
"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc"
}
},
"settings": {
"layout": {
"type": "single_color",
"color_one": "#000000",
"color_background": "#ffffff"
},
"eyes": {
"type": "simple"
},
"modules": {
"type": "simple"
},
"logo": {
"excavate": false,
"width": 60,
"url": "https://example.com/my-logo.png"
}
}
}Example with an image overlay (the QR modules are filled with an image — this is the only case that uses image_overlay, and a background image is required; note there is no logo here):
{
"data": {
"type": "url",
"resolution": "dynamic",
"url": "https://example.com/my-page"
},
"settings": {
"layout": {
"type": "image_overlay",
"color_background": "#ffffff"
},
"eyes": {
"type": "simple"
},
"modules": {
"type": "simple"
},
"background": {
"url": "https://example.com/my-asset.png",
"brightness": 0.5,
"contrast": 0.5
}
}
}Details
Data object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | true | Type of QR Code, one of "url, vcard" |
| resolution | string | true | Resolution of your QR Code, one of "dynamic, dynamic-pro, static" |
| domain_id | string | false | Mandatory if resolution type is "dynamic-pro" |
| url | string | true | Final URL to which your QR Code should redirect to upon scanning |
| label | string | false | Name for your QR code |
| preferred_short_url | string | false | Custom short URL segment (alphanumeric, no special characters) |
| campaign | object | false | Campaign to associate with the QR code (contains uuid field) |
Settings object:
For more details about the settings object please refer to the Settings object in the QR Code Preview section
Response fields
Example
{
"status": "created",
"qrcode_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"content_url": "https://qrcode.link/a/f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"qr_code_image_url": "https://cdn-public.unitag.io/aaaaaaa-7dec-11d0-a765-00a0c91e6bf6/preview.png"
}| field | type | description |
|---|---|---|
| status | string | Status of the QR Code, one of "created, updated" |
| qrcode_id | string | ID of the QR Code created |
| content_url | string | Resolution URL, your own domain name if resolution is set to "dynamic-pro" |
| qr_code_image_url | string | QR Code image available from our CDN |
Retrieve a QR Code via its ID
HTTP request
GET /qrcode/<qrcode_id>
Retrieve all QR Codes associated with the account
HTTP request
GET /qrcodes/paginated?page=1&sortField=creation_date&sortOrder=desc&maxRows=5
Above is request with default args.
Parameters:
- sortField [field to sort with, one of Label, Destination, Campaign, Organisation, creation_date, short_url]
- sortOrder [desc/asc]
- filterLabel [string, partial search allowed]
- filterDestination [string, partial search allowed]
- filterCampaign [campaign_uuid]
- filterOrganisation [organisation_uuid]
- filterDate [string, dd/mm/yyyy]
- filterType [string, type of QR code]
- filterShortUrl [string, partial search allowed]
- filterBatchId [batchID]
- maxRows [integer]
Update the destination URL of a QR Code
This request is only available for Dynamic and Dynamic Pro QR Codes
HTTP request
PUT /qrcode/<qrcode_id>/url
Request fields
Example
{
"url": "https://unitag.io/features"
}Data object:
| field | type | mandatory | description |
|---|---|---|---|
| url | string | true | The final URL of your QR Code |
Update the label (name) of a QR Code
HTTP request
PUT /qrcode/<qrcode_id>/label
Request fields
{
"label": "My updated QR code name"
}| field | type | mandatory | description |
|---|---|---|---|
| label | string | true | The new label for your QR Code |
Response
{
"status": "updated"
}Delete a QR Code
HTTP request
DELETE /qrcode/<qr_code_uuid>
Download a QR Code in multiple formats
HTTP request
POST /asset/download/qrcode_bundle
Request fields
Example single download
{
"asset_uuid": "<qrcode_id>",
"format": "png",
"size": 300,
"unit": "px"
}Example with frame
{
"asset_uuid": "<qrcode_id>",
"format": "png",
"size": 300,
"unit": "px",
"hasFrame": true,
"frame": {
"style": "simple",
"text_top": "",
"text_bottom": "SCAN ME",
"background_color": "#000000"
}
}| field | type | mandatory | description |
|---|---|---|---|
| asset_uuid | string | true | Asset ID to be downloaded |
| format | string | true | Desired output format, one of "png", "svg", "jpeg", "pdf" |
| size | number | true | Desired output size, range between 100 and 2500 |
| unit | string | true | Desired output unit, one of "px", "mm" |
| color_profile | string | false | One of "RGB" (default), "GRAYSCALE" |
| hasFrame | boolean | false | Whether to include a frame around the QR Code |
| frame | object | false | Frame settings (see Frame object in Preview section) |
Response
Binary output matching your request representing the image of your asset
POST /asset/download/export
Example batch download
{
"qrcode_ids": ["<qrcode_id_1>", "<qrcode_id_2>", "<qrcode_id_3>"],
"format": "png",
"size": 300,
"unit": "px"
}| field | type | mandatory | description |
|---|---|---|---|
| qrcode_ids | Array of string | true | Assets ID to be downloaded |
| format | string | true | Desired output format, one of "png", "svg", "jpeg", "pdf" |
| size | number | true | Desired output size, range between 100 and 2500 |
| unit | string | true | Desired output unit, one of "px", "mm" |
| color_profile | string | false | One of "RGB" (default), "GRAYSCALE" |
| hasFrame | boolean | false | Whether to include a frame around each QR Code |
| frame | object | false | Frame settings (see Frame object in Preview section) |
Response
A compressed archive (zip) containing all your assets
Templates allow you to save and reuse QR Code designs.
Retrieve the list of existing templates
HTTP request
GET /qrcodes/templates
Response
An array of template objects with their UUIDs, names, and associated design.
When creating a QR code using a template design, pass the UUID of the template to the POST create QR Code request:
{
"settings": {
"redundancy": "H",
"template_id": "c7a5dc7c-4bcb-435d-a5a3-b21658189f7a"
},
"data": {
"type": "url",
"resolution": "dynamic",
"url": "https://example.com"
}
}Save a QR Code's design as a reusable template
HTTP request
POST /qrcode/<qrcode_id>/template
Request fields
{
"template_name": "My template"
}| field | type | mandatory | description |
|---|---|---|---|
| template_name | string | true | Name for the template |
Response
{
"template_id": "c7a5dc7c-4bcb-435d-a5a3-b21658189f7a"
}Update the name of an existing template
HTTP request
PUT /qrcodes/template/<template_id>
Request fields
{
"name": "Updated template name"
}Response
{
"status": "updated",
"uuid": "c7a5dc7c-4bcb-435d-a5a3-b21658189f7a",
"name": "Updated template name"
}HTTP request
DELETE /qrcodes/template/<template_id>
NB: Filters are only available for Dynamic and Dynamic Pro QR Codes
Filters are a way to redirect your users based on certain conditions. Those conditions are evaluated upon scanning a QR Code. If those conditions are fulfilled the filter will be triggered
Filters available:
- Language based
- Device based
- Location based (module required)
- Combination of all the above
On GS1 Digital Link QR codes a filter can additionally carry a link_type, which turns it into a
typed destination rather than a plain conditional redirection — see
Typed destinations.
Time-based changes are not filters. A filter is evaluated on every scan, against properties of the scan itself (device, language, country). To change where a code points at a given date and time, use a schedule instead: it rewrites the destination once, when the moment arrives.
HTTP request
POST /qrcode/<qrcode_id>/filter
Request fields
Example
In this example we are creating a language filter which is read as followed: If an english speaker scans this QR Code it will be redirected to https://www.unitag.io/welcome
{
"final_url": "https://www.unitag.io/welcome",
"statements": [
{
"type": "language",
"value": "en"
}
]
}Details
Filter object:
| field | type | mandatory | description |
|---|---|---|---|
| final_url | string | true | The destination URL used if the filter statements are fulfilled |
| statements | Array of Statement | true | List of Statements for the filter |
| link_type | string | false | GS1 Web Vocabulary link type. Makes this a typed GS1 destination. GS1 codes only — see Typed destinations |
| active | boolean | false | Whether the resolver may select this destination. Defaults to true on create; omitting it on update leaves the current value unchanged |
Statement object:
| field | type | mandatory | description |
|---|---|---|---|
| type | string | true | Type of Statement, one of "device", "language", "location_country", "context" |
| value | string | true | Refer to next paragraph for formats specifications |
Statement values format
device type Statement
Supported values: "blackberry", "iphone", "ipad", "windows_phone", "windows_tablet", "windows_computer", "apple_computer", "android_phone", "android_tablet"
{
"type": "device",
"value": "iphone"
}language type Statement
Supported values: Any Alpha-2 code part of ISO 639 -- Wikipedia Source
{
"type": "language",
"value": "en"
}location_country type Statement
Supported values: Any Alpha-2 code part of ISO 3166 -- ISO Search
{
"type": "location_country",
"value": "US"
}context type Statement
Scopes a typed GS1 destination to an audience. Only meaningful alongside a link_type — a
context statement without one is rejected with a 400.
Supported values: consumer, healthcareProfessional, patient, recycler, retailer,
supplier (bare, or prefixed with gs1:)
{
"type": "context",
"value": "patient"
}HTTP request
GET /qrcode/<qrcode_id>/filters
Update an existing filter's statements and/or destination URL
HTTP request
PUT /qrcode/<qrcode_id>/filter/<filter_id>
Request fields
{
"final_url": "https://www.unitag.io/updated-page",
"statements": [
{
"type": "language",
"value": "fr"
}
]
}Response
{
"filter_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"final_url": "https://www.unitag.io/updated-page",
"status": "updated"
}HTTP request
DELETE /qrcode/<qrcode_id>/filter/<filter_id>
Response
{
"status": "deleted",
"filter_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
}A GS1 Digital Link QR code encodes a product identifier as a web URI, so a single printed code can be read by both a consumer's phone and a supply-chain scanner:
https://id.yourbrand.com/01/03512345678900/10/LOT42?17=261231
│ │ │ │ │
│ └─ GTIN │ └─ lot └─ expiry (data attribute)
└─ AI 01 └─ AI 10
Unitag operates a GS1-certified Conformant Resolver (GS1 Conformant Resolver standard v1.2.0, live certification and verification completed).
Because the identifier and the domain are both yours, the printed code is not tied to Unitag: it carries no vendor-specific token, so pointing the domain elsewhere keeps every printed code working.
NB: GS1 Digital Link codes are Dynamic or Dynamic Pro. Use
dynamic-prowith a registered domain to serve codes from your own domain.
Primary keys — the identifier that opens the path:
| AI | Identifier | AI | Identifier |
|---|---|---|---|
01 | GTIN | 8003 | GRAI |
00 | SSCC | 8004 | GIAI |
253 | GDTI | 8006 | ITIP |
255 | GCN | 8010 | CPID |
401 | GINC | 8017 | GSRN (provider) |
402 | GSIN | 8018 | GSRN (recipient) |
414 / 415 / 417 | GLN variants |
GTINs (AI 01) are validated: 14 digits with a correct GS1 mod-10 check digit. A 13-digit value is
left-padded to 14 automatically. An invalid check digit is rejected at creation, and a malformed
GTIN in a scanned URI is answered 400 by the resolver.
Key qualifiers — appended to the path, always emitted in the standard order:
| AI | Qualifier |
|---|---|
22 | Consumer product variant (CPV) |
10 | Batch / lot |
21 | Serial |
Data attributes — appended as query parameters, not path segments. This includes expiry date
(AI 17), production date (11), best-before (15), net weight (310n), price, and the rest of
the GS1 Application Identifier set. Placing them in the query string rather than the path is what
the GS1 Digital Link standard requires — 17 is a data attribute, not a key qualifier.
Attributes outside the supported set are ignored rather than rejected.
Validates identifiers and returns the canonical Digital Link path. Creates nothing.
HTTP request
POST /gs1/look-up
Request fields
{
"gtins": [
{
"primary_key": "01",
"value": "03512345678900",
"key_identifiers": [
{ "key": "10", "value": "LOT42" }
],
"data_attributes": [
{ "key": "17", "value": "261231" }
]
}
]
}Response
{
"GS1ShortURL": [
{
"primary_key": "01",
"short_url": "/01/03512345678900/10/LOT42?17=261231"
}
]
}Key qualifiers are emitted in the order 22 → 10 → 21 regardless of the order you supply them in.
HTTP request
POST /qrcode/serial
Request fields
Supply either a short_url containing /01/, or the gs1 object. When both are present the
short_url wins and is parsed as the canonical source.
{
"data": {
"type": "url",
"resolution": "dynamic-pro",
"domain_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"url": "https://brand.example/products/welcome",
"gs1": {
"gtin": "03512345678900",
"batch": "LOT42",
"cpv": "",
"serial": ""
}
},
"settings": {
"redundancy": "M"
}
}Details
data.gs1 object:
| field | type | mandatory | description |
|---|---|---|---|
| gtin | string | true | 14-digit GTIN with a valid mod-10 check digit |
| cpv | string | false | Consumer product variant (AI 22) |
| batch | string | false | Batch / lot (AI 10) |
| serial | string | false | Serial (AI 21) |
To include data attributes (expiry, weights, …), pass a full path in data.short_url instead —
build it with /gs1/look-up:
{
"data": {
"type": "url",
"resolution": "dynamic-pro",
"domain_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"url": "https://brand.example/products/welcome",
"short_url": "/01/03512345678900/10/LOT42?17=261231"
}
}Notes
- The GS1 identifier path is immutable once created. The destination (
data.url) and the filters stay editable — that is what lets you repoint a printed code without reprinting it. - Each distinct Digital Link path is its own QR code. A GTIN, a GTIN+lot and a GTIN+serial are three separate codes with their own destinations and their own rules.
- Creating a code for a path that already exists returns
406.
A GS1 Digital Link code can expose several destinations at once, each labelled with a
GS1 Web Vocabulary link type — a product page, a Digital Product
Passport, safety information, a recipe — and a scanner asks for the one it wants with ?linkType=.
Typed destinations are created through the ordinary filter endpoints by adding a
link_type:
POST /qrcode/<qrcode_id>/filter
{
"link_type": "dpp",
"final_url": "https://dpp.brand.example/03512345678900/LOT42",
"statements": []
}The value is a GS1 Web Vocabulary token, bare (dpp) or CURIE-prefixed (gs1:dpp). The bare token
is what gets stored and returned. Unknown tokens are rejected with a 400, as is any link_type
sent for a QR code that is not a GS1 Digital Link code.
Accepted tokens:
activityIdeas · allergenInfo · appDownload · backgroundInfo · brandHomepageClinical
brandHomepagePatient · careersInfo · certificationInfo · consumerHandlingStorageInfo · dpp
eifu · epcis · epil · eventsInfo · faqs · handledBy · hasRetailers · homepage · ingredientsInfo
instructions · jws · leaveReview · locationInfo · logisticsInfo · loyaltyProgram · masterData
menuInfo · nutritionalInfo · openingHoursInfo · paymentLink · pip · productSustainabilityInfo
promotion · purchaseSuppliesOrAccessories · quickStartGuide · recallStatus · recipeInfo
registerProduct · registryEntry · relatedImage · relatedVideo · reportFound · review · safetyInfo
scheduleTime · serviceInfo · smartLabel · smpc · socialMedia · statisticInfo · subscribe · support
sustainabilityInfo · traceability · tutorial · userAgreement · verificationService · whatsInTheBox
Combining with conditions. A typed destination can also carry statements, so the same link
type can resolve differently by country or language:
{
"link_type": "safetyInfo",
"final_url": "https://brand.example/fr/securite",
"statements": [
{ "type": "location_country", "value": "FR" }
]
}Among the destinations of a requested link type, one whose statements match the scan wins; otherwise the unconditioned destination of that type is used.
Audience scoping. A context statement narrows a destination to a GS1 audience. A destination
stating an audience is only reachable when the scan asks for it (?context=patient), so a context
acts as a tie-break between destinations of the same link type — it can never capture an ordinary
scan:
{
"link_type": "epil",
"final_url": "https://brand.example/leaflet/clinical",
"statements": [
{ "type": "context", "value": "healthcareProfessional" }
]
}Switching a destination off. Set active: false to take a typed destination out of service
without deleting it. The resolver then falls back to the unconditioned destination of that link
type, and stops advertising the deactivated one. Omitting active on an update leaves it unchanged.
How a Unitag-resolved GS1 Digital Link URI answers a scan.
Redirect (default). A plain scan is answered 307 Temporary Redirect to the destination, after
evaluating any country / language / device filters. Responses are marked non-cacheable so a
destination change takes effect immediately.
Requesting a link type.
GET https://id.yourbrand.com/01/03512345678900?linkType=dpp
linkType accepts the bare token, the gs1: CURIE, or the full vocabulary URI. ?linkType=homepage
resolves to the code's default destination. Requesting a link type the code does not have is
answered 404 — the standard requires this rather than a silent fallback — and the response still
advertises the link types that are available.
Linkset. The full set of links for a code, as application/linkset+json
(RFC 9264):
GET https://id.yourbrand.com/01/03512345678900?linkType=all
GET https://id.yourbrand.com/01/03512345678900 Accept: application/linkset+json
{
"linkset": [
{
"anchor": "https://id.yourbrand.com/01/03512345678900",
"https://ref.gs1.org/voc/defaultLink": [
{ "href": "https://brand.example/products/welcome", "title": "Homepage" }
],
"https://ref.gs1.org/voc/homepage": [
{ "href": "https://brand.example/products/welcome", "title": "Homepage" }
],
"https://ref.gs1.org/voc/dpp": [
{ "href": "https://dpp.brand.example/03512345678900", "title": "Digital Product Passport" }
]
}
]
}The anchor is your own domain. Deactivated destinations are omitted, so the linkset never
advertises a link that ?linkType= would refuse.
Link headers. Redirect responses carry RFC 8288Link headers advertising the available typed links and the linkset:
Link: <https://brand.example/products/welcome>; rel="https://ref.gs1.org/voc/defaultLink"
Link: <https://dpp.brand.example/03512345678900>; rel="https://ref.gs1.org/voc/dpp"; title="Digital Product Passport"
Link: <https://id.yourbrand.com/01/03512345678900?linkType=all>; rel="linkset"; type="application/linkset+json"
Discovery document. Served on each resolver domain:
GET https://id.yourbrand.com/.well-known/gs1resolver
{
"name": "Unitag GS1 Digital Link Resolver",
"resolverRoot": "https://id.yourbrand.com",
"supportedPrimaryKeys": ["00", "01", "253", "255", "401", "402", "414", "415", "417", "8003", "8004", "8006", "8010", "8017", "8018"],
"supportedContextValuesEnumerated": ["country", "language", "device"]
}Status codes.
| Code | Meaning |
|---|---|
307 | Redirect to the resolved destination |
200 | Linkset returned (application/linkset+json) |
300 | Several products match the URI equally; the candidates are listed rather than guessed |
400 | Malformed Digital Link URI — e.g. a GTIN failing its mod-10 check digit |
404 | Unknown code, or a linkType this code does not have |
HEAD is supported and resolves like GET without a body.
Not supported: compressed GS1 Digital Link URIs (and therefore no owl:sameAs header).
Content negotiation covers application/linkset+json; other media types fall through to the
redirect.
Schedule a QR code or packaging product to change its destination at a given date and time — a campaign that goes live at midnight, a product page that switches to end-of-life information on a known date, seasonal content.
This is a scheduled change, not a scan-time condition. A rule fires once, at trigger_at, and
rewrites the destination; every scan after that resolves to the new URL until the next rule fires.
It is not a "route to X between 09:00 and 17:00" filter — the resolver evaluates device, language
and country on each scan, but never the clock. If you need the destination to depend on the moment
of the scan itself, that is not currently supported.
Because a schedule rewrites the same field you would edit by hand, it composes with everything else: filters keep applying after the swap, and on a GS1 code the Digital Link path is untouched.
Schedules are served by a dedicated host rather than the main REST API:
| Environment | Base URL |
|---|---|
| Production | https://scheduler-api.unitag.io |
| Sandbox | https://scheduler-api.sandbox.unitag.io |
In production the same service is also reachable at https://api-v2.unitag.io/_b/scheduler.
Authentication is the same X-Unitag-ApiKey header as the rest of this API. You may additionally
send X-Unitag-Org-Id to scope the request to a specific organisation; it falls back to the key's
owner for standalone accounts.
If a schedule lookup cannot be completed, these endpoints return 503 rather than 401 — a 401
here always means the key itself was rejected, never a transient failure.
HTTP request
POST /schedules
Request fields
{
"target_type": "qrcode",
"target_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"rules": [
{
"trigger_at": "2026-12-24T23:00:00Z",
"destination_url": "https://brand.example/christmas",
"position": 1
},
{
"trigger_at": "2026-12-26T23:00:00Z",
"destination_url": "https://brand.example/sales",
"position": 2
}
]
}Details
| field | type | mandatory | description |
|---|---|---|---|
| target_type | string | false | qrcode (default) or packaging_product |
| target_id | string (uuid) | true | UUID of the QR code or packaging product |
| qr_code_id | string (uuid) | false | Legacy alias for target_id on qrcode targets |
| rules | Array of Rule | true | Between 1 and 3 rules |
Rule object:
| field | type | mandatory | description |
|---|---|---|---|
| trigger_at | string (RFC 3339) | true | When the change applies. Must be in the future |
| destination_url | string | true | The new destination |
| position | integer | true | Order of the rule, 1–3 |
Constraints
- Maximum 3 rules per schedule
- Only one active schedule per target at a time
- The target must exist and belong to your organisation, otherwise
403 trigger_atmust be in the future at creation- Destinations on packaging product targets must be
https://
Response
{
"id": 12,
"uuid": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"organisation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"target_type": "qrcode",
"target_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"qr_code_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"status": "pending",
"created_at": "2026-08-05T10:00:00Z",
"updated_at": "2026-08-05T10:00:00Z",
"rules": [
{
"id": 34,
"uuid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"trigger_at": "2026-12-24T23:00:00Z",
"destination_url": "https://brand.example/christmas",
"position": 1,
"fired_at": null
}
]
}status is derived from the rules: pending (none fired), active (some fired, some pending),
expired (all fired). fired_at is null until a rule has been applied.
Rules are applied by a background job that runs every few seconds, so a change takes effect within
seconds of trigger_at rather than exactly on it.
HTTP request
GET /schedules?page=1&page_size=20
Response
{
"schedules": [],
"total": 42,
"page": 1,
"page_size": 20
}HTTP request
GET /schedules/<schedule_id>
Replaces all rules on the schedule. Send the complete set you want, not just the changed ones.
HTTP request
PUT /schedules/<schedule_id>
Request fields
{
"rules": [
{
"trigger_at": "2026-12-25T23:00:00Z",
"destination_url": "https://brand.example/boxing-day",
"position": 1
}
]
}Deletes the schedule and all its rules. Rules that have already fired are not rolled back — the destination stays where it was last set.
HTTP request
DELETE /schedules/<schedule_id>
Domain names allows to use your own domain name in your QR code for Dynamic Pro QR codes
NB: Custom domain names are available from Gold subscription and upwards
Registering a domain name is the first operation in order to use your custom domain name with Unitag Resolver engine
Supported domain names examples:
- APEX (eg: example.org)
- SUBDOMAIN (eg: qr.example.org, www.example.org, foo.example.org)
Request
POST /domains
Example
{
"domain_name": "example.org"
}Data object:
| field | type | mandatory | description |
|---|---|---|---|
| domain_name | string | true | Domain name that you want to register with us |
Response
{
"type": "APEX",
"requested_domain": "example.org",
"domain_uuid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"fqdn": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6.qrcode.link",
"ip": "1.2.3.4"
}Data object:
| field | type | description |
|---|---|---|
| type | string | Either APEX or SUBDOMAIN based on the given domain |
| requested_domain | string | The domain you have submitted |
| domain_uuid | string | The UUID of your domain |
| fqdn | string | If SUBDOMAIN - Fully Qualified Domain Name - For registration purposes |
| ip | string | If APEX - IP address - For registration purposes |
Once you have received the response from the API you need to update your domain name with your registrar
If your domain name is an APEX you will need to create an A record such as:example.org A 1.2.3.4
If your domain name is a SUBDOMAIN you will need to create a CNAME record such as:foo.example.org CNAME f81d4fae-7dec-11d0-a765-00a0c91e6bf6.qrcode.link.
NB: If you are registering a SUBDOMAIN please make sure to add a dot (.) at the end of the CNAME entry
Once your domain has been registered with us you need to update your domain with your registrar (eg: Gandi, Google, OVH). Once done you will be able to finalize the registration of your domain by validating your configuration against our API
Request
GET /domains/<domain_id>/check
Response
HTTP code 200
{
"domain_uuid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
}HTTP code 409
{
"message": "The current entry does not resolves to a Unitag domain name, please make sure your DNS zone is correctly configured"
}Retrieve the list of domain available for the account
Request
GET /domains
Response
HTTP code 200
NB: As an owner of a domain name more fields might be added if you have shared the domain across accounts
[
{
"domain_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
"domain_name": "example.org",
"ssl_activated": true,
"activated": true,
"status": ""
} ]Data []object:
| field | type | description |
|---|---|---|
| domain_id | string | The ID of the domain |
| domain_name | string | The domain name as string |
| ssl_activated | boolean | Status of the SSL service |
| activated | boolean | Status of the domain name |
| status | string | Used for deactivation purposes |
The campaigns endpoint allows you to work around the campaign management tool, helping you to sort your QR codes.
Retrieve the list of created campaigns and sub-campaigns
RequestGET /campaigns
Response
[
{
"uuid": "c2bd62df-3642-4a93-9754-aa68f65c6622",
"name": "HEALTHCHECK",
"color": "#673ab7",
"creation_date": "2021-02-23T22:31:29.32Z",
"sub_campaigns": [
{
"uuid": "0f877077-ac95-4e4c-b3af-5ad4fb8c1655",
"name": "health1",
"color": "orange",
"creation_date": "2021-02-23T22:31:29.32Z",
"sub_campaigns": null
}
]
},
{
"uuid": "a88dc890-015f-4e93-8640-a65fd66646a6",
"name": "CAMPAIGN 1",
"color": "#2196f3",
"creation_date": "2021-02-24T13:51:35.127Z",
"sub_campaigns": null
}
]Request
POST /qrcodes/campaign
{"name": "New Campaign", "color": "blue"}| field | type | mandatory | description |
|---|---|---|---|
| name | string | true | Name for the campaign |
| color | string | true | Color for the campaign (hex color or predefined: purple, red, blue, green, orange, yellow) |
Response
{
"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc",
"name": "New Campaign",
"color": "blue",
"creation_date": "2023-05-26T12:39:52.921Z"
}Request
POST /qrcodes/campaign
Pass the uuid of the parent campaign:
{"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc", "name": "Sub Campaign", "color": "yellow"}Request
PUT /campaigns/<campaign_id>
{"name": "Updated Campaign Name", "color": "#f44336"}Response
{
"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc",
"name": "Updated Campaign Name",
"color": "#f44336"
}Retrieve the number of QR codes associated with a campaign
Request
GET /campaigns/<campaign_id>/assets_count
Response
{
"id": "fd3180e2-e5e2-45ca-a8e5-b737627132bc",
"count": 42
}Deleting a campaign will also delete all sub-campaigns, if any.
Request
DELETE /campaigns/<campaign_uuid>
Response
{"id": "fd3180e2-e5e2-45ca-a8e5-b737627132bc", "status": "deleted"}The Analytics endpoint allows you to retrieve stats generated by your QR Codes
Unitag pre aggregates data in order to offload calculations for you
Request
POST /analytics/qrcodes/search
Example
The following query reads as:
- Get all stats from 2 weeks ago to now (type: 1 relate to a relative time search )
{
"time_range": {
"type": 1,
"number": 2,
"unit": "weeks"
}
}- Get all stats for an absolute date range (type 0)
{
"time_range": {
"type": 0,
"start_date": "2022-07-31T23:00:00.000Z",
"end_date": "2022-09-01T23:00:00.000Z"
}
}- Get all stats for specifics qrcodes
{
"filters": [
{
"field": "QR Codes",
"operator": "",
"value": [
"6b8e573e-f5ca-4402-b300-3641ee9484c0",
"b229d8bc-92f6-4d88-a9d4-6e8adc7187ef"
]
}
],
"time_range": {
"type": 1,
"number": 2,
"unit": "weeks"
}
}Data object:
| field | type | mandatory | description |
|---|---|---|---|
| time_range.type | number | true | 0 for absolute date range, 1 for relative |
| time_range.number | number | false | Length of the relative look-back window, expressed as a count of unit periods — i.e. "the last N units". Paired with unit: number: 2 + unit: weeks means the last 2 weeks (and likewise the last N days, months, etc.). Required when type is 1; not used for absolute ranges (type 0). |
| time_range.unit | string | false | Unit of the relative look-back window, paired with number — one of: minutes, hours, days, weeks, months. E.g. number: 3 + unit: months means the last 3 months. Required when type is 1; not used for absolute ranges (type 0). |
| time_range.start_date | string | false | ISO 8601 date, required if type is 0 |
| time_range.end_date | string | false | ISO 8601 date, required if type is 0 |
| filters | array | false | Array of filter objects to narrow results |
| filters[].field | string | false | "QR Codes" to filter by specific QR code UUIDs |
| filters[].value | array | false | Array of QR code UUIDs |
Response
{
"histogram": [
{
"date": 1635984000000,
"count": 0
},
{
"date": 1637193600000,
"count": 1
}
],
"visits_count": 1,
"visits_count_comparison": 0,
"unique_visitors_count": 1,
"unique_visitors_count_comparison": 0,
"countries_histogram": [
{
"country_iso": "GB",
"count": 1
}
],
"visits_history": {
"cursor_id": "",
"visits": [
{
"date": 1637251153589,
"label": "My QR code",
"country_iso": "GB",
"model": "iPhone"
}
]
}
}Retrieve detailed visit records for your QR Codes
Request
POST /analytics/qrcodes/search/visit-records
Uses the same request body format as the aggregated stats endpoint above.
Response
{
"visits_history": {
"cursor_id": "",
"visits": [
{
"date": 1637251153589,
"label": "My QR code",
"country_iso": "GB",
"city_iso": "London",
"model": "iPhone"
}
]
}
}Export analytics data as an Excel spreadsheet
Request
POST /analytics/qrcodes/export
Uses the same request body format as the aggregated stats endpoint. Add time_zone to localize dates in the export.
{
"time_range": {
"type": 1,
"number": 4,
"unit": "weeks"
},
"time_zone": "Europe/Paris"
}Response
Binary file (Excel .xlsx spreadsheet)





































