{"openapi": "3.1.0", "info": {"title": "xengo API", "version": "1.0", "description": "Branded links, dynamic QR and real-time click/scan events. Authenticate with an API key as `Authorization: Bearer xgo_\u2026` or `x-api-key`. Requests are rate-limited per plan (429 + Retry-After when exceeded). Webhook deliveries are HMAC-signed (Svix-compatible headers) with automatic retries and a per-attempt delivery log."}, "servers": [{"url": "https://api.neon.xengo.io"}], "components": {"securitySchemes": {"apiKey": {"type": "apiKey", "in": "header", "name": "x-api-key"}, "bearer": {"type": "http", "scheme": "bearer"}}, "schemas": {"Link": {"type": "object", "properties": {"code": {"type": "string", "description": "The short-link stub."}, "domain": {"type": "string"}, "shortUrl": {"type": "string", "format": "uri"}, "destination": {"type": "string", "format": "uri"}, "status": {"type": "string", "enum": ["active", "disabled"]}, "title": {"type": "string"}, "tags": {"type": "object", "additionalProperties": {"type": "string"}}, "expiresAt": {"type": "string", "format": "date-time"}, "lastClickAt": {"type": "string", "format": "date-time"}, "notifyChannels": {"type": "array", "items": {"type": "string"}}, "createdAt": {"type": "string", "format": "date-time"}}}, "LinkWrite": {"type": "object", "required": ["destination"], "properties": {"destination": {"type": "string", "format": "uri"}, "code": {"type": "string", "description": "Custom stub (optional)."}, "domain": {"type": "string", "description": "One of your verified domains."}, "tags": {"type": "object", "additionalProperties": {"type": "string"}}, "expiresAt": {"type": "string", "format": "date-time"}, "notifyChannels": {"type": "array", "items": {"type": "string"}, "description": "Channel ids to alert on click/scan."}}}, "Channel": {"type": "object", "properties": {"id": {"type": "string"}, "type": {"type": "string", "description": "e.g. webhook, zapier, n8n, slack, email"}, "name": {"type": "string"}, "enabled": {"type": "boolean"}, "config": {"type": "object"}, "signed": {"type": "boolean", "description": "Deliveries carry Svix-compatible HMAC signature headers."}, "confirmed": {"type": "boolean", "description": "Email channels are double opt-in; false until the recipient confirms."}, "signingSecret": {"type": "string", "description": "whsec_\u2026 \u2014 returned ONCE on create for signed channel types."}}}, "Error": {"type": "object", "properties": {"error": {"type": "string"}, "code": {"type": "string"}}}, "Gs1Record": {"type": "object", "required": ["gtin"], "properties": {"gtin": {"type": "string", "description": "GTIN-8/12/13/14; check digit is validated."}, "cpv": {"type": "string", "description": "AI 22 \u2014 consumer product variant."}, "batch": {"type": "string", "description": "AI 10 \u2014 batch/lot."}, "serial": {"type": "string", "description": "AI 21 \u2014 serial number."}, "expiry": {"type": "string", "description": "AI 17 \u2014 expiry, YYMMDD."}, "destination": {"type": "string", "description": "Single destination (redirector mode). Ignored when `linkset` is given."}, "linkset": {"type": "array", "description": "Resolver mode \u2014 one entry per GS1 link type. Requires the resolver entitlement when more than one entry is supplied.", "items": {"type": "object", "required": ["href"], "properties": {"linkType": {"type": "string", "default": "gs1:pip"}, "href": {"type": "string"}, "default": {"type": "boolean", "description": "The link the code resolves to by default."}}}}, "ai": {"type": "object", "additionalProperties": {"type": "string"}, "description": "Data attributes keyed by AI, e.g. {\"17\": \"271231\"}. Requires the resolver entitlement."}, "tags": {"type": "array", "items": {"type": "string"}, "description": "User-defined tags (max 20, 32 chars each) for filtering views and exports."}, "domain": {"type": "string", "description": "Front domain; defaults to the tenant's."}}}, "Gs1BulkResult": {"type": "object", "properties": {"requested": {"type": "integer"}, "created": {"type": "integer"}, "updated": {"type": "integer"}, "skipped": {"type": "integer"}, "failed": {"type": "integer"}, "results": {"type": "array", "description": "One entry per submitted record, in order.", "items": {"type": "object", "properties": {"row": {"type": "integer"}, "gtin": {"type": "string"}, "code": {"type": "string", "description": "Derived Digital Link path, e.g. 01/09506000134352/10/LOT01."}, "status": {"type": "string", "enum": ["created", "updated", "skipped", "failed"]}, "error": {"type": "string"}}}}}}, "Gs1Export": {"type": "object", "properties": {"jobId": {"type": "string"}, "name": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "rendering", "ready", "failed"]}, "count": {"type": "integer", "description": "Codes rendered (once ready)."}, "sizeBytes": {"type": "integer"}, "truncated": {"type": "boolean", "description": "True when the job hit the 5000-code cap."}, "error": {"type": "string"}, "createdAt": {"type": "string"}, "finishedAt": {"type": "string"}, "options": {"type": "object", "description": "The options the job was created with, echoed back."}, "downloadUrl": {"type": "string", "description": "Presigned zip URL \u2014 only on GET, only when ready. Expires after 7 days."}, "urlExpiresIn": {"type": "integer"}}}, "Gs1ExportWrite": {"type": "object", "properties": {"name": {"type": "string"}, "query": {"type": "string", "description": "Free-text filter over code, destination, title and tags."}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Export only records carrying ALL of these tags."}, "formats": {"type": "array", "items": {"type": "string", "enum": ["svg", "pdf", "eps", "ai"]}}, "symbologies": {"type": "array", "items": {"type": "string", "enum": ["qr", "dm"]}}, "sizeMode": {"type": "string", "enum": ["total", "xdim"], "default": "total"}, "sizeMm": {"type": "number", "default": 25, "description": "Total printed size (sizeMode=total)."}, "xdimMm": {"type": "number", "default": 0.495, "description": "Module size (sizeMode=xdim). GS1 retail POS range is 0.396-0.990 mm."}, "ec": {"type": "string", "enum": ["L", "M", "Q", "H"], "default": "H"}, "hri": {"type": "boolean", "default": true, "description": "Print the human-readable interpretation beneath the code."}, "ean13": {"type": "boolean", "description": "Also render a linear EAN-13 per code (2D Sunrise transition)."}, "sheet": {"type": "string", "enum": ["none", "a4", "avery-l7160", "roll-100"]}, "raster": {"type": "string", "enum": ["none", "png300", "png600", "tiff300", "tiff600"]}, "bleedMm": {"type": "number", "description": "0-10 mm bleed beyond trim."}, "marks": {"type": "boolean", "description": "Crop marks + registration crosshairs."}, "ink": {"type": "string", "enum": ["k100", "spot"], "default": "k100"}, "spotName": {"type": "string", "description": "Separation name when ink=spot, e.g. \"PANTONE 3005 C\"."}, "iccIntent": {"type": "string", "description": "Print condition declared as a PDF output intent, e.g. FOGRA39."}}}}}, "paths": {"/v1/links": {"get": {"summary": "List links", "description": "\n\nRequires the `links:read` scope.", "security": [{"apiKey": []}, {"bearer": []}]}, "post": {"summary": "Create a link", "description": "\n\nRequires the `links:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LinkWrite"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Link"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/links/bulk": {"post": {"summary": "Bulk-create links", "description": "Up to 500 rows per request; per-plan row caps apply.\n\nRequires the `links:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"rows": {"type": "array", "items": {"$ref": "#/components/schemas/LinkWrite"}}}}}}}}}, "/v1/links/{code}": {"parameters": [{"name": "code", "in": "path", "required": true, "schema": {"type": "string"}}], "get": {"summary": "Get a link", "description": "\n\nRequires the `links:read` scope.", "security": [{"apiKey": []}, {"bearer": []}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Link"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "patch": {"summary": "Update / re-point a link", "description": "Change destination (re-points the live link and any printed QR), status, tags, expiry or notifyChannels.\n\nRequires the `links:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LinkWrite"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Link"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "delete": {"summary": "Delete a link", "description": "\n\nRequires the `links:write` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/links/{code}/analytics": {"parameters": [{"name": "code", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "window", "in": "query", "schema": {"type": "string", "enum": ["24h", "7d", "30d"]}}], "get": {"summary": "Per-link click/scan stats", "description": "\n\nRequires the `analytics` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/links/{code}/qr": {"parameters": [{"name": "code", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "scale", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 40}, "description": "Module size in px (default 8)."}], "get": {"summary": "The link's QR code as SVG", "description": "Plain (unstyled) QR encoding the short URL \u2014 for print pipelines and labels. Because the destination is re-pointable, responses are marked no-store.\n\nRequires the `qr` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/analytics": {"parameters": [{"name": "from", "in": "query", "schema": {"type": "string"}}, {"name": "to", "in": "query", "schema": {"type": "string"}}], "get": {"summary": "Tenant-wide analytics", "description": "Time series, human/bot split and breakdowns (device, browser, OS, country, city, referrer, source channel).\n\nRequires the `analytics:read` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/channels": {"get": {"summary": "List notification channels", "description": "\n\nRequires the `notifications:read` scope.", "security": [{"apiKey": []}, {"bearer": []}]}, "post": {"summary": "Create a notification channel", "description": "Webhook-class channels return a one-time `signingSecret` (whsec_\u2026) for verifying the Svix-compatible signature headers on every delivery. Email channels are double opt-in and deliver only after the recipient confirms.\n\nRequires the `notifications:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Channel"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/channels/{id}": {"parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "delete": {"summary": "Delete a notification channel", "description": "\n\nRequires the `notifications:write` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/gs1/links/bulk": {"post": {"summary": "Bulk create/update GS1 Digital Links", "description": "Creates a Digital Link per record, keyed by its GTIN (plus any CPV/batch/serial), with the linkset, data attributes and user tags attached in one call. Up to 500 records per request, and per-plan row caps apply.\n\nRecords are independent: a bad row is reported in `results` and never aborts the batch. A record whose code already exists is updated by default \u2014 send `upsert: false` to skip instead.\n\nOn update the destination is always re-pointed; the linkset, data attributes and tags are replaced only when the record supplies them, so a destination-only re-import leaves the existing ones untouched.\n\nRequires the `gs1:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["records"], "properties": {"records": {"type": "array", "items": {"$ref": "#/components/schemas/Gs1Record"}}, "tags": {"type": "array", "items": {"type": "string"}, "description": "Applied to every record, merged with each record's own tags."}, "upsert": {"type": "boolean", "default": true, "description": "Update a record whose code already exists (false = skip it)."}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Gs1BulkResult"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/gs1/exports": {"get": {"summary": "List GS1 export jobs", "description": "\n\nRequires the `gs1:read` scope.", "security": [{"apiKey": []}, {"bearer": []}]}, "post": {"summary": "Request a GS1 export", "description": "Queues an async render of the tenant's GS1 Digital Links to print-ready artwork (QR and/or GS1 DataMatrix; SVG/PDF/EPS/AI, optional print-DPI PNG/TIFF, EAN-13 pairing, imposed label sheets) plus a manifest, packaged as a zip.\n\nReturns immediately with `202` and a `jobId`. Poll `GET /v1/gs1/exports/{id}` until `status` is `ready`, then use the `downloadUrl`. Jobs are capped at 5000 codes and expire after 30 days.\n\nRequires the `gs1:write` scope.", "security": [{"apiKey": []}, {"bearer": []}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Gs1ExportWrite"}}}}, "responses": {"202": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Gs1Export"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/gs1/exports/{id}": {"parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "get": {"summary": "Get a GS1 export job", "description": "Includes a freshly minted presigned `downloadUrl` once the job is ready.\n\nRequires the `gs1:read` scope.", "security": [{"apiKey": []}, {"bearer": []}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Gs1Export"}}}}, "429": {"description": "Rate limit exceeded (see Retry-After)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "delete": {"summary": "Delete a GS1 export job and its zip", "description": "\n\nRequires the `gs1:write` scope.", "security": [{"apiKey": []}, {"bearer": []}]}}, "/v1/openapi.json": {"get": {"summary": "This document", "security": []}}}}