Setup Handoff
If your plugin needs configuration that SSP's generic settings form can't
express — connecting a provider account, choosing a store, running an OAuth
dance — you can host your own setup page. SSP hands the operator off to it
after install, along with a single-use token your backend exchanges for the
installation's runtime pik_ key.
setup_url plugin gets its production keyFor plugins with a setup_url, the exchange endpoint is the sole place the
runtime key is minted. The restaurant operator never sees it.
When you need this
| Your plugin… | Use |
|---|---|
| Works from static config the operator types into SSP | The standard install flow — no setup_url |
| Needs its own UI, OAuth, or account linking | Setup handoff |
The flow
Step 1 — Declare a setup URL
Set setup_url when creating or updating your plugin. It must be an https
URL. Operators are handed off here after install.
Step 2 — Receive the redirect
SSP appends two query parameters to your setup_url:
| Parameter | Description |
|---|---|
token | 64-character alphanumeric, single-use, 5-minute lifetime |
return_url | Where to send the operator when setup finishes |
https://plugin.example.com/setup?token=aB3…&return_url=https%3A%2F%2Fmanager.ssppos.com%2Fmarketplace%2Finstalled
The handoff is only offered when the installation is active and your plugin
actually has a setup_url; operators are rate-limited to 10 setup starts per
minute.
Step 3 — Exchange the token, server-side
The response contains the installation's pik_ key. Pass the token from your
page to your server and exchange it there.
POST https://api.ssppos.com/api/v1/plugins/setup/exchange
Content-Type: application/json
{ "token": "aB3…" }
This endpoint takes no authentication header — the single-use token is the credential. It's rate-limited to 20 requests/minute.
Response 200:
{
"api_key": "pik_a3f2…",
"installation_id": "1234",
"organization_id": "56",
"is_sandbox": false,
"developer_sandbox_id": null,
"location_id": "7",
"plugin_id": "89",
"config": { "existing": "settings" }
}
| Field | Notes |
|---|---|
api_key | The runtime pik_ key. Store it immediately — this response is the only place it appears |
installation_id | Your tenant key. Store the API key against this |
organization_id | null for sandbox installs — they are org-less |
is_sandbox | Distinguishes a sandbox exchange from a production one |
developer_sandbox_id | Set only when is_sandbox is true |
location_id | Set when the installation is pinned to one location |
config | Any configuration already stored on the installation |
Response 401: {"message": "Invalid or expired token."} — the token was
already used, has expired, or doesn't match the installation it claims. The same
message covers all cases.
Node.js
app.get('/setup', async (req, res) => {
const { token, return_url } = req.query;
const { data } = await axios.post(
'https://api.ssppos.com/api/v1/plugins/setup/exchange',
{ token },
);
await db.installations.upsert({
installation_id: data.installation_id,
organization_id: data.organization_id,
location_id: data.location_id,
is_sandbox: data.is_sandbox,
api_key: encrypt(data.api_key), // never store plaintext
});
// Now render your configuration UI, and send the operator to return_url when done.
res.render('setup', { installationId: data.installation_id, returnUrl: return_url });
});
Python
@app.route('/setup')
def setup():
token = request.args['token']
return_url = request.args.get('return_url')
resp = requests.post(
'https://api.ssppos.com/api/v1/plugins/setup/exchange',
json={'token': token},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
save_installation(
installation_id=data['installation_id'],
organization_id=data['organization_id'],
api_key=encrypt(data['api_key']),
is_sandbox=data['is_sandbox'],
)
return render_template('setup.html', return_url=return_url)
Step 4 — Configure and return
Run whatever your setup needs, then write any settings back:
curl -X POST "https://api.ssppos.com/api/plugin/v1/installation/config" \
-H "X-Plugin-Api-Key: pik_YOUR_NEW_KEY" \
-H "Content-Type: application/json" \
-d '{"config": {"provider_store_id": "store_xyz", "mode": "live"}}'
Config updates merge — keys you don't send are preserved.
Finally, redirect the operator to the return_url you were given.
Token security
| Property | Value |
|---|---|
| Lifetime | 5 minutes |
| Reuse | Impossible — consumed atomically, so two racing exchanges can't both succeed |
| Binding | Bound to the exact installation and plugin. A mismatch returns 401 |
| Rate limits | 10 setup starts/min per operator; 20 exchanges/min |
return_url | Validated against an allowlist — an attacker-supplied return URL can't be used as an open redirect |
Re-running setup
An operator can start setup again at any time. Each run mints a fresh token and
a fresh pik_ key: exchanging always re-mints, and the previous key stops
working immediately. There is exactly one live key per installation, so always
overwrite the stored key with what the latest exchange returned.
Sandbox testing
The handoff works from your sandbox too. Open SSP Manager against your sandbox
(see Sandboxes),
install your plugin from the sandbox plugin card, and click Configure — you
get the same redirect and the same exchange, with is_sandbox: true and a null
organization_id.
Make sure your code branches on is_sandbox rather than assuming
organization_id is always present.
Next steps
- Authentication — what to do with the
pik_key - Plugin Lifecycle — declaring
setup_urlon your listing - Security — storing per-installation credentials safely