{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-developer-resources/ingrid-api/guides/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Service Account Token Flow for Frontend Apps","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"service-account-token-flow-for-frontend-apps","__idx":0},"children":["Service Account Token Flow for Frontend Apps"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This guide shows a safe token handoff pattern for frontend apps:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The frontend asks your backend for a temporary consumer token."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Your backend authenticates to the Merchants backend with a service account (private key based)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Your backend requests a short-lived site access token."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The backend returns that temporary token to the frontend."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The frontend uses the token as a Bearer token when calling Ingrid APIs."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"why-this-pattern","__idx":1},"children":["Why this pattern"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Keeps service account private keys on the server only."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Issues short-lived tokens to browsers/mobile apps."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Reduces blast radius if a consumer token leaks."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Lets you add your own authorization checks before minting a token."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"architecture","__idx":2},"children":["Architecture"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"Frontend app\n  -> POST /api/frontend-token (your backend)\n     -> Authenticate service account with private key (Merchants backend)\n     -> POST /v1/sites/{siteId}/access-tokens (Merchants backend)\n  <- { accessToken, tokenType, expiresAt, expiresIn }\n\nFrontend app\n  -> Ingrid APIs with Authorization: Bearer <accessToken>\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"backend-example-nodejs","__idx":3},"children":["Backend example (Node.js)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The exact service-account auth endpoint can differ by environment/version. In this example it is represented as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MERCHANTS_AUTH_URL"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"import crypto from \"node:crypto\";\n\nconst MERCHANTS_API_BASE = process.env.MERCHANTS_API_BASE!;\nconst MERCHANTS_AUTH_URL = process.env.MERCHANTS_AUTH_URL!;\nconst SERVICE_ACCOUNT_ID = process.env.SERVICE_ACCOUNT_ID!;\nconst SERVICE_ACCOUNT_PRIVATE_KEY = process.env.SERVICE_ACCOUNT_PRIVATE_KEY!;\nconst SITE_ID = process.env.SITE_ID!;\n\nfunction createSignedAssertion() {\n  const now = Math.floor(Date.now() / 1000);\n\n  const payload = {\n    sub: SERVICE_ACCOUNT_ID,\n    iat: now,\n    exp: now + 60,\n    aud: MERCHANTS_AUTH_URL,\n  };\n\n  const header = { alg: \"RS256\", typ: \"JWT\" };\n\n  const encode = (obj: object) =>\n    Buffer.from(JSON.stringify(obj)).toString(\"base64url\");\n\n  const input = `${encode(header)}.${encode(payload)}`;\n  const signature = crypto\n    .createSign(\"RSA-SHA256\")\n    .update(input)\n    .end()\n    .sign(SERVICE_ACCOUNT_PRIVATE_KEY, \"base64url\");\n\n  return `${input}.${signature}`;\n}\n\nasync function getServiceAccountAccessToken() {\n  const assertion = createSignedAssertion();\n\n  const response = await fetch(MERCHANTS_AUTH_URL, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      grantType: \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n      assertion,\n    }),\n  });\n\n  if (!response.ok) {\n    throw new Error(`Service account auth failed: ${response.status}`);\n  }\n\n  const data = await response.json();\n  return data.accessToken as string;\n}\n\nexport async function issueFrontendToken() {\n  const serviceToken = await getServiceAccountAccessToken();\n\n  const response = await fetch(\n    `${MERCHANTS_API_BASE}/v1/sites/${SITE_ID}/access-tokens`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${serviceToken}`,\n      },\n      body: JSON.stringify({\n        scopes: [\"view\"],\n        ttlSeconds: 300,\n        externalRef: \"frontend-session\",\n      }),\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Access token mint failed: ${response.status}`);\n  }\n\n  return response.json();\n}\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"frontend-example","__idx":4},"children":["Frontend example"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"export async function fetchFrontendToken() {\n  const res = await fetch(\"/api/frontend-token\", {\n    method: \"POST\",\n    credentials: \"include\",\n  });\n\n  if (!res.ok) {\n    throw new Error(\"Could not get frontend token\");\n  }\n\n  return (await res.json()) as {\n    accessToken: string;\n    tokenType: \"Bearer\";\n    expiresAt: string;\n    expiresIn: number;\n  };\n}\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use the returned token in API calls:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"const { accessToken } = await fetchFrontendToken();\n\nawait fetch(\"https://api.ingrid.com/v1/delivery/options\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: `Bearer ${accessToken}`,\n  },\n  body: JSON.stringify(payload),\n});\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"security-checklist","__idx":5},"children":["Security checklist"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Never expose service account private keys to the client."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Keep temporary token TTL short (for example 5 minutes)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Scope token permissions narrowly (for example ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["view"]}," only)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Authenticate your own user/session before issuing tokens."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Log token issuance with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["externalRef"]}," for traceability."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Rotate service account keys regularly."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"operational-notes","__idx":6},"children":["Operational notes"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Refresh on expiry: when frontend receives ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["401"]},", fetch a fresh temporary token and retry once."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Cache server-side service-account auth token briefly to reduce auth calls."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Add rate limits on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/frontend-token"]}," to protect against abuse."]}]}]},"headings":[{"value":"Service Account Token Flow for Frontend Apps","id":"service-account-token-flow-for-frontend-apps","depth":1},{"value":"Why this pattern","id":"why-this-pattern","depth":2},{"value":"Architecture","id":"architecture","depth":2},{"value":"Backend example (Node.js)","id":"backend-example-nodejs","depth":2},{"value":"Frontend example","id":"frontend-example","depth":2},{"value":"Security checklist","id":"security-checklist","depth":2},{"value":"Operational notes","id":"operational-notes","depth":2}],"frontmatter":{"title":"Guide - Service Account Token Flow","description":"Frontend integration example for obtaining a temporary consumer token via your backend and merchant service account credentials.","seo":{"title":"Service Account Token Flow for Frontend Apps"}},"lastModified":"2026-02-24T19:14:57.409Z","pagePropGetterError":{"message":"","name":""}},"slug":"/developer-resources/ingrid-api/guides/frontend-service-account-token-flow","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}