Physical POS
API Reference
Registers

Registers

Who calls these endpoints?External API (s2s)
Base path
/api/v1/physical-pos
Identity
Ed25519 request signature on every request — X-Key-Id, X-Date, X-Nonce, X-Signature, plus Digest on 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 gets 403 PRODUCT_NOT_ENABLED; without the scope 403 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

MethodPathScope
GET/api/v1/physical-pos/stores/{storeId}/registersphysical_pos:view
POST/api/v1/physical-pos/stores/{storeId}/registersphysical_pos:manage
PATCH/api/v1/physical-pos/registers/{registerId}physical_pos:manage
POST/api/v1/physical-pos/registers/{registerId}/archivephysical_pos:manage
POST/api/v1/physical-pos/registers/{registerId}/restorephysical_pos:manage
GET/api/v1/physical-pos/registers/{registerId}/auditphysical_pos:view

The cURL examples compute the signature with lunixi-sign.sh: Authentication › Signing helper.

Register object

FieldTypeDescription
idstring (UUID)Register ID.
storeIdstring (UUID)The register's store.
codestringRegister code; upper-case, unique within the store.
namestringRegister name.
registerStatusstringACTIVE or ARCHIVED.
rowVersionintegerConcurrency version. → rowVersion
createdAtstringCreation time.
updatedAtstringLast change time.

List registers

GET /api/v1/physical-pos/stores/{storeId}/registers

Query parameterTypeDescription
statusstringACTIVE or ARCHIVED. If omitted, both are returned. Any other value gets 400 physical_pos.input.filter_invalid.
limitintegerRecords per page, 1–100. Default 25.
cursorstringpageInfo.nextCursor from the previous response.
includeTotalbooleanWhen 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

FieldTypeRequiredDescription
codestringYesRegister code: A-Z, 0-9, ., _, -; at most 64 characters. Upper-cased; unique within the store (archived registers included).
namestringYesRegister 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-Key to create a register in another store gets 409 physical_pos.idempotency_mismatch.

Rename a register

PATCH /api/v1/physical-pos/registers/{registerId}

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
namestringNoNew 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

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
reasonCodestringYesSTORE_ARCHIVED, REGISTER_REMOVED, REGISTER_REPLACED, STORE_CLOSED, TEMPORARILY_CLOSED, CREATED_IN_ERROR, DATA_CORRECTION, OTHER. → Reason codes

Response: { "register": { … } } with registerStatus: "ARCHIVED".

BlockerError
A device is assigned to the register409 physical_pos.register.has_devices — move the devices to another register or store, move them to storage or retire them.
The register is already archived409 physical_pos.register.invalid_transition

Restore a register

POST /api/v1/physical-pos/registers/{registerId}/restore

FieldTypeRequiredDescription
rowVersionintegerYesThe rowVersion you last read.
reasonCodestringYesREOPENED, 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

HTTPcodeWhen
400physical_pos.idempotency.key_required / physical_pos.idempotency.key_invalidIdempotency-Key missing or malformed on create.
400physical_pos.input.code_invalidcode is malformed.
400physical_pos.input.text_invalidname breaks the rules.
400physical_pos.input.reason_code_invalidreasonCode is not on the list or does not fit the operation.
400physical_pos.cursor.invalidBroken cursor.
400physical_pos.input.filter_invalidInvalid list filter.
400physical_pos.input.empty_patchEmpty update.
403physical_pos.environment.test_key_read_onlyWrite request made with a test key.
404physical_pos.store.not_foundThe store does not exist or is not yours.
404physical_pos.register.not_foundThe register does not exist or is not yours.
409physical_pos.register.code_takenA register with the same code exists in this store.
409physical_pos.store.archivedCreating a register in an archived store, or restoring a register of an archived store.
409physical_pos.register.archivedTried to update an archived register.
409physical_pos.register.has_devicesTried to archive a register with assigned devices.
409physical_pos.register.invalid_transitionArchiving an archived register or restoring an active one.
409physical_pos.version_conflictrowVersion is not current.
409physical_pos.idempotency_mismatchThe same Idempotency-Key was used with a different request.
429RATE_LIMITEDStore and register write limit exceeded. → Rate limits

All codes: Error Codes.