Skip to main content

Login with SendSeven (OAuth 2.0)

This guide explains how to add "Login with SendSeven" to your application using the standard OAuth 2.0 Authorization Code flow. Users are redirected to SendSeven's consent screen, approve access, and are sent back to your app with an authorization code you can exchange for an access token.

No OIDC library required

This flow works with plain HTTP requests. If you prefer using an OIDC library with auto-discovery, see the OIDC SSO Guide instead.


Prerequisites

Before you start, you need a SendSeven OAuth App. Contact your SendSeven partner manager or create one at Settings > OAuth Apps.

You will receive:

CredentialDescription
Client IDYour app's public identifier (format: s7_app_...)
Client SecretYour app's secret key (format: s7_secret_...) — keep this secure!
Redirect URIThe callback URL in your app where users are sent after login (e.g. https://yourapp.com/auth/callback)

How It Works

┌──────────┐       ┌──────────────────┐       ┌──────────────┐
│ Your App │ │ SendSeven Consent│ │ SendSeven API│
└────┬─────┘ └────────┬─────────┘ └──────┬───────┘
│ 1. Redirect user │ │
│ ─────────────────────────>│ │
│ │ User logs in │
│ │ & approves access │
│ 2. Redirect back with │ │
│ authorization code │ │
│ <─────────────────────────│ │
│ │ │
│ 3. Exchange code for access token │
│ ───────────────────────────────────────────────>│
│ │
│ 4. Access token returned │
│ <───────────────────────────────────────────────│
│ │
│ 5. Fetch user profile │
│ ───────────────────────────────────────────────>│
│ │
│ 6. User info returned │
│ <───────────────────────────────────────────────│

Step 1: Redirect the User to SendSeven

When the user clicks "Login with SendSeven", redirect them to:

https://app.sendseven.com/oauth/consent
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/auth/callback
&scope=openid profile email
&state=RANDOM_STATE_STRING

Parameters

ParameterRequiredDescription
response_typeYesAlways code
client_idYesYour OAuth app's client ID
redirect_uriYesMust exactly match a registered redirect URI
scopeYesSpace-separated list of scopes (see below)
stateYesA random string (min. 8 characters) for CSRF protection. Store it in the user's session and verify it in Step 2.
tenant_idNoTenant ID hint to pre-select a specific workspace on the consent screen. Useful when re-authorizing for a known tenant.

Available Scopes for Login

ScopeWhat it grants
openidRequired — enables login
profileAccess to user's name and profile picture
emailAccess to user's email address
offline_accessReturns a refresh token for long-lived access
API scopes

You can also request API scopes like conversations:read or contacts:read if your integration needs to access SendSeven resources on behalf of the user. See the Authentication guide for the full list of available scopes.

Node.js (Express)

const crypto = require('crypto');

app.get('/auth/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.SENDSEVEN_CLIENT_ID,
redirect_uri: 'https://yourapp.com/auth/callback',
scope: 'openid profile email',
state: state,
});

res.redirect(`https://app.sendseven.com/oauth/consent?${params}`);
});

Python (Flask)

import secrets
from urllib.parse import urlencode
from flask import redirect, session

@app.route('/auth/login')
def login():
state = secrets.token_hex(16)
session['oauth_state'] = state

params = urlencode({
'response_type': 'code',
'client_id': SENDSEVEN_CLIENT_ID,
'redirect_uri': 'https://yourapp.com/auth/callback',
'scope': 'openid profile email',
'state': state,
})

return redirect(f'https://app.sendseven.com/oauth/consent?{params}')

Step 2: Handle the Callback

After the user approves (or denies) access, SendSeven redirects them back to your redirect_uri:

On success:

https://yourapp.com/auth/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE_STRING

On denial:

https://yourapp.com/auth/callback?error=access_denied&state=RANDOM_STATE_STRING
Verify the state parameter

Always verify that the state parameter matches the value you stored in Step 1. If it doesn't match, reject the request — it may be a CSRF attack.


Step 3: Exchange the Code for an Access Token

Make a server-side POST request to the token endpoint:

POST https://api.sendseven.com/api/v1/oauth-apps/token
Content-Type: application/x-www-form-urlencoded

Parameters:

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&redirect_uri=https://yourapp.com/auth/callback
Keep your secret server-side

This request must be made from your backend server, never from the browser. Your client secret must stay confidential.

Response

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"tenant_id": "64f3a695-6ce7-4822-b3ae-dc20d2967c26",
"tenant_name": "My Company"
}
FieldDescription
access_tokenUse this to call SendSeven APIs (valid for 1 hour)
refresh_tokenUse this to get a new access token when it expires (valid for 30 days, only if offline_access scope was requested)
expires_inToken lifetime in seconds
id_tokenA signed JWT containing user info (only if openid scope was requested)
tenant_idThe workspace/tenant ID this token is scoped to. Use this to store tokens per-tenant when a user belongs to multiple workspaces.
tenant_nameHuman-readable name of the workspace (e.g. "My Company")

Node.js

app.get('/auth/callback', async (req, res) => {
const { code, state, error } = req.query;

// Verify state
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state parameter');
}

// Check for errors
if (error) {
return res.redirect('/login?error=access_denied');
}

// Exchange code for tokens
const response = await fetch('https://api.sendseven.com/api/v1/oauth-apps/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: process.env.SENDSEVEN_CLIENT_ID,
client_secret: process.env.SENDSEVEN_CLIENT_SECRET,
redirect_uri: 'https://yourapp.com/auth/callback',
}),
});

const tokens = await response.json();

if (!response.ok) {
return res.status(400).send('Token exchange failed');
}

// Store tokens securely per tenant, then fetch user profile (Step 4)
// tenant_id tells you which workspace the user authorized
const tenantId = tokens.tenant_id;
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tenantId = tenantId;

// Continue to Step 4...
});

Python

import requests

@app.route('/auth/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
error = request.args.get('error')

if state != session.get('oauth_state'):
abort(403, 'Invalid state parameter')

if error:
return redirect('/login?error=access_denied')

response = requests.post(
'https://api.sendseven.com/api/v1/oauth-apps/token',
data={
'grant_type': 'authorization_code',
'code': code,
'client_id': SENDSEVEN_CLIENT_ID,
'client_secret': SENDSEVEN_CLIENT_SECRET,
'redirect_uri': 'https://yourapp.com/auth/callback',
},
)

tokens = response.json()

if not response.ok:
abort(400, 'Token exchange failed')

# tenant_id tells you which workspace the user authorized
session['access_token'] = tokens['access_token']
session['refresh_token'] = tokens.get('refresh_token')
session['tenant_id'] = tokens.get('tenant_id')

# Continue to Step 4...

Step 4: Fetch the User Profile

Use the access token to get the logged-in user's information:

curl -X GET "https://api.sendseven.com/api/v1/oauth-apps/userinfo" \
-H "Authorization: Bearer ACCESS_TOKEN"

Response

{
"sub": "usr_a1b2c3d4e5f6",
"name": "Jane Doe",
"email": "[email protected]",
"email_verified": true,
"picture": "https://cdn.sendseven.com/avatars/jane.jpg",
"tenant_id": "ten_x1y2z3"
}
FieldScope RequiredDescription
subopenidUnique, stable user ID — use this as the primary identifier
nameprofileUser's display name
pictureprofileURL to profile picture (may be null)
emailemailUser's email address
email_verifiedemailWhether the email is verified
tenant_idopenidThe SendSeven workspace the user authorized

Node.js

const userResponse = await fetch('https://api.sendseven.com/api/v1/oauth-apps/userinfo', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` },
});

const user = await userResponse.json();

// Create or update user in your database
await upsertUser({
sendseven_id: user.sub, // unique, stable identifier
name: user.name,
email: user.email,
avatar_url: user.picture,
tenant_id: user.tenant_id,
});

Python

user_response = requests.get(
'https://api.sendseven.com/api/v1/oauth-apps/userinfo',
headers={'Authorization': f'Bearer {tokens["access_token"]}'},
)

user = user_response.json()

# Create or update user in your database
upsert_user(
sendseven_id=user['sub'],
name=user.get('name'),
email=user.get('email'),
avatar_url=user.get('picture'),
tenant_id=user.get('tenant_id'),
)

Refreshing Tokens

Access tokens expire after 1 hour. If you requested the offline_access scope, use the refresh token to get a new access token without requiring the user to log in again:

curl -X POST "https://api.sendseven.com/api/v1/oauth-apps/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"

Response

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email",
"tenant_id": "64f3a695-6ce7-4822-b3ae-dc20d2967c26",
"tenant_name": "My Company"
}

Refresh tokens are valid for 30 days. After that, the user must log in again.


Revoking Access

To disconnect a user's SendSeven access (e.g. on logout or account deletion):

curl -X POST "https://api.sendseven.com/api/v1/oauth-apps/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=ACCESS_OR_REFRESH_TOKEN&token_type_hint=refresh_token&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"

This always returns 200 OK regardless of whether the token was valid (per RFC 7009).


PKCE (Recommended)

For additional security, you can use PKCE (Proof Key for Code Exchange). This is especially recommended for single-page applications or mobile apps.

How to add PKCE

1. Generate a code verifier (random string, 43-128 characters):

const codeVerifier = crypto.randomBytes(32).toString('base64url');

2. Create a code challenge (SHA-256 hash of the verifier):

const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');

3. Include in the authorization URL (Step 1):

&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256

4. Include in the token exchange (Step 3):

&code_verifier=CODE_VERIFIER

The server verifies that sha256(code_verifier) == code_challenge, ensuring the same application that initiated the flow is completing it.


Multi-Tenant Support

If a user belongs to multiple SendSeven workspaces, they will see a workspace selector on the consent screen. The token response includes tenant_id and tenant_name so your application can track which workspace was authorized.

Storing tokens per workspace

If your integration needs to work with multiple workspaces, store tokens keyed by tenant_id:

// After token exchange
const { access_token, refresh_token, tenant_id, tenant_name } = tokens;

// Store per-tenant
await db.upsertOAuthToken({
user_id: userId,
tenant_id: tenant_id,
tenant_name: tenant_name,
access_token: access_token,
refresh_token: refresh_token,
});

Pre-selecting a workspace on re-authorization

When you need the user to re-authorize for a specific workspace (e.g., after a token expires and refresh fails), pass the tenant_id as a hint:

https://app.sendseven.com/oauth/consent
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/auth/callback
&scope=openid profile email
&state=RANDOM_STATE_STRING
&tenant_id=64f3a695-6ce7-4822-b3ae-dc20d2967c26

The consent screen will pre-select that workspace. The user can still choose a different one if they want.

Always check tenant_id in the response

Even when passing a tenant_id hint, always verify the tenant_id in the token response matches what you expected. The hint is a suggestion, not a guarantee.


Security Checklist

Best Practices
  • Always use HTTPS for your redirect URI
  • Validate the state parameter on every callback to prevent CSRF attacks
  • Keep your client secret on the server — never expose it in frontend code or public repositories
  • Store tokens securely — use encrypted server-side sessions, not cookies or localStorage
  • Handle token expiry — refresh tokens proactively before they expire
  • Verify the redirect URI matches exactly what's registered (no wildcards)

Troubleshooting

ErrorCauseSolution
invalid_clientWrong client ID or secretDouble-check your credentials
invalid_grantAuthorization code expired or already usedCodes expire after 10 minutes and can only be used once. Restart the flow.
redirect_uri_mismatchRedirect URI doesn't matchThe URI must exactly match what's registered (including trailing slashes)
access_deniedUser clicked "Deny" on consent screenHandle gracefully — show a message or redirect to your login page
invalid_scopeRequested a scope your app isn't allowedContact your SendSeven partner manager to enable additional scopes
401 Unauthorized on API callsAccess token expiredUse the refresh token to get a new access token
404 on API calls after re-authUser selected a different workspace during re-authorizationCheck the tenant_id in the token response. Pass tenant_id hint in the authorization URL to pre-select the correct workspace.

Quick Reference

ItemValue
Authorization URLhttps://app.sendseven.com/oauth/consent
Token Endpointhttps://api.sendseven.com/api/v1/oauth-apps/token
UserInfo Endpointhttps://api.sendseven.com/api/v1/oauth-apps/userinfo
Token Revocationhttps://api.sendseven.com/api/v1/oauth-apps/revoke
OIDC Discoveryhttps://api.sendseven.com/.well-known/openid-configuration
JWKS (public keys)https://api.sendseven.com/.well-known/jwks.json
Access Token Lifetime1 hour
Refresh Token Lifetime30 days
Auth Code Lifetime10 minutes

Need Help?

Contact your SendSeven partner manager or reach out to [email protected].