Getting Started
Build your first SSP integration end to end. If you don't have a developer account yet, start with Developer Account — this guide picks up once you're logged in and past the agreements screen.
Prerequisites
- A verified SSP developer account with the developer agreements accepted
- A tool to make HTTP requests (curl, Postman, or your language of choice)
- For webhooks: a publicly reachable HTTPS endpoint (see ngrok, below)
Step 1 — Create a project
In the Developer Portal, choose New Project.
Project names are lowercase letters, digits and hyphens, starting and ending
with a letter or digit — acme-payments, not Acme Payments.
You become the project's owner.
Step 2 — Provision a sandbox
On the project page, choose Provision Sandbox. Two secrets appear, each shown once — save both now:
| Value | Format | Purpose |
|---|---|---|
| API key | pik_ + 64 hex chars (68 total) | Authenticating REST calls |
| Webhook secret | 64 chars | HMAC-SHA256 signing, both directions |
API key — Regenerate Key in the sandbox settings (the old key is revoked immediately). Webhook secret — Rotate Secret on the plugin. Neither can be read back without rotating.
Details of what's seeded, how resets work, and expiry: Sandboxes.
Step 3 — Your first API call
curl -s "https://api.ssppos.com/api/plugin/v1/orders" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" | jq
You'll see the six seeded orders — one in each status:
{
"data": [
{
"id": 1,
"unique_orderid": "SBX-0001",
"order_status": "open",
"order_type": "DINE_IN",
"subtotal": "25.50",
"tax": "3.83",
"total_amount": "29.33",
"location_id": 1,
"created_at": "2026-04-17T10:22:00+00:00"
}
],
"meta": { "count": 6, "limit": 50 }
}
The REST endpoints return the plugin vocabulary (open, in_progress,
ready, …), but the ?status= filter matches the internal column
(CREATED, COOKING, SERVED, …). So ?status=CREATED finds the orders that
come back as "order_status": "open". Webhooks also carry internal values.
Full mapping: Order vocabulary.
Explore the rest
BASE="https://api.ssppos.com/api/plugin/v1"
KEY="pik_YOUR_KEY_HERE"
curl -s "$BASE/health" -H "X-Plugin-Api-Key: $KEY" | jq # {"status":"healthy",...}
curl -s "$BASE/installation" -H "X-Plugin-Api-Key: $KEY" | jq # org, location, config, features
curl -s "$BASE/locations" -H "X-Plugin-Api-Key: $KEY" | jq
curl -s "$BASE/menu" -H "X-Plugin-Api-Key: $KEY" | jq '.data[:3]'
curl -s "$BASE/customers" -H "X-Plugin-Api-Key: $KEY" | jq
curl -s "$BASE/inventory" -H "X-Plugin-Api-Key: $KEY" | jq
Start with GET /installation — it tells you your organization_id, whether
you're pinned to a single location_id, your stored config, and the
enabled_features recorded on the installation. Everything comes back under a
data wrapper.
enabled_features is not what gates write endpointsCapability checks read supported_features on the plugin listing, not
enabled_features on the installation. An empty enabled_features does not
mean orders:create is unavailable — check your listing.
Step 4 — Write to the sandbox
Transition an order
curl -X PUT "https://api.ssppos.com/api/plugin/v1/orders/1" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"order_status": "in_progress"}'
Only status transitions are accepted — line items, totals and discounts stay
server-authoritative. An illegal target returns 422 invalid_transition with
the list of legal next states, so your code can self-correct.
Update menu availability
curl -X PATCH "https://api.ssppos.com/api/plugin/v1/menu/1" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"is_available": false}'
Only is_available, price, and preparation_time are writable here.
Record a stock movement
curl -X POST "https://api.ssppos.com/api/plugin/v1/inventory/1/movements" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"location_id": 1, "delta": -3.0, "reason": "consume", "note": "Lunch service"}'
This fires inventory.stock_updated, and inventory.low_stock if the movement
takes the ingredient to or below its reorder point.
Create an order, capture a payment
These two need capabilities on your plugin listing — orders:create and
payments:capture respectively. Without them you get 403 feature_not_enabled.
See Capability gating.
# Create a delivery order
curl -X POST "https://api.ssppos.com/api/plugin/v1/orders" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"location_id": 1,
"order_type": "DELIVERY",
"external_order_id": "UE-ABC123",
"items": [{ "menu_item_id": 1, "quantity": 2 }]
}'
# Capture a payment against it
curl -X POST "https://api.ssppos.com/api/plugin/v1/orders/1/payments" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"amount": 29.33,
"currency": "CAD",
"payment_method": "upi",
"provider": "razorpay",
"provider_transaction_id": "pay_test_001",
"captured_at": "2026-04-17T10:30:00Z"
}'
currency has to equal the order's location currency, or you get
422 currency_mismatch. The seeded sandbox locations are Canadian.
Step 5 — Receive webhooks
Set your plugin's webhook_endpoint (an https URL — plain HTTP and private
or loopback addresses are rejected), then verify every delivery:
const crypto = require('crypto');
const express = require('express');
const app = express();
// express.raw() matters: the HMAC covers the exact bytes SSP sent.
app.post('/webhooks/ssp', express.raw({ type: 'application/json' }), (req, res) => {
const received = req.headers['x-ssp-signature'] || '';
const expected = crypto
.createHmac('sha256', process.env.SSP_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const { event, installation_id, data } = JSON.parse(req.body.toString('utf8'));
console.log(`[${installation_id}] ${event}`, data);
// Answer fast; do the real work off the request path.
res.json({ status: 'ok' });
});
Then fire a test from the portal's webhook tester, pick an event, and poll the returned log ID for the outcome. Full catalog and payloads: Webhooks.
Local development with ngrok
SSP only delivers to public HTTPS addresses, so expose your local server:
ngrok http 3000
# Use the https://<id>.ngrok.io URL as your webhook_endpoint
Step 6 — Forward events from your provider to SSP
When your provider (Razorpay, DoorDash, …) notifies you, forward it to SSP:
BODY='{"event_type":"external.payment_confirmed","payload":{"transaction_id":"pay_123"},"idempotency_key":"evt-001"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "YOUR_WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST "https://api.ssppos.com/api/plugin/v1/webhooks/external" \
-H "X-Plugin-Api-Key: pik_YOUR_KEY_HERE" \
-H "X-SSP-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d "$BODY"
A 201 returns a log_id you can poll at GET /webhooks/external/{logId}.
Reusing an idempotency_key within 24 hours returns 200 with the original log
instead of creating a duplicate.
Step 7 — Promote your scaffold into a real plugin
When the integration works, turn the project's scaffold into a listable plugin:
Create Plugin on the project page. You'll supply a name, type,
api_endpoint, webhook_endpoint, event subscriptions, and pricing.
Creation returns a new webhook_secret, shown once — update your
environment with it.
The plugin starts in draft. When you're ready, Submit for Review. See
Plugin Lifecycle for the full path to active.
Common issues
401 invalid_api_key_format
The key must start with pik_ and be exactly 68 characters. Check for
trailing whitespace or newlines in your environment variable.
401 invalid_api_key
The key doesn't match any installation — usually because it was regenerated or the sandbox was re-provisioned.
403 installation_inactive
The installation isn't active. In a sandbox this usually means it was
provisioned but something interrupted setup.
403 plugin_listing_unavailable
Affects production installations only: the listing is not in approved or
active status (suspended, deprecated, rejected, still under review, or
deleted). Sandbox installations are exempt.
409 sandbox_resetting
A reset is in flight. Wait a moment and retry.
401 invalid_signature on inbound webhooks
Compute the HMAC over the exact raw bytes you transmit, not a re-serialized
copy, and use the plugin's webhook_secret as the key. Send it hex-encoded in
X-SSP-Signature.
429 rate_limit_exceeded
60 requests/minute per installation, enforced on every plugin/v1 route —
including inbound webhook forwarding, which shares the same budget rather than
getting a free 600/minute lane. Honour the Retry-After header and pace your
forwarding. See Rate limits.
What's next
- Authentication — credentials and HMAC in depth
- Plugin Data API — every endpoint, with examples
- Webhooks — the event catalog
- Setup Handoff — host your own configuration UI
- Plugin Lifecycle — review, pricing, and publishing
Support
- Email: developers@ssppos.com
- Examples: github.com/ssppos/examples