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
- Create a print job (JSON layout or pre‑rendered image).
- Track the job status via Server‑Sent Events (SSE) until final.
- Treat the job as complete on
PRINTEDorFAILED.
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.
1.1 JSON Layout (Recommended)
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
NOT_PRINTED, PRINTED, FAILED, etc.).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 (
PRINTEDorFAILED). - 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 codesBAD_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
- Choose JSON layout when you need dynamic content (tables, alignment, QR codes, logos). Use image upload when you already have a rasterized receipt.
- 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. - Implement SSE reconnection; the server may close idle streams. Stop listening when a final status is received.
- Handle failure reasons and surface actionable errors to cashiers (e.g., out of paper).
- Do not attempt to modify a job once it is
PRINTEDorFAILED.
Example End‑to‑End Flow
- ePOS sends
POST /poslink/v1/receipt-requestswith JSON layout or image. - API returns
receipt_idandstatus=NOT_PRINTED. - ePOS opens
GET /poslink/v1/receipt-requests/{receipt_id}/status(SSE). - Card Machine downloads the artifact via
fileUrland prints. - SSE emits
PRINTEDorFAILEDand closes; the job is complete.