# ESC/POS Print API

*The following documentation is a work-in-progress and may be subject to change.*

The Softpos app exposes a broadcast intent API that lets external POS applications trigger the Sunmi terminal's built-in printer directly, without having to foreground the Softpos app. This allows you to integrate custom receipt printing into your existing app flow without any disruptive app switching or deep linking. You can send raw ESC/POS commands as a byte array, and optionally receive a callback with the result of the print command.

The following sections describe how to integrate and use this API in your app.

{% stepper %}
{% step %}

#### Declare the permission

Add to your app's `AndroidManifest.xml`:

```xml
<uses-permission android:name="io.vibrant.softpos.permission.PRINT_ESC_COMMAND" />
```

The Softpos app declares this permission. Without it the broadcast is silently dropped by the OS.
{% endstep %}

{% step %}

#### Send the print intent

An ESC/POS print command is sent as a broadcast intent with the action `io.vibrant.softpos.action.PRINT_ESC_COMMAND` and the raw command bytes in the `io.vibrant.softpos.extra.ESC_BYTES` extra. For example:

```kotlin
fun printEscReceipt(
    context: Context,
    escBytes: ByteArray,
    correlationId: String? = null,          // optional — echoed back in the result callback
) {
    val intent = Intent("io.vibrant.softpos.action.PRINT_ESC_COMMAND").apply {
        component = ComponentName(
            "io.vibrant.softpos",
            "io.vibrant.softpos.print.EscPrintBroadcastReceiver"
        )
        putExtra("io.vibrant.softpos.extra.ESC_BYTES", escBytes)

        // Optional: request a result callback (see step 3)
        putExtra("io.vibrant.softpos.extra.CALLBACK_ACTION", "com.your.app.action.PRINT_RESULT")
        putExtra("io.vibrant.softpos.extra.CALLBACK_PACKAGE", "com.your.app")
        correlationId?.let { putExtra("io.vibrant.softpos.extra.CORRELATION_ID", it) }
    }
    context.sendBroadcast(intent)
}
```

The explicit `ComponentName` is required on Android 8+. Implicit broadcasts to exported receivers are blocked by the OS unless the component is named directly.

> **Note:** Do not pass a permission string as the second argument to `sendBroadcast`. That parameter filters *receivers* by a permission they must hold — it does not assert the sender's permission. The sender permission is enforced automatically by the `android:permission` attribute on the receiver; you only need `<uses-permission>` in your manifest.
> {% endstep %}

{% step %}

#### Receive the result (optional)

You can optionally receive a callback with the result of the print command by including the extras `CALLBACK_ACTION` and `CALLBACK_PACKAGE` in your intent. The Softpos app sends a broadcast with the specified action and package when the print command is processed, including extras for the result code, message, and correlation ID (if provided).

This allows you to handle success or failure of the print command and display appropriate feedback to the user. For example, you could show a confirmation message on success or an error dialog if the printer is unavailable.

To receive the callback, declare a `BroadcastReceiver` in your app with an intent filter matching the action you specified in `CALLBACK_ACTION`. For example:

```xml
<receiver android:name=".PrintResultReceiver" android:exported="false">
    <intent-filter>
        <action android:name="com.your.app.action.PRINT_RESULT" />
    </intent-filter>
</receiver>
```

```kotlin
class PrintResultReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val code = intent.getIntExtra("io.vibrant.softpos.extra.RESULT_CODE", -1)
        val message = intent.getStringExtra("io.vibrant.softpos.extra.RESULT_MESSAGE")
        val correlationId = intent.getStringExtra("io.vibrant.softpos.extra.CORRELATION_ID")

        when (code) {
            0 -> { /* success */ }
            1 -> { /* printer not available — hardware issue or non-Sunmi device */ }
            2 -> { /* feature not enabled for this merchant's plan */ }
            3 -> { /* invalid input — check message for details */ }
            4 -> { /* unexpected error — check message for details */ }
            5 -> { /* app not initialized — ask the merchant to open the Softpos app first */ }
        }
    }
}
```

{% endstep %}
{% endstepper %}

### Reference

#### Print request extras

<table><thead><tr><th width="225">Key</th><th width="109">Type</th><th width="113">Required</th><th>Description</th></tr></thead><tbody><tr><td><code>ESC_BYTES</code></td><td><code>byte[]</code></td><td>Yes</td><td>Raw ESC/POS command buffer (no charset init needed — see section 5)</td></tr><tr><td><code>CHARSET</code></td><td><code>byte[]</code></td><td>No</td><td>Charset init bytes sent before the payload — defaults to UTF-8 (<code>1C 43 FF</code>), see section 5</td></tr><tr><td><code>CALLBACK_ACTION</code></td><td><code>String</code></td><td>No</td><td>Broadcast action for the result callback</td></tr><tr><td><code>CALLBACK_PACKAGE</code></td><td><code>String</code></td><td>No</td><td>Your package name (required if using callback)</td></tr><tr><td><code>CORRELATION_ID</code></td><td><code>String</code></td><td>No</td><td>Arbitrary ID echoed in the callback</td></tr></tbody></table>

*Note: All extras should be prefixed with* `io.vibrant.softpos.extra.`*. The keys in the table above are shown without the prefix for readability.*

#### Result codes

<table><thead><tr><th width="113">Code</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Print successful</td></tr><tr><td><code>1</code></td><td>Printer not available (hardware issue, broken printer, or non-Sunmi device)</td></tr><tr><td><code>2</code></td><td>Feature not enabled for this merchant's plan</td></tr><tr><td><code>3</code></td><td>Invalid input (e.g. empty or oversized byte array — see <code>RESULT_MESSAGE</code>)</td></tr><tr><td><code>4</code></td><td>Unexpected error — see <code>RESULT_MESSAGE</code> for details</td></tr><tr><td><code>5</code></td><td>App not initialized — the Softpos app must be opened and signed in before a print command can be processed</td></tr></tbody></table>

#### Intent targeting

<table><thead><tr><th width="192">Type</th><th>Value</th></tr></thead><tbody><tr><td>Action</td><td><code>io.vibrant.softpos.action.PRINT_ESC_COMMAND</code></td></tr><tr><td>Target package</td><td><code>io.vibrant.softpos</code></td></tr><tr><td>Target class</td><td><code>io.vibrant.softpos.print.EscPrintBroadcastReceiver</code></td></tr><tr><td>Sender permission</td><td><code>io.vibrant.softpos.permission.PRINT_ESC_COMMAND</code></td></tr></tbody></table>

### Character encoding

Charset initialisation is handled automatically — do not include any charset command in your payload. The Softpos app sends the charset init as a separate command immediately before your payload.

The default is **UTF-8** (`1C 43 FF` — Sunmi proprietary command). To use a different charset, supply its raw initialisation bytes via the `CHARSET` extra:

```kotlin
// Default — UTF-8. No CHARSET needed.
"Tak for dit køb!".toByteArray(Charsets.UTF_8)

// Example: PC850 via ESC t 02 — only if your payload is CP850-encoded
putExtra("io.vibrant.softpos.extra.CHARSET", byteArrayOf(0x1B, 0x74, 0x02))
"Tak for dit køb!".toByteArray(Charsets.forName("CP850"))
```

Refer to the [Sunmi ESC/POS documentation](https://developer.sunmi.com/) for the correct init bytes for your target charset. Any valid Sunmi charset command can be passed — there is no fixed list of supported values.

> **Do not start your payload with `ESC @` (RESET — `0x1B 0x40`).** The printer reset command reverts the printer to power-on defaults, which undoes the charset init the Softpos app sent just before your payload. If you include a RESET in your payload, the charset will revert to the printer's default and your text may print blank or garbled. Begin your payload directly with your formatting commands (alignment, font size, etc.).

### Minimal test

A simple payload to verify the integration end-to-end before building your full receipt:

```kotlin
val testBytes = byteArrayOf(
    0x1B, 0x61, 0x01,                                          // ESC a 1 — center align
    *"Integration test\n\n".toByteArray(Charsets.UTF_8),       // content
    0x1D, 0x56, 0x42, 0x00                                     // GS V B 0 — partial cut
)

val intent = Intent("io.vibrant.softpos.action.PRINT_ESC_COMMAND").apply {
    component = ComponentName("io.vibrant.softpos", "io.vibrant.softpos.print.EscPrintBroadcastReceiver")
    putExtra("io.vibrant.softpos.extra.ESC_BYTES", testBytes)
    putExtra("io.vibrant.softpos.extra.CALLBACK_ACTION", "com.your.app.action.PRINT_RESULT")
    putExtra("io.vibrant.softpos.extra.CALLBACK_PACKAGE", "com.your.app")
    putExtra("io.vibrant.softpos.extra.CORRELATION_ID", "test-001")
}
context.sendBroadcast(intent)
```

A successful result prints "Integration test" centred with a partial cut, and your callback receiver gets result code `0`.

### Constraints

* **Max payload:** 512 KB. Payloads above this are rejected with result code `3`.
* **Timing:** Send after your payment confirmation is fully processed. The Softpos app will be in the background at that point and will handle the broadcast without any app switch.
* **Softpos must be installed:** If the app is not installed the broadcast is a no-op.
* **Merchant must have printing enabled:** Accounts without the printing feature return code `2`.
