Quickstart
From a new application to your first record and your first webhook. Every step runs against the
live API at https://api.bind.co. Questions at any step: support@bind.co.
bind.co has no sandbox or test mode, by design: the records come from real inboxes, so a copy of
the API with made-up data would not tell you how your integration behaves. Once your application is
APPROVED, your developers test it end to end on their own real inbox; see
Test your integration.
1. Register your application
Sign up as a developer, then, in the
developer portal, create an application
with its name, your verified domain and the redirect URIs bind.co may send users back to. Choose the
use cases it needs: they set the scopes your application may request. You get a client_id and a
client_secret. The secret is shown when the application is created and when you reset it, never
again; keep it on your server.
A new application is a DRAFT. Submit it for review. Approval is a manual step: a person at bind.co
reviews the application, and once they approve it, it is APPROVED and can sign users in. What each
status allows is in Test your integration.
While you wait, build everything that does not need a signed-in user: the redirect to the consent
screen and the code exchange (steps 3 and 4), and your webhook handler with its signature check (the
webhooks guide has reference code). GET /v1/oauth2/authorize/preflight works at
every status, so you can check your authorize URL before approval (step 3).
2. Register your webhook endpoint
Register your endpoint before anyone signs in, so you receive the events of their first sync.
Webhook endpoints are managed with your application's own credentials over HTTP Basic, not a user's
token, and need an APPROVED application:
curl -u "$CLIENT_ID:$CLIENT_SECRET" -X POST https://api.bind.co/v1/webhook-endpoints \
-H 'Content-Type: application/json' \
-d '{"url": "https://app.example.com/bind/webhooks",
"event_types": ["consent.granted", "email_sync.first_completed",
"expenses.first_completed", "expenses.completed"]}'
The answer carries the endpoint's secret, shown once. Every delivery is signed with it in the
X-Webhook-Signature header: verify the signature on the raw body before you trust the event.
POST /v1/webhook-endpoints/{public_id}/test sends a signed test delivery right away so you can check your
handler. The webhooks guide has the signature scheme, retries and every event type.
Testing locally: the URL must be HTTPS on a public host; bind.co refuses private and loopback addresses. Expose your local handler through a tunnel that gives it a public HTTPS URL, and register that URL.
3. Send the user to the consent screen
Authorization uses OAuth 2.0 with PKCE, and PKCE is required: method S256 only. For each sign-in,
make a random code_verifier (43 to 128 characters), keep it, and send its challenge:
import base64, hashlib, secrets
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).rstrip(b"=").decode()
Then send the user's browser to the authorize endpoint. The OAuth endpoints live at the API root,
with no /v1:
https://api.bind.co/oauth2/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/bind/callback
&scope=profile:read%20operations:expenses:read
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
scope is a space-separated list; each scope must be registered for your application. Ask for
profile:read every time: it is the scope that tells you who the user is. Each data area has a
:read scope and a :write scope that includes it. The
authentication guide lists the scopes each route needs.
The user signs in with Google or Microsoft and approves your scopes on bind.co's consent screen.
bind.co then redirects to your redirect_uri with ?code=...&state=.... Check that state is the
value you sent. If the user declines, you get ?error=access_denied&error_description=...&state=...
instead.
To check an authorize URL before sending anyone to it, call GET /v1/oauth2/authorize/preflight
with the same parameters and your application's credentials over HTTP Basic. It answers 200 with
ok, each check in the order the real flow runs them, and the first one that fails.
4. Exchange the code for tokens
The code is valid for 10 minutes and works once. Exchange it, with the code_verifier, from your
server:
curl -u "$CLIENT_ID:$CLIENT_SECRET" https://api.bind.co/oauth2/token \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://app.example.com/bind/callback \
-d code_verifier="$CODE_VERIFIER"
{
"access_token": "q8Jf0...",
"refresh_token": "Zt3k9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "profile:read operations:expenses:read"
}
The access token lasts an hour. Before it expires, get a new pair with the refresh token; the authentication guide covers refresh, rotation and revocation.
5. Wait for the first sync
A new user's records are not there the moment they consent: bind.co first reads their inbox. Your
endpoint receives email_sync.first_completed when the most recent mail is stored, then
<type>.first_completed (here expenses.first_completed) when the first records of that operation
type are ready. Later batches arrive as <type>.completed.
"First" belongs to the user, not to your application: someone who already uses bind.co through
another application had their first sync before you connected. Treat consent.granted followed by a
read as your own "this user is ready" signal, and subscribe to <type>.completed as well as
<type>.first_completed.
6. Make your first call
Send the access token as a bearer token. It names the user, so no route that acts for a user takes a
user_id: a user_id in the query string or the body is refused with 400 invalid_request. (The
webhook delivery log, GET /v1/webhook-events, is the exception: your application filters it by
user_id.)
Learn who signed in:
curl https://api.bind.co/v1/oauth2/clients/me -H "Authorization: Bearer $ACCESS_TOKEN"
{"user_id": "usr_0b6f6f5e-2a8e-4b7e-9d0c-3c1f2a4b5d6e", "email": "dana@example.com"}
Then read records. This searches the user's expenses dated October 2026, newest first:
curl https://api.bind.co/v1/operations/expenses/search \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"expense_date_from": "2026-10-01", "expense_date_to": "2026-10-31",
"sort_by": ["-expense_date"], "page_number": 1, "page_size": 50}'
The answer is one page of records (abridged here):
{
"total_items": 1,
"total_pages": 1,
"page_number": 1,
"page_size": 50,
"items": [
{
"operation_id": "op_5b1f0c7e-8a2d-4c3b-9e61-2f7d4a9c0b13",
"operation_type": "expenses",
"status": "active",
"vendor_normalised": {"value": "Figma", "bind_inference_confidence": 0.97},
"expense_date": {"value": "2026-10-03", "bind_inference_confidence": 0.99},
"total_amount": {"value": "45.00", "bind_inference_confidence": 0.98},
"currency_code": {"value": "USD", "bind_inference_confidence": 0.99},
"evidence": [
{
"evidence_type": "email",
"role": "primary",
"created_at": "2026-10-03T09:14:02Z",
"file": null,
"email": {"email_id": "eml_HUGA0aawHmv", "email_sender": "billing@figma.com", "email_timestamp": "2026-10-03T09:12:40Z"}
}
],
"schema": {"operation_type": "expenses", "version": 3}
}
]
}
Each field carries its value and a bind_inference_confidence between 0 and 1. evidence points to
the emails and files the record was built from. GET /v1/operations/expenses/schema describes every
field, with no token needed. Paging works the same way on every list; see
Pagination and idempotency.
Next
- How the records work: Profile, Contacts and roles, Emails, Files, Drafts and goals, Operations and Mailbox sync.
- Authentication and scopes: the consent chain, scopes per route, token lifetimes and revocation.
- Webhooks: signatures, retries, and every event type with its payload.
- Pagination and idempotency: paging through lists and retrying writes safely.
- Errors and Rate limits: what can go wrong and how to retry.
- Generate a client: a typed client for your language, from the OpenAPI reference.