Skip to content
Home

Authorization Flow

Status: Draft Last Updated: 2026-06-15 Audience: customer

This is the complete reference for sending an end user through the consent flow — from building the /authorize URL through receiving and exchanging the authorization code. If you just want the happy path, Quickstart covers it in two steps; this guide covers the full lifecycle, per-source handoffs, and every error case.

All example values are synthetic. Replace them with your own.

Granting your product access to an end user’s data is a four-stage loop:

  1. You construct an /authorize URL and send the end user to it.
  2. The end user authorizes the sources you requested on a consent page.
  3. You receive a one-time authorization code on your redirect_uri.
  4. You exchange the code for an end-user token at /oauth/token.

You drive stages 1 and 4 server-side; the end user drives stages 2 and 3 in a browser. The token you receive in stage 4 is what you use for all data queries — see Authentication and data-api.md.

Constructing the /authorize URL

The flow begins at GET /authorize. Build the URL with these query parameters:

ParameterRequiredDescription
client_idYesYour client ID from onboarding.
end_user_idYesYour own opaque identifier for this end user (1–256 characters). This is the only identity Unbound stores for them; you choose it (e.g. user-42).
sourcesYesComma-separated source identifiers to request, e.g. gmail,imessage. At least one; no duplicates; each must be a known source. See data-sources.md for the identifier strings.
redirect_uriYesAn absolute http/https URL that exactly matches one of your registered redirect URIs. No fragment, no userinfo.
stateRecommendedAn opaque, unguessable string you generate per request. Returned unchanged on the callback — verify it to protect against CSRF.

Example (URL-encode parameter values in your own code):

https://api.example-unbound.com/authorize
?client_id=a1b2c3d4
&end_user_id=user-42
&sources=gmail,imessage
&redirect_uri=https://your-app.example.com/unbound/callback
&state=syn_state_9f3c1a7e

One source per request for Android SMS. android_sms must be requested on its own — it cannot be bundled with other sources in the same sources list. Request it in a separate authorization round.

When the end user opens the URL, they see a server-rendered consent page:

  • It shows your account’s display name (set during onboarding) as the brand element. The page is neutral — there is no Unbound product branding.
  • It lists each source you requested with a human-readable label.
  • Each source is authorized individually. The end user may approve some and decline others.

The end user reviews the sources, makes their selections, and submits their decision. From your perspective, you do not interact with this page — you only build the entry URL and handle the callback.

Per-source authorization handoffs

What happens after the end user approves a source depends on the source type. Some complete entirely in the browser; others hand off to an external provider or a device agent. Each source’s end-user experience and time-to-data is detailed in data-sources.md; the summary:

  • OAuth sources — Gmail, Google Contacts, Google Calendar. The end user is taken to Google’s standard OAuth consent screen and grants access there. Authorization completes in-browser; data is typically available within minutes.
  • macOS sources — iMessage, Apple Contacts. The end user installs the macOS sync agent (a .dmg), grants it Full Disk Access in System Settings, and the agent completes authorization. iMessage and Apple Contacts are handled by the same agent. Because the agent’s first sync runs on the user’s machine, data can land minutes to hours later — register a webhook to know when it’s ready.
  • Android SMS (android_sms). The end user runs Google’s Data Portability export on their Android device. This is a historical, one-time export — data becomes available after roughly 24 hours of processing, not in real time.
  • Local files. The end user picks files or folders through a browser file picker; the upload completes in-session.

A note on asynchronous completion

The authorization code is issued as soon as the end user finishes the consent flow — before any data has necessarily been imported. For sources whose first import completes later (notably the macOS agent and Android SMS), use webhooks to drive your “syncing → ready” UI rather than assuming data exists the moment you receive the token. See webhooks.md.

Receiving the callback

After the end user completes the flow, Unbound redirects them to your redirect_uri with two query parameters:

https://your-app.example.com/unbound/callback?code=syn_authcode_5b2e8d1f4a6c0072&state=syn_state_9f3c1a7e

Do two things on the callback, in this order:

  1. Verify state equals the value you generated for this request. If it does not match, treat the callback as hostile and discard it — do not exchange the code.
  2. Capture code. It is single-use and expires in 10 minutes. Exchange it promptly (next section).

Exchanging the code for a token

Trade the code for an end-user token at POST /oauth/token using the authorization_code grant:

Terminal window
curl -X POST https://api.example-unbound.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=syn_authcode_5b2e8d1f4a6c0072" \
-d "client_id=a1b2c3d4" \
-d "client_secret=ldb_syn_clientsecret_REPLACE_ME"

Response:

{
"access_token": "cct_syn_2a7b9c1d_K8mP3qR6sT0uV4wX7yZ1aB5cD9eF2gH",
"token_type": "Bearer",
"end_user_id": "user-42"
}
  • access_token (prefix cct_) is the end-user token. By default it does not expire; if your account is configured with a token TTL, an expires_in field is present.
  • end_user_id echoes the identifier you passed to /authorize. Store the token server-side keyed by your own user record so you can correlate this token with that user later.

You may present client credentials either in the form body (shown above) or via HTTP Basic auth — both are accepted. Keep client_secret server-side only.

Partial authorization

If you requested three sources and the end user approves only two, the flow still succeeds: a token is issued, and only the approved sources are grantable. The declined source simply has no grant.

To learn which sources are actually active for an end-user token, query grants (it returns one entry per active grant):

Terminal window
curl -X POST https://api.example-unbound.com/graphql/v1 \
-H "Authorization: Bearer cct_syn_2a7b9c1d_K8mP3qR6sT0uV4wX7yZ1aB5cD9eF2gH" \
-H "Content-Type: application/json" \
-d '{"query": "{ grants { source grantedAt } }"}'
{
"data": {
"grants": [
{ "source": "gmail", "grantedAt": "2026-06-13T17:04:05Z" },
{ "source": "imessage", "grantedAt": "2026-06-13T17:05:12Z" }
]
}
}

Never assume a source is connected because you requested it — check grants.

Re-authorization

You can send the same end user through /authorize again — for example, to add a source they declined the first time, or to re-confirm access. Re-authorizing a source that already has an active grant preserves the existing grant; it does not create a duplicate. Sources that were not previously granted are added.

This makes the flow safe to re-run: a “Connect more sources” button in your UI can route the user back through /authorize without risk of double-granting.

Error cases

SituationWhat you observeWhat to do
User denies consentRedirect to your redirect_uri with error=access_denied (no code).Treat as “not connected.” Offer to retry later.
OAuth provider errorThe handoff to Google (or another provider) fails before returning.Prompt the user to retry the flow.
state mismatchThe state on the callback does not match what you sent.Discard the callback. Do not exchange the code.
Expired or reused code/oauth/token returns 400 with {"error": "invalid_grant"}.The code expired (10-minute limit) or was already exchanged. Send the user through /authorize again.
Wrong client credentials/oauth/token returns {"error": "invalid_client"}.Check your client_id/client_secret. See Authentication.
Bad /authorize parametersThe /authorize request is rejected (e.g. redirect_uri does not match a registered URI, unknown source, android_sms bundled with another source).Fix the URL parameters and rebuild it.

For the full set of /oauth/token error codes and how to handle auth failures on data requests, see Authentication.

  • Quickstart — The happy-path version of this flow in sequence.
  • Authentication — What to do with the token once you have it; token tiers, rotation, and auth error codes.
  • Data API — Querying data with the end-user token, including the grants query and the revokeGrant mutation.
  • Data Sources — Per-source authorization experience and time-to-data.
  • Webhooks — Knowing when an asynchronous first import has completed.