API Reference
The complete REST surface your plugin uses to read and write restaurant data. For worked examples, see Plugin Data API.
Base URL
https://api.ssppos.com/api/plugin/v1
Sandbox and production share this URL. Your pik_ key determines which data
plane you reach — no environment branch needed in your code.
Authentication
X-Plugin-Api-Key: pik_a3f200001234567890abcdef...
Inbound webhook forwarding additionally requires an HMAC signature:
X-Plugin-Api-Key: pik_...
X-SSP-Signature: <hex HMAC-SHA256 of the raw body>
Full detail: Authentication.
Rate limits
| Bucket | Limit | Scope |
|---|---|---|
Plugin Data API — every plugin/v1 route | 60 requests/minute | Per installation |
| Inbound webhooks — additional ceiling | 600 requests/minute | Per installation |
Developer API (ssp_dev_ on /graphql) | 60 requests/minute | Per key |
| Setup-token exchange | 20 requests/minute | Per IP |
| Sandbox operator JWT | 30 requests/minute | Per IP |
A 429 carries Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining
headers, plus a retry_after field in the body.
The two buckets are not independent. The 60/min installation limit is
enforced on every plugin/v1 route, including POST /webhooks/external; the
600/min figure is a second, higher ceiling checked after it. In practice:
- 60/min is your real inbound webhook ceiling, not 600.
- Forwarded webhooks consume your data-API budget. A burst of provider callbacks eats into the same 60 requests you need for reads and writes.
Queue and pace your forwarding rather than relaying provider callbacks
one-for-one, and honour Retry-After on the 429.
Endpoints
22 endpoints across eight groups.
Health & installation
| Method | Path | Summary |
|---|---|---|
GET | /health | Returns {"status":"healthy","installation_id":…,"timestamp":…} and records a health check. Note: this is the endpoint you call on SSP — not the /health SSP polls on your service |
GET | /installation | Your installation: plugin, organization, location, config, enabled_features |
POST | /installation/config | Merge keys into your installation's config (unsent keys are preserved) |
Orders
| Method | Path | Summary |
|---|---|---|
GET | /orders | List orders — filters: location_id, status, since, limit (max 100, default 50) |
GET | /orders/{orderId} | Full order: items, customer, payments |
POST | /orders | Create an order. Requires orders:create |
PUT | /orders/{orderId} | Transition status. Accepts a numeric ID or a unique_orderid |
Payments
| Method | Path | Summary |
|---|---|---|
POST | /orders/{orderId}/payments | Record a capture. Requires payments:capture |
Menu
| Method | Path | Summary |
|---|---|---|
GET | /menu | List items — filters: location_id, category_id, available_only (defaults to true) |
GET | /menu/{itemId} | Item detail |
PATCH | /menu/{itemId} | Update is_available, price (0–9999.99), preparation_time (0–480 min) |
POST | /menu/bulk-availability | Bulk availability update, max 100 items, partial success |
Locations
| Method | Path | Summary |
|---|---|---|
GET | /locations | All locations visible to this installation |
GET | /locations/{locationId} | Location detail |
Customers
| Method | Path | Summary |
|---|---|---|
GET | /customers | List — filters: location_id, search (name/email/phone), since, limit |
GET | /customers/{id} | Detail, with visible order count and recent order IDs |
Inventory
| Method | Path | Summary |
|---|---|---|
GET | /inventory | Ingredients with stock — filters: location_id, ingredient_id, low_stock, limit |
GET | /inventory/{ingredientId} | Detail with per-location stock, SKUs, last 20 movements |
PATCH | /inventory/{ingredientId} | Update metadata (name, sku, unit, cost_per_unit, supplier, allergens, metadata) |
POST | /inventory/{ingredientId}/movements | Append a stock movement |
Inbound webhooks
| Method | Path | Summary |
|---|---|---|
POST | /webhooks/external | Forward a provider event to SSP. Requires X-SSP-Signature |
GET | /webhooks/external/{logId} | Poll the status of a submitted event |
Response shapes
Success
Collections:
{
"data": [ … ],
"meta": { "count": 5, "limit": 50 }
}
Single resources return {"data": { … }} — including GET /orders/{id}
and GET /installation. The one exception is GET /health, which returns its
object at the top level.
Error
{
"error": "validation_error",
"message": "Human-readable description",
"details": { "field": ["what was wrong"] }
}
error is a machine-readable snake_case code — branch on it, not on the
message. details appears only on validation failures.
Those endpoints return a human sentence in the error field and no message
key at all:
{ "error": "Order not found" }
So error === 'not_found' is false for GET /orders/{id},
GET /menu/{itemId} and GET /locations/{locationId}. Treat any 404 as
not-found regardless of body, and only parse error as a code on other
statuses. Customers and inventory do return not_found properly.
Error codes
Authentication and access
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No X-Plugin-Api-Key header |
invalid_api_key_format | 401 | Not pik_ + 64 hex (68 chars) |
invalid_api_key | 401 | No installation matches |
installation_inactive | 403 | Installation status isn't active |
plugin_listing_unavailable | 403 | Production only — listing isn't approved/active |
sandbox_not_ready | 401 | Sandbox isn't active |
sandbox_resetting | 409 | Reset in flight — retry shortly |
sandbox_misconfigured | 500 | Sandbox schema problem — contact support |
rate_limit_exceeded | 429 | Over the bucket limit |
Requests
| Code | HTTP | Meaning |
|---|---|---|
validation_error | 400 / 422 | Body failed validation (details carries the field errors) |
not_found | 404 | Not present, or not visible to this installation. Customers and inventory only — see the note below |
feature_not_enabled | 403 | Missing capability (e.g. orders:create) |
location_forbidden | 403 | Location outside this installation's scope |
Orders
| Code | HTTP | Meaning |
|---|---|---|
invalid_transition | 422 | Target unreachable from the current state — response includes allowed_next_states |
customization_location_mismatch | 422 | Customization template belongs to a different location |
customization_rule_violation | 422 | Violates the item's customization rules (min/max selections) |
create_failed | 500 | Order creation failed and was rolled back |
Payments
| Code | HTTP | Meaning |
|---|---|---|
currency_mismatch | 422 | Doesn't match the order's location currency |
overpayment | 422 | Exceeds the remaining balance |
subscription_locked | 403 | Blocked by the organization's subscription state |
capture_failed | 500 | Capture failed and was rolled back |
Inbound webhooks
| Code | HTTP | Meaning |
|---|---|---|
invalid_signature | 401 | X-SSP-Signature missing or wrong |
unknown_event_type | 400 | Not a catalog event, not external.*, no wildcard match |
Order state machine
PUT /orders/{orderId} drives a coarse, plugin-facing state machine.
| From | Allowed targets |
|---|---|
open | in_progress, cancelled |
in_progress | ready, cancelled |
ready | completed, cancelled |
completed | paid |
paid, cancelled | terminal |
cancellation_reason (max 2000 chars) is required when the target is
cancelled. metadata, if supplied, is shallow-merged.
Plugin vocabulary vs internal statuses
SSP's internal status set is finer-grained than the plugin one. The mapping:
| Plugin status | Internal statuses that read as it | Writing it sets |
|---|---|---|
open | CREATED | CREATED |
in_progress | COOKING | COOKING |
ready | PREPARED, SERVED | SERVED |
completed | COMPLETED | COMPLETED |
paid | PAID | PAID |
cancelled | CANCELLED | CANCELLED |
The mapping is deliberately many-to-one and does not round-trip: an order in
PREPARED reads as ready, and writing ready back moves it to SERVED.
That's the intended direction of travel.
- REST response bodies (
GET /orders,GET /orders/{id}) return the plugin vocabulary. - The
?status=filter onGET /ordersmatches the internal column — passCREATED, notopen. - Webhook payloads (
order.created,order.status_changed) carry internal values.
Normalise on your side rather than assuming one vocabulary throughout.
Orders in EDITABLE, PENDING, PROCESSING, FAILED, VOIDED, REFUNDED,
PARTIALLY_REFUNDED, ABANDONED, PARTIALLY_PAID, or
AWAITING_AUTHENTICATION have no plugin-facing equivalent, so any PUT
against them returns 422 invalid_transition.
Capability gating
Two write endpoints require a capability in your plugin's supported_features:
| Capability | Gates |
|---|---|
orders:create | POST /orders |
payments:capture | POST /orders/{orderId}/payments |
Without it: 403 feature_not_enabled. Scaffold plugins never carry these —
declare them when you create the plugin from your project.
Idempotency
Three endpoints support safe replay. In every case a duplicate returns 200
with the original record rather than an error — matching Stripe and Razorpay
convention.
| Endpoint | Key | Window |
|---|---|---|
POST /orders | external_order_id in the body, or the X-Idempotency-Key header | Per organization |
POST /orders/{id}/payments | provider_transaction_id | Per organization |
POST /webhooks/external | idempotency_key in the body (max 128 chars) | 24 hours, per installation |
A first-time create returns 201; a replay returns 200. Branch on the status
code, or on the duplicate: true flag that webhook replays carry.
Field limits
| Constraint | Value |
|---|---|
| Items per created order | 100 |
| Quantity per item | 1–999 |
| Customizations per item | 50 |
| Bulk availability items | 100 |
List limit | 1–100 (default 50) |
cancellation_reason | 2000 chars |
kitchen_instructions | 2000 chars |
idempotency_key | 128 chars |
OpenAPI specification
A generated OpenAPI 3.0 spec covers every endpoint with typed request and response schemas, security schemes, and error shapes.
- Browse it interactively in the Developer Portal's API Reference screen, which renders the live spec.
- Import it into Postman, Insomnia, or Swagger Editor to generate a client.
The portal also provides a sandbox console that issues real, allowlisted
calls against your own sandbox with your pik_ key.
Next steps
- Plugin Data API — the same endpoints, with examples
- Webhooks — the event catalog
- Payment Gateways · Delivery Platforms