Skip to content
SayGMDocs
Dashboard

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.

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.

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.

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.

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.

For every authorization attempt, generate:

  • An unpredictable state, bound to the initiating browser session.
  • A random PKCE code_verifier with 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.

The variables below represent credentials stored securely by your app. Do not put them in URLs, logs, analytics, public source code or browser bundles.

Requires profile:read. Use the OAuth access token:

Terminal window
curl -fsS 'https://account.saygm.com/v1/me' \
-H "Authorization: Bearer $SAYGM_ACCESS_TOKEN"

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:

Terminal window
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.

Use the dedicated inference key on the account API:

Terminal window
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.

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.

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.

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.

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.

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.