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:

POST /printers/{id}/print
{
  "content": "<ticket><c>ORDER 42</c><cut/></ticket>",
  "idempotencyKey": "order-42-receipt"
}

Request fields

FieldRequiredTypeNotes
contentyesstring The ticket. In the default mode this is Ticket XML whose root element must be <ticket>.
idempotencyKeynostring, ≤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.
formatno"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 422 that 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.

FieldMeaning
idThe interaction id. Store it; it is how you ask about this job later.
statusWhere it starts: Routed if the venue is online, Queued if not.
acceptedAtWhen 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.
monitorUrlAbsolute URL of the status endpoint for this interaction.
attemptCountDelivery attempts so far. 0 at submission.
createdAtRow 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 → 200 with 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.

GET /devices/{id}/interactions/{interactionId}
{
  "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.

Pending
In Progress
In Progress
In Progress

Held

Waiting on the world, and self-resolving. Not an error, and not yours to retry.

Queued
Blocked

Terminal

Final. The job will never change status again.

Completed
Printed
Sent (unconfirmed)
Failed
Timed Out
Cancelled
Expired

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.

Every interaction status, in the three groups your code should branch on. Badges are the dashboard's own, so a status looks the same here as it does in History.

Terminal statuses

StatusMeansDo
PrintConfirmedThe 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.
FailedThe job could not be delivered or the device rejected it.Read failureReason, surface it, let a human decide.
ExpiredexpiresAt 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.
CancelledSomebody withdrew it: you, or an operator in the dashboard.Nothing.
TimedOutA 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

StatusMeans
CreatedAccepted, not yet routed.
RoutedOn its way to the connector.
DeliveredThe connector has it.
AcceptedThe connector has taken it for execution.
QueuedHeld 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

CodeMeans
404No such printer, or not yours.
409The printer has no hardware assigned, or the idempotency key was reused with different content.
422The 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.