Sign-in Providers
Some services have no key to paste. You do not get a QuickBooks API key the way you get a Stripe one — you sign in, consent to a scope, and the service hands back a token. TAP runs that flow for you: the tokens are minted server-side, stored encrypted, pinned to the service’s own API hosts, and refreshed or extended before a call that needs it. No token ever reaches the agent.
This page covers the providers configured by table. Google and Microsoft are sign-in providers too, with their own implementation and the same agent-facing shape — see Credential Setup.
What a provider is
A provider is a row of configuration, not code:
| Field | What it is |
|---|---|
id | URL segment and stored-bundle discriminator (quickbooks) |
label | What a person reads on a connect card |
authorize_url / token_url | The provider’s two OAuth endpoints |
scopes | Selectable permission bundles, (id, label, scopes) — callers send bundle ids, never raw scope strings |
pkce | Send an RFC 7636 S256 challenge on authorize and the verifier on exchange |
client_auth | basic (HTTP Basic) or post (form fields) at the token endpoint |
extra_authorize_params | Query parameters every consent URL for this provider carries |
callback_params_to_keep | Callback parameters that are part of the connection, not the handshake — QuickBooks’ realmId |
allowed_hosts | Host pin for the created credential; never empty |
token_header | Header the access token is injected into (default Authorization: Bearer) |
refresh_rotates | The provider issues a new refresh token on every redemption and kills the old one |
send_scope / scope_separator | Whether the consent URL carries scope, and how scopes are joined (Meta wants commas) |
authorize_param_vars | Consent URL parameters read from the deployment — Facebook’s config_id |
lifecycle | RefreshToken (RFC 6749), or LongLived for providers that issue no refresh token (Meta): an optional exchange for a long-lived token at consent, and an optional in-place extension before expiry |
resource_token_lookup | Per-resource tokens fetched at consent and injected when a request’s path names that resource — Facebook Page tokens |
redact_response_keys | JSON keys redacted from every response to the credential — Meta’s access_token |
token_prefix_overrides | Hosts that take a different token prefix — Meta’s upload host takes OAuth |
The table lives in crates/tap-proxy/src/oauth_providers.rs. Adding a provider
is a row plus two environment variables on the deployment — no new handler, no
new callback, no new routing branch.
Endpoints
Each configured provider gets the same four routes, from one set of handlers:
| Route | Who calls it |
|---|---|
POST /team/oauth/{provider}/start | The dashboard — session-authenticated, owners and admins |
POST /team/oauth/{provider}/reauthorize | The dashboard, to repair an existing credential |
POST /app/users/{ext_id}/credentials/oauth/{provider}/start | A partner’s app key, for a managed end-user |
POST /agent/credentials/oauth/{provider}/start | A host app’s connect card, with the signed-in person’s token |
GET /oauth/{provider}/callback | The provider, returning the browser |
GET /agent/connectors lists every provider
this deployment offers with kind: "oauth", its scope bundles, and available
— false when the client id or secret is missing here, with
unavailable_reason naming the variable (never a value). A connect card reads
that listing and needs no per-provider code of its own.
The agent start endpoint refuses an X-TAP-Key outright: consent is a human
act, so it takes the signed-in person’s own token and applies their workspace
role. Full request and response shapes:
API Reference.
GET /team/oauth/providers is the same listing for the dashboard, which holds a
session rather than an agent key. It returns every row as {id, label, available, unavailable_reason?, scopes, default_scopes, api_base, allowed_hosts}; unavailable_reason names the missing variable, never a value.
Any workspace member may read it. Starting or reauthorizing a flow is still
owners and admins only.
In the dashboard
Add credential lists every provider on the table under Sign-in providers,
built from GET /team/oauth/providers, so a new row shows up with no dashboard
change. A provider this deployment has not configured appears as a disabled
tile naming the missing variable. Picking one asks for a credential name, a
description and the scope bundles (defaults pre-checked), then sends the
person to the provider’s consent screen.
Credentials a provider created are recognised by their exact api_base and
badged with the provider’s label. Each such row has a Reconnect button,
which calls POST /team/oauth/{provider}/reauthorize and keeps the scopes the
credential already has. When consent returns, the callback’s dashboard
redirect carries provider=<id> next to oauth= (and reason= on failure), so
the result banner names the provider.
Using the credential
The credential lands as an ordinary TAP credential: reference it by name in
X-TAP-Credential and call /forward as usual. TAP injects the provider’s
token header — the agent never sees a token and cannot ask for one.
Two things worth knowing:
- The host pin is real. The credential is bound to the provider’s own API
hosts at creation. A
/forwardaimed anywhere else is refused before any injection, so a prompt-injected agent has nowhere to send the token. - Connection parameters are not secrets. Anything the provider’s callback
returns that the API needs in the URL — QuickBooks’ company id — is stored
with the credential and surfaced in that credential’s
/agent/servicesentry under its own field (realm_id), alongsideoauth_provider. The agent puts it in the URL itself.
Access tokens are cached in the stored bundle and refreshed only when expired. On a provider that rotates its refresh token, the new one is written back atomically, so two proxy instances refreshing at once cannot leave the credential holding a token the provider has already invalidated.
When a refresh finally fails with invalid_grant, /forward answers 502 with
error_code: "oauth_reauth_required" and a reauth_url — the person reconnects
and the same credential name keeps working.
QuickBooks Online
| Provider id | quickbooks |
| Authorize | https://appcenter.intuit.com/connect/oauth2 |
| Token | https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer |
| Client auth | HTTP Basic only. Intuit rejects the secret in the form body, and answers invalid_client when client_id is repeated in the body alongside the header |
| Scope bundles | accounting (com.intuit.quickbooks.accounting, the default), payments (com.intuit.quickbooks.payment) |
| Kept callback parameter | realmId → realm_id |
| Allowed hosts | quickbooks.api.intuit.com, sandbox-quickbooks.api.intuit.com |
| Refresh token | Rotates on every redemption; expires after 100 days idle |
| Access token | One hour |
Deployment setup
Create an app in the Intuit developer portal, and register TAP’s callback as a redirect URI exactly as the proxy will send it:
https://<your TAP proxy host>/oauth/quickbooks/callbackThen set:
QUICKBOOKS_OAUTH_CLIENT_ID=...
QUICKBOOKS_OAUTH_CLIENT_SECRET=...
# Optional — only if the callback URL differs from {TAP_PROXY_URL}/oauth/quickbooks/callback
QUICKBOOKS_OAUTH_REDIRECT_URI=https://proxy.example/oauth/quickbooks/callbackUntil both the id and the secret are set, /agent/connectors reports
QuickBooks as available: false and every start endpoint answers 503 — a
connect card renders no dead button.
Intuit issues sandbox keys immediately and production keys after review; the
sandbox host (sandbox-quickbooks.api.intuit.com) is in the host pin so the
same credential works against either.
Calling the API
QuickBooks addresses a company, so every request path carries the realm id. Get
it from the credential’s /agent/services entry — never invent one:
# realm_id comes from GET /agent/services -> services["quickbooks"].realm_id
curl -X POST "$TAP_PROXY_URL/forward" \
-H "X-TAP-Key: $TAP_AGENT_KEY" \
-H "X-TAP-Credential: quickbooks" \
-H "X-TAP-Target: https://quickbooks.api.intuit.com/v3/company/$REALM_ID/companyinfo/$REALM_ID" \
-H "X-TAP-Method: GET" \
-H "Accept: application/json"| Provider id | facebook |
| Login | Facebook Login for Business — https://www.facebook.com/v25.0/dialog/oauth |
| Token | https://graph.facebook.com/v25.0/oauth/access_token |
| Client auth | Form fields |
| Scopes | None on the URL: the login configuration (config_id) sets the permissions |
| Allowed hosts | graph.facebook.com, rupload.facebook.com |
| Access token | A system-user token that does not expire (a user-token configuration is exchanged for a 60-day token) |
| Page tokens | Fetched at consent from /me/accounts, and injected when a request’s path names that Page |
Deployment setup
In Meta for Developers, create a Business app and add Facebook Login for Business. Create a login configuration with token type System-user access token, the Pages asset (and Instagram accounts, to reach them through Facebook), and the permissions you need — typically pages_show_list, pages_read_engagement and pages_manage_posts, plus instagram_basic and instagram_content_publish for linked Instagram accounts. Copy the configuration id.
Register TAP’s callback as a valid OAuth redirect URI:
https://<your TAP proxy host>/oauth/facebook/callbackThen set:
FACEBOOK_OAUTH_CLIENT_ID=... # the Meta app id
FACEBOOK_OAUTH_CLIENT_SECRET=... # the Meta app secret
FACEBOOK_OAUTH_CONFIG_ID=... # the login configuration idUntil all three are set, /agent/connectors reports Facebook available: false, naming the first missing variable.
Only people with a role on the Meta app can sign in until Meta grants Advanced Access for each permission through App Review, which requires Business Verification. That review is what lets another company’s people connect their Pages.
Calling the API
The credential’s /agent/services entry lists the connected Pages as page_ids, and their linked Instagram accounts as instagram_business_account_ids. Name a Page in the path and TAP sends that Page’s token:
curl -X POST "$TAP_PROXY_URL/forward" \
-H "X-TAP-Key: $TAP_AGENT_KEY" \
-H "X-TAP-Credential: facebook" \
-H "X-TAP-Target: https://graph.facebook.com/v25.0/$PAGE_ID/feed" \
-H "X-TAP-Method: POST" \
-H "Content-Type: application/json" \
-d '{"message": "Hello from our agent"}'Any other path — /me, an Instagram account id — gets the system-user token. Pages are captured at consent, so after granting another Page, reconnect.
Every access_token field in a response is redacted, so /me/accounts or ?fields=access_token never hands the agent a token. Resumable Page video uploads to rupload.facebook.com name no Page in their path and are not supported yet; publish video by file_url instead.
| Provider id | instagram |
| Login | Instagram API with Instagram Login — https://www.instagram.com/oauth/authorize |
| Token | https://api.instagram.com/oauth/access_token, then https://graph.instagram.com/access_token for the long-lived token |
| Client auth | Form fields |
| Scope bundles | basic (instagram_business_basic), publish (+ instagram_business_content_publish), comments (+ instagram_business_manage_comments), messages (+ instagram_business_manage_messages); default basic and publish |
| Allowed hosts | graph.instagram.com, rupload.facebook.com |
| Access token | 60 days. TAP extends it in place once fewer than 30 days remain, at most once a day |
For Instagram professional (Business or Creator) accounts; no Facebook Page is needed. Every bundle includes instagram_business_basic, because Meta refuses to extend a token without it.
Deployment setup
In your Meta app, add Instagram → API setup with Instagram login, and register this redirect URI under its Business login settings:
https://<your TAP proxy host>/oauth/instagram/callbackCopy the Instagram app id and secret shown there — they are not the Meta app’s own id and secret — and set:
INSTAGRAM_OAUTH_CLIENT_ID=...
INSTAGRAM_OAUTH_CLIENT_SECRET=...As with Facebook, accounts outside the app’s own roles need Advanced Access through App Review.
Calling the API
curl -X POST "$TAP_PROXY_URL/forward" \
-H "X-TAP-Key: $TAP_AGENT_KEY" \
-H "X-TAP-Credential: instagram" \
-H "X-TAP-Target: https://graph.instagram.com/v25.0/me?fields=user_id,username" \
-H "X-TAP-Method: GET"If the token lapses anyway — access revoked, a password change — /forward answers 502 with error_code: "oauth_reauth_required" and a reauth_url.
Adding a provider
- Add a row to
PROVIDERSincrates/tap-proxy/src/oauth_providers.rs. The table tests check every row for the invariants the handlers assume — a non-empty host pin, default bundles that exist in the catalog, an id that is URL-safe and does not shadowgoogleormicrosoft. - Name the row’s environment variables — the client id and secret, plus any
authorize_param_vars— and set them on the deployment. - Register the callback URL,
{TAP_PROXY_URL}/oauth/{id}/callback, with the provider’s developer app.
There is no step 4. Routes, the connect-card listing, the consent flow, the callback, forward-time refresh and rotation write-back all read the row.