Create an app
To build an app for Stok.ly, you first register it in the developer portal. Creating an app gives you the credentials your app needs, a Client ID, a Client Secret and a Webhook Secret, and is where you declare what your app does, via a manifest, before anyone can install it into their account.
The developer portal is still under active development. This page describes the intended workflow, specifics that aren't finalized yet are flagged inline below.
Before you start
You'll need a developer account, and your developer account will need to accept Stok.ly's developer agreement before you can create an app, and before you publish a release.
Creating your app
From the developer portal, an app has these details:
| Field | Required | Description |
|---|---|---|
| Name | yes | Shown to users on your app's marketplace listing. Up to 128 characters. |
| Description | yes | A short summary shown to users considering installing your app. Up to 255 characters. |
| Support email | yes | Where users can contact you about your app. |
| Icon | no | Shown alongside your app's name. |
| Long description | no | The full description on your listing. Up to 8000 characters. |
| Screenshots | no | Images shown on your listing. |
| Privacy policy URL | no | Linked from your listing. |
| Homepage URL | no | Linked from your listing. |
Uploading your manifest
Each release of your app is defined by a manifest.json file. It describes what your app needs and does: the scopes it requests, where Stok.ly should redirect users after they authorize your app, the pages it adds, and the extension points it uses (or, in future, the providers it implements). See App Manifest for every field.
A minimal manifest looks like this:
{
"schemaVersion": 1,
"version": "1.0.0",
"scopes": [
"catalogue.items.read",
"catalogue.items.create"
],
"oauth": {
"redirectUrls": ["https://myapp.example.com/callback"]
}
}Manifest validation
When you upload a manifest, Stok.ly validates it before a release can be created or updated, and reports every problem at once. Every URL in your manifest, including your OAuth redirectUrls, is checked against the same rules used for webhook URLs:
- HTTPS only, on port 443
- The hostname must resolve in DNS
- No localhost or private IP ranges
- No loopback addresses
- No internal hostnames
- Must be a valid, well-formed URI
- No credentials embedded in the URI
- Up to 2048 characters
On the dev environment only, local URLs are allowed too. See Local development.
Your redirect URL is checked again on every OAuth request (when a user starts an install or update, and when you exchange the code), not only at upload, so a URL that passed validation but now resolves to a disallowed address (for example, through a changed DNS record) is refused at the point it's used.
The URLs your app serves content from (its worker, pages and extension frames) must also be on origins that no other developer's app uses, and can't be on Stok.ly's own domains. See URLs and origins.
Getting your credentials
Once your app is created, you'll receive:
- A Client ID, a public identifier for your app. It is your app's id, the same one used in its Stok.ly URLs and as the audience of its session tokens.
- A Client Secret, used to authenticate your app when exchanging an authorization code for tokens and when refreshing them. Treat this like a password, it's shown once and can't be retrieved again after you leave the page.
- A Webhook Secret, used to check that webhooks really come from Stok.ly. See Verifying webhooks. It's also shown once.
If your client secret is lost or leaked, use Regenerate client secret on your app's page in the developer portal. The old secret stops working straight away for code exchanges and token refreshes, so update your backend with the new one. Access tokens already issued keep working until they expire, which is at most an hour.
If your webhook secret is lost or leaked, use Regenerate webhook secret. Webhooks are signed with the new secret within a few moments, so update your backend straight away.
Your listing URL
Every app has a listing in the Stok.ly marketplace. Its link is shown on your app's page in the developer portal, next to your Client ID, and works once you publish a release. It has the form
https://app.stok.ly/marketplace/{appId}
On dev, the host is https://app.dev.stok.ly.
Share this link with users (or embed it in your own onboarding) so they can install your app. The listing shows your app, the scopes you requested and what it adds to and removes from Stok.ly, and if they click Install, Stok.ly redirects to your app to finish the install. See Installing your app.
Installing your app
When a user clicks Install on your listing, Stok.ly redirects their browser to the first entry in your manifest's oauth.redirectUrls, adding two query parameters:
https://myapp.example.com/callback?code=<authorization code>&state=<JWT>
Your backend then does two things: verify state, and exchange code for tokens. Exchanging the code is what completes the install. Until your app exchanges it, the app isn't installed on the account.
Verifying state
statestate is a JWT signed with RS256 by Stok.ly. It tells you which account and user the install is for, and it can't be forged or altered. Verify it before you do anything else, using the same public keys as session tokens:
- Production:
https://api.stok.ly/v1/app-extensions/jwks.json, issuerhttps://api.stok.ly/apps. - Dev:
https://api.dev.stok.ly/v1/app-extensions/jwks.json, issuerhttps://api.dev.stok.ly/apps.
| Claim | Value |
|---|---|
iss | the Stok.ly issuer above |
aud | your app's Client ID, which is its app id |
sub | the Stok.ly user id of the user who installed the app |
accountkey | the account the app is being installed on |
app_id | your app's id |
release_id | the release being installed |
token_use | always "app_install" |
jti | a unique id for this install request |
iat | when the state was issued |
exp | when the state expires, 10 minutes after iat |
Check the signature (using the key that matches the token's kid), iss, aud (your own Client ID), exp, and that token_use is "app_install". You can also remember each jti you've handled, to reject a state that's replayed.
Exchanging the code
Send the code from your backend, never from the browser, because it needs your Client Secret:
POST https://api.stok.ly/v1/oauth/token
Use https://api.dev.stok.ly on dev. The endpoint is public and takes a JSON or form body with the standard OAuth fields:
| Field | Value |
|---|---|
grant_type | authorization_code |
code | the code query parameter |
client_id | your app's Client ID |
client_secret | your app's Client Secret |
redirect_uri | the redirect URL Stok.ly used, which must equal the first entry in your oauth.redirectUrls exactly |
The code expires after 2 minutes and can be used once. If an exchange fails, the user has to start again from your listing URL.
A request with a missing field or an unsupported grant_type gets a 400 with a message naming the problem. A code that is wrong, expired or already used, or a wrong client_id, client_secret or redirect_uri, fails with Invalid credentials, so treat any non-2xx response as a failed exchange.
The response contains the tokens for the account:
{
"data": {
"accessToken": "stk.at.…",
"refreshToken": "stk.rt.…",
"expiry": "2026-01-01T12:00:00.000Z",
"accountkey": "acme"
},
"metadata": {}
}Before you store the tokens, check that the accountkey in the response equals the accountkey claim from state. Store the tokens securely, keyed by account. They are how your app calls the Stok.ly API for that account.
Refreshing tokens
An access token lasts 1 hour (expiry in the response). To get a new one, send the refresh token from your backend to the same endpoint:
| Field | Value |
|---|---|
grant_type | refresh_token |
refresh_token | the account's current refresh token |
client_id | your app's Client ID |
client_secret | your app's Client Secret |
The response holds the new accessToken, refreshToken and expiry, in the same data wrapper as the code exchange. It has no accountkey, so keep track of which account each refresh token belongs to. A refresh token lasts 28 days and is replaced every time you use it, so always store the new one and never reuse the old one.
When an account uninstalls your app, its access and refresh tokens stop working within moments, and you receive an app.uninstalled webhook if you've subscribed to it.
Example
In Node, with the jose library, following the same pattern as the session token example:
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://api.stok.ly/v1/app-extensions/jwks.json"));
const usedStates = new Set(); // use a shared store with expiry in a real app
app.get("/callback", async (req, res) => {
const { code, state } = req.query;
// 1. verify state
const { payload } = await jwtVerify(state, jwks, {
issuer: "https://api.stok.ly/apps",
audience: process.env.STOKLY_CLIENT_ID,
algorithms: ["RS256"],
});
if (payload.token_use !== "app_install") {
throw new Error("not a Stok.ly app install state");
}
if (usedStates.has(payload.jti)) {
throw new Error("install state already used");
}
usedStates.add(payload.jti);
// 2. exchange the code, which completes the install
const response = await fetch("https://api.stok.ly/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
client_id: process.env.STOKLY_CLIENT_ID,
client_secret: process.env.STOKLY_CLIENT_SECRET,
redirect_uri: "https://myapp.example.com/callback", // your first oauth.redirectUrls entry
}),
});
if (!response.ok) {
throw new Error(`code exchange failed: ${response.status}`);
}
const { data: tokens } = await response.json();
// 3. the tokens must be for the account the install was started on
if (tokens.accountkey !== payload.accountkey) {
throw new Error("account mismatch");
}
// store tokens.accessToken and tokens.refreshToken securely, keyed by tokens.accountkey
res.send(`Installed on account ${tokens.accountkey}`);
});In Express 5 a rejected promise in a handler becomes a 500 response. In production, catch these errors and show the user a clear message.
Updating your app
To ship a change, publish a new release. Its version must be a semantic version (major.minor.patch) greater than your latest published release, or the publish is rejected. A published release never changes, so every change, including a fix, is a new version.
Accounts that have your app installed keep running the release they installed until a user with permission to install apps on that account chooses to update. Stok.ly shows an Update available tag on the app in the Apps list, and a banner with the installed and new versions on the app's page and its marketplace listing. Clicking Update shows a summary of what changes before the user confirms. Updates always go to your latest published release. There's no rollback to an earlier release.
What happens next depends on whether the new release asks for more scopes:
- No new scopes. Stok.ly applies the update straight away without contacting your app. The account's pages, extensions, hooks and
worker_urlswitch to the new release, and session tokens carry the newrelease_id. Scopes you dropped are removed from the app's access. Your existing tokens keep working. - New scopes. The user has to approve them first. Stok.ly redirects to your install redirect URL (the first entry in
oauth.redirectUrls) with acodeandstate, exactly as for an install, and thestatecarries the newrelease_id. Exchange the code as you would for an install. Exchanging it completes the update. The response has fresh tokens for the same installation, and the app is not installed a second time. Your existing tokens stay valid, and the new scopes apply to them once the code is exchanged.
Your app can tell an update from an install by whether it already holds tokens for accountkey. Replace the stored tokens with the new ones.
Testing with a sandbox account
Creating an app will also let you create a sandbox account, a real Stok.ly account flagged for testing, with tighter limits (no real external calls, tight rate limits, emails only deliverable to your own address) so you can build and test your app without touching a live account.
What's next
- App Manifest - every field your manifest can contain.
- Extension Points - if your app renders UI inside Stok.ly or hooks into its actions.
- App Bridge - the SDK your frames and worker use to talk to Stok.ly.
- Providers - if your app implements a shipping, channel, or accounting integration (coming in a later phase).
- Access Scopes - the full list of scopes you can request in your manifest.
- Authentication - how installation and the OAuth flow work end to end.
Updated about 7 hours ago
