Skip to content

The BeMyGuest API supports two authentication schemes:

  1. Bearer tokens (recommended) — short-lived access tokens with single-use rotating refresh tokens. Issued via POST /v2/oauth/token.
  2. Legacy API keys — your long-lived API key sent in the X-Authorization header on every request. Still fully supported, but considered deprecated for new integrations.

New integrations should use Bearer tokens. Existing integrations may migrate at their own pace — both schemes remain supported indefinitely.


Why Bearer Tokens

Compared to the long-lived X-Authorization header:

  • Short-lived access tokens (15 min) mean a stolen token has a tightly bounded blast radius.
  • Single-use rotating refresh tokens (30 days) mean a stolen refresh token is detected the moment the legitimate client refreshes next.
  • 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 not locked, so you can recover immediately via client_credentials without an admin unlock.

Lifetimes

TokenLifetimeReusable?
Access token15 minYes, until it expires
Refresh token30 daysNo — single-use. Each refresh mints a new refresh token.

Flow

1. Exchange your API key for a token pair

Send your long-lived API key as client_secret using the client_credentials grant:

curl -X POST https://api.bemyguest.com.sg/v2/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'client_secret=YOUR_API_KEY'

Response:

{
  "token_type": "Bearer",
  "access_token": "bmg_at_f1e2d3c4...",
  "refresh_token": "bmg_rt_a1b2c3d4...",
  "expires_in": 900
}

2. Call the API with the access token

curl https://api.bemyguest.com.sg/v2/products \
  -H 'Authorization: Bearer bmg_at_f1e2d3c4...'

3. Refresh before the access token expires

curl -X POST https://api.bemyguest.com.sg/v2/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=bmg_rt_a1b2c3d4...'

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.


Integration Guidelines

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 client_credentials. Following these guidelines keeps you well inside the grace window and avoids chain-revocation incidents in the first place:

  1. 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.

  2. 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, do not retry the refresh — the server may already have rotated. Fall back to 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.)

  3. 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.

  4. 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.

  5. 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.

  6. 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 client_credentials with your API key again — you don't need an admin to unlock anything. Only an explicit admin lockout (manual_admin) requires contacting support.


IP Whitelisting

If your account has an API IP whitelist configured, it applies to the token endpoint as well as to regular API calls. Both the initial client_credentials exchange and every subsequent refresh_token call must originate from a whitelisted IP, otherwise you'll see an authentication error.

Note the error shape differs by grant type:

  • client_credentials from a non-whitelisted IP returns 401 invalid_client with a clear "IP not permitted" description — you're the key owner, so we tell you plainly.
  • refresh_token from a non-whitelisted IP returns 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.

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.


Error Responses

All errors follow RFC 6749 §5.2:

{
  "error": "invalid_grant",
  "error_description": "The refresh token is invalid, expired, or has been revoked."
}
HTTPerrorMeaning
400invalid_requestA required parameter is missing.
400unsupported_grant_typegrant_type is neither client_credentials nor refresh_token.
400invalid_grantThe refresh token is unknown, expired, revoked, or has already been used. The response is deliberately opaque about which — enumerating would help attackers.
401invalid_clientThe client_secret (API key) is unknown, expired, or the key has been locked.

If you see a persistent invalid_grant on refresh, by far the most common cause is parallel refreshes from your own client — see guideline 1 above.


Chain revocation vs. account lockout

Two different things can happen in response to suspicious activity:

Chain revocation (automatic, self-recovery)

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.

Your API key is NOT locked. Recovery is immediate: call client_credentials with your API key to mint a fresh pair and continue. No admin involvement required.

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 client_credentials) continues.

Account lockout (manual, requires admin unlock)

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 X-Authorization.

Recovery: contact BMG support to unlock the key.


Migration from X-Authorization

Both schemes are supported in parallel and will remain so. You can migrate gradually:

  1. Add the OAuth exchange + Bearer header handling to your client, feature-flagged.
  2. Roll it out to a subset of traffic.
  3. Once stable, flip to Bearer for all requests.
  4. Keep the API key safely stored — you'll still use it as client_secret for the initial exchange and after any chain-revocation recovery.

There is no sunset date for X-Authorization at this time. When one is set, we will notify partners via email and add Sunset/Deprecation headers to legacy-auth responses well in advance.