# 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.

---

### 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**
```json
{
  "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**
```json
{
  "receipt_id": "rcpt_123",
  "status": "NOT_PRINTED",
  "created_at": "2025-09-10T12:34:56Z"
}
```

**Response fields**

| Field        | Type   | Description                                                                 |
|--------------|--------|-----------------------------------------------------------------------------|
| receipt_id   | string | Unique identifier for the receipt request.                                  |
| status       | string | Current status of the print job (`NOT_PRINTED`, `PRINTED`, `FAILED`, etc.). |
| created_at   | string | ISO 8601 timestamp indicating when the receipt request was created.         |
**Common errors:** `400`, `401`, `403`, `429`, `500`.

**cURL**
```bash
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**
```json
{ "receipt_id": "rcpt_789", "status": "NOT_PRINTED" }
```

**cURL**
```bash
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`](https://www.npmjs.com/package/eventsource) polyfill)**
```js
// 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**
```json
{ "status": "PRINTING", "terminal_id": "TERM-001" }
```
```json
{ "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)
```json
{ "type": "Items", "items": [
  { "type": "Text", "text": "Header", "bold": true, "align": "CENTER" },
  { "type": "Text", "text": "VAT 123456789", "align": "CENTER" }
]}
```

#### Table row (example)
```json
{
  "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
```json
{ "type": "Spacer" }
```

#### Divider row
```json
{ "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.

```json
{
  "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:

```json
{
  "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**
```json
{
  "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.
