Docs
Sign in with Bloom
A button on a seller's own site. Their customer clicks it, approves once, and comes back signed in, with the licences they bought from that shop attached if the app asked for them. Standard OAuth authorisation code, with the redirect URI checked exactly rather than by prefix.
Requires the seller's workspace to be on a plan that includes sign_in_with_bloom (Pro and above) to register a new app; apps already registered keep working regardless.
Register an app
From the workspace's Sign-in apps page: a name, an optional product to scope licences to, one or more redirect URIs (https, except on localhost), and the scopes it may ask for. Registering returns a client_id and a client secret shown once, prefixed bloomsec_.
The flow
- Send the customer to the approval screen with your
client_id,redirect_uriand requestedscope. Before showing anything, Bloom checks the client id exists, is not revoked, that the redirect URI is registered exactly, and that every requested scope is one the app is allowed to ask for. A failure here is shown on Bloom's own page rather than redirected: bouncing to an unverified redirect_uri is how a phishing page gets a Bloom-shaped error. - The customer, signed in to their own Bloom account, presses Allow. Bloom issues a single-use authorisation code, valid for 5 minutes, and redirects to your
redirect_uriwith?code=...(and&state=...if you sent one). - Your server exchanges the code for the identity at
POST /api/oauth/token, using your client id and secret. The code can only be redeemed once, against the exact redirect URI it was issued for.
Scopes
| Scope | Grants |
|---|---|
| identity | Who signs in: their name and email address. Always included; requesting it explicitly is optional. |
| licenses | The licences they hold from this shop, scoped to the app's own product if it names one. |
Exchanging the code
POST /api/oauth/token accepts either form-encoded or JSON, reading both the snake_case names OAuth libraries usually send and the camelCase equivalents:
{
"client_id": "bloom_...",
"client_secret": "bloomsec_...",
"code": "...",
"redirect_uri": "https://your-site.example/callback",
"code_verifier": "..." /* only if you sent a code_challenge */
}Response, per-app and single-shop:
{
"user": {
"id": "a per-app id, not the same across two of your apps",
"email": "[email protected]",
"emailVerified": true,
"name": "Customer name",
"avatarUrl": null
},
"scopes": ["identity", "licenses"],
"licenses": [
{
"key": "BLM-XXXX-XXXX-XXXX",
"status": "active",
"expiresAt": null,
"isLifetime": true,
"product": "Your Product",
"package": "Pro"
}
]
}licenses is present only when licenses was in the granted scope, and only ever holds licences from your own shop, filtered to your app's product if it names one. user.id is hashed per app, so two of your apps (or two different sellers) cannot compare it to learn the same person shops with both.
PKCE
Optional. Send a codeChallenge (S256, base64url) when requesting approval, and the matching code_verifier when exchanging the code. If you sent a challenge, the verifier is required at exchange or the token call fails.
Errors
The token exchange fails with invalid_client (unknown client id, revoked app, or wrong secret) or invalid_grant (unknown, already used, or expired code; a redirect URI that does not match the one the code was issued for; a missing or wrong PKCE verifier). Approval-screen failures (bad client id, bad redirect URI, an unregistered or disallowed scope) are shown as a page, not redirected.