The BeMyGuest API supports two authentication schemes:
- Bearer tokens (recommended) — short-lived access tokens with single-use rotating refresh tokens. Issued via
POST /v2/oauth/token. - Legacy API keys — your long-lived API key sent in the
X-Authorizationheader 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.
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_credentialswithout an admin unlock.
| Token | Lifetime | Reusable? |
|---|---|---|
| Access token | 15 min | Yes, until it expires |
| Refresh token | 30 days | No — single-use. Each refresh mints a new refresh token. |
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
}curl https://api.bemyguest.com.sg/v2/products \
-H 'Authorization: Bearer bmg_at_f1e2d3c4...'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.
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:
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.
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_credentialswith 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.)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.
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.
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.
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_credentialswith your API key again — you don't need an admin to unlock anything. Only an explicit admin lockout (manual_admin) requires contacting support.
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_credentialsfrom a non-whitelisted IP returns401 invalid_clientwith a clear "IP not permitted" description — you're the key owner, so we tell you plainly.refresh_tokenfrom a non-whitelisted IP returns400 invalid_grantwith 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.
All errors follow RFC 6749 §5.2:
{
"error": "invalid_grant",
"error_description": "The refresh token is invalid, expired, or has been revoked."
}| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_request | A required parameter is missing. |
| 400 | unsupported_grant_type | grant_type is neither client_credentials nor refresh_token. |
| 400 | invalid_grant | The refresh token is unknown, expired, revoked, or has already been used. The response is deliberately opaque about which — enumerating would help attackers. |
| 401 | invalid_client | The 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.
Two different things can happen in response to suspicious activity:
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.
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.
Both schemes are supported in parallel and will remain so. You can migrate gradually:
- Add the OAuth exchange + Bearer header handling to your client, feature-flagged.
- Roll it out to a subset of traffic.
- Once stable, flip to Bearer for all requests.
- Keep the API key safely stored — you'll still use it as
client_secretfor 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.