/v1/qr-tags
Create QR tags
Creates QR tags and returns the text to print in each QR code. Printago mints every tag ID and the batch ID; you only name the targets, or ask for a batch of blank tags, which aren't saved. Needs the QR Workflows add-on. Send targets or blankCount, not both; neither or both is a 400.
A target is one of:
| entityType | Other fields | The tag stands for |
|---|---|---|
printer |
entityId: the printer ID |
A printer. |
printer_slot |
entityId: the printer ID, amsIndex, slotIndex |
One filament slot of a printer: amsIndex is the AMS (or MMU, CFS) unit from 0, or -1 for an external spool holder, and slotIndex the slot in it from 0. The app names it A1/3 (AMS 1, slot 3) or S1 (first external holder). |
material_variant |
entityId: the variant ID |
One color of a material. |
material_instance |
entityId: the spool (material instance) ID |
One physical spool. The tag's displaySnapshot.code is the spool's label code, when it has one. |
Blank tags
blankCount (1 to 300) returns that many blank tags to print, and saves none of them: printing blanks changes nothing in the store, and a blank isn't listed by GET /v1/qr-tags until it is set up. A blank tag stands for nothing yet: its status and entityType are blank, its entityId is null, and displaySnapshot.code is a short code such as 7F3K-9QXP to print on it, so two blanks can be told apart. The code is read from the tag's ID, so the same ID always has the same code. A blank is set up by its first pair scan in the Printago app: scanned with a printer, a slot, a filament or a spool, POST /v1/qr-tags/execute saves it as the tag for that target, active; scanned with a filament, the default action registers a new spool with it. A blank's payload is a link to its page on Printago on the web, so a phone's camera opens it there, or in the Printago app when it's installed, and the tag can be set up by picking what it's for instead of scanning it (a resolve and execute with a target). A blank keeps its printed payload ("t": "blank") after it is set up; its tag record says what it is now. Open /qr-tags/print?blanks=<count> in Printago on the web to print blanks.
A blank's ID can be any 1 to 24 letters, digits, - or _, so a code you make yourself, such as https://app.printago.io/qr/shelf-7?v=1&t=blank&s={storeId}, works like one Printago printed. Its ID can't be one another store's tag already uses: setting it up is then a 409 with code: "tag_id_taken".
A slot tag names the slot by its position, not by a slot row, so it keeps working when a printer that reports its AMS at runtime (Bambu Lab, Creality CFS) recreates its slots. The slot must be one the printer has now, from its capabilities, its slots, or what it reports; any other slot is a 400 with code: "unknown_slot", and no tags are created. Send every slot of a printer in one request to print a full set.
By default (reuseActive: true) a target that already has an active tag gets that tag back, so printing a sheet again produces the same QR codes and earlier labels keep working. Send reuseActive: false to mint a fresh tag for every target, for example when a label was damaged. Revoke the old tag with PATCH /v1/qr-tags/revoke so it stops working.
Each target appears once in the response, in request order, even if you list it twice. Any other entityType is a 400. If a printer, variant or spool isn't in your store the whole request is a 404 whose message lists the missing IDs, and no tags are created.
Payload format (v1)
payload is compact JSON, 165 characters or fewer, so it scans quickly. A blank tag's is a link instead, with the same keys as query parameters: https://app.printago.io/qr/{id}?v=1&t=blank&s={storeId}, on the web app of the Printago environment that printed it.
| Key | Meaning |
|---|---|
v |
Payload version, 1. A reader that sees a higher number should ask the user to update rather than guess. |
t |
Entity type of the tag when it was printed: printer, printer_slot, material_variant, material_instance or blank. |
id |
The QR tag ID (qr_tags.id). This is what POST /v1/qr-tags/resolve looks up. |
s |
The store ID the tag belongs to. |
d |
Hints for instant display: {"printerId"} for a printer, {"printerId", "amsIndex", "slotIndex"} for a slot, {"variantId"} for a variant, {"instanceId"} for a spool, {} for a blank. The tag record on the server is the source of truth, so never act on t or d alone. |
Encode payload byte for byte; don't reformat it. Text that isn't JSON with a numeric v, or an http(s) link to /qr/{id} with a numeric v in its query, is not a Printago tag.
Send scanned text straight to POST /v1/qr-tags/resolve instead of parsing it yourself. Resolve checks the version, the store, whether the tag was revoked and whether its target still exists, and returns what the scanner can do now.
Request Body
Targets to tag and whether to reuse active tags, or a count of blank tags
Return this many blank tags (1 to 300) to print instead of tagging targets. Nothing is saved.
Return each target's newest active tag instead of minting another. Defaults to true.
Example Request
{
"targets": [
{
"entityType": "printer",
"entityId": "string"
}
],
"blankCount": 1,
"reuseActive": true
}
Examples
Tag two printers
The first printer already had an active tag, so it comes back unchanged; the second is minted in this request's batch.
{
"targets": [
{
"entityType": "printer",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa"
},
{
"entityType": "printer",
"entityId": "q1w3e5r7t9y1u3i5o7p9a1sd"
}
]
}
{
"batchId": "b4t8c2h6x0z4v8n2m6k0j4ha",
"tags": [
{
"id": "k3q9x2m8v7c1n4b6z0w5y2ta",
"status": "active",
"entityType": "printer",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"label": "X1C-01",
"batchId": "a9s8d7f6g5h4j3k2l1z0x9cv",
"payloadVersion": 1,
"payload": "{\"v\":1,\"t\":\"printer\",\"id\":\"k3q9x2m8v7c1n4b6z0w5y2ta\",\"s\":\"cm1r8s2t4u6v8w0x2y4z6a8b\",\"d\":{\"printerId\":\"p7h2j4k6m8n0q2r4s6t8v0wa\"}}"
},
{
"id": "r5t7y9u1i3o5p7a9s1d3f5gh",
"status": "active",
"entityType": "printer",
"entityId": "q1w3e5r7t9y1u3i5o7p9a1sd",
"label": "X1C-02",
"batchId": "b4t8c2h6x0z4v8n2m6k0j4ha",
"payloadVersion": 1,
"payload": "{\"v\":1,\"t\":\"printer\",\"id\":\"r5t7y9u1i3o5p7a9s1d3f5gh\",\"s\":\"cm1r8s2t4u6v8w0x2y4z6a8b\",\"d\":{\"printerId\":\"q1w3e5r7t9y1u3i5o7p9a1sd\"}}"
}
]
}
Tag every slot of a printer, and a filament color
{
"targets": [
{
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"amsIndex": 0,
"slotIndex": 0
},
{
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"amsIndex": 0,
"slotIndex": 1
},
{
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"amsIndex": 0,
"slotIndex": 2
},
{
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"amsIndex": 0,
"slotIndex": 3
},
{
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"amsIndex": -1,
"slotIndex": 0
},
{
"entityType": "material_variant",
"entityId": "v2n4m6b8c0x2z4a6s8d0f2gh"
}
]
}
{
"batchId": "c5v7b9n1m3q5w7e9r1t3y5ua",
"tags": [
{
"id": "uyk6qzl3cah7b8nccab3yigm",
"status": "active",
"entityType": "printer_slot",
"entityId": "p7h2j4k6m8n0q2r4s6t8v0wa",
"slotAmsIndex": 0,
"slotSlotIndex": 0,
"label": "A1/1 · X1C-01",
"displaySnapshot": {
"name": "X1C-01",
"provider": "Bambu",
"slotRef": "A1/1"
},
"payload": "{\"v\":1,\"t\":\"printer_slot\",\"id\":\"uyk6qzl3cah7b8nccab3yigm\",\"s\":\"cm1r8s2t4u6v8w0x2y4z6a8b\",\"d\":{\"printerId\":\"p7h2j4k6m8n0q2r4s6t8v0wa\",\"amsIndex\":0,\"slotIndex\":0}}"
},
{
"id": "nvioxp9s1d7o3van7wqhkmtp",
"status": "active",
"entityType": "material_variant",
"entityId": "v2n4m6b8c0x2z4a6s8d0f2gh",
"label": "PLA Basic · Black",
"displaySnapshot": {
"name": "Black",
"materialName": "PLA Basic",
"brand": "Bambu Lab",
"color": "#000000FF"
},
"payload": "{\"v\":1,\"t\":\"material_variant\",\"id\":\"nvioxp9s1d7o3van7wqhkmtp\",\"s\":\"cm1r8s2t4u6v8w0x2y4z6a8b\",\"d\":{\"variantId\":\"v2n4m6b8c0x2z4a6s8d0f2gh\"}}"
}
]
}
A batch of blank tags
{
"blankCount": 2
}
{
"batchId": "zt2xuatx1n9w7jsrihpojftt",
"tags": [
{
"id": "v61yt345k7x9a5eaw1tuw1vy",
"status": "blank",
"entityType": "blank",
"entityId": null,
"label": "Q9FJ-2JA4",
"displaySnapshot": {
"code": "Q9FJ-2JA4"
},
"boundAt": null,
"payload": "https://app.printago.io/qr/v61yt345k7x9a5eaw1tuw1vy?v=1&t=blank&s=nlxbd7jgazwhsb3yt12fhba8"
},
{
"id": "qvq9w2cmd8j16ijv05rjgq5z",
"status": "blank",
"entityType": "blank",
"entityId": null,
"label": "KBWP-MR7C",
"displaySnapshot": {
"code": "KBWP-MR7C"
},
"boundAt": null,
"payload": "https://app.printago.io/qr/qvq9w2cmd8j16ijv05rjgq5z?v=1&t=blank&s=nlxbd7jgazwhsb3yt12fhba8"
}
]
}
Tag a spool
The spool was registered from a blank, so its existing tag comes back: set up, with the blank's code.
{
"targets": [
{
"entityType": "material_instance",
"entityId": "z5y7mec4haa3tnu7xzqbwamd"
}
]
}
{
"batchId": "m98bnxulntrqpazg0fxhocpk",
"tags": [
{
"status": "active",
"entityType": "material_instance",
"entityId": "z5y7mec4haa3tnu7xzqbwamd",
"label": "PLA Basic · Jade White",
"displaySnapshot": {
"code": "HY7Y-MFHE",
"name": "Jade White",
"brand": "Bambu Lab",
"color": "#E8EFE8FF",
"materialName": "PLA Basic"
},
"boundAt": "2026-09-30T04:33:01.192Z",
"payload": "…"
}
]
}
A slot the printer doesn't have
{
"targets": [
{
"entityType": "printer_slot",
"entityId": "q1w3e5r7t9y1u3i5o7p9a1sd",
"amsIndex": 0,
"slotIndex": 0
}
]
}
{
"statusCode": 400,
"code": "unknown_slot",
"message": "Prusa XL has no slot A1/1 (amsIndex 0, slotIndex 0)"
}
Response Schema
The batch the tag was minted in. Reused tags keep the batch they were first minted in.
The short code printed on a blank tag, read from its ID. It stays after the blank is set up, and a spool registered with the blank takes it as its label code.
The text to encode in the QR code. See the payload format above.
Example Response
{
"batchId": "string",
"tags": [
{
"payload": "string",
"id": "string",
"status": "blank",
"entityType": "blank",
"entityId": "string",
"slotAmsIndex": 1,
"slotSlotIndex": 1,
"label": "string",
"batchId": "string",
"payloadVersion": 1,
"displaySnapshot": {
"name": "string",
"provider": "string",
"slotRef": "string",
"materialName": "string",
"brand": "string",
"color": "string",
"code": "string"
},
"printedAt": "2025-01-15T12:00:00.000Z",
"boundAt": "2025-01-15T12:00:00.000Z",
"boundBy": "string",
"revokedAt": "2025-01-15T12:00:00.000Z",
"revokedBy": "string",
"lastScannedAt": "2025-01-15T12:00:00.000Z",
"scanCount": 1,
"storeId": "string",
"updatedAt": "2025-01-15T12:00:00.000Z",
"createdAt": "2025-01-15T12:00:00.000Z"
}
]
}