Building OAuth apps

Written By Matias

Last updated About 7 hours ago

Building on the Synci API previously meant using a personal access token, which only reaches your own account. Now you can register an OAuth app and let other Synci users grant it access to their own financial data, through a "Connect to Synci" flow. Think of it like authorizing a third-party app to read your Google Drive or GitHub: the user approves your app on a Synci consent screen, chooses exactly which accounts it can see, and can revoke it at any time. Your app never sees their password.

It's self-serve. You register apps, invite testers, submit for review, and (once approved) can opt to have your app listed in the Synci directory so users can discover it. Apps are managed in the new Developers > Apps area of your dashboard, and built on OAuth 2.0 with PKCE (following current OAuth 2.1 security best practices).

Just need direct access to your own data with a token? That's the existing API, unchanged. This guide is about building an app that other users connect to.

Registering an app

Create an app under Developers β†’ Apps β†’ New App. You'll provide:

  • Name: shown to users on the consent screen.

  • Redirect URIs: 1 to 5 exact-match URLs. https:// only, no wildcards or fragments. http:// is allowed only for localhost and 127.0.0.1 during development.

  • Permissions (scopes): the scopes your app is allowed to request. Users approve them at connect time. See the scope catalog below.

  • Branding: developer or company name, description, homepage URL, privacy policy URL, and a logo. Branding is required before you can submit for review.

Credentials

Every app is a confidential (server-side) client and is issued a client secret at creation.

  • Client ID is public.

  • Client secret is shown once. Store it securely. Your app's backend sends it during the token exchange.

  • The secret can be rotated at any time. Rotating instantly invalidates the previous secret.

Public / PKCE-only clients (browser-only or mobile apps with no backend) are not offered right now. We may consider enabling it in the future.

The OAuth flow

  • Protocol: OAuth 2.0, authorization code grant with PKCE (required), following current OAuth 2.1 best practices.

  • Access tokens are short-lived (60 minutes).

  • Refresh tokens last 30 days and rotate on every use.

  • Your app only ever receives the scopes the user approved, limited to the accounts the user selected.

Authorizing a user

This is the authorization code + PKCE flow your app runs to get an access token for a Synci user. Every app is confidential, so the token exchange sends both your client secret and the PKCE code verifier.

Endpoints

Purpose

Method and URL

Authorization

GET https://api.synci.io/oauth/authorize

Token exchange and refresh

POST https://api.synci.io/oauth/token

Scope catalog

GET https://api.synci.io/api/v1/oauth/scopes

Token requests are sent as application/x-www-form-urlencoded.

1. Create a PKCE challenge

Generate a random code verifier (a high-entropy string, 43 to 128 characters) and keep it server-side for this authorization attempt. Derive the code challenge from it:

code_challenge = BASE64URL( SHA256( code_verifier ) ) 

Also generate a random state value to protect against CSRF.

2. Send the user to the authorization endpoint

Redirect the user's browser to the authorization endpoint with your parameters:

https://api.synci.io/oauth/authorize? response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=accounts:read%20transactions:read &state=RANDOM_STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 

Scopes are space-separated (URL-encoded as %20). Synci handles sign-in and shows the consent screen with the account picker, then the user approves or denies.

  • On approval, Synci redirects to your redirect_uri with a single-use code and your state:

    https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STATE 

    Verify state matches what you sent before continuing.

  • On denial, Synci redirects with ?error=access_denied.

The authorization code is single-use and expires quickly, so exchange it right away.

3. Exchange the code for tokens

From your backend, POST to the token endpoint with the code, your client secret, and the code verifier from step 1:

curl -X POST https://api.synci.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d redirect_uri=https://yourapp.com/callback \ -d code_verifier=YOUR_CODE_VERIFIER \ -d code=AUTH_CODE 

A successful response returns the tokens:

{ "token_type": "Bearer", "expires_in": 3600, "access_token": "...", "refresh_token": "..." } 

Access tokens last 60 minutes; refresh tokens last 30 days and rotate on use.

4. Call the API

Send the access token as a bearer token on any endpoint the user's scopes cover:

curl https://api.synci.io/api/v1/user \ -H "Authorization: Bearer ACCESS_TOKEN" 

You only ever receive the scopes the user approved, limited to the accounts they selected.

5. Refresh the token

When the access token expires, exchange the refresh token for a new pair:

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

Refresh tokens rotate: each refresh returns a new refresh token and invalidates the old one, so always store the latest. If a refresh token is expired or already used, send the user back through step 2.

Scopes

Request the least your integration needs. Financial data is read-only by default; there is no write scope for accounts or transactions. GET /api/v1/oauth/scopes returns the full catalog (14 scopes):

Scope

What it grants

profile:read

Name, email, and profile details.

accounts:read / :write

View / manage financial accounts and balances.

transactions:read / :write

View / manage transactions.

sensitive:read

Full account identifiers: IBAN, account and card numbers, and account-holder names.

financial-connections:read / :write

View bank connections and their status / manage them (reauthorize, disable, remove).

transfer-links:read / :write

View / manage transfer links and their transfer history.

rules:read / :write

View / manage rules and rule activity.

webhooks:read / :write

View / manage webhook endpoints.

destinations:read / :write

View / manage destination connections.

Notes:

  • sensitive:read exposes sensitive identifiers. By default, third-party apps don’t receive IBANs, account or card numbers, or account-holder names; those fields are redacted. sensitive:read unlocks them. Only request it if your integration truly needs sensitive identifiers, and expect it to attract extra scrutiny during review. On its own it exposes nothing; it only makes sense alongside accounts:read and/or transactions:read. It's clearly flagged on the consent screen so users see exactly what they're granting. Note that consent is all-or-nothing on the app: a user either approves the app with the scopes it requests, or denies it. They can't approve the app while stripping out a single scope.

  • financial-connections covers the bank link itself: status and health, reauthorize, disable, remove. Creating a new bank connection still runs through the underlying provider's own consent flow, so it isn't something a headless app does on its own.

  • Read-only AI assistant access uses a separate mcp:use scope through a different flow. See AI assistants.

App review lifecycle

New apps start in Testing. To make your app usable by any Synci user, submit it for review.

State

Who can authorize

Editing

Testing

You and invited testers (invited by email)

Changes apply immediately.

Pending review

Awaiting Synci; no new authorizations

Configuration is locked while under review.

Approved

Any Synci user; shown as verified

Live. Later edits are re-reviewed before going live (see below).

Rejected

No one; a reason is given

Edit and resubmit.

Once approved, you can opt to have your app listed in Synci so users can discover it, or keep it unlisted and share your own connect link. Apps that request sensitive:read can expect closer review. Return to development unpublishes an approved app without revoking existing user connections. You'll be emailed when your app or its branding is approved, rejected, deleted, or suspended.

Editing a live app

On an approved app, changes to branding, scopes, and redirect URIs are queued as pending and re-reviewed before they replace the live values. The dashboard shows a "Pending review" badge next to the current live value while you wait.

Security

  • Sensitive identifiers are redacted by default. IBANs, account and card numbers, and account-holder names are omitted from data returned to external apps unless the user has granted the sensitive:read scope.

  • Password confirmation is required to rotate a client secret or delete an app (not for a normal create or edit).

  • Least privilege. An app is confined to the scopes it registered for and the accounts the user picked. Anything else is denied, including account data embedded in other responses.

  • Confidential clients only. Secrets and refresh tokens stay server-side, off the browser.

Related tooling

  • Personal API tokens for direct, server-to-server access to your own data. Created under Developers β†’ API Tokens, shown once, and currently grant full access. See the API article.

  • Webhooks for event notifications with a delivery log. See Webhooks .

  • MCP connector for read-only AI assistant access to your own data. See AI assistants.