Aller au contenu principal

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.

This is the only way a setup_url plugin gets its production key

For 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 SSPThe standard install flow — no setup_url
Needs its own UI, OAuth, or account linkingSetup 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:

ParameterDescription
token64-character alphanumeric, single-use, 5-minute lifetime
return_urlWhere 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​

Exchange from your backend, never the browser

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" }
}
FieldNotes
api_keyThe runtime pik_ key. Store it immediately — this response is the only place it appears
installation_idYour tenant key. Store the API key against this
organization_idnull for sandbox installs — they are org-less
is_sandboxDistinguishes a sandbox exchange from a production one
developer_sandbox_idSet only when is_sandbox is true
location_idSet when the installation is pinned to one location
configAny 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​

PropertyValue
Lifetime5 minutes
ReuseImpossible — consumed atomically, so two racing exchanges can't both succeed
BindingBound to the exact installation and plugin. A mismatch returns 401
Rate limits10 setup starts/min per operator; 20 exchanges/min
return_urlValidated 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​