Sign in with SayGM
Let customers connect their SayGM account without copying an API key. Your app redirects them to SayGM to review permissions, then exchanges an authorization code for credentials. If they allow inference, your app receives a dedicated key that spends their SayGM credits.
SayGM uses Authorization Code with PKCE (S256) for partner apps. Customers
can view their connections, change spending limits and revoke access in
Connected apps.
Register your app
Section titled “Register your app”Contact the SayGM team through Discord with:
- Your application’s name and a short description of what it does.
- Each exact callback URL your app will use.
- The permissions you need from the list below.
The team registers the app and gives you a public client_id. This flow uses
PKCE and does not require a client secret. Registration is managed by SayGM;
there is no public self-service registration endpoint.
Callback URLs must use HTTPS, or loopback HTTP for local development, such as
http://127.0.0.1:3000/callback. Register the exact URL, including its port and
path. Wildcards, URL fragments and embedded credentials are not supported.
For accounts with a shared organization balance, the primary billing account owner must approve the connection.
Endpoints
Section titled “Endpoints”| Purpose | URL |
|---|---|
| Browser authorization | https://saygm.com/oauth/authorize |
| Code exchange and token refresh | https://account.saygm.com/v1/oauth/token |
| Revoke access | https://account.saygm.com/v1/oauth/revoke |
| Account API | https://account.saygm.com/v1 |
| Inference API | https://api.saygm.com/v1 |
Keep the account and inference hosts separate. An OAuth access token is used
for permitted account API calls. The inference_api_key is used for model
requests. Neither credential grants access to the customer’s other API keys.
Choose permissions
Section titled “Choose permissions”Request only the permissions your app needs. The customer can approve a subset;
read the returned scope instead of assuming every requested permission was granted.
| Scope | Access |
|---|---|
profile:read |
Read the customer’s profile through GET /v1/me, including their user ID, email, sign-in provider and organization context. |
inference:use |
Receive a dedicated inference key. Also permits the account API’s model catalog and capabilities endpoints. |
usage:read |
Permission for usage associated with this app’s dedicated key; it does not grant account-wide usage or billing access. See public reporting below. |
Partner access does not include listing, revealing, creating, editing or deleting the customer’s general API keys, changing their balance allocations, changing billing settings, administering accounts or changing the app’s spending limit.
Public usage reporting
Section titled “Public usage reporting”The public reporting endpoint is GET /v1/usage/self on the account API. It
accepts the dedicated inference key, not an OAuth access token, and returns
only that key’s history. An app that requests only usage:read does not currently
have a public OAuth-token usage endpoint. For public usage reporting, request
inference:use and use the returned key as shown below.
Customers can also see the app’s spending in Connected apps. Usage history can lag recent requests; use its reporting watermark rather than treating missing records as zero spend.
1. Send the customer to SayGM
Section titled “1. Send the customer to SayGM”For every authorization attempt, generate:
- An unpredictable
state, bound to the initiating browser session. - A random PKCE
code_verifierwith 43–128 URL-safe characters. - A
code_challenge: the base64url-encoded SHA-256 hash of the verifier, without padding.
This Node.js example assumes your application has secure server-side session middleware. Keep the verifier in that session until the callback; send only the challenge to SayGM.
import { createHash, randomBytes } from "node:crypto";
const clientId = process.env.SAYGM_CLIENT_ID;const redirectUri = "https://your-app.example/oauth/saygm/callback";
function startSaygmLogin(req, res) { const state = randomBytes(32).toString("base64url"); const verifier = randomBytes(48).toString("base64url"); const challenge = createHash("sha256") .update(verifier) .digest("base64url");
req.session.saygmOAuth = { state, verifier };
const url = new URL("https://saygm.com/oauth/authorize"); url.search = new URLSearchParams({ response_type: "code", client_id: clientId, redirect_uri: redirectUri, scope: "profile:read inference:use", state, code_challenge: challenge, code_challenge_method: "S256", }).toString();
res.redirect(url.toString());}Use your registered callback URL in place of your-app.example. Persist the
session before redirecting if your session store requires an explicit save.
Keep state compact: it must be 16–512 characters, and the full authorization
request must fit SayGM’s browser continuation limit. Store application return
paths in your own session rather than encoding a large object in state.
The customer signs in, reviews your app’s requested permissions and chooses a monthly spending limit when inference is requested. They can explicitly choose unlimited instead. Your app cannot set or override that choice through the URL.
2. Handle the callback and exchange the code
Section titled “2. Handle the callback and exchange the code”On approval, SayGM redirects to your registered callback with code and state.
If the customer declines, the callback contains error=access_denied and state.
Verify state against the initiating session before exchanging the code or
handling the result. Codes are single-use and expire after five minutes. The
exchange must include the same callback URL and the original verifier.
Continue the Node.js example with this callback handler. The
storeSaygmConnection function represents your application’s encrypted
credential store; implement it before using this handler.
async function finishSaygmLogin(req, res) { const pending = req.session.saygmOAuth; if (!pending || typeof req.query.state !== "string" || req.query.state !== pending.state) { return res.status(400).send("Invalid sign-in state"); } delete req.session.saygmOAuth;
if (req.query.error) { return res.status(400).send("SayGM connection was not approved"); } if (typeof req.query.code !== "string") { return res.status(400).send("Missing authorization code"); }
const response = await fetch("https://account.saygm.com/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/json" }, redirect: "error", body: JSON.stringify({ grant_type: "authorization_code", client_id: clientId, code: req.query.code, redirect_uri: redirectUri, code_verifier: pending.verifier, }), }); if (!response.ok) { return res.status(400).send("Could not connect SayGM; start again"); }
const tokens = await response.json(); const scopes = new Set((tokens.scope ?? "").split(" ")); await storeSaygmConnection(req.session.userId, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000, scopes: [...scopes], inferenceKey: tokens.inference_api_key ?? null, }); res.redirect("/settings/connections");}Bind credentials to the user who started the connection in your app. For a
standalone sign-in experience, request profile:read and retrieve /v1/me to
identify the SayGM user before establishing your own application session.
The response includes access_token, token_type, expires_in,
refresh_token and the granted scope. It includes inference_api_key only
when the customer approved inference:use. Store that key securely at code
exchange; token refresh does not return it again. If a required permission was
not granted, explain which feature is unavailable instead of assuming access.
3. Use the right credential
Section titled “3. Use the right credential”The variables below represent credentials stored securely by your app. Do not put them in URLs, logs, analytics, public source code or browser bundles.
Read the profile
Section titled “Read the profile”Requires profile:read. Use the OAuth access token:
curl -fsS 'https://account.saygm.com/v1/me' \ -H "Authorization: Bearer $SAYGM_ACCESS_TOKEN"Run inference
Section titled “Run inference”Use the dedicated inference key returned by code exchange. Choose an
available model from GET https://api.saygm.com/v1/models and substitute its ID:
curl -fsS 'https://api.saygm.com/v1/chat/completions' \ -H "Authorization: Bearer $SAYGM_INFERENCE_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "YOUR_MODEL_ID", "messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 64 }'The customer pays for these requests from their SayGM credits. Your app’s key is linked to that customer and app connection. See API overview for the other supported inference interfaces.
Read this key’s usage
Section titled “Read this key’s usage”Use the dedicated inference key on the account API:
curl -fsS --get 'https://account.saygm.com/v1/usage/self' \ -H "Authorization: Bearer $SAYGM_INFERENCE_KEY" \ --data-urlencode 'group_by=day'The default window is the last 30 days. Optional from and to parameters are
UTC timestamps; a window can cover at most 90 days. This endpoint does not accept
a selector for another customer or key. See Usage.
Refresh access tokens
Section titled “Refresh access tokens”Use expires_in to track expiry. Refresh through the same token endpoint:
const response = await fetch("https://account.saygm.com/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/json" }, redirect: "error", body: JSON.stringify({ grant_type: "refresh_token", client_id: clientId, refresh_token: storedRefreshToken, }),});On success, atomically replace both stored OAuth tokens. Refresh tokens rotate;
serialize refreshes for each connection so concurrent requests do not reuse an
old token. Keep the inference key already stored. Omit scope from refresh
requests; refresh retains the granted permissions and cannot add new ones.
If refresh returns invalid_grant, send the customer through a new authorization
flow instead of repeatedly retrying the same token.
A successful new authorization exchange replaces the previous OAuth sessions for that customer/app connection. Replace your saved credentials together. A reconnect may return the current inference key again; always use the latest successful exchange response.
Spending limits
Section titled “Spending limits”Customers choose a monthly dollar limit during consent and can change it later in Connected apps. The limit applies to spending by your app against that customer’s billing account during the UTC calendar month.
- Available credit is the lower of the key’s available account credit and the app’s remaining monthly allowance.
- Top-ups, refreshes and reconnects do not reset the app’s monthly spending.
- The customer can set zero to block further spending, raise the limit, or explicitly select unlimited. Your app cannot change the limit itself.
- The selection is saved when the customer approves consent. Abandoning the callback does not undo a changed limit on an existing connection.
This is a soft spending limit. Cached balances, concurrent requests and settlement delays can allow spending to exceed the selected amount. Treat it as spend protection rather than a guaranteed hard ceiling. A top-up does not clear an exhausted app allowance.
When inference returns HTTP 402, ask the customer to check both their SayGM
credits and their app limit. Avoid automatically retrying billable requests.
Disconnect and revoke
Section titled “Disconnect and revoke”Customers can revoke the app in Connected apps. Your app can also disconnect by posting one of its current OAuth tokens to the revocation endpoint:
await fetch("https://account.saygm.com/v1/oauth/revoke", { method: "POST", headers: { "Content-Type": "application/json" }, redirect: "error", body: JSON.stringify({ token: storedRefreshToken }),});Check that revocation succeeded before marking disconnect complete. Revocation disables the current app grant, its OAuth sessions, outstanding authorization codes and its dedicated inference key. Inference rejection follows the gateway’s authentication-cache refresh. Remove local credentials after successful revoke; a new consent flow is needed to reconnect. Disabling an app or changing its allowed permissions also revokes its customer connections.
Referral attribution
Section titled “Referral attribution”If the SayGM team has given you a referral code, add ref=YOUR_REFERRAL_CODE to
the browser authorization URL on saygm.com. This participates in the
existing signup attribution flow. Adding ref to the token endpoint or an
inference request does not capture a referral.
An OAuth connection does not automatically enroll a developer in an affiliate program, create a payout entitlement or replace an existing customer’s attribution. Agree referral and commission terms with the SayGM team separately.
Errors to handle
Section titled “Errors to handle”| Result | What your app should do |
|---|---|
Callback error=access_denied |
Keep the customer disconnected and offer a fresh attempt if they choose. |
Token endpoint invalid_grant |
Check the code’s expiry, single-use status, redirect URI and verifier. For a revoked or consumed refresh token, reconnect. |
invalid_scope |
Request only registered permissions and inspect the returned granted scope. |
Account API HTTP 403 |
The token does not authorize this operation. Do not retry with the same permissions. |
Inference HTTP 401 |
The key may be invalid or revoked. Check the connection before asking the customer to reconnect. |
Inference HTTP 402 |
Ask the customer to check account credits and this app’s monthly limit. |
HTTP 429 |
Respect Retry-After and reduce request frequency. |
This guide covers registered partner apps. SayGM’s own desktop and CLI clients use their existing device-login flow; connecting those clients does not currently create a partner-app monthly spending limit.