Receive prints on your server
Last updated:
With the monthly or yearly subscription, iiziticket can send every print to a web address you choose: your management software, a script, Zapier, Make, n8n, Google Apps Script. You get one item per printed copy, with the form values and the counter numbers. Totals, summaries and anything else are up to your own services.
Prints are sent by the phone or tablet that prints them: the data never goes through iiziticket's servers.
In short
- One JSON
POSTrequest per print, with one item per copy. - Signed with HMAC-SHA256: your server checks it comes from your device.
- If the server does not answer, the print waits in a queue and is sent again; printing never waits for it.
- Set it up in the app: Settings → Integrations.
Setup
- Address (URL): where to receive prints. It must start with
https://. For a server on the shop's network,http://is allowed with local addresses (192.168.x.x,10.x.x.x,172.16–31.x.x,*.local). - Continuous sending: on, each print is sent right after it comes out; off, prints wait in the queue until you tap Send now.
- Device name: sent with every print (e.g. "Till 1"), useful when printing from several devices.
- Signing secret: lets you check that requests really come from your device. Copy it from the ⋮ menu; "Regenerate" changes it.
- Extra header (optional): for services that want a fixed token, for example
Authorization: Bearer …. - Send test: sends a
testrequest to check the address and the signature.
The setup applies to the device you make it on: with several tills, repeat it on each one.
When prints arrive
- With continuous sending: right after each print and when the app is opened.
- Always: with Send now.
- Prints are sent one at a time, in order. If your server does not answer or answers with an error, that print and the following ones wait in the queue and are tried again at the next print, when the app opens or with Send now.
- Nothing is sent while the app is closed.
- The same print can arrive more than once (for example if the server's answer gets lost): recognise it by its
id.
In Integrations you can see, day by day, the tickets printed and how many were sent, are queued or failed. A print your server always refuses can be discarded.
The request
POST with a JSON body, one request per print:
{
"id": "0b6c2f0e-6a1d-4c39-9d0e-0a4f1f1d2b7c",
"type": "print.completed",
"version": 1,
"createdAt": "2026-11-09T20:15:03.120+01:00",
"device": { "id": "d7a1…", "name": "Cassa 1", "counterPrefix": "A" },
"data": {
"printId": "0b6c2f0e-6a1d-4c39-9d0e-0a4f1f1d2b7c",
"reprint": false,
"reprintOf": null,
"template": { "id": "3f2c…", "name": "Biglietto ingresso" },
"fields": [
{ "key": "tipo", "label": "Tipo", "type": "select", "value": "Intero", "text": "Intero" },
{ "key": "prezzo", "label": "Prezzo", "type": "price", "value": 5, "text": "5,00 €" }
],
"copies": [
{ "id": "0b6c…-1", "n": 1, "counters": { "numero": { "value": 12, "text": "A-0012" } } },
{ "id": "0b6c…-2", "n": 2, "counters": { "numero": { "value": 13, "text": "A-0013" } } }
]
}
}fields: the form fields, the same for every copy.valueis the raw value (numbers as numbers, dates in ISO 8601:2026-11-09for a date, with the time zone for date and time),textis as printed.typeis one oftext,number,price,select,date,datetime.copies: one item per printed copy, with its ownidand the numbers of the counter fields (with the device prefix, if any). For one row per ticket, use one item ofcopiesper row.- Reprints:
reprint: trueandreprintOfwith the id of the original print; the copies have the same numbers as the original. They are not new tickets: leave them out of totals if needed. - Only the copies that actually came out of the printer are sent.
type: "test": the Send test request, with the same format and sample data.
Your server must answer with a 2xx code (the response body is not read) within 15 seconds. A 302 or 303 after the request is followed with a GET without data (as Google Apps Script does); 301, 307 and 308 are errors: update the address.
Headers and signature
Content-Type: application/json; charset=utf-8
User-Agent: iiziticket/1.0.0
X-Iiziticket-Event: print.completed
X-Iiziticket-Delivery: <id>
X-Iiziticket-Timestamp: 1794251703
X-Iiziticket-Signature: sha256=<hex>The signature is the HMAC-SHA256, in hex, of timestamp + "." + body, keyed with the secret. To check it:
- read the body as is, before parsing it as JSON;
- compute the signature and compare it with
X-Iiziticket-Signaturein constant time; - refuse requests whose
X-Iiziticket-Timestampis more than a few minutes old.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/iiziticket', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Iiziticket-Timestamp');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.IIZITICKET_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const signature = req.get('X-Iiziticket-Signature') ?? '';
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
if (!fresh || signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
// event.data.copies: one item per printed ticket
res.sendStatus(204);
});$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_IIZITICKET_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('IIZITICKET_SECRET'));
$signature = $_SERVER['HTTP_X_IIZITICKET_SIGNATURE'] ?? '';
if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(204);import hashlib, hmac, os, time
from flask import Flask, request
app = Flask(__name__)
@app.post("/iiziticket")
def iiziticket():
body = request.get_data()
timestamp = request.headers.get("X-Iiziticket-Timestamp", "")
expected = "sha256=" + hmac.new(
os.environ["IIZITICKET_SECRET"].encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256,
).hexdigest()
signature = request.headers.get("X-Iiziticket-Signature", "")
if abs(time.time() - int(timestamp or 0)) > 300 or not hmac.compare_digest(expected, signature):
return "", 401
event = request.get_json()
return "", 204Services such as Zapier or Make do not check the signature: in that case use an address that is hard to guess (the one they give you) or the extra header, if the service supports it.
Example: a Google Sheet
- Create a sheet with the columns you need (e.g. date, till, template, type, price, number, reprint).
- Extensions → Apps Script, paste:
function doPost(e) {
const event = JSON.parse(e.postData.contents);
if (event.type !== 'print.completed') return ContentService.createTextOutput('ok');
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
const field = (key) => (event.data.fields.find((f) => f.key === key) || {}).value;
for (const copy of event.data.copies) {
const counter = Object.values(copy.counters)[0];
sheet.appendRow([
event.createdAt, event.device.name, event.data.template.name,
field('tipo'), field('prezzo'), counter ? counter.text : '', event.data.reprint,
]);
}
return ContentService.createTextOutput('ok');
}- Deploy → New deployment → Web app, access "Anyone", and paste the URL into iiziticket.
Apps Script cannot read headers, so it cannot check the signature: keep the URL private.
Privacy
Prints go from your device to the address you choose: iiziticket neither receives nor stores them. You are responsible for that data and for where you send it. Everything else is in the privacy policy.