Skip to content
Home

Quickstart

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

Get from “I have credentials” to “I received data” in a handful of requests. This guide is a linear walkthrough — every step builds on the previous one, so run them in order. Deeper references for each topic are linked at the end.

Tip: You can run this entire walkthrough against the sandbox without connecting any real Google account or real device. The sandbox issues working tokens and serves obviously synthetic data, so you can exercise the full flow end to end before going live. Ask your Unbound operator for sandbox credentials.

Fastest path to a working integration: clone the Connect starter kit. It is a copyable backend-for-frontend (BFF) that already mints a Connect session, exchanges the authorization code, stores the token, and verifies webhooks — drop in your credentials and you have a running integration. The steps below explain the same flow request by request.

Prerequisites

Before you start, you need:

  • Customer credentials from your onboarding packet: a client_id, a client_secret, and at least one registered redirect_uri. These are issued by your Unbound operator when your account is provisioned. Keep client_secret server-side only.
  • The API base URL for your environment (your operator provides this; written as https://api.example-unbound.com throughout this guide).
  • An HTTP client (curl, Postman, or any HTTP library) and, optionally, a GraphQL client.

All example values below are synthetic — replace them with the real values from your packet.

Concept primer

Five things to know before you start; follow the links for depth.

  • Three actors. The Unbound operator runs the platform and provisions your account. You (the customer) integrate the API into your product. Your end users are the people whose data you access — but only after they explicitly authorize you.
  • A grant is one end user’s authorization for one data source (e.g. alice@example.com authorized gmail). Grants are per-source: an end user can authorize some sources and not others. See Authorization Flow.
  • A token is your bearer credential. There are two tiers: an end-user token (cct_…) scopes data queries to one end user, and a management token (ldb_…) gives an account-wide read-only view. See Authentication.
  • Empty is not an error. When an end user has no active grants, data queries succeed and return empty lists — they do not return 401/403. Call the grants query to tell “no data yet” apart from “nothing authorized.”
  • You never see internal IDs. You identify each end user by your own opaque string (your end_user_id, e.g. user-42). Unbound stores no other identity for them.

Step 1 — Get a management token

Exchange your client credentials for a management token using the client_credentials grant. This token gives you an account-wide view (Step 2) and never touches end-user data.

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

Response:

{
"access_token": "ldb_syn_7h3k9m2p_X4f8Qa2bN6cR1dS5tU9vW0xY3zZ",
"token_type": "Bearer",
"expires_in": 3600
}

The access_token is your management token. Note:

  • It is prefixed ldb_.
  • It always expires (expires_in is in seconds — one hour here). When it expires, simply call this endpoint again to mint a fresh one.
  • There is no end_user_id in this response — management tokens are account-scoped, not user-scoped.

Step 2 — Check your account

Confirm the token works by listing the end users who currently have at least one active grant. Send a GraphQL request to POST /graphql/v1 with the management token in the Authorization header.

Terminal window
curl -X POST https://api.example-unbound.com/graphql/v1 \
-H "Authorization: Bearer ldb_syn_7h3k9m2p_X4f8Qa2bN6cR1dS5tU9vW0xY3zZ" \
-H "Content-Type: application/json" \
-d '{"query": "{ authorizedEndUsers(first: 10) { totalCount edges { node { endUserID hasActiveGrant } } } }"}'

On a brand-new account, an empty list is expected and correct — nobody has authorized you yet:

{
"data": {
"authorizedEndUsers": {
"totalCount": 0,
"edges": []
}
}
}

You will return to this query after Step 4 to see your first authorized user appear. The full management surface is documented in management-api.md.

Step 3 — Send a test user through /authorize

To gain access to an end user’s data, you send them through the consent flow. You build a /authorize URL, the end user opens it in a browser and approves, and you receive a one-time code on your redirect_uri.

Construct the URL with these query parameters:

ParameterRequiredValue
client_idYesYour client ID.
end_user_idYesYour own opaque identifier for this user (e.g. user-42). You choose this; Unbound stores nothing else about them.
sourcesYesComma-separated source identifiers to request, e.g. gmail or gmail,imessage.
redirect_uriYesMust exactly match one of your registered redirect URIs.
stateRecommendedAn opaque, unguessable string you generate. It is returned to you unchanged — verify it on the callback to protect against CSRF.

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

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

Open this URL in a browser. The end user sees a neutral consent page branded with your account’s display name and a list of the sources you requested. They approve each source individually, then are redirected back to your redirect_uri:

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

Two things to do on the callback:

  1. Verify state matches the value you sent. If it does not, discard the request.
  2. Capture code — it is single-use and expires in 10 minutes. Exchange it promptly in Step 4.

Authorization handoffs differ per source (some complete in-browser, some require a device agent). The full lifecycle, including denied-consent and partial-authorization cases, is in Authorization Flow.

Step 4 — Exchange the code

Trade the code for an end-user 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"
}

The access_token is the end-user token you use to query this user’s data. Note:

  • It is prefixed cct_.
  • By default it does not expire (no expires_in field). If your account is configured with a token TTL, expires_in will be present.
  • end_user_id echoes the identifier you sent in Step 3. Store the token server-side, keyed by your own user record, so you can correlate it later.

Step 5 — Query the data

Now query the user’s data with the end-user token. Always check grants first so you know which sources are actually available before you query them.

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" }
]
}
}

The user has an active gmail grant, so you can query messages. List results are cursor-paginated (edgesnode, with pageInfo for the next page):

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": "{ messages(first: 5) { edges { node { body sentAt platform } } pageInfo { hasNextPage endCursor } } }"}'
{
"data": {
"messages": {
"edges": [
{
"node": {
"body": "Lunch tomorrow at noon?",
"sentAt": "2026-06-12T19:22:00Z",
"platform": "gmail"
}
}
],
"pageInfo": { "hasNextPage": false, "endCursor": "c3Vu..." }
}
}
}

That is the full round trip: credentials → management token → consent → end-user token → data. Re-run the Step 2 query and user-42 now appears in authorizedEndUsers.

Empty results are normal. If grants is [], the user has revoked all access or never authorized — not an error. Data queries still succeed and return empty lists. Querying grants is how you tell the difference. The full data API, including filtering by source and the revokeGrant mutation, is in data-api.md.

Some imports complete asynchronously, after the end user has left the flow — most notably the macOS sync agent’s first sync. Rather than polling, register an HTTPS webhook endpoint to receive signed events when imports complete. This lets you drive a “syncing → ready” state in your UI.

Registration is done by your Unbound operator (ask them to add your endpoint). See Webhooks for the full event vocabulary, HMAC verification recipe, dedup contract, and payload shape.

What’s next

  • Authorization Flow — The complete consent lifecycle: per-source handoffs, partial and re-authorization, and every error case.
  • Authentication — Both token tiers in detail: bearer headers, rotation, storage, and auth error codes.
  • Data API — Every data query, filtering by source, pagination, and the revokeGrant mutation.
  • Management API — Account-wide visibility: who has connected, who has gone dark, and current grant state.
  • Webhooks — Real-time import notifications: payload, HMAC verification, and delivery guarantees.
  • Data Sources — Each available source: data types, the end-user experience, and time-to-data.