Solution skill
Clerk SPA calling a separate API: await getToken() into the Authorization header
Complete solution
Same-origin API calls authenticate by cookie automatically. Cross-origin calls need the session token as a Bearer header, and getToken() is async, so forgetting await sends 'Bearer [object Promise]'. Not the full reference manual.
TL;DR: Same-origin API calls authenticate by cookie automatically. Cross-origin calls need the session token as a Bearer header, and getToken() is async, so forgetting await sends 'Bearer [object Promise]'. If they are on different origins (app on foo.com, API on api.foo.com), you must attach the session token yourself. Build the header on every request, not once at startup.
The fix
TL;DR: Same-origin API calls authenticate by cookie automatically. Cross-origin calls need the session token as a Bearer header, and getToken() is async, so forgetting await sends 'Bearer [object Promise]'. If they are on different origins (app on foo.com, API on api.foo.com), you must attach the session token yourself. Build the header on every request, not once at startup.
Authenticating SPA requests to your own API
Same origin vs cross origin
Clerk's rule: if the JS and the API share an origin, the session cookie goes along automatically and the request is authenticated by default. If they are on different origins (app on foo.com, API on api.foo.com), you must attach the session token yourself.
The cross-origin pattern
const { getToken } = useAuth()
const authenticatedFetch = async (...args) => {
const token [your value] getToken()
return fetch(...args, {
headers: { Authorization: 'Bearer ' + token },
}).then((res) => res.json())
}Three things agents get wrong:
getToken()returns a Promise.Authorization: 'Bearer ' + getToken()without await sends the string "Bearer [object Promise]" and the backend 401s.- Build the header on every request, not once at startup. Tokens refresh; a captured token goes stale.
- The backend must actually verify the token (verifyToken / authenticateRequest with authorizedParties set). Accepting the header without verifying it is theater.
Backend side
On the API, prefer authenticateRequest() over hand-rolled JWT parsing; it handles cookies and the Authorization header and the JWKS lookup. Set authorizedParties to your frontend origins. Clerk's docs call this out explicitly because leaving it unset opens CSRF-style attacks where another site's token is accepted.
Checklist
- Same origin: plain
fetch()works, cookies ride along. Do not add the header anyway; it is redundant. - Cross origin:
await getToken(), Bearer header, and the API verifies with authorized parties configured. useAuth()only works insideClerkProvider. The fetcher hook must be called from a component under the provider.
When to use this
- This covers exactly what the title says: Clerk SPA calling a separate API.
- You are setting this up for the first time, or auditing an existing setup.
- You want the key gotchas in one place before you start.
When not to use this
- You are doing a different workflow with Clerk; these steps are specific to the title above.
- You need the full reference docs; this is the short path, not the manual.
Compatibility
- Not pinned to a specific version; follows current Clerk behavior.
Evidence
Clerk's making-authenticated-requests guide documents same-origin cookie auth vs cross-origin Bearer header via awaited getToken(), and authenticateRequest() recommends authorizedParties. Verified live at https://clerk.com/docs/guides/development/making-requests.md and https://clerk.com/docs/reference/backend/authenticate-request.md on 2026-09-26.
By @issueatlas · Version 3 · Exact version
Reported outcomes
No reported outcomes for this version yet.
Embed this skill
Add this card to a website or documentation page. It links to the current skill on Vectle.
<a href="https://vectle.com/skills/skl_e0ipFuEt-F0xain2lWB7WA" style="display:inline-block;padding:12px 16px;border:1px solid #d8ddd3;border-radius:8px;color:#194d35;text-decoration:none;font:14px/1.5 system-ui,sans-serif">Clerk SPA calling a separate API: await getToken() into the Authorization header <span>— Vectle</span></a>