Registers
- Base path
/api/v1/physical-pos- Identity
- Ed25519 request signature on every request —
X-Key-Id,X-Date,X-Nonce,X-Signature, plusDigeston requests with a body. ±5 min clock window, single-use nonce. A Bearer token or a panel session is not accepted here. - Authorization
- The key must carry the Physical POS product (
PHYSICAL_POS) and the scope for the endpoints on this page:physical_pos:view·physical_pos:manage. Without the product the call gets403 PRODUCT_NOT_ENABLED; without the scope403 API_INSUFFICIENT_SCOPE. See Authentication and Scopes. - Cross-surface
- These endpoints are the s2s surface — call them from your own server with a signed request. The same records have screens in the panel, but panel endpoints cannot be called with an API key, and these endpoints cannot be called with a panel session.
A register is a checkout point inside a store and always belongs to exactly one store. A register's store and code never change. Like stores, registers are environment-less; a test key can only read registers. → Environments
| Method | Path | Scope |
|---|---|---|
GET | /api/v1/physical-pos/stores/{storeId}/registers | physical_pos:view |
POST | /api/v1/physical-pos/stores/{storeId}/registers | physical_pos:manage |
PATCH | /api/v1/physical-pos/registers/{registerId} | physical_pos:manage |
POST | /api/v1/physical-pos/registers/{registerId}/archive | physical_pos:manage |
POST | /api/v1/physical-pos/registers/{registerId}/restore | physical_pos:manage |
GET | /api/v1/physical-pos/registers/{registerId}/audit | physical_pos:view |
The cURL examples compute the signature with lunixi-sign.sh: Authentication › Signing helper.
Register object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Register ID. |
storeId | string (UUID) | The register's store. |
code | string | Register code; upper-case, unique within the store. |
name | string | Register name. |
registerStatus | string | ACTIVE or ARCHIVED. |
rowVersion | integer | Concurrency version. → rowVersion |
createdAt | string | Creation time. |
updatedAt | string | Last change time. |
List registers
GET /api/v1/physical-pos/stores/{storeId}/registers
| Query parameter | Type | Description |
|---|---|---|
status | string | ACTIVE or ARCHIVED. If omitted, both are returned. Any other value gets 400 physical_pos.input.filter_invalid. |
limit | integer | Records per page, 1–100. Default 25. |
cursor | string | pageInfo.nextCursor from the previous response. |
includeTotal | boolean | When true, pageInfo.totalCount is filled. |
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores/00000000-0000-0000-0000-000000000101/registers?status=ACTIVE"
lunixi_sign GET "$REQ_PATH"
curl "https://api-gateway.lunixi.com$REQ_PATH" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"{
"status": "success",
"code": "SUCCESS",
"data": {
"items": [
{
"id": "00000000-0000-0000-0000-000000000201",
"storeId": "00000000-0000-0000-0000-000000000101",
"code": "KASA-1",
"name": "Front Register",
"registerStatus": "ACTIVE",
"rowVersion": 1,
"createdAt": "2026-01-01T07:10:00.000Z",
"updatedAt": "2026-01-01T07:10:00.000Z"
}
],
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null }
},
"pageInfo": { "hasMore": false, "nextCursor": null, "totalCount": null },
"statusCode": 200,
"requestId": "00000000-0000-0000-0000-000000000000"
}If the store does not exist or is not yours: 404 physical_pos.store.not_found.
Create a register
POST /api/v1/physical-pos/stores/{storeId}/registers · Idempotency-Key required
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Register code: A-Z, 0-9, ., _, -; at most 64 characters. Upper-cased; unique within the store (archived registers included). |
name | string | Yes | Register name, at most 120 characters. Follows the text rules. |
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/stores/00000000-0000-0000-0000-000000000101/registers"
BODY='{"code":"kasa-1","name":"Front Register"}'
lunixi_sign POST "$REQ_PATH" "$BODY"
curl -X POST "https://api-gateway.lunixi.com$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "Digest: $DIGEST" \
-H "X-Signature: $SIGNATURE" \
-H "Idempotency-Key: 4f9b2c1e-0000-4000-8000-000000000002" \
--data-raw "$BODY"{
"status": "success",
"code": "SUCCESS",
"data": {
"register": {
"id": "00000000-0000-0000-0000-000000000201",
"storeId": "00000000-0000-0000-0000-000000000101",
"code": "KASA-1",
"name": "Front Register",
"registerStatus": "ACTIVE",
"rowVersion": 1,
"createdAt": "2026-01-01T07:10:00.000Z",
"updatedAt": "2026-01-01T07:10:00.000Z"
},
"replayed": false
},
"statusCode": 201,
"requestId": "00000000-0000-0000-0000-000000000000"
}- A register cannot be created in an archived store:
409 physical_pos.store.archived. - Using the same
Idempotency-Keyto create a register in another store gets409 physical_pos.idempotency_mismatch.
Rename a register
PATCH /api/v1/physical-pos/registers/{registerId}
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
name | string | No | New name, at most 120 characters. |
Only the register's name changes; sending code or storeId in the body returns 400 VALIDATION_ERROR. Without name the request gets 400 physical_pos.input.empty_patch; an empty name gets 400 physical_pos.input.text_invalid. An archived register cannot be updated: 409 physical_pos.register.archived.
source lunixi-sign.sh
REQ_PATH="/api/v1/physical-pos/registers/00000000-0000-0000-0000-000000000201"
BODY='{"rowVersion":1,"name":"Main Register"}'
lunixi_sign PATCH "$REQ_PATH" "$BODY"
curl -X PATCH "https://api-gateway.lunixi.com$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $LUNIXI_KEY_ID" \
-H "X-Date: $DATE" \
-H "X-Nonce: $NONCE" \
-H "Digest: $DIGEST" \
-H "X-Signature: $SIGNATURE" \
--data-raw "$BODY"Response: { "register": { … } }.
Archive a register
POST /api/v1/physical-pos/registers/{registerId}/archive
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
reasonCode | string | Yes | STORE_ARCHIVED, REGISTER_REMOVED, REGISTER_REPLACED, STORE_CLOSED, TEMPORARILY_CLOSED, CREATED_IN_ERROR, DATA_CORRECTION, OTHER. → Reason codes |
Response: { "register": { … } } with registerStatus: "ARCHIVED".
| Blocker | Error |
|---|---|
| A device is assigned to the register | 409 physical_pos.register.has_devices — move the devices to another register or store, move them to storage or retire them. |
| The register is already archived | 409 physical_pos.register.invalid_transition |
Restore a register
POST /api/v1/physical-pos/registers/{registerId}/restore
| Field | Type | Required | Description |
|---|---|---|---|
rowVersion | integer | Yes | The rowVersion you last read. |
reasonCode | string | Yes | REOPENED, ARCHIVED_IN_ERROR, DATA_CORRECTION, OTHER. |
The register's store must be active; if the store is archived: 409 physical_pos.store.archived — restore the store first. If the register is already active: 409 physical_pos.register.invalid_transition.
Response: { "register": { … } } with registerStatus: "ACTIVE".
Change history
GET /api/v1/physical-pos/registers/{registerId}/audit — takes limit and cursor; the response shape is the same as the store history.
Register events: physical_pos.audit.register_created, …register_updated, …register_archived, …register_restored. For registers closed while their store was archived, the event's reasonCode is STORE_ARCHIVED and the store's reason is in changes.storeReason.
Errors
| HTTP | code | When |
|---|---|---|
400 | physical_pos.idempotency.key_required / physical_pos.idempotency.key_invalid | Idempotency-Key missing or malformed on create. |
400 | physical_pos.input.code_invalid | code is malformed. |
400 | physical_pos.input.text_invalid | name breaks the rules. |
400 | physical_pos.input.reason_code_invalid | reasonCode is not on the list or does not fit the operation. |
400 | physical_pos.cursor.invalid | Broken cursor. |
400 | physical_pos.input.filter_invalid | Invalid list filter. |
400 | physical_pos.input.empty_patch | Empty update. |
403 | physical_pos.environment.test_key_read_only | Write request made with a test key. |
404 | physical_pos.store.not_found | The store does not exist or is not yours. |
404 | physical_pos.register.not_found | The register does not exist or is not yours. |
409 | physical_pos.register.code_taken | A register with the same code exists in this store. |
409 | physical_pos.store.archived | Creating a register in an archived store, or restoring a register of an archived store. |
409 | physical_pos.register.archived | Tried to update an archived register. |
409 | physical_pos.register.has_devices | Tried to archive a register with assigned devices. |
409 | physical_pos.register.invalid_transition | Archiving an archived register or restoring an active one. |
409 | physical_pos.version_conflict | rowVersion is not current. |
409 | physical_pos.idempotency_mismatch | The same Idempotency-Key was used with a different request. |
429 | RATE_LIMITED | Store and register write limit exceeded. → Rate limits |
All codes: Error Codes.