DZBuild POS
DZBuild POS is the free Windows till for shops that also sell online. It keeps working offline and, once linked, stays in sync with one or more DZBuild stores. This page lists the calls a linked till makes, so support staff and partners can see what a till can and cannot do.
These endpoints answer only tokens issued to the till. A personal API key or an app token gets 403 on the POS-only paths, and a till token gets 403 on every path outside its list.
Who it is for
- Shop owners who sell in a physical shop and on their DZBuild store. Only the store owner can link a till; team members cannot.
- Every plan. A till is not an API key, so it needs no Enterprise plan and takes no key slot.
- Once the first till registers, the DZBuild POS extension appears in the dashboard with the linked tills, a disconnect button and the latest sales, refunds and Z closures.
https://dzbuild.com/dashboard/connected-devicesopens that page.
How a till links
- Discovery:
GET https://dzbuild.com/.well-known/oauth-authorization-server(RFC 8414). - The till is a public OAuth 2.0 client,
dzbuild-pos-windows, with no secret. Only PKCES256is accepted, and the redirect ishttp://127.0.0.1:PORT/oauth/callbackon any port. - The owner signs in in the system browser and picks the stores the till may use, up to 10. A till can also link with a code: it shows one, and the owner types it at
https://dzbuild.com/devicefrom a phone. - The access token starts with
dzpos_and lasts 15 minutes. The refresh token is replaced on every use and ends after 30 days without use or 180 days in all. Sending an old refresh token again ends the link. - Every call carries
X-DZ-Storewith the store id, exceptGET /v1/me. A store the owner did not pick answers403. - Every
POST,PATCHandDELETEcarries anIdempotency-Key, with the rules in Idempotency. - Limits: 120 requests a minute per till and 600 per store. The monthly API quota does not apply to tills.
- Disconnecting the till in the dashboard ends the link: the next refresh answers
400 invalid_grantand the next heartbeat410 device_revoked.
The 19 scopes
The till always asks for all 19, and DZBuild always grants all 19.
| Scopes | What the till can do |
|---|---|
openid, profile, offline_access | Know who linked it and stay linked |
store:read | Read the stores of the link, their language, plan and product cap |
products:read, products:write | Read products, create and update them in batches, add photos |
inventory:read, inventory:write | Read and adjust product stock |
orders:read, orders:write | Receive the store's orders, claim one, move it, cancel it |
customers:read | Check one phone number before a sale |
pos:sales:read, pos:sales:write | Record sales, refunds and Z closures |
locations:read, locations:write | Register the shop the till sits in |
backups:read, backups:write | Reserved: backups are not offered |
devices:self | Register the till, send heartbeats, unlink |
events:read | Read the change feed |
Endpoints
| Method and path | What it does |
|---|---|
GET /v1/me | The owner and the stores of the link |
GET /v1/store | The chosen store: name, language, currency DZD, stock timing, plan and product cap |
POST /v1/devices, GET /v1/devices | Register the till (one row per till and store), list the tills |
POST /v1/devices/{id}/heartbeat | Every 15 minutes: version, pending and failed uploads |
DELETE /v1/devices/{id} | Unlink this till |
GET /v1/locations, POST /v1/locations | The shop the till sits in |
POST /v1/products/batch | Create or update up to 100 products |
GET /v1/products, GET /v1/products/{id} | Products changed since a date, one product |
POST /v1/media/uploads, POST /v1/products/{id}/images | Photo ticket, then attach the uploaded photo |
POST /v1/inventory/adjustments/batch | Up to 500 stock sets or changes |
POST /v1/pos/sales, POST /v1/pos/sales/{sale_id}/refunds, POST /v1/pos/closures | Sales, refunds, Z closures |
GET /v1/orders, GET /v1/orders/{id} | The store's orders in the till's shape |
POST /v1/orders/{id}/claim, PATCH /v1/orders/{id}, POST /v1/orders/{id}/cancel | Take an order, move it, cancel it |
GET /v1/customers?phone= | Flags for one phone number |
GET /v1/events | The change feed |
Some paths are shared with API keys (products, orders, customers). A till gets the shapes on this page; an API key keeps the shapes in Resources.
Products batch
POST /v1/products/batch takes items, up to 100. Each item has the till's own external_id, name, an optional sku and barcode, pricing.price as a string with two decimals ("4500.00"), inventory.track_stock, status (active, draft or archived) and an optional category with its own external_id and name.
- An item updates the product already linked to its
external_id. Failing that, it adopts a product without variants that has the sameskuand no link yet. Failing that, it creates a product with 0 in stock. - A category is found by its
external_id, or created from its name. - The answer lists every item:
external_id,id,status(createdorupdated) anderror: null. A refused item has anerrorobject and noidorstatus. - Plan cap: when the store already has as many active products as its plan allows, a batch that would create a product, draft or not, answers
402 product_limit_reached. Items before that one stay written. Switching an existing product toactivepast the cap is an error on that item only. - The batch never sets stock. Stock goes through the adjustments call.
Photos
POST /v1/media/uploadswithfilename,content_type(image/jpeg,image/pngorimage/webp),size(up to 8 MiB) andsha256. The answer carriesmedia_idand a signedupload_urlthat works for 10 minutes.- The till sends the raw bytes with
PUTtoupload_url, without the DZBuild headers. POST /v1/products/{id}/imageswithmedia_idandpositionattaches the photo. The size and the sha256 must match the ticket, or the call answers422 media_mismatch. An unknown or expired ticket answers404 media_not_found. A product holds up to 20 photos.
Stock
POST /v1/inventory/adjustments/batch takes up to 500 items. Each item names product_external_id, target: "product", either set (whole pieces) or delta, a reason (pos_sale, pos_return, restock, count or loss) and a unique ref.
- Only products that track stock at the product level can be adjusted. A product with stock per variant answers the item error
variant_product, one that does not track stocknot_tracked, a product that no longer existsunknown_product. - The answer lists every item by
refwithstatusokorerror. - Till stock changes never appear in the dashboard's change history and never offer an undo.
POS documents
POST /v1/pos/sales records a ticket, an invoice or a delivery note; POST /v1/pos/sales/{sale_id}/refunds a return note or a credit note against that sale; POST /v1/pos/closures a Z closure with its totals and hash anchors.
- Documents are kept as sent and never change: there is no update or delete path.
- A sale answers
201with{"id": 99120, "stock_applied": false}. Stock moves through the adjustments call, never through a document. - POS documents live apart from orders. They do not count toward the store's monthly order limit, do not notify anyone, and never reach Google Sheets or webhooks.
- Money is a string with two decimals, quantities a number with up to 3 decimals, up to 500 lines and 20 payments per document.
- Sending a document whose
external_idis already recorded answers409 already_existswith the recorded id inerror.details.id. The till reads that as success. - A refund for a sale of another store answers
404 not_found.
Orders
GET /v1/orders?updated_since=...andGET /v1/orders/{id}give the store's orders in the till's shape, with their items.order_numberis the store's short number when it has one.POST /v1/orders/{id}/claimwithdevice_idandterminal: the first till wins. The same till claiming again gets200; another till gets409 order_claimed.PATCH /v1/orders/{id}withstatusneeds the claim (409 claim_requiredotherwise). Allowed moves: frompendingorconfirmedtoprocessing,shippedordelivered, fromprocessingtoshippedordelivered, and fromshippedtodelivered. Any other move answers409 transition_not_allowed.POST /v1/orders/{id}/canceltakesreason(out_of_stock,customer_unreachable,duplicateorother) and an optionalnoteup to 500 characters. Stock comes back under the store's own stock rules. If another till holds the claim, cancel answers409 order_claimed.- A status change from the till runs the same follow-up as one made in the dashboard, notifications and the Google Sheets update included.
Customers
GET /v1/customers?phone=0550123456 answers a page with at most one item: id, is_banned and fraud_score. It looks only at the store's own customers, accepts the phone with or without +213, and gives no name, address or history. An unknown number gives an empty page.
Events
GET /v1/events?wait=25&limit=200&cursor=... returns items, next_cursor and has_more. next_cursor is always present, also on an empty page; the till sends it back on the next call.
- The edge holds the call up to 25 seconds and answers as soon as something changed.
- The first call, without a cursor, starts with
order.updatedfor everypending,confirmedandprocessingorder. - Types:
order.created,order.updated,product.updated,product.deleted,inventory.level_changed,customer.updatedanddevice.revoked. Every event has a stableid, so the till can ignore repeats. inventory.level_changedcarriesold,new,deltaand asource:orderwhen an order moved the stock (with the order number),dashboardfor any other change made outside the till, andposwhen another till of the same store changed it (the till ignores that source).- The till's own product and stock writes are not sent back to it.
device.revokedcarriesdevice_idas a string, once, after the till is disconnected.- A cursor this API did not issue answers
400 bad_request.
Backups
DZBuild does not store till backups. GET /v1/backups, POST /v1/backups, POST /v1/backups/{id}/complete and GET /v1/backups/{id}/download always answer 501 not_implemented, and the till keeps its backups on the PC.
Error codes
| Status | code | When |
|---|---|---|
| 400 | bad_request | A wrong updated_since or a cursor this API did not issue |
| 401 | unauthorized | Token missing, wrong or expired, or the link was ended |
| 402 | product_limit_reached | A batch would create a product past the plan's cap |
| 403 | forbidden | A path outside the till's list, or a store the owner did not pick |
| 404 | not_found | Product, sale or order not on this store |
| 404 | device_not_found | A till id that is not this till on this store |
| 404 | media_not_found | Photo ticket unknown or expired |
| 409 | already_exists | A document with this external_id is recorded; its id is in details.id |
| 409 | order_claimed | Another till claimed the order |
| 409 | claim_required | Moving an order this till has not claimed |
| 409 | transition_not_allowed | The move is not allowed from the order's status |
| 410 | device_revoked | The till was disconnected |
| 413 | payload_too_large | Body larger than 1 MiB |
| 422 | validation_error | A field is wrong; details.field names it |
| 422 | media_mismatch | The uploaded photo does not match its ticket |
| 422 | idempotency_key_reuse | Same Idempotency-Key with a different body |
| 429 | rate_limited | Over a limit; wait for Retry-After |
| 501 | not_implemented | The backup paths |
| 503 | storage_unavailable | Photo storage is not reachable; retry later |