Skip to main content

Authentication

The Tax Kit is JWT-authenticated. The iframe never manages its own session — it asks the parent app for a fresh token every time it needs to authenticate, and the parent app gets that token from your backend.

note

Tokens are issued by CoinTracker's Auth0 tenant, not by your own signing keys. Your backend obtains a token on behalf of the signed-in user using credentials CoinTracker provisions during kickoff. The exact token-issuance flow (Auth0 M2M, Auth0 social/database connection, or another arrangement) is decided per partner — coordinate with your integration owner.

The flow

Loading diagram…
  1. The iframe checks whether its current token expires within 30 seconds before each GraphQL request.
  2. If a fresh token is needed, it sends REQUEST_ACCESS_TOKEN to the parent app over post-robot.
  3. TaxKitProvider invokes your fetchAccessToken function.
  4. Your function calls your backend, which obtains a token from CoinTracker's Auth0 tenant and returns it.
  5. The token is sent back to the iframe over post-robot. The iframe uses it for the request that triggered the rotation.

The iframe deduplicates token requests internally: if multiple in-flight GraphQL queries discover their token is expired at the same time, only one REQUEST_ACCESS_TOKEN event fires across the bridge. Your fetchAccessToken is invoked once per refresh window, not once per query.

What the token must contain

The embedded-api Worker validates every incoming token against CoinTracker's Auth0 JWKS endpoint and checks the following claims. Your token-issuance flow must produce tokens that satisfy them:

{
"https://cointracker.com/claims/partner_name": "<your-partner-slug>",
"https://cointracker.com/claims/partner_user_id": "<stable-user-id>",
"aud": "https://embedded.cointracker.com",
"iss": "https://auth.cointracker.com/",
"exp": 1735689600
}
https://cointracker.com/claims/partner_namestringrequired

Your lowercase, immutable partner slug. Provided by CoinTracker during kickoff and registered server-side in CoinTracker's Partner enum. Cannot change after launch.

https://cointracker.com/claims/partner_user_idstringrequired

A stable identifier for the user across sessions in your system. CoinTracker uses this as the join key for the user's tax data — if this value changes for the same user, they'll appear as a new user to CoinTracker.

Choose carefully. A database primary key works well. An email address does not (changes when the user updates their email).

audstringrequired

Audience claim. Must be exactly https://embedded.cointracker.com. The embedded-api Worker rejects tokens with any other audience.

issstringrequired

Issuer claim. Must match CoinTracker's Auth0 tenant for the environment you're targeting — https://auth.cointracker.com/ in production, https://auth-staging.cointracker.com/ in staging. (Trailing slash is required.) CoinTracker fetches signing keys from this issuer's /.well-known/jwks.json endpoint.

expnumberrequired

Standard JWT expiry — Unix timestamp in seconds. See "Token rotation" below for recommended lifetime.

Signing

CoinTracker validates signatures against the JWKS endpoint of its own Auth0 tenant. You do not host a JWKS endpoint and you do not sign tokens with your own private key — the Auth0 tenant signs them. Your backend's job is to obtain an Auth0-issued token for the right user with the right custom claims.

The token-issuance pattern (Auth0 Machine-to-Machine, Resource Owner Password, a custom Action that injects the custom claims, etc.) is set up during kickoff with your integration owner.

warning

Even though the Auth0 tenant signs the token, your client secret or whatever Auth0 credential you've been issued must stay server-side. The frontend should only ever see the resulting opaque token returned from fetchAccessToken.

What the frontend has to do

Implement a fetchAccessToken function that returns a fresh token from your backend:

const fetchAccessToken = async () => {
const res = await fetch('/api/cointracker-token', {
method: 'POST',
credentials: 'include', // forward your session cookie
});

if (!res.ok) {
return null; // signals unauthenticated to the iframe
}

const { access_token } = await res.json();
return access_token;
};

<TaxKitProvider fetchAccessToken={fetchAccessToken}>
<YourApp />
</TaxKitProvider>;

Return values

  • A non-empty string — the iframe uses it as the bearer token.
  • null or undefined — the iframe treats this as an unauthenticated state. Surface the appropriate "please sign in" UI from your end; the iframe will not attempt to recover.
  • An expired token — the iframe will request a fresh one within 30 seconds. Returning expired tokens repeatedly will degrade UX.

Errors

If fetchAccessToken throws, the iframe surfaces a TaxKitError.AuthenticationError. Handle this in your useTaxKit().errors array — typically by showing a re-authentication prompt and reloading the kit.

Token rotation

The iframe rotates tokens automatically:

  • Tokens are refreshed when they're within 30 seconds of expiry.
  • All in-flight iframe queries share the result of the active rotation — no thundering-herd of token requests.
  • If your minted tokens are short-lived (under a minute), the rotation will fire frequently. Consider issuing longer-lived tokens (a few minutes) to reduce backend load.

Recommended lifetime: 5–15 minutes. Long enough that rotation is rare during a normal session, short enough that a leaked token has limited blast radius. Don't issue tokens with exp more than an hour out without coordinating with your CoinTracker contact.

Cookies and CORS

The iframe runs on a different origin from your parent app, so cookies are not shared. The provider does not rely on cookies for auth — only the JWT returned by fetchAccessToken.

Your /api/cointracker-token endpoint on your own domain can use whatever authentication mechanism your app already uses (session cookies, JWTs in headers, etc.) — that part stays on your origin.

OAuth-driven exchange import

If you're also a CoinTracker exchange integration and you want users to import their on-platform balances/transactions into the kit without re-entering API keys, the kit supports an OAuth-driven import flow.

Required endpoints on your side

  • OAuth authorize endpoint — the URL the iframe redirects to when a user opts in. You agreed on this URL with CoinTracker during kickoff.
  • OAuth token exchange endpoint — the standard OAuth 2.0 token endpoint that exchanges the auth code for an access token. CoinTracker calls this server-to-server.

Required redirect URL on CoinTracker's side

The callback URL you register with your OAuth authorization server is always:

https://embedded.cointracker.com/authorized/<your-partner-slug>

After the OAuth flow completes, CoinTracker redirects the user back into the iframe with the import result. The <your-partner-slug> segment is the same lowercase slug from your JWT's partner_name claim.

PKCE handling

By default CoinTracker performs PKCE on the iframe side. If your backend handles the auth-code exchange directly and already takes care of PKCE, coordinate with your integration owner on disabling CT's PKCE step.