Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

42 Commits

Repository files navigation

API V2

📘 Interactive reference: this API is now described by an OpenAPI 3.1 spec (openapi.yaml), rendered with Scalar. See DEPLOY.md for hosting. The spec is the source of truth; this README is kept as a human-readable mirror.

Introduction

Unitag provides a simple yet powerful REST API to integrate the generation of QR Codes in your application

General

Data Centers

Our services are hosted in Amazon Ireland (eu-west-1) and OVH France

Sandbox

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

Sandbox URLs

When testing your API connectivity make sure to use the following URLs:

REST API

https://api-v2.sandbox.unitag.io

Content

API

The REST API endpoints are as follows:

REST API

https://api-v2.unitag.io

Requests

All requests and responses of Content-Typeapplication/json and follow typical HTTP response status codes for success and failures

Errors

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 CodeReason
400Bad Request - Invalid request format
401Unauthorized - Bad API key
403Forbidden - Insufficient rights or credits to perform the operation
404Not found - The resource cannot be found for the user
500Internal error - Please contact our support

Successes

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.

Types

IDs

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

Rate Limits

When a rate limit is exceeded, a status of 429 Too Many Requests will be returned.

Sandbox

1000 calls a day

Production

1000000 calls a day

Each of those counters are reset every day at midnight (00:00am GMT)

Authentication

Generating an API Key

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>

Environments

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.

QR Codes

Preview a QR Code design

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:

fieldtypemandatorydescription
typestringtrueType of QR Code, one of "url, vcard"
resolutionstringtrueResolution of your QR Code, one of "dynamic, dynamic-pro, static"
urlstringtrueFinal URL to which your QR Code should redirect to upon scanning
domain_idstringfalseMandatory if resolution type is "dynamic-pro"
preferred_short_urlstringfalseCustom short URL segment (alphanumeric, no special characters)

Settings object:

  • Layout object:
fieldtypemandatorydescription
typestringtrueOne of "single_color, gradient, image_overlay" — controls how the QR modules are coloured/filled, not the logo (see note below)
color_onestringfalseHexadecimal color for QR Code modules and eyes
color_twostringfalseHexadecimal color for gradient end (only when type is "gradient")
color_backgroundstringfalseHexadecimal color for the background
gradient_typestringfalseOne of "horizontal, vertical, diagonal, diagonal_inverted, radial" (only when type is "gradient")
color_shadowstringfalseHexadecimal color for shadow
force_shadowstringfalseShadow 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 anylayout.type — keep type as single_color or gradient. You do not need image_overlay for a logo.
  • image_overlay is a separate effect that fills the QR modules themselves with an image. It requires a Background object with a url (or asset_uuid); sending type: "image_overlay" without a background image returns 400.
  • Eyes object:
fieldtypemandatorydescription
typestringfalseType of Eye (see table below)
color_ext_top_leftstringfalseHexadecimal color for the external top-left eye
color_ext_top_rightstringfalseHexadecimal color for the external top-right eye
color_ext_bottom_leftstringfalseHexadecimal color for the external bottom-left eye
color_int_top_leftstringfalseHexadecimal color for the internal top-left eye
color_int_top_rightstringfalseHexadecimal color for the internal top-right eye
color_int_bottom_leftstringfalseHexadecimal color for the internal bottom-left eye

Please read the table below which illustrates the different options:

Eye typeRepresentation
simplealt text
diamondalt text
roundedalt text
rounded_lightalt text
rounded_strongalt text
eye_rightalt text
eye_leftalt text
shieldalt text
pillowalt text
staralt text
leafalt text
sievealt text
dotsalt text
wavealt text
sharpalt text
curvedalt text
tik_tak_toealt text
octagonalalt text
alienalt text
gridalt text
square_diamondalt text
thin_circlealt text
  • Modules object:
fieldtypemandatorydescription
typestringfalseType of Module (see table below)

Please read the table below which illustrates the different options:

Module typeRepresentation
simplealt text
roundedalt text
rounded_lightalt text
rounded_strongalt text
sievealt text
angularalt text
paintalt text
dotsalt text
arrowsalt text
styledalt text
rectanglesalt text
tik_tak_toealt text
connectionsalt text
shinnyalt text
diamondsalt text
small_circlesalt text
  • 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 scanners

This Background object is only used when layout.type is image_overlay, and is required in that case. It is not how you add a logo — for a logo, use the Logo object instead.

fieldtypemandatorydescription
urlstringfalseThe URL of your remote asset to use as overlay
brightnessfloatfalseFrom 0.0 to 1.0 (default 0.5), brightness will be applied on your asset
contrastfloatfalseFrom 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 — keep type as single_color (or gradient); you do not need image_overlay. To simply place a logo in the centre, send the url and width and omit x_norm/y_norm — the logo defaults to the QR centre.

fieldtypemandatorydescription
excavatebooleanfalse(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.
xnumberfalseAbsolute origin position in pixels on the x axis in the QR Code
ynumberfalseAbsolute origin position in pixels on the y axis in the QR Code
x_normnumberfalseNormalized x position offset (defaults to 0 = centred when omitted)
y_normnumberfalseNormalized y position offset (defaults to 0 = centred when omitted)
widthnumberfalseWidth in pixels of your logo
heightnumberfalseHeight in pixels of your logo
urlstringfalseThe 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.

fieldtypemandatorydescription
stylestringtrueFrame style (see list below)
text_topstringfalseText displayed above the QR Code (supported by some styles)
text_bottomstringfalseText displayed below the QR Code
background_colorstringfalseHexadecimal 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

headerdescription
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

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 logo object and does not require image_overlay; x_norm/y_norm are omitted so the logo is centred. See the Logo vs. image_overlay note 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:

fieldtypemandatorydescription
typestringtrueType of QR Code, one of "url, vcard"
resolutionstringtrueResolution of your QR Code, one of "dynamic, dynamic-pro, static"
domain_idstringfalseMandatory if resolution type is "dynamic-pro"
urlstringtrueFinal URL to which your QR Code should redirect to upon scanning
labelstringfalseName for your QR code
preferred_short_urlstringfalseCustom short URL segment (alphanumeric, no special characters)
campaignobjectfalseCampaign 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"
}
fieldtypedescription
statusstringStatus of the QR Code, one of "created, updated"
qrcode_idstringID of the QR Code created
content_urlstringResolution URL, your own domain name if resolution is set to "dynamic-pro"
qr_code_image_urlstringQR Code image available from our CDN

Retrieve a QR Code

Retrieve a QR Code via its ID

HTTP request

GET /qrcode/<qrcode_id>


Retrieve all QR Codes

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 a QR Code destination URL

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:

fieldtypemandatorydescription
urlstringtrueThe final URL of your QR Code

Update a QR Code label

Update the label (name) of a QR Code

HTTP request

PUT /qrcode/<qrcode_id>/label

Request fields

{
"label": "My updated QR code name"
}
fieldtypemandatorydescription
labelstringtrueThe new label for your QR Code

Response

{
"status": "updated"
}

Delete a QR Code

Delete a QR Code

HTTP request

DELETE /qrcode/<qr_code_uuid>


Download a QR Code

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"
}
}
fieldtypemandatorydescription
asset_uuidstringtrueAsset ID to be downloaded
formatstringtrueDesired output format, one of "png", "svg", "jpeg", "pdf"
sizenumbertrueDesired output size, range between 100 and 2500
unitstringtrueDesired output unit, one of "px", "mm"
color_profilestringfalseOne of "RGB" (default), "GRAYSCALE"
hasFramebooleanfalseWhether to include a frame around the QR Code
frameobjectfalseFrame settings (see Frame object in Preview section)

Response

Binary output matching your request representing the image of your asset


Download multiple QR Codes

POST /asset/download/export

Example batch download

{
"qrcode_ids": ["<qrcode_id_1>", "<qrcode_id_2>", "<qrcode_id_3>"],
"format": "png",
"size": 300,
"unit": "px"
}
fieldtypemandatorydescription
qrcode_idsArray of stringtrueAssets ID to be downloaded
formatstringtrueDesired output format, one of "png", "svg", "jpeg", "pdf"
sizenumbertrueDesired output size, range between 100 and 2500
unitstringtrueDesired output unit, one of "px", "mm"
color_profilestringfalseOne of "RGB" (default), "GRAYSCALE"
hasFramebooleanfalseWhether to include a frame around each QR Code
frameobjectfalseFrame settings (see Frame object in Preview section)

Response

A compressed archive (zip) containing all your assets


Templates

Templates allow you to save and reuse QR Code designs.

Retrieve QR Code templates

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"
}
}

Create a template from a QR Code

Save a QR Code's design as a reusable template

HTTP request

POST /qrcode/<qrcode_id>/template

Request fields

{
"template_name": "My template"
}
fieldtypemandatorydescription
template_namestringtrueName for the template

Response

{
"template_id": "c7a5dc7c-4bcb-435d-a5a3-b21658189f7a"
}

Update a template

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"
}

Delete a template

HTTP request

DELETE /qrcodes/template/<template_id>


Filters

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.

Create a Filter

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:

fieldtypemandatorydescription
final_urlstringtrueThe destination URL used if the filter statements are fulfilled
statementsArray of StatementtrueList of Statements for the filter
link_typestringfalseGS1 Web Vocabulary link type. Makes this a typed GS1 destination. GS1 codes only — see Typed destinations
activebooleanfalseWhether the resolver may select this destination. Defaults to true on create; omitting it on update leaves the current value unchanged

Statement object:

fieldtypemandatorydescription
typestringtrueType of Statement, one of "device", "language", "location_country", "context"
valuestringtrueRefer 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"
}

Retrieve filters for a QR Code

HTTP request

GET /qrcode/<qrcode_id>/filters


Update a filter

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"
}

Delete a filter

HTTP request

DELETE /qrcode/<qrcode_id>/filter/<filter_id>

Response

{
"status": "deleted",
"filter_id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
}

GS1 Digital Link

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-pro with a registered domain to serve codes from your own domain.

Supported identifiers

Primary keys — the identifier that opens the path:

AIIdentifierAIIdentifier
01GTIN8003GRAI
00SSCC8004GIAI
253GDTI8006ITIP
255GCN8010CPID
401GINC8017GSRN (provider)
402GSIN8018GSRN (recipient)
414 / 415 / 417GLN 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:

AIQualifier
22Consumer product variant (CPV)
10Batch / lot
21Serial

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.

Build a Digital Link path

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.


Create a GS1 Digital Link QR code

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:

fieldtypemandatorydescription
gtinstringtrue14-digit GTIN with a valid mod-10 check digit
cpvstringfalseConsumer product variant (AI 22)
batchstringfalseBatch / lot (AI 10)
serialstringfalseSerial (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.

Typed destinations (link types)

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.


Resolver behaviour

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.

CodeMeaning
307Redirect to the resolved destination
200Linkset returned (application/linkset+json)
300Several products match the URI equally; the candidates are listed rather than guessed
400Malformed Digital Link URI — e.g. a GTIN failing its mod-10 check digit
404Unknown 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.


Scheduled Destination Changes

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.

Base URL

Schedules are served by a dedicated host rather than the main REST API:

EnvironmentBase URL
Productionhttps://scheduler-api.unitag.io
Sandboxhttps://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.

Create a schedule

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

fieldtypemandatorydescription
target_typestringfalseqrcode (default) or packaging_product
target_idstring (uuid)trueUUID of the QR code or packaging product
qr_code_idstring (uuid)falseLegacy alias for target_id on qrcode targets
rulesArray of RuletrueBetween 1 and 3 rules

Rule object:

fieldtypemandatorydescription
trigger_atstring (RFC 3339)trueWhen the change applies. Must be in the future
destination_urlstringtrueThe new destination
positionintegertrueOrder 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_at must 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.


List schedules

HTTP request

GET /schedules?page=1&page_size=20

Response

{
"schedules": [],
"total": 42,
"page": 1,
"page_size": 20
}

Retrieve a schedule

HTTP request

GET /schedules/<schedule_id>


Update a schedule

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
}
]
}

Delete a schedule

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

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

Register a domain name

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:

fieldtypemandatorydescription
domain_namestringtrueDomain 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:

fieldtypedescription
typestringEither APEX or SUBDOMAIN based on the given domain
requested_domainstringThe domain you have submitted
domain_uuidstringThe UUID of your domain
fqdnstringIf SUBDOMAIN - Fully Qualified Domain Name - For registration purposes
ipstringIf 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


Validate a domain name

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"
}

List all domain names available

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:

fieldtypedescription
domain_idstringThe ID of the domain
domain_namestringThe domain name as string
ssl_activatedbooleanStatus of the SSL service
activatedbooleanStatus of the domain name
statusstringUsed for deactivation purposes

Campaigns

The campaigns endpoint allows you to work around the campaign management tool, helping you to sort your QR codes.

List campaigns

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
}
]

Create a campaign

Request

POST /qrcodes/campaign

{"name": "New Campaign", "color": "blue"}
fieldtypemandatorydescription
namestringtrueName for the campaign
colorstringtrueColor 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"
}

Create a sub-campaign

Request

POST /qrcodes/campaign

Pass the uuid of the parent campaign:

{"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc", "name": "Sub Campaign", "color": "yellow"}

Update a campaign

Request

PUT /campaigns/<campaign_id>

{"name": "Updated Campaign Name", "color": "#f44336"}

Response

{
"uuid": "fd3180e2-e5e2-45ca-a8e5-b737627132bc",
"name": "Updated Campaign Name",
"color": "#f44336"
}

Get campaign assets count

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
}

Delete a campaign

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"}

Analytics

The Analytics endpoint allows you to retrieve stats generated by your QR Codes

Retrieve QR codes aggregated stats

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:

fieldtypemandatorydescription
time_range.typenumbertrue0 for absolute date range, 1 for relative
time_range.numbernumberfalseLength 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.unitstringfalseUnit 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_datestringfalseISO 8601 date, required if type is 0
time_range.end_datestringfalseISO 8601 date, required if type is 0
filtersarrayfalseArray of filter objects to narrow results
filters[].fieldstringfalse"QR Codes" to filter by specific QR code UUIDs
filters[].valuearrayfalseArray 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 visit records

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

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)

About

Documentation for the QR Code generation of the API v2

Resources

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages