Aller au contenu principal

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:

ValueFormatPurpose
API keypik_ + 64 hex chars (68 total)Authenticating REST calls
Webhook secret64 charsHMAC-SHA256 signing, both directions
Lost one?

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 }
}
Reads and filters speak different vocabularies

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 endpoints

Capability 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 must match the location

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​

Support​