Connect OAuth
Let your users connect their Groupilot account to your platform with one click, through OAuth 2.1 with PKCE, with the full flow in Node and the partner account that comes with it.
"Connect Groupilot" is for platforms with several customers in common with Groupilot: webinar tools, CRMs, AI assistants. The user clicks a button in your product, logs in to Groupilot (or creates an account), picks the WhatsApp account, sees the rights you ask for, and you receive a token for that account. No key to copy, revocable by the user at any time.
Endpoints
Issuer: https://app.groupilot.com.
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-authorization-server | Discovery (RFC 8414) |
GET /.well-known/oauth-protected-resource | Resource metadata (RFC 9728), used by MCP clients |
GET /oauth/authorize | Consent page |
POST /oauth/token | Code exchange and refresh |
POST /oauth/revoke | Revoke a token |
POST /oauth/register | Dynamic client registration (RFC 7591) |
Register your client
Two ways:
- Partner client, for a platform: write to contact@groupilot.com with your product name, logo and redirect URIs. You receive a
client_id(gpc_…), aclient_secrethanded over once, and a partner account (see below). - Dynamic registration, for a tool that runs on the user's machine (Claude Desktop, Cursor, a CLI):
POST /oauth/register
Content-Type: application/json
{
"client_name": "My tool",
"redirect_uris": ["http://127.0.0.1:52341/callback"],
"token_endpoint_auth_method": "none",
"client_uri": "https://example.com"
}The response carries client_id and, for client_secret_post or client_secret_basic, a client_secret. Redirect URIs must be HTTPS, or HTTP on 127.0.0.1 / localhost with any port for local tools. 10 registrations per hour per IP. A dynamically registered client is shown as an unverified application on the consent screen, and a client_name that uses a brand name (Groupilot, WhatsApp, a partner's name) is refused.
The flow
Create a PKCE pair
PKCE with S256 is mandatory; plain is refused.
import crypto from "node:crypto";
const verifier = crypto.randomBytes(48).toString("base64url"); // 43 to 128 characters
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
const state = crypto.randomBytes(16).toString("base64url");Send the user to the consent page
const url = new URL("https://app.groupilot.com/oauth/authorize");
url.search = new URLSearchParams({
response_type: "code",
client_id: "gpc_…",
redirect_uri: "https://yourapp.com/oauth/groupilot/callback",
scope: "read write",
state,
code_challenge: challenge,
code_challenge_method: "S256",
}).toString();
// open url in the browserThe user logs in or signs up, picks the WhatsApp account to connect, sees the scopes and chooses Authorize or Refuse. Groupilot redirects to your redirect_uri with code and state, or with error (access_denied, invalid_request, invalid_scope, unsupported_response_type) and error_description. An unknown client_id or an unregistered redirect_uri shows an error page and never redirects.
Exchange the code
Check that state matches, then:
const res = await fetch("https://app.groupilot.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
code_verifier: verifier,
redirect_uri: "https://yourapp.com/oauth/groupilot/callback",
client_id: "gpc_…",
client_secret: "…", // confidential clients only; HTTP Basic also works
}),
});
const tokens = await res.json();{ "access_token": "gp_at_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "gp_rt_…", "scope": "read write" }The body can also be JSON. Access tokens last 1 hour, refresh tokens 90 days.
Call the API
Authorization: Bearer gp_at_… on every /api/v1 endpoint, exactly like an API key: same scopes read, write, send, same limits. Idempotency-Key works too, scoped to the grant.
Refresh
POST /oauth/token
grant_type=refresh_token&refresh_token=gp_rt_…&client_id=gpc_…Refresh tokens rotate: the old one dies and a new pair comes back. Store the new pair every time.
Errors
OAuth format, { "error": "…", "error_description": "…" }: invalid_client (401), invalid_grant (400, code already used, expired, wrong verifier or wrong redirect URI), slow_down (429, read Retry-After). The token endpoint allows 60 calls per minute per IP.
Revocation
POST /oauth/revoke
token=gp_rt_…&token_type_hint=refresh_token&client_id=gpc_…Revoking either token, access or refresh, revokes the whole connection. A refresh token that is replayed after rotation revokes the connection too. An unknown token answers 200. The user can also disconnect your app from Settings, Integrations, Connected applications: your tokens then fail within a minute with 401.
Partner account and affiliation
A platform with a partner client also gets:
- a partner key (
gp_partner_…) forGET /api/v1/partner/connections: every account that authorized one of your apps, with its email (the user consented to the connection), the connected WhatsApp account, the scopes,signup_via_partner,connected_at,last_used_at, andrevoked_atwhen the user disconnected you. Reference: Partners; - a signup link
https://app.groupilot.com/signup?partner=<your-slug>: an account created through it and that then authorizes your app is attributed to you in the Groupilot affiliate programme, nothing to set up on your side. Terms on groupilot.firstpromoter.com.
Scopes to ask for
Ask for the minimum. A webinar platform that pushes sequences and creates events needs read and write. Ask for send only if your product sends messages on behalf of the user, and say so on your side before the consent screen.
MCP clients
An MCP client pointed at the Groupilot MCP server receives a 401 with WWW-Authenticate: Bearer realm="groupilot", resource_metadata="https://mcp.groupilot.com/.well-known/oauth-protected-resource" (on /mcp, that URL ends with /mcp). Discovery, dynamic registration and the flow above follow by themselves. See MCP.