On this page and tools
Sign in with Bynefit
OAuth 2.0 / OpenID Connect
Add a "Sign in with Bynefit" button to your site or app. Bynefit is a standards-compliant OAuth 2.0 / OpenID Connect provider — the same flow you use for Google, Apple, or Microsoft.
Issuer
All flows start at id.bynefit.com — the dedicated auth
domain, which serves the endpoints below and nothing else. Bookmark the
discovery document if your client supports auto-configuration:
GET https://id.bynefit.com/.well-known/openid-configuration
It advertises the canonical endpoints:
| Endpoint | URL |
|---|---|
| Authorization | https://id.bynefit.com/oauth/authorize |
| Token | https://id.bynefit.com/oauth/token |
| UserInfo | https://id.bynefit.com/oauth/userinfo |
| JWKS | https://id.bynefit.com/oauth/jwks.json |
Tokens are signed RS256. Always validate iss equals
https://id.bynefit.com and aud equals your
client_id before trusting the claims.
Register a client
Team admins register OAuth clients at
/dash/sites/oauth. Each client is
scoped to one team and gets a unique client_id.
Site-registered clients are public clients:
- No
client_secret— they run in browsers and can't keep one. - PKCE is required on every authorization request.
- Scopes are
openid profile emailto start.creator.readandearnings.readcan be asked for on the client's page and are approved by Bynefit staff (see Scopes). - Redirect URIs are exact-matched and must be HTTPS (or
http://localhostfor development).
The flow (PKCE)
- Generate a high-entropy
code_verifier(43–128 chars). Computecode_challenge = BASE64URL(SHA256(code_verifier)). - Redirect the user to:
Step 1 — AuthorizeGET
https://id.bynefit.com/oauth/authorize ?client_id=site_xxxxxxxxxxxx &redirect_uri=https://yoursite.com/auth/callback &response_type=code &scope=openid+profile+email &state=<random> &code_challenge=<S256-of-verifier> &code_challenge_method=S256
- The user signs in (if needed) and approves the scopes. Bynefit redirects back to your
redirect_uriwith?code=…&state=…. Verifystatematches what you sent. - Exchange the code for tokens:
Response (JSON):Step 2 — Token exchangePOST
POST https://id.bynefit.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=<code> &redirect_uri=<same redirect_uri> &client_id=site_xxxxxxxxxxxx &code_verifier=<the verifier>
access_token,refresh_token,id_token(JWT),expires_in,token_type=Bearer,scope. - Verify the
id_token: signature against/oauth/jwks.json,iss == https://id.bynefit.com,aud == your client_id, expiry. - Call
GET /oauth/userinfowithAuthorization: Bearer <access_token>for profile claims (or just trust the id_token claims for openid + profile + email).
Code for each step
Make the verifier and challenge
Keep the verifier until the token exchange. In a browser keep it in sessionStorage; on a server keep it in the session.
function base64url(bytes) {
return btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function pkcePair() {
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
return { verifier, challenge: base64url(digest) };
}import base64
import hashlib
import secrets
def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
def pkce_pair():
verifier = b64url(secrets.token_bytes(32))
challenge = b64url(hashlib.sha256(verifier.encode()).digest())
return verifier, challengefunction base64url(string $bytes): string
{
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
function pkce_pair(): array
{
$verifier = base64url(random_bytes(32));
$challenge = base64url(hash('sha256', $verifier, true));
return [$verifier, $challenge];
}Send the person to Bynefit
https://id.bynefit.com/oauth/authorize?response_type=code &client_id=site_0123456789abcdef01234567 &redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fcallback &scope=openid%20profile%20email &state=RANDOM_STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256
Exchange the code
The code works once and expires after 10 minutes. The request is form-encoded, not JSON.
curl -X POST https://id.bynefit.com/oauth/token \
-d grant_type=authorization_code \
-d code=CODE_FROM_THE_REDIRECT \
-d client_id=site_0123456789abcdef01234567 \
-d redirect_uri=https://example.com/auth/callback \
-d code_verifier=YOUR_CODE_VERIFIERconst res = await fetch("https://id.bynefit.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: "site_0123456789abcdef01234567",
redirect_uri: "https://example.com/auth/callback",
code_verifier: verifier
})
});
const tokens = await res.json();import requests
res = requests.post("https://id.bynefit.com/oauth/token", data={
"grant_type": "authorization_code",
"code": code,
"client_id": "site_0123456789abcdef01234567",
"redirect_uri": "https://example.com/auth/callback",
"code_verifier": verifier,
})
tokens = res.json()$ch = curl_init('https://id.bynefit.com/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => 'site_0123456789abcdef01234567',
'redirect_uri' => 'https://example.com/auth/callback',
'code_verifier' => $verifier,
]),
]);
$tokens = json_decode(curl_exec($ch), true);{
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "9f2c…",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIs…"
}
id_token is only there when openid was granted. The access token lasts an hour and the refresh token 60 days.
Errors are JSON with error and error_description: 400 invalid_request, invalid_grant or unsupported_grant_type, and 401 invalid_client.
Refresh
Trade a refresh token for new tokens before the access token runs out. Each refresh token works once: the answer carries a new one, and the old one stops working at once, so store the new one.
A new id_token comes back too, so a site signed in weeks ago sees today's name and picture.
curl -X POST https://id.bynefit.com/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN \
-d client_id=site_0123456789abcdef01234567const res = await fetch("https://id.bynefit.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: tokens.refresh_token,
client_id: "site_0123456789abcdef01234567"
})
});
tokens = await res.json();$ch = curl_init('https://id.bynefit.com/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => 'site_0123456789abcdef01234567',
]),
]);
$tokens = json_decode(curl_exec($ch), true);Ask who is signed in
curl https://id.bynefit.com/oauth/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"const res = await fetch("https://id.bynefit.com/oauth/userinfo", {
headers: { Authorization: "Bearer " + tokens.access_token }
});
const me = await res.json();$ch = curl_init('https://id.bynefit.com/oauth/userinfo');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $tokens['access_token']],
]);
$me = json_decode(curl_exec($ch), true);{
"sub": "bu_0123456789abcdef0123456789abcdef",
"name": "Sam Rivera",
"nickname": "samr",
"picture": "https://bynefit.com/media/avatars/samr.jpg",
"profile": "https://bynefit.com/p/samr",
"bio": null,
"email": "sam@example.com",
"email_verified": true
}
Which claims appear depends on the scopes granted: profile gives name, nickname, picture, profile and bio;
email gives email and email_verified.
The person chooses, per site, which email, phone number and business a site sees, so the answer can differ between sites and can change.
When they share a business with the site, a business claim is added; it is absent when they chose not to share one.
A 401 invalid_token means the token is expired, revoked, or was issued to a different client.
email_verified with care for older accounts.
Accounts made before 11 May 2026 report true without having confirmed the address. Do not use it alone as proof that someone controls an email.
Client rules
- A client id looks like
site_and 24 hex characters. - A client can have up to 10 return addresses. Each is
https(orhttp://localhostfor development) with no#fragment, and matched exactly. - A native app returns through an
httpslink on your own domain. A custom scheme is your own domain written backwards, such asvip.example.app://callback. A plain word such asmyapp://is refused, and Bynefit staff approve each scheme for you first, then it is matched exactly. Apps must use PKCE with S256, and an authorization code is single-use and expires after 10 minutes. By approval - The client name is shown on the consent screen. Names that imitate Bynefit or sound like a sign-in prompt ("Sign in", "Login", "Account") are refused.
- Creator status and earnings (
creator.read,earnings.read) can be asked for on a client, and Bynefit reviews each use before it works. By approval
Sign in on your own page
A sign-in sheet that opens over your page, without sending people away, is available to the websites listed on the client.
Everything the person types is inside a frame served by the sign-in host, which your page's script cannot read.
On a Bynefit-hosted site the auth kit module draws the button and runs the whole flow; elsewhere you can still use the redirect flow above.
Allow the sign-in host in your frame-src and connect-src if your site sets a Content Security Policy, and keep scripts on the page trusted, because tokens are kept in the browser.
If the frame cannot load, the sheet offers to continue in a new window.
Silent sign-in
For returning users already signed into Bynefit, pass prompt=none on the authorize call. The server either redirects back with a code immediately (no UI shown) or returns one of these errors so your client can fall back to manual sign-in:
| Error | Meaning |
|---|---|
login_required | User has no active Bynefit session — show the manual button. |
consent_required | User is signed in but hasn't yet approved your scopes — start the normal flow. |
Scopes
| Scope | Grants |
|---|---|
openid | Required. Identifies this as an OIDC request and authorizes an id_token. |
profile | Name, username, bio, and profile picture (picture, an absolute https URL). |
email | Email address + verification status. |
phone | Phone number + verification status. Available to first-party clients only at this time. |
creator.read | The person's creator status, milestones, tiers, activity and content stats, through the creator hub endpoints below. Needs staff approval for the client. |
earnings.read | The person's creator earnings, bonuses, payouts and ambassador earnings, through the creator hub endpoints below. Needs staff approval for the client. |
Scopes that need approval
creator.read and earnings.read are not open by default. A manager or admin of the client's Space asks for them on the client's page at
/dash/sites/oauth; Bynefit staff are told and approve or decline each one. Until a scope is approved it is left out of the grant: the authorization still succeeds with the scopes the client may use, the token response's scope omits it and the person is never shown it on the consent screen
(the request fails with invalid_scope only when no allowed scope remains). Check the scope in the token response before you call an API that needs one. Once approved, the person sees it on the consent screen as
"Your creator status, milestones, tiers, and content stats" or "Your creator earnings and payout status", and can still decline.
Additional scopes (teams:read, wallet:read, tasks:read, purchases:read, feeds:read) exist for first-party Bynefit apps and aren't open to site-registered clients. Reach out if your use case needs one.
What a signed-in token can call
Signing in gives your site an access token. Besides the userinfo endpoint, it is the bearer token for two Bynefit APIs, each gated by what the person approved:
| API | Needs | What it does |
|---|---|---|
| Client portal API | A sign-in client owned by a practice Space; no extra scope. The person must be that practice's customer, and some routes need the practice add-on (see the page) | The signed-in person reads their own engagements, documents and messages on a practice site, and a practice manager can run the pipeline from its admin page. Calls look in the Space that owns the client. |
Creator hub API, /api/creator/web/* | creator.read or earnings.read on the client | The creator's own numbers. With creator.read: activity, analytics, milestones, tiers, top-content. With earnings.read: earnings-breakdown, bonuses, payouts, ambassador-earnings. overview returns the sections the token's scopes allow. |
Call the creator hub with Authorization: Bearer. All its endpoints are GET, answer only about the signed-in person, and send no cross-origin permission except to https://creators.bynefit.com and any origin Bynefit has added for a partner hub, so a call from another website's browser is refused (403 on the preflight); call it from your own server if you need it elsewhere. A token without the scope answers 403 insufficient_scope; an expired or revoked token, or one from a first-party Bynefit app, answers 401 invalid_token.
Subject identifier (sub)
The sub claim is a stable, opaque, per-issuer user identifier. Treat it as your primary key when storing per-user data on your side — never key on email (which can change) or display name. Two different Bynefit users will never share a sub; the same user signing into two different clients gets the same sub.
Errors
Returned as standard OAuth error params on the redirect_uri or as JSON from the token endpoint:
| Error code | When |
|---|---|
invalid_request | Missing required parameter, bad redirect_uri, or missing PKCE. |
invalid_client | client_id is unknown, inactive, or doesn't match the redirect_uri. |
invalid_grant | code expired (10-minute TTL), already used, or code_verifier doesn't match the original challenge. |
invalid_scope | Requested a scope not allowed for this client. |
access_denied | User clicked Deny on the consent screen. |
login_required / consent_required | Silent (prompt=none) flow couldn't complete without UI. |
Revocation
Users can revoke your client's access at /dash/me/apps. When revoked, the consent record is deleted and every outstanding access + refresh token is marked revoked. Your next call to /oauth/token with a refresh token will return invalid_grant; access tokens stop working immediately.
The browser kit (for static sites)
If your site is hosted by Bynefit (*.site.bynefit.com or a verified custom domain), the auth module of the Bynefit kit wraps the entire flow into one config block plus a couple of HTML attributes — no PKCE plumbing required. See the site-config guide.
CORS
/oauth/token, /oauth/userinfo, and
/oauth/jwks.json respond with
Access-Control-Allow-Origin: * so browser-based clients
on any origin can call them. Credentials are never echoed — the
flow's security comes from PKCE plus the redirect-URI allowlist on
your client.