Integrate
Printing a ticket
Submit a job, read the 202, and follow it to a real outcome.
Submitting a job
POST /printers/{id}/print with the ticket as Ticket XML:
{
"content": "<ticket><c>ORDER 42</c><cut/></ticket>",
"idempotencyKey": "order-42-receipt"
}Request fields
| Field | Required | Type | Notes |
|---|---|---|---|
content | yes | string | The ticket. In the default mode this is Ticket XML whose root element must be <ticket>. |
idempotencyKey | no | string, ≤200 chars | Your safe-retry key. Use a stable business identity such as order id + receipt type. Omitting it means a retried request prints twice. See below. |
format | no | "Xml" | "Svg" | Render mode; defaults to "Xml". Use the default unless you specifically want the graphic renderer. "Svg" input is validated by rendering it, so a ticket that cannot be rendered is rejected at submission rather than failing at the printer. The device can override this: a printer whose command language is a bitmap-only dialect has no text mode to render into, so a job sent as "Xml" is silently promoted to "Svg" rather than rejected. Author the same Ticket XML either way and it does not matter. One job holds about a hundred labels; a very large ticket may still be refused, so split a longer batch. |
There is no priority, no copies count, and no printer-settings override in the request. Paper width, character set and command language belong to the device, not to the job, so the same ticket submitted to two printers comes out correct on both. They are configured once, in the dashboard: see Printer settings.
Printing to a label printer
The request is identical. What changes is the page: a label printer prints onto one label rather than onto a roll that ends where the content does, and Raven lays your ticket out on that page before it leaves the platform. Nothing about the ticket you author changes, and there is no field to set on the job. The label's size lives on the printer, in Media.
Three consequences are worth knowing before you send one:
- The ticket has to fit one label. If it does not, submission is rejected with a
422that names how many printable rows the label holds. Shorten the ticket or declare longer stock; Raven will not print a fragment and carry the rest onto the next label, because nothing downstream could tell you that it had. - A batch is written with
<page/>, not<cut/>.<page/>ends the page and cuts nothing, which is what separating fifty labels needs: the stock already comes apart at the die-cut gap.<cut/>ends the page and drives the cutter, so fifty labels with one cut at the end is forty-nine<page/>and a single trailing<cut/>. Each page is measured against the label on its own, so a two-page ticket is two labels, not one long one. - Where a cut may fall is limited by the hardware. A label head is told "cut every n labels" and nothing finer, so cuts have to be evenly spaced: after every label, after every third, or once at the end of the batch (the common case). A ticket asking for anything else is refused rather than approximated, because both approximations damage stock silently: one cuts a label the ticket wanted whole, the other leaves a strip to separate by hand while reporting success.
<pulse/>fails the job. A label printer has no cash-drawer port, so there is nothing for it to drive. Raven refuses the job rather than dropping the drawer kick quietly, because in a venue where that kick opens the till, silence is the worse answer. Send drawer kicks to the receipt printer that has the drawer.
What comes back
The response is always 202 Accepted, whether the venue is online or not.
| Field | Meaning |
|---|---|
id | The interaction id. Store it; it is how you ask about this job later. |
status | Where it starts: Routed if the venue is online, Queued if not. |
acceptedAt | When Raven took ownership. |
expiresAt | The deadline: acceptedAt plus the printer's job lifetime, 24 hours by default. If nobody can deliver it by then the job ends as Expired. This is the only budget that ends a job; there is no attempt ceiling. Read the field rather than assuming the default; it is per-printer and an operator can shorten it. |
monitorUrl | Absolute URL of the status endpoint for this interaction. |
attemptCount | Delivery attempts so far. 0 at submission. |
createdAt | Row creation time. |
Accepted is not printed. It means Raven has taken durable ownership of the job. If the connector is offline the job is held as Queued and delivered when the venue comes back. You do not need to retry, and retrying is how duplicate receipts happen.
Because the default lifetime is a full day, a ticket accepted after a venue closes is still waiting when it reopens. That is usually what you want for a kitchen order and rarely what you want for a table receipt, so decide per printer rather than per job.
Idempotency
Send an idempotencyKey built from your own business identity: the order id plus the receipt type, for instance. Then:
- Same key, same content →
200with the original interaction. Nothing prints twice. - Same key, different content →
409. You are reusing a key for a new job; fix the key. - Two concurrent first submissions of one key → one interaction, never two.
This is what makes a network timeout on your side safe: repeat the request with the same key and you either create the job or learn it already exists.
Did it actually print?
Poll the monitorUrl until the status is terminal.
{
"id": "8f1c…",
"deviceType": "Printer",
"status": "PrintConfirmed",
"failureReason": null,
"attemptCount": 1,
"expiresAt": "2026-08-03T09:14:22Z",
"nextAttemptAt": null,
"lastCheckedAt": "2026-08-02T09:14:25Z",
"completedAt": "2026-08-02T09:14:25Z"
}This is the status set for every interaction, not just printing, which is why a few entries below are marked as belonging to scales and bridges. Three groups, and the group matters more than the individual status:
Moving
Raven owns it and is working on it. Keep polling.
Held
Waiting on the world, and self-resolving. Not an error, and not yours to retry.
Terminal
Final. The job will never change status again.
A job leaves Held on its own, when a connector reconnects or somebody puts paper in. Only the deadline (expiresAt) moves it to a terminal state against its will.
Terminal statuses
| Status | Means | Do |
|---|---|---|
PrintConfirmed | The printer positively confirmed the job. | Done. Tell your user it printed. |
SentUnconfirmed | The bytes were delivered, but this printer cannot confirm the outcome. Neither a success nor a failure; the reason is in failureReason. | Treat as probably-printed. Do not resend automatically; a duplicate receipt is worse than a missing one that staff can reprint. |
Completed | Terminal success for a scale or bridge. Printer jobs never reach it; their successes are the two above. | Done. The response body already gave you the result. |
Failed | The job could not be delivered or the device rejected it. | Read failureReason, surface it, let a human decide. |
Expired | expiresAt passed before anyone could deliver it. This is the only budget that ends a job. There is no attempt ceiling. | The venue was down for the whole window. Resubmit if the ticket still matters. |
Cancelled | Somebody withdrew it: you, or an operator in the dashboard. | Nothing. |
TimedOut | A synchronous request got no answer in time (scales and bridges, not printing). | Retry if the operation is safe to repeat. |
Statuses that are still moving
| Status | Means |
|---|---|
Created | Accepted, not yet routed. |
Routed | On its way to the connector. |
Delivered | The connector has it. |
Accepted | The connector has taken it for execution. |
Queued | Held because the venue is offline. It will be delivered when the connector reconnects. |
Blocked | Held at the device on a clearable fault: out of paper, cover open. The connector resumes it once the fault clears; failureReason says which one. |
Blocked is the one worth surfacing in your own UI: it is not an error you should retry around, it is a person needing to put paper in a printer.
Cancelling
Any non-terminal job can be withdrawn:
curl -X POST https://raven-api.formfl.be/devices/{deviceId}/interactions/{interactionId}/cancel \
-H "Authorization: ApiKey $RAVEN_KEY"Looking back at past jobs
An API key reads interactions one at a time, by id, using the same endpoint monitorUrl points at. It stays readable after the job is terminal, so it is also the after-the-fact answer to "what happened to order 42".
There is no key-reachable endpoint that lists a device's interactions today. Two consequences worth designing around:
- Store the interaction id against your own order. It is the only handle you get, and you cannot search for it later.
- Browsing, filtering and per-device failure summaries live in the dashboard's History view, which a person signs in to. It is an operator surface, not an integration one.
Scales are the exception: GET /scales/{id}/readings lists past readings for a scale. See Weighing.
Errors at submission
| Code | Means |
|---|---|
404 | No such printer, or not yours. |
409 | The printer has no hardware assigned, or the idempotency key was reused with different content. |
422 | The ticket could not be rendered: malformed, wrong root element, or too large. |
429 | Rate limited. Back off and retry with the same idempotencyKey; nothing was accepted. See Rate limits. |