Skip to Content
Authentication

Authentication / API keys

GetRatesAPI uses two independent, header-only credential schemes. No cookie or query-string credential is accepted anywhere.

SchemeHeaderUsed for
Session tokenAuthorization: Session <token>Account/API-key/usage/billing management endpoints
API keyAuthorization: ApiKey <key>The data plane — rates, conversion, asset catalog — and every hosted MCP tool call

A session token is never accepted on a data-plane endpoint or by any MCP tool; an API key is never accepted on account-management endpoints.

1. Register

curl -X POST https://your-getratesapi-host/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"a-strong-password"}'
await fetch("https://your-getratesapi-host/v1/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "[email protected]", password: "a-strong-password" }), });

Registration sends a verification email. Open the link in it (or paste the code from it on the portal’s verification page) before logging in. No email? Ask for a new one:

curl -X POST https://your-getratesapi-host/v1/auth/verify-email/resend \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]"}'

The link is single-use and valid for 24 hours. Completing a password reset also confirms the address, since the reset link arrives by email too.

2. Log in

curl -X POST https://your-getratesapi-host/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"a-strong-password"}'

Success 200:

{ "session_token": "gra_sess_0000000000000000000000000000000000000000000", "expires_at": "2026-10-09T12:00:00Z" }

Until the email address is confirmed, a correct password returns 403 with code email_not_verified and no session:

{ "code": "email_not_verified", "message": "Verify your email address before signing in. Check your inbox for the verification link.", "request_id": "…" }

A wrong password is always 401 unauthorized, whether or not the address is verified.

The session token has a fixed 24-hour expiration. There is no refresh endpoint — log in again once it expires.

If the account has two-factor authentication enabled, login returns a challenge instead of a session:

{ "mfa_required": true, "mfa_token": "gra_mfa_00000000000000000000000000000000", "mfa_expires_at": "2026-10-08T12:05:00Z" }

3. Create an API key

Using the session token from step 2:

curl -X POST https://your-getratesapi-host/v1/account/api-keys \ -H "Authorization: Session gra_sess_0000000000000000000000000000000000000000000" \ -H "Content-Type: application/json" \ -d '{"name":"my first key"}'
await fetch("https://your-getratesapi-host/v1/account/api-keys", { method: "POST", headers: { Authorization: "Session gra_sess_0000000000000000000000000000000000000000000", "Content-Type": "application/json", }, body: JSON.stringify({ name: "my first key" }), });

Success 201:

{ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "my first key", "key_prefix": "gra_live_00000000", "created_at": "2026-10-08T12:00:00Z", "api_key": "gra_live_0000000000000000000000000000000000000000000000000000000000000000" }

api_key is returned exactly once in this response and is never retrievable again — store it securely immediately. The key format is gra_live_ followed by 64 lowercase hex characters.

key_prefix is a 17-character value: the gra_live_ prefix plus the first 8 hexadecimal characters of the key’s own secret portion — it is derived from the secret, not a shared constant. It is included so you can visually distinguish keys in a list; it never reveals enough of the secret to reconstruct or guess the full key.

4. Use the API key

curl https://your-getratesapi-host/v1/rates/EUR/USD \ -H "Authorization: ApiKey gra_live_0000000000000000000000000000000000000000000000000000000000000000"

Use the same Authorization: ApiKey <key> header for every hosted MCP tool call.

Managing API keys

  • GET /v1/account/api-keys (Session required) — lists all keys for your account, including revoked ones. Secrets are never included in list output — only id, name, key_prefix, created_at.
  • DELETE /v1/account/api-keys/{id} (Session required) — revokes a key. 204 on success. Unknown, foreign, or already-revoked ids return 404 not_found.
curl -X DELETE https://your-getratesapi-host/v1/account/api-keys/3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -H "Authorization: Session gra_sess_0000000000000000000000000000000000000000000"

Deleting revoked keys

Revoked keys stay in the list until you delete them. Only a revoked key can be deleted; its past usage remains in your usage history (shown as “Deleted key”). Both endpoints take no request body.

  • POST /v1/account/api-keys/{id}/remove — delete one revoked key. 204 on success; an active key returns 409 key_active (revoke it first).
  • POST /v1/account/api-keys/remove-revoked — delete every revoked key. Returns {"removed": 3}.
curl -X POST https://your-getratesapi-host/v1/account/api-keys/remove-revoked \ -H "Authorization: Session gra_sess_0000000000000000000000000000000000000000000"

Rotating a key

POST /v1/account/api-keys/{id}/rotate (Session required) issues a replacement key and revokes the old one in the same transaction:

curl -X POST https://your-getratesapi-host/v1/account/api-keys/3fa85f64-5717-4562-b3fc-2c963f66afa6/rotate \ -H "Authorization: Session gra_sess_0000000000000000000000000000000000000000000"

The 201 response has the same shape as key creation, including the one-time api_key value. The rotated key keeps its name; the old secret stops working immediately, so deploy the new one before rotating a key that is serving live traffic.

Two-factor authentication

TOTP, compatible with any standard authenticator app. All four endpoints require a session.

MethodPathPurpose
GET/v1/account/2faCurrent status
POST/v1/account/2fa/enrollStart enrollment, returns a secret and otpauth:// URI
POST/v1/account/2fa/confirmProve a code and activate
POST/v1/account/2fa/disableDeactivate; requires your password
POST/v1/account/2fa/recovery-codesRegenerate recovery codes; requires your password

Enrollment is a two-step flow deliberately: a factor you have not yet proven you can use is never allowed to lock you out. Only confirm activates it.

In the dashboard (Security) the otpauth:// URI is shown as a QR code — scan it with Google Authenticator, Microsoft Authenticator, 1Password, Authy or any TOTP app — together with the setup key for manual entry (time-based, 6 digits, 30 seconds, SHA-1). Codes may be typed with or without the space the app shows: 123456, 123 456 and 123-456 are all accepted.

Confirmation returns ten single-use recovery codes, shown once. Each can be used in place of a TOTP code exactly once.

Completing a login

When login returns mfa_required, exchange the challenge token and a code for a session:

curl -X POST https://your-getratesapi-host/v1/auth/login/mfa \ -H "Content-Type: application/json" \ -d '{"mfa_token":"gra_mfa_00000000000000000000000000000000","code":"123456"}'

The challenge token is valid for five minutes and is single-use. It is not a session: it authorises nothing except completing this login.

Changing your password

POST /v1/account/password with {"current_password":"…","new_password":"…"}.

A successful change revokes every session on the account, including the one that made the request. That is intentional: a password change is what you do when you suspect a session is in the wrong hands, and it would be useless if it left those sessions alive. Log in again afterwards.

Federated sign-in

Google and GitHub sign-in, when the operator has configured them. GET /v1/auth/oauth/providers reports what is available:

{ "providers": ["google", "github"] }

The flow is authorization-code with PKCE where the provider supports it:

  1. Generate a random binding value (32–128 base64url characters), keep it in the browser tab, and send the browser to GET /v1/auth/oauth/{provider}/start?binding=….
  2. The provider returns to /v1/auth/oauth/{provider}/callback, which redirects to the portal at /signin?oauth_code=….
  3. The app exchanges that code: POST /v1/auth/oauth/exchange with {"code":"gra_oauth_…","binding":"…"} returns a normal session token — or, when the user has two-factor authentication enabled, the same MFA challenge as a password login, completed at /v1/auth/login/mfa.

The code only works together with the binding from the browser that started the flow, so a code planted in or copied from another browser cannot sign anyone in.

A session token never travels in a redirect URL. The handoff code is single-use and expires in two minutes, so a leaked referrer or browser history entry does not hand over an account.

A provider identity is attached to an existing local account only when the provider asserts the email is verified. Otherwise a new account is created. This prevents anyone who can register an unverified address at a provider from seizing a local account that happens to share it.

If the matching local account never verified its address, the provider’s verified assertion wins: the account’s password, sessions, API keys, two-factor setup and other linked identities are revoked before it is attached, because whoever registered it never proved they own the address.

GET /v1/account/identities lists linked providers; DELETE /v1/account/identities/{provider} unlinks one. Unlinking is refused if it would leave you with no way to sign in.

Email delivery

Verification, password-reset, and quota-notification emails are sent as HTML with a plain-text alternative, through the operator’s configured transport (MAIL_TRANSPORT). In local development the default writes messages to disk instead of sending them — see the repository README.

Known gaps

There is no team/multi-account selector: an account has a single owner.