Physical POS
Common Rules

Common Rules

The rules on this page apply to every endpoint of the Physical POS management API. The endpoint pages link here.

Response envelope

On success the data is under data. In this API every endpoint that returns a single record wraps it under the entity name; related data sits in sibling fields:

Endpoint typeShape of dataExample
Read, update or status change of one record{ <entity> } plus related fields, if any{ "store": { … } }, { "terminal": { … }, "acquirers": { … } }
Create{ <entity>, replayed }{ "register": { … }, "replayed": false }
List{ items, pageInfo }{ "items": [ … ], "pageInfo": { … } }
Catalog listArray[ { … }, { … } ]
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "store": {
      "id": "00000000-0000-0000-0000-000000000101",
      "code": "IST-KADIKOY",
      "name": "Kadıköy Store",
      "storeStatus": "ACTIVE",
      "rowVersion": 1
    },
    "replayed": false
  },
  "statusCode": 201,
  "requestId": "00000000-0000-0000-0000-000000000000"
}

The store object is shortened in the example; the response contains every field.

  • Create endpoints return 201 for a new record and 200 for a repeat with the same Idempotency-Key. → Idempotency-Key
  • On list endpoints pageInfo is also at the top level of the envelope; both copies are identical.
  • Envelope fields not shown in the examples for brevity (message, timestamp …) are also in the response. All envelope fields and the error envelope: Error Handling.
  • On errors, branch on code, not on message. This product's codes: Error Codes.
  • For physical_pos.* errors the response carries a docsUrl field that links to that code's row on the Error Codes page.
  • Error details are in details: single-field errors carry details.field and details.reason; a validation error covering several fields (physical_pos.validation.invalid) carries a details.errors array, each item { field, errors: [<reason>] }. The value you sent is never echoed in details.

Field rules

RuleDetail
IDsEvery ID (id, storeId, terminalId …) is a UUID. A malformed ID in the path gets the same 404 as a record that does not exist.
TimesISO 8601, UTC (2026-01-01T07:00:00.000Z).
Empty valuesWhen an optional field has no value, the field is present as null.
Status fieldsStatus fields are named after the entity: storeStatus, registerStatus, terminalStatus, acquirerStatus, lifecycleStatus.
Unknown fieldsAn undefined body field or query parameter returns 400 VALIDATION_ERROR on every endpoint.
EnvironmentSending environment in the query or the body gets 400 physical_pos.input.environment_invalid (details.reason: not_allowed). The organization and environment are resolved only from your key. → Environments
Empty updateA PATCH request with no field to change gets 400 physical_pos.input.empty_patch. A PATCH whose values equal the current ones is not an error: it returns 200, the record does not change, rowVersion does not increase and no change-history event is written.
List filtersAn invalid filter value (status outside the set, malformed ID or code, q breaking the rules or longer than 64 characters) gets 400 physical_pos.input.filter_invalid; details.field names the filter. Value sets are case-sensitive.

Pagination

List endpoints use cursor-based pagination:

ParameterDescription
limitRecords per page, 1–100. Default 25. Out-of-range values return 400 VALIDATION_ERROR.
cursorpageInfo.nextCursor from the previous response. Not sent on the first page.
includeTotalOn record lists (stores, registers, terminals, bank rows, devices). When true, pageInfo.totalCount is filled. History lists (…/audit, …/assignments, …/links) do not take this parameter.
"pageInfo": { "hasMore": true, "nextCursor": "eyJ0IjoiMjAyNi0…", "totalCount": null }
  • Every list is sorted newest first (createdAt descending, ties by id descending); the order cannot be changed.
  • If pageInfo.hasMore is true, request the next page with cursor=<nextCursor>; on the last page nextCursor is null.
  • totalCount is null unless you send includeTotal=true. Counting is expensive; hasMore is enough to page through.
  • Do not build or modify the cursor yourself. A broken cursor returns 400 physical_pos.cursor.invalid; start again from the first page.
  • Reset the cursor when you change filters.

Catalog endpoints are not paginated; the whole list comes back as an array in one response.

Idempotency-Key

Endpoints that create something require an Idempotency-Key header so you can safely retry after a network failure:

EndpointOperation
POST /storesCreate a store
POST /stores/{storeId}/registersCreate a register
POST /terminalsCreate a logical terminal
POST /terminals/{terminalId}/acquirersAdd a bank row
POST /terminals/{terminalId}/device-link/closeUnlink the device
POST /devicesRegister a device
POST /devices/{deviceId}/assignment/closeMove a device to storage
POST /devices/{deviceId}/retireRetire a device

Format: 8–200 printable ASCII characters without spaces. The value is not trimmed or corrected. A missing header gets 400 physical_pos.idempotency.key_required, a malformed one 400 physical_pos.idempotency.key_invalid.

Behaviour:

SituationResult
New keyThe operation runs; create endpoints return 201, replayed: false.
Same key, same endpoint, same bodyThe operation is not repeated; the record of the first operation is returned in its current state, 200, replayed: true.
Same key, different body409 physical_pos.idempotency_mismatch. No new record is created.
Same key, another operation (e.g. create a store, then a terminal)409 physical_pos.idempotency_mismatch.
  • Bodies are compared after the field rules are applied; field order does not matter.
  • For operations tied to another record, such as creating a register or adding a bank row, the path parameter is part of the comparison: using the same key for another store gets 409.
  • A key can be used for one operation only in your key's environment and is kept indefinitely. Do not reuse a used key for a new operation.
  • Values used with test and live keys are kept apart.

Use a new UUID for every new operation and store it with the operation. After a timeout or a dropped connection, retry with the same key and the same body; if the first attempt had completed, you get the same record with replayed: true.

The other write endpoints (PATCH, archive, restore, close a terminal, assign a device, replace the device) do not use Idempotency-Key; the header is ignored there. These endpoints are protected by rowVersion: if the first attempt had completed, the retry gets 409 physical_pos.version_conflict (409 physical_pos.terminal.closed when closing a terminal). In that case read the record and check whether your change was applied.

Concurrency: rowVersion

Every changeable record (store, register, logical terminal, bank row) carries a rowVersion integer. In requests that change a record you send the rowVersion you last read in the body:

{ "rowVersion": 3, "name": "Kadıköy Store (New)" }
  • rowVersion is required; it is a JSON integer between 1 and 2147483647. Sending a string ("3") gets 400 VALIDATION_ERROR.
  • If the record was changed by another request after you read it, the request gets 409 physical_pos.version_conflict and nothing is changed. Read the record again, apply your change to its current state and resend with the new rowVersion.
  • After every successful change, use the current rowVersion from the response.
  • A bank row update takes the row's own rowVersion, device link operations take the terminal's rowVersion, and device operations take the device's rowVersion.

Code rules

Store, register and terminal codes are stored in canonical form:

FieldRuleExample
code (store, register, terminal)Leading and trailing spaces are trimmed and the value is upper-cased. Only A-Z, 0-9, ., _, -; starts with a letter or digit; at most 64 characters.ist-kadikoy → IST-KADIKOY
vendorCodeUpper-cased. Starts with a letter, only A-Z and 0-9, 2–32 characters. Must be the code of a vendor in the catalog.pavo → PAVO
externalRef (store)The ID in your own system. Printable ASCII without spaces, at most 128 characters. Not trimmed; case is kept.store_00042
  • A code with non-ASCII characters is rejected, not converted: KADIKÖY is not accepted, write KADIKOY.
  • A malformed code gets 400 physical_pos.input.code_invalid, a malformed vendor code 400 physical_pos.input.vendor_code_invalid, a malformed external reference 400 physical_pos.input.ref_invalid.
  • Codes do not change after creation.

Text rules

These rules apply to free-text fields (name, displayName, city, district, addressLine, label, the q search parameter):

  1. The text is normalized to Unicode NFC and leading and trailing spaces are trimmed.
  2. Text that is empty after trimming is rejected.
  3. These characters are rejected: control characters, zero-width and invisible characters, characters that change text direction, space characters other than the normal space (no-break space etc.), variation selectors and tag characters.
  4. Length is counted in characters (Unicode code points); the limits are listed in each field table.

Text that breaks the rules is not corrected; the request gets 400 physical_pos.input.text_invalid. details.field gives the field name and details.reason the cause. The same violation in a list search (q) gets 400 physical_pos.input.filter_invalid.

details.reasonMeaning
not_stringThe value is not text.
emptyEmpty after trimming.
malformed_unicodeInvalid Unicode sequence.
forbidden_characterContains a forbidden character.
too_longExceeds the field's limit.

Visible characters such as Turkish letters and emoji can be used in free text.

Time zone

A store's timezone field is an IANA time zone name (e.g. Europe/Istanbul). If you do not send it, Europe/Istanbul is used.

  • The accepted names are the zone names of the IANA time zone database (tzdb) and UTC. The name must match exactly, including case.
  • Old or renamed names are stored under their current name; responses always return the current name:
SentStored
Europe/KievEurope/Kyiv
Asia/CalcuttaAsia/Kolkata
Asia/IstanbulEurope/Istanbul
Etc/UTC, Universal, ZuluUTC
  • Unknown names, and old names and abbreviations that do not map to a single zone (such as EET, HST, Turkey), are rejected: 400 physical_pos.validation.invalid with details.errors[0] = { "field": "timezone", "errors": ["iana_unknown"] }.

Reason codes

Archiving, restoring, closing a terminal, device link and device operations require a reasonCode. The codes come from fixed lists and are written in upper case exactly as listed; a code that is not on the list or does not fit the operation gets 400 physical_pos.input.reason_code_invalid.

OperationAccepted codes
Archive a storeSTORE_CLOSED, TEMPORARILY_CLOSED, RELOCATED, MERGED, CREATED_IN_ERROR, DATA_CORRECTION, OTHER
Archive a registerSTORE_ARCHIVED, REGISTER_REMOVED, REGISTER_REPLACED, STORE_CLOSED, TEMPORARILY_CLOSED, CREATED_IN_ERROR, DATA_CORRECTION, OTHER
Restore a store or registerREOPENED, ARCHIVED_IN_ERROR, DATA_CORRECTION, OTHER
Close a terminalMERCHANT_REQUEST, CONTRACT_ENDED, BANK_TERMINATED, CREATED_IN_ERROR, DUPLICATE_RECORD, MIGRATED, OTHER
Link the first device to a terminalINITIAL_SETUP, RELOCATION, CORRECTION, OTHER
Replace the linked deviceDEVICE_REPLACEMENT, DEVICE_FAULT, DEVICE_LOST, DEVICE_STOLEN, RELOCATION, CORRECTION, OTHER
Unlink the deviceDEVICE_REPLACEMENT, DEVICE_FAULT, DEVICE_LOST, DEVICE_STOLEN, RELOCATION, CORRECTION, OTHER
Assign a device, move to storage, retireThe tables on the Devices page
  • When a store is archived, its active registers are archived in the same operation; their change history gets the STORE_ARCHIVED code and the store's reason is kept in changes.storeReason.
  • Use OTHER only for cases that have no match in the list.

Rate limits

Write endpoints (POST and PATCH) have a 60-second window per organization and API key. All writes of an endpoint family share one counter; reads are not limited:

Endpoint groupAt most per 60 seconds
Store and register writes (create, update, archive, restore)60 requests
Terminal family writes (terminal, bank row, device link)30 requests
Device writes (register, update, assign, move to storage, retire)30 requests

Over the limit the request gets 429 RATE_LIMITED; the Retry-After header and details.retryAfterSeconds give the seconds to wait. Platform-wide limits: Rate Limits.

Change history

Every change to a store, register or terminal is written to the change history in the same operation. You read the history page by page from the GET …/audit endpoints.

FieldDescription
idEvent ID.
actionEvent type, as physical_pos.audit.<entity>_<action> (e.g. physical_pos.audit.store_archived).
actorTypeWho made the change: USER (panel user), API_KEY (API key), or OPERATOR / SYSTEM for platform-side operations.
actorIdauth-user:<user id> for a panel user, kid:<key id> for an API key.
reasonCodeThe reason code of the operation; null for operations without a reason.
changesA summary of the change. It carries only defined fields and simple values; the values of free text such as names and addresses are not written, only the fact that they changed.
createdAtEvent time.

Store, register and device events also carry requestId (the ID of the request that made the change); terminal events carry entityType, entityId and environment.

Retries

  • 4xx errors are not fixed by retrying; correct the request.
  • 503 physical_pos.service.busy (details.retryable: true) means the record was busy with another operation at that moment; retry the same request after a short wait.
  • For other 5xx errors, retry with backoff. For create requests, reuse the same Idempotency-Key.