Print Custom Receipt

Overview

The Custom Receipt API allows ePOS partners to print receipts, invoices, and images on Teya payment card machines. Integrations use the POSLink service to create a print job and monitor status until the card machine completes printing.

Workflow summary

  1. Create a print job (JSON layout or pre‑rendered image).
  2. Track the job status via Server‑Sent Events (SSE) until final.
  3. Treat the job as complete on PRINTED or FAILED.

Environments

Environment Base URL
Production https://api.teya.com
Staging https://api.teya.xyz

Authentication

All endpoints require a valid JWT in the Authorization header.

Authorization: Bearer <your-jwt-token>

Status Model

Status Final Description
NOT_PRINTED No Job created but not yet processed.
ENQUEUED No Job queued for delivery to the card machine.
PRINTING No Card Machine is printing.
PRINTED Yes Printing completed successfully.
FAILED Yes Printing failed.
UNKNOWN - Status cannot be determined.

Failure reasons when status = FAILED may include: TIMEOUT, BUSY, OUT_OF_PAPER, OVERHEATED, COVER_OPEN, LOW_BATTERY, OTHER.

Final statuses (PRINTED, FAILED) are immutable.


Step 1 - Create a Custom Receipt

This step creates a print job that the card machine will render and print.
You can provide either a structured JSON layout (recommended) or a pre‑rendered image (PNG/JPG).
Both methods return a receipt_id that you can later use to track print status.


Use this when you want to generate dynamic receipts with text, tables, QR codes, or images.

Endpoint

POST /poslink/v1/receipt-requests

Headers

Content-Type: application/json
Authorization: Bearer <token>

Request Body Example

{
  "store_id": "4b33fd70-7c92-4060-9898-f066e77d3ae1",
  "terminal_id": "80c267d3",
  "print_model": {
    "rows": [
      { "type": "Items", "items": [
        { "type": "Text", "text": "My Store", "bold": true, "align": "CENTER" },
        { "type": "Text", "text": "VAT 123456789", "align": "CENTER" }
      ]},
      { "type": "Divider" },
      { "type": "Table",
        "header_cells": [
          { "element": { "type": "Text", "text": "QTY PRODUCT" }, "weight": 1.5 },
          { "element": { "type": "Text", "text": "VAT", "align": "RIGHT" }, "weight": 1.0 },
          { "element": { "type": "Text", "text": "TOTAL", "align": "RIGHT" }, "weight": 1.0 }
        ],
        "rows": [
          { "cells": [
            { "type": "Text", "text": "1 Coffee" },
            { "type": "Text", "text": "23%", "align": "RIGHT" },
            { "type": "Text", "text": "2.50", "align": "RIGHT" }
          ]}
        ]
      },
      { "type": "Spacer" },
      { "type": "Items", "items": [
        { "type": "QrCode", "url": "https://teya.com", "align": "LEFT" }
      ]}
    ]
  }
}

Response

{
  "receipt_id": "rcpt_123",
  "status": "NOT_PRINTED",
  "created_at": "2025-09-10T12:34:56Z"
}

Response fields

receipt_idstring
Unique identifier for the receipt request.
statusstring
Current status of the print job (NOT_PRINTED, PRINTED, FAILED, etc.).
created_atstring
ISO 8601 timestamp indicating when the receipt request was created.
Common errors: 400, 401, 403, 429, 500.

cURL

curl -X POST "$BASE_URL/poslink/v1/receipt-requests" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d @custom-receipt.json

1.2 Pre‑Rendered Image

Use this when you already have a complete raster image of the receipt.

Endpoint

POST /poslink/v1/receipt-requests

Headers

Authorization: Bearer <token>
Content-Type: multipart/form-data

Query parameters

  • store_id (UUID, required)
  • terminal_id (string, required)

Form fields

  • image (file, required)

Image File Constraints

To ensure successful printing, your uploaded image must meet the following requirements:

Enforced by the service. An upload that breaks one of these is rejected before it is processed:

Constraint Value On breach
Supported formats PNG, JPEG 415 UNSUPPORTED_MEDIA_TYPE, description is Invalid request parameter, with {"path": "image", "reason": "Only PNG and JPEG images are supported"} in invalid_params
Max file size 200 KB 400 BAD_REQUEST, description is Uploaded file exceeds the maximum allowed size
Max request size 205 KB 400 BAD_REQUEST, same description. Covers the image part and multipart framing; store_id and terminal_id travel in the query string and do not count

Send the image with a matching Content-Type of image/png or image/jpeg. The format is taken
from that header, so a mislabelled file is rejected even if its contents are valid.

Recommendations. Not enforced; images outside these are accepted but may print poorly:

Constraint Recommendation
Width 384 px (matches printer width; wider images are scaled down)
Height Up to 10000 px
DPI 203 DPI (printer native resolution)
Color Grayscale (the printer is monochrome)
  • For best results, use PNG, 384 px wide, 203 DPI, converted to grayscale.

Response

{ "receipt_id": "rcpt_789", "status": "NOT_PRINTED" }

cURL

curl -X POST \
  "$BASE_URL/poslink/v1/receipt-requests?store_id=<uuid>&terminal_id=<id>" \
  -H "Authorization: Bearer $JWT" \
  -F "image=@/path/to/receipt.png"

Step 2 - Track Print Status (SSE)

Use when: you need real‑time updates of a job until final status.

Endpoint

GET /poslink/v1/receipt-requests/{receipt_id}/status

Headers

Accept: text/event-stream
Authorization: Bearer <token>

Behavior

  • Emits JSON payloads as the job progresses.
  • The stream closes automatically on a final status (PRINTED or FAILED).
  • If the job is already in a final status, a single event is sent and the connection closes.
  • The server may close the connection after approximately one minute of inactivity; reconnect if you still expect updates.

Sample event frame

data: {"receipt_id":"rcpt_123","status":"ENQUEUED","status_reason":null,"updated_at":"2025-09-10T12:35:05Z"}

Minimal Node.js example (requires eventsource polyfill)

// Requires: npm install eventsource
const EventSource = require('eventsource');

const es = new EventSource(
  `${BASE_URL}/poslink/v1/receipt-requests/${receiptId}/status`,
  { headers: { Authorization: `Bearer ${JWT}` } }
);

es.onmessage = (evt) => {
  const msg = JSON.parse(evt.data);
  if (msg.status === 'PRINTED' || msg.status === 'FAILED') es.close();
};

Terminal‑Side Endpoints (Reference)

These endpoints are called exclusively by Teya terminal applications and should not be called by ePOS integrations.

Get Receipt File

GET /poslink/v1/receipt-requests/{receipt_id}/fileUrl

Returns an HTTP 302 redirect to a presigned S3 URL used by the terminal to download the printable artifact.

Update Receipt Status

PATCH /poslink/v1/receipt-requests/{receipt_id}

Body examples

{ "status": "PRINTING", "terminal_id": "TERM-001" }
{ "status": "FAILED", "status_reason": "OUT_OF_PAPER", "terminal_id": "TERM-001" }

Responses: 200 OK, 404 NOT_FOUND, 409 CONFLICT (if the job is already final).


Data Model

PrintDocument (JSON layout)

Describes the printable receipt.

  • rows (array, required): between 1 and 150 items, each representing a row.

Enforced size limit. The whole JSON request body must not exceed 250 KB. Embedded images
are carried as Base64, which inflates them by roughly a third, so a receipt with a logo can approach
the cap faster than the row count suggests. A larger body is rejected with 400 BAD_REQUEST before it is
processed. description is Invalid payload, with
{"path": "request.body", "reason": "must not exceed 250KB"} in invalid_params.

Row types

  • Items: a horizontal list of elements.
  • Table: header with weighted columns and an array of data rows.
  • Spacer: vertical spacing.
  • Divider: horizontal rule.

Items row (example)

{ "type": "Items", "items": [
  { "type": "Text", "text": "Header", "bold": true, "align": "CENTER" },
  { "type": "Text", "text": "VAT 123456789", "align": "CENTER" }
]}

Table row (example)

{
  "type": "Table",
  "header_cells": [
    { "element": { "type": "Text", "text": "QTY PRODUCT" }, "weight": 1.5 },
    { "element": { "type": "Text", "text": "VAT", "align": "RIGHT" }, "weight": 1.0 },
    { "element": { "type": "Text", "text": "TOTAL", "align": "RIGHT" }, "weight": 1.0 }
  ],
  "rows": [
    { "cells": [
      { "type": "Text", "text": "1 Coffee" },
      { "type": "Text", "text": "23%", "align": "RIGHT" },
      { "type": "Text", "text": "2.50", "align": "RIGHT" }
    ]}
  ]
}

Spacer row

{ "type": "Spacer" }

Divider row

{ "type": "Divider" }

Element types

  • Text

    • type: "Text"
    • text (string, required)
    • bold (boolean, optional)
    • align (LEFT | RIGHT | CENTER, optional)
  • QrCode

    • type: "QrCode"
    • url (string, required)
    • align (LEFT | RIGHT | CENTER, optional)
  • Image

    • type: "Image"
    • base64 (string, required)
    • align (LEFT | RIGHT | CENTER, optional)

SSE Event Schema

Each SSE frame contains a JSON object on the data: line, terminated by a blank line.

{
  "receipt_id": "rcpt_123",
  "status": "ENQUEUED",
  "status_reason": null,
  "updated_at": "2025-09-10T12:35:05Z"
}

Error Model

All error responses use a standard envelope:

{
  "code": "ERROR_CODE",
  "description": "Human-readable description",
  "invalid_params": [
    { "path": "field.path", "reason": "Validation error reason" }
  ]
}

Typical codes
BAD_REQUEST, UNAUTHORISED, FORBIDDEN, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS, UNSUPPORTED_MEDIA_TYPE, INTERNAL_SERVER_ERROR, GONE.

Validation example

{
  "code": "BAD_REQUEST",
  "description": "Invalid input",
  "invalid_params": [
    { "path": "print_model.rows", "reason": "must not be empty" },
    { "path": "store_id", "reason": "must be a valid UUID" }
  ]
}

Best Practices

  1. Choose JSON layout when you need dynamic content (tables, alignment, QR codes, logos). Use image upload when you already have a rasterized receipt.
  2. Stay within the 1-150 row limit for print_model.rows, and keep the whole JSON body under 250 KB. Base64-embedded images count toward that.
  3. Implement SSE reconnection; the server may close idle streams. Stop listening when a final status is received.
  4. Handle failure reasons and surface actionable errors to cashiers (e.g., out of paper).
  5. Do not attempt to modify a job once it is PRINTED or FAILED.

Example End‑to‑End Flow

  1. ePOS sends POST /poslink/v1/receipt-requests with JSON layout or image.
  2. API returns receipt_id and status=NOT_PRINTED.
  3. ePOS opens GET /poslink/v1/receipt-requests/{receipt_id}/status (SSE).
  4. Card Machine downloads the artifact via fileUrl and prints.
  5. SSE emits PRINTED or FAILED and closes; the job is complete.