{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"OAuth Tokens (Recommended)"},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"oauth-tokens-recommended","__idx":0},"children":["OAuth Tokens (Recommended)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The BeMyGuest API supports two authentication schemes:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Bearer tokens"]}," (recommended) — short-lived access tokens with single-use rotating refresh tokens. Issued via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/oauth/token"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Legacy API keys"]}," — your long-lived API key sent in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Authorization"]}," header on every request. Still fully supported, but considered deprecated for new integrations."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["New integrations should use Bearer tokens. Existing integrations may migrate at their own pace — both schemes remain supported indefinitely."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"why-bearer-tokens","__idx":1},"children":["Why Bearer Tokens"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Compared to the long-lived ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Authorization"]}," header:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Short-lived access tokens"]}," (15 min) mean a stolen token has a tightly bounded blast radius."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Single-use rotating refresh tokens"]}," (30 days) mean a stolen refresh token is detected the moment the legitimate client refreshes next."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Refresh-reuse detection"]}," — if a refresh token is ever presented twice (outside a short grace window), the chain that token belongs to is revoked and the event is recorded for investigation. The API key itself is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," locked, so you can recover immediately via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," without an admin unlock."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"lifetimes","__idx":2},"children":["Lifetimes"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Token"},"children":["Token"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Lifetime"},"children":["Lifetime"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Reusable?"},"children":["Reusable?"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Access token"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["15 min"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes, until it expires"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Refresh token"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["30 days"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No"]}," — single-use. Each refresh mints a new refresh token."]}]}]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"flow","__idx":3},"children":["Flow"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-exchange-your-api-key-for-a-token-pair","__idx":4},"children":["1. Exchange your API key for a token pair"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Send your long-lived API key as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_secret"]}," using the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," grant:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -X POST https://api.bemyguest.com.sg/v2/oauth/token \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'grant_type=client_credentials' \\\n  -d 'client_secret=YOUR_API_KEY'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Response:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"token_type\": \"Bearer\",\n  \"access_token\": \"bmg_at_f1e2d3c4...\",\n  \"refresh_token\": \"bmg_rt_a1b2c3d4...\",\n  \"expires_in\": 900\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-call-the-api-with-the-access-token","__idx":5},"children":["2. Call the API with the access token"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl https://api.bemyguest.com.sg/v2/products \\\n  -H 'Authorization: Bearer bmg_at_f1e2d3c4...'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-refresh-before-the-access-token-expires","__idx":6},"children":["3. Refresh before the access token expires"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -X POST https://api.bemyguest.com.sg/v2/oauth/token \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'grant_type=refresh_token' \\\n  -d 'refresh_token=bmg_rt_a1b2c3d4...'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The response has the same shape as step 1 — a new access + refresh pair. The old refresh token is now marked single-use and will be rejected if sent again."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"integration-guidelines","__idx":7},"children":["Integration Guidelines"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A short grace window (currently 30 seconds) absorbs the most common client-side retry races — a refresh retry fired within that window of the original use is tolerated silently without triggering reuse detection. Beyond the window, reuse revokes the chain and you'll need to bootstrap a new pair with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]},". Following these guidelines keeps you well inside the grace window and avoids chain-revocation incidents in the first place:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Serialise refresh calls."]}," Only one refresh in flight per API key at a time. If multiple workers share a credential, use a shared lock (DB row, Redis, mutex) so only one performs the refresh. Parallel refreshes from the same credential will cross the grace window if any single call takes longer than ~30s."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Treat the refresh response as atomic."]}," Read the response, store the new tokens, then release your lock. If the HTTP call times out or you lose the response for any reason, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["do not retry the refresh"]}," — the server may already have rotated. Fall back to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," with your API key to bootstrap a fresh pair. (A retry within the grace window will be silently rejected and leave your previous chain intact; outside the window it will revoke the chain.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["If credentials are shared across services, nominate one token broker."]}," Have a single service own refresh; other services pull current access tokens from it. Independent refreshes from multiple services race, and even a race window of a few seconds across many requests makes the grace window a safety net rather than a guarantee."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Pre-refresh, don't on-expiry refresh."]}," Refresh when the access token has used ~80% of its lifetime (about 12 min of a 15 min token), not when a request fails with 401. Otherwise two concurrent requests can both see 401, both attempt a refresh, and the second trips the detector."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Persist tokens durably."]}," Don't keep them only in-memory. A worker restart that loses in-flight new tokens looks identical to a stolen reuse from the server's perspective."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Know the recovery path."]}," If your chain gets revoked (because you crossed the grace window, or because of an actual token-theft event), simply call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," with your API key again — you don't need an admin to unlock anything. Only an explicit admin lockout (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["manual_admin"]},") requires contacting support."]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"ip-whitelisting","__idx":8},"children":["IP Whitelisting"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If your account has an ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/ip-address-whitelisting"},"children":["API IP whitelist"]}," configured, it applies to the token endpoint as well as to regular API calls. Both the initial ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," exchange and every subsequent ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["refresh_token"]}," call must originate from a whitelisted IP, otherwise you'll see an authentication error."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Note the error shape differs by grant type:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," from a non-whitelisted IP returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["401 invalid_client"]}," with a clear \"IP not permitted\" description — you're the key owner, so we tell you plainly."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["refresh_token"]}," from a non-whitelisted IP returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400 invalid_grant"]}," with the standard \"refresh token invalid\" description. The response is deliberately opaque: an attacker holding a stolen refresh token should not be able to tell whether the token is still valid."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If your backend's egress IP changes (new NAT, DR failover, migration to a new provider), update the whitelist in the Agent Market Place BEFORE the cutover."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"error-responses","__idx":9},"children":["Error Responses"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All errors follow RFC 6749 §5.2:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"error\": \"invalid_grant\",\n  \"error_description\": \"The refresh token is invalid, expired, or has been revoked.\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"HTTP"},"children":["HTTP"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"error"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["error"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Meaning"},"children":["Meaning"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["400"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["invalid_request"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A required parameter is missing."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["400"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["unsupported_grant_type"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["grant_type"]}," is neither ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," nor ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["refresh_token"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["400"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["invalid_grant"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The refresh token is unknown, expired, revoked, or has already been used. The response is deliberately opaque about which — enumerating would help attackers."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["401"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["invalid_client"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_secret"]}," (API key) is unknown, expired, or the key has been locked."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you see a persistent ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["invalid_grant"]}," on refresh, by far the most common cause is parallel refreshes from your own client — see guideline 1 above."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"chain-revocation-vs-account-lockout","__idx":10},"children":["Chain revocation vs. account lockout"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Two different things can happen in response to suspicious activity:"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"chain-revocation-automatic-self-recovery","__idx":11},"children":["Chain revocation (automatic, self-recovery)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A refresh token presented a second time outside the grace window revokes the whole chain it belongs to — every refresh token in that lineage (both ancestors and descendants) and every live access token for your key. The event is recorded in our audit log for investigation."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Your API key is NOT locked."]}," Recovery is immediate: call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]}," with your API key to mint a fresh pair and continue. No admin involvement required."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The reason we scope revocation to the chain rather than the whole key is simple: if we locked the key, anyone holding a stolen refresh token could repeatedly reuse it to DOS your integration. Scoping to the chain means the attacker only burns the already-compromised chain each time, and your legitimate traffic (bootstrapped via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_credentials"]},") continues."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"account-lockout-manual-requires-admin-unlock","__idx":12},"children":["Account lockout (manual, requires admin unlock)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["An API key is locked only when a BMG admin explicitly locks it, typically after investigating a security incident. A locked key rejects auth on BOTH schemes — Bearer and legacy ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Authorization"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Recovery"]},": contact BMG support to unlock the key."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"migration-from-x-authorization","__idx":13},"children":["Migration from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Authorization"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Both schemes are supported in parallel and will remain so. You can migrate gradually:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Add the OAuth exchange + Bearer header handling to your client, feature-flagged."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Roll it out to a subset of traffic."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Once stable, flip to Bearer for all requests."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Keep the API key safely stored — you'll still use it as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["client_secret"]}," for the initial exchange and after any chain-revocation recovery."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["There is no sunset date for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Authorization"]}," at this time. When one is set, we will notify partners via email and add ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Sunset"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Deprecation"]}," headers to legacy-auth responses well in advance."]}]},"headings":[{"value":"OAuth Tokens (Recommended)","id":"oauth-tokens-recommended","depth":1},{"value":"Why Bearer Tokens","id":"why-bearer-tokens","depth":2},{"value":"Lifetimes","id":"lifetimes","depth":2},{"value":"Flow","id":"flow","depth":2},{"value":"1. Exchange your API key for a token pair","id":"1-exchange-your-api-key-for-a-token-pair","depth":3},{"value":"2. Call the API with the access token","id":"2-call-the-api-with-the-access-token","depth":3},{"value":"3. Refresh before the access token expires","id":"3-refresh-before-the-access-token-expires","depth":3},{"value":"Integration Guidelines","id":"integration-guidelines","depth":2},{"value":"IP Whitelisting","id":"ip-whitelisting","depth":2},{"value":"Error Responses","id":"error-responses","depth":2},{"value":"Chain revocation vs. account lockout","id":"chain-revocation-vs-account-lockout","depth":2},{"value":"Chain revocation (automatic, self-recovery)","id":"chain-revocation-automatic-self-recovery","depth":3},{"value":"Account lockout (manual, requires admin unlock)","id":"account-lockout-manual-requires-admin-unlock","depth":3},{"value":"Migration from X-Authorization","id":"migration-from-x-authorization","depth":2}],"frontmatter":{"seo":{"title":"OAuth Tokens (Recommended)"}},"lastModified":"2026-10-08T03:39:40.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/guides/oauth-tokens","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}