Create an API key in Store Config → API Settings — choosing Read or Read/Write access and an optional employee scope — and send it in the x-api-key header to the v1 REST API. Outbound webhooks POST JSON to your URLs on new or edited customers, sales, receivings, items and work orders, and the Sidekick and Zapier integrations build on the same plumbing.
When the built-in integrations do not cover what you need — a custom website, an internal dashboard, a warehouse tool — the REST API gives your own software the same data the POS uses. Pair it with outbound webhooks and your systems can react to store activity the moment it happens, without polling.
API keys and webhooks are both managed in Store Config, which requires the Store Config module permission (permissions and templates).
What the API covers
The API is versioned as v1 and most endpoints are documented in an OpenAPI specification, so you can explore it interactively or generate clients. Endpoints cover the breadth of the application: items, item kits, sales, customers, suppliers, employees, receivings, gift cards, expenses and expense categories, invoices, appointments and appointment types, deliveries, price rules, registers, locations, reports, tags, tiers, modifiers, attributes, categories, manufacturers, sale types and permission requests.
The endpoint reference lives at the API page. Requests are made against your own store, for example:
https://yourstore.phppointofsale.com/index.php/api/v1/items
A few conventions apply everywhere:
- There is no
PUTorPATCH. You create withPOST /<resource>and update withPOST /<resource>/{id}— the id in the URL is what makes it an update. - Two endpoints take an extra path segment instead of a plain id: invoices are addressed by type,
customerorsupplier(GET /invoices/customer,POST /invoices/supplier/{id}), and invoice payments are a sub-resource — record one withPOST /invoices/payments/{type}and fetch one withGET /invoices/payments/{type}/{payment_id}. - Deliveries can only be updated, with
POST /deliveries/{id}against an existing delivery id — deliveries are created by the POS when a sale ships, not through the API, and there is no deliveries batch endpoint. - Permission requests are the one resource a Read key can write:
POST /permission_requestssubmits a request andPOST /permission_requests/approve/{id}approves one — the API side of permission requests.
Create an API key
- Go to Store Config and search for API to open API Settings.
- The API Keys table lists existing keys with their Description, API Key Ending In, Permissions, Employee Scope and a Delete link.
-
Click Add API Key. The modal generates a key and offers:
- Description — name the system that will use the key (one key per integration makes revoking painless).
- API Key — the generated value, with a Copy to Clipboard button. Copy it now: for security only the last characters are shown again after saving.
- Permissions — Read (fetching data; the only writes it can make are the permission-request endpoints above) or Read/Write (can also create, update and delete).
- Employee Scope — No scope (full access at the chosen level) or a specific employee. A scoped key can only reach modules and actions that employee's permissions allow. A key for your fulfillment tool can be limited exactly like a fulfillment employee account.
- Confirm the prompt (it reminds you the key will not be shown again) and the key is saved.
To revoke a key, click Delete in its row and confirm — requests with that key stop working immediately.
What a scoped key checks
For a scoped key, most endpoints check the module of the same name, and the default rule is: reading needs that module's search permission, creating or updating needs Add, Update, deleting needs Delete. The module is not always one-to-one with the endpoint, though, and some endpoints check different actions:
| Endpoint(s) | Module checked | Actions that differ from the default |
|---|---|---|
attributes, categories, manufacturers, modifiers, tags
|
Items | writes to categories, tags and manufacturers check Manage categories, Manage tags, Manage Manufacturers |
registers |
Locations | — |
sale_types, tiers
|
Sales | read-only endpoints |
expenses_categories |
Expenses | writes check Manage categories |
sales |
Sales |
POST checks Complete Sale, DELETE checks Delete Sale
|
receivings |
Receivings |
POST checks Edit receiving, DELETE checks Delete receiving
|
invoices, appointments
|
Invoices, Appointments |
POST checks both the add and edit actions (Add Invoice and Edit Invoice; Add Appointments and Edit Appointments) |
reports |
Reports | each report additionally checks its own view action |
Make your first request
Every request authenticates by sending the key in the x-api-key header:
curl -H "x-api-key: YOUR_API_KEY" \
"https://yourstore.phppointofsale.com/index.php/api/v1/items"
A successful response returns JSON. Errors return a small JSON envelope, {"status": false, "error": "..."}, with the HTTP code telling you which kind:
-
403 — no key, or an invalid key (
Invalid API key); also a scoped key whose employee lacks the module or action a request needs. - 401 — the key's level is too low (a Read key attempting a write), or you are rate limited.
Rate limits are worth designing around: requests are counted per API key across all endpoints together, and the budget is 60 requests per 60-second window. Going over returns HTTP 401 with {"status":false,"error":"This API key has reached the time limit for this method"} — and every rejected request restarts the window, so the counter only clears after a full minute of sending nothing at all. Back off for 60 seconds when you see it; hammering just keeps you locked out. Spread calls out, cache what you fetch, and use the batch endpoints below to do more per request.
Reading lists: pagination and filters
List endpoints accept limit and offset, and every list response includes an x-total-records response header with the total row count — read it to know when to stop paging. The caps differ by resource:
| Resource | Default limit
|
Maximum limit
|
|---|---|---|
| Most resources (customers, suppliers, employees, gift cards, expenses, appointments, invoices, item kits, price rules, registers, locations, deliveries…) | 20 | 100 |
items |
20 | 1000 |
sales, receivings
|
500 | 1000 |
reports |
your store's Number Of Items Per Page setting (20 if unset) | 500 |
modifiers, sale_types, tiers
|
returns the whole list — no paging | — |
Most list endpoints also take search, search_field, sort_col, sort_dir and location_id where they make sense — the spec at /api.php documents which fields each resource supports.
Sales searches go further, since sales are the biggest dataset. GET /sales accepts:
-
verbosity(orprojection) —minimal,mediumorfull(the default). Useminimalwhen you only need headline fields;fullreturns every line item, payment and tax. -
customer_id, oremail_addressto look the customer up by email. -
suspended_type— a comma-separated list oflayaway,estimateor a sale type name, to fetch suspended sales instead of completed ones. - Date ranges:
start_date/end_date(sale time),start_date_created/end_date_created,start_date_updated/end_date_updated, andstart_payment_date/end_payment_date— each accepts a date or a full date-time. Addinclude_created_sales_in_rangeto include sales created in the range as well.
Receivings support the same start_date/end_date and created/updated ranges.
Writing data and the batch endpoints
Single writes are POST /<resource> (create) and POST /<resource>/{id} (update). When you have many changes, seventeen resources also expose POST /<resource>/batch: appointment types, appointments, attributes, categories, customers, employees, expenses, expense categories, gift cards, item kits, items, manufacturers, modifiers, price rules, registers, suppliers and tags. The body groups the work:
{
"create": [ { "...": "records to create" } ],
"update": [ { "person_id": 12, "...": "changed fields" } ],
"delete": [ 34, 56 ]
}
Each section is optional, delete takes plain ids, and the response echoes the same three sections with the resulting records (or an error marker per record). One batch call counts as one request against the rate limit, which makes it the right tool for imports and nightly syncs.
Writes to sales, receivings, customers and appointments accept one extra body flag: "skip_webhook": true. It suppresses the outbound webhook that save would normally fire — set it when your integration is the one making the write, so your own webhook endpoint does not receive an echo of the change it just pushed and loop.
Retrying a card charge safely
POST /sales/charge_card accepts an optional idempotency_key in the body — your own unique string, up to 64 characters, identifying one intended charge.
Send it and the first request is processed normally while the key is recorded with the outcome. If the same key arrives again with the same charge details, the stored response is replayed instead of charging the card a second time — which is exactly what you want after a timeout or a dropped connection, where you cannot tell whether the original request went through.
Two guards make it safe to rely on:
- Reusing a key with different charge details is rejected (
idempotency_key was already used for a different charge request) rather than replaying an unrelated response. - A retry that arrives while the first request is still running is rejected with a conflict (
A charge with this idempotency_key is already in progress), so two retries cannot both charge.
Generate a fresh key per intended charge — a UUID is the usual choice — and reuse it only when retrying that same charge.
Running reports over the API
Every report in the reports catalog is callable. GET /reports returns a catalogue of all 138 report keys, each with the model behind it, the Reports-module permission action it checks, and the inputs it accepts (date ranges, location ids, dropdown filters and so on). GET /reports/{report_key} runs one, taking those inputs as query parameters plus limit (capped at 500) and offset. A scoped key needs the Reports module plus the individual report's view action.
Outbound webhooks
Webhooks push events to you instead of making you poll. Open Store Config and search for web hooks to find the Web Hooks section, then paste your endpoint's URL into any of the ten fields:
- New Customer Web Hook URL and Edit Customer Web Hook URL
- New Sale Web Hook URL and Edit Sale Web Hook URL
- New Receiving Web Hook and Edit Receiving Web Hook URL
- New Item Web Hook and Edit Item Web Hook
- New Work Order Web Hook and Edit Work Order Web Hook
When the event happens, the POS sends an HTTP POST to your URL with header Content-Type: application/json. What the body contains depends on the event:
- Sales, receivings and work orders send the full record that was created or edited — line items, payments and all.
- Customers and items send the fields that were submitted in that save, which is not necessarily the complete record. Treat the payload as a change notification carrying the id, and read the full record back through the API when you need every field.
Two timing quirks to plan for. Saving an item fires the Edit Item Web Hook on every save of an existing item — and creating one usually fires it too, because the item form follows the insert with further writes (pricing, images, location data) in the same save. Expect a brand-new item to hit both the New Item and Edit Item URLs, and de-duplicate by item_id. The Edit Work Order Web Hook, by contrast, only fires when a work order is saved from the Work Orders screen — edits that happen through other paths do not trigger it.
The request times out after about five seconds and is not retried, so make your endpoint respond fast (queue the work, return immediately). A minimal PHP receiver looks like:
<?php
$jsonStr = file_get_contents("php://input");
$json = json_decode($jsonStr);
Webhook POSTs are unsigned — there is no secret or signature header, so anyone who knows the URL can send you an identical-looking request. Treat the URL itself as a secret (use a long, unguessable path), and verify anything that matters by reading the record back through the API instead of trusting the payload. If your integration also writes through the API, send skip_webhook on those writes so it does not receive its own changes back.
That is enough to drive most reactive integrations: sync a new customer into your mailing platform, notify a channel when a big sale closes, or kick off fulfillment when a work order changes. Leave a field blank to disable that event.
One thing you never configure here: the ecommerce integrations. Connecting Shopify, WooCommerce or Square registers their own inbound webhooks automatically, so orders and catalog changes flow into the POS without touching the Web Hooks fields — see how ecommerce sync works, connecting Shopify and connecting WooCommerce.
Sidekick reviews integration
Sidekick can automatically ask customers for a review after they buy. It is configured per location:
- Go to Locations in the left menu, select the location, and click Edit.
- Open the Integrations tab and scroll to the Sidekick API Key field near the bottom.
- Paste the API key from your Sidekick account.
- Check Sidekick Automatically Ask For Reviews After Sale to send a review request email after each sale at that location.
- Click Save.
A sale only triggers a review request when it is attached to a customer who has an email address or phone number on file (see customer profiles). To make existing customers available to Sidekick, go to Customers → Customers, click the ellipsis (...) in the top right, and choose Export to Sidekick — this pushes your whole customer list once; new and edited customers then sync to Sidekick automatically. Repeat the location steps for each additional location you want on Sidekick.
Connect thousands of apps with Zapier
If you would rather not write code at all, Zapier connects PHP Point Of Sale to thousands of other apps through this same API — spreadsheets, email tools, CRMs and more, wired together with point-and-click automation. See Zapier integration for setup.
Common questions
Where do I find my API key after creating it? You don't — only the last characters are displayed in the API Keys table. Copy the key from the creation modal (there is a Copy to Clipboard button). If it is lost, delete the key and create a new one.
What header does authentication use?
x-api-key, sent on every request with the key as its value.
How do I update a record — there's no PUT?
Correct: updates are POST /<resource>/{id}. The presence of the id in the URL is what distinguishes an update from a create.
Why am I getting a permissions error on a request that used to work?
Three usual causes: the key is Read and the request is a write (401); the key has an Employee Scope and that employee lost the module or action permission the endpoint needs (403); or the key was deleted (403 Invalid API key). Check the key's row in Store Config → API Settings.
Why did my requests suddenly start failing after many calls?
You hit the rate limit: 60 requests per 60-second window, counted per key across all endpoints. The response is HTTP 401 with "This API key has reached the time limit for this method". Stop sending entirely for a minute — requests made while over the limit restart the window, so retrying in a tight loop keeps you locked out. Then batch and pace your calls.
How do I page through a large list?
Send limit and offset, and read the x-total-records response header to know the total. Most resources cap limit at 100; items, sales and receivings allow up to 1000; reports cap at 500.
Do webhooks retry if my server is down? No. The POST is sent once with a short timeout and no retry. Use the REST API to backfill anything missed — for example, poll recent sales on startup.
What is in the webhook body?
For sales, receivings and work orders, the complete record that triggered the event. For customers and items, the fields submitted in that save — fetch the record via the API when you need all of it. All bodies are JSON with Content-Type: application/json, and none are signed, so keep your webhook URLs secret.
Sidekick isn't sending review requests — why? Confirm the location's Sidekick API Key is filled in, Sidekick Automatically Ask For Reviews After Sale is checked on that location, and the sale has a customer with an email address or phone number. Also run Export to Sidekick once so existing customers are known to Sidekick.
Is there a sandbox to explore endpoints? The OpenAPI spec at /api.php documents most endpoints, parameters and response shapes; a Read key against your own store is the safest way to explore live data.
Comments
0 comments
Please sign in to leave a comment.