Getting started
Authentication
Log in for an access token and send it as a Bearer credential.
The GrivaOne API uses JWT bearer tokens. You exchange an email and password for a short-lived access token, then send that token on every subsequent request. There are no API keys or OAuth flows today — see the roadmap for what is planned.
1. Log in for an access token
POST /auth/login takes a JSON body with email and password. It is a public endpoint, but it is rate limited to 5 attempts per minute per IP — see rate limits.
curl -X POST https://gulfpay-production.up.railway.app/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "you@company.com",
"password": "your-password"
}'A successful login returns the caller, their tenant context and the tokens:
{
"data": {
"requiresOtp": false,
"user": { "id": "usr_...", "email": "you@company.com", "name": "..." },
"organization": { "id": "org_...", "name": "..." },
"companies": [ { "id": "cmp_...", "name": "..." } ],
"membership": { "role": "OWNER", "companyAccess": "ALL" },
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 900,
"refreshToken": "b7c1e2...",
"refreshExpiresAt": "2026-09-13T09:00:00.000Z"
},
"meta": { "requestId": "req_...", "timestamp": "2026-09-06T09:00:00.000Z" }
}- accessToken — the JWT you send as a bearer credential. It expires in 15 minutes (
expiresInis the lifetime in seconds). - refreshToken — a longer-lived token (valid 7 days) used to mint a fresh access token without re-entering credentials. It is also set as an
httpOnlycookie for browser clients; server-to-server callers use the value from the body. - membership — the caller's role (e.g.
OWNER,ADMIN) and which companies they may access.
2. Send the token on every request
Attach the access token in the Authorization header as a bearer credential. Any request to a protected route without a valid token is rejected with 401 Unauthorized.
curl https://gulfpay-production.up.railway.app/api/v1/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Two-factor authentication
If an account has TOTP two-factor enabled (or is challenged with an emailed one-time code), POST /auth/login does not return tokens. Instead it returns a short-lived pre-auth token:
{
"data": {
"requiresOtp": true,
"preAuthToken": "eyJ...",
"email": "you@company.com",
"otpChannel": "totp"
},
"meta": { ... }
}Complete the challenge by calling POST /auth/verify-otp with the pre-auth token as the bearer credential and the 6-digit code. On success it returns the same token payload as a normal login. A pre-auth token can only call verify-otp / resend-otp — it cannot reach any other route.
curl -X POST https://gulfpay-production.up.railway.app/api/v1/auth/verify-otp \
-H "Authorization: Bearer <PRE_AUTH_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'Refreshing an access token
When the access token expires, exchange the refresh token for a new one with POST /auth/refresh. The refresh token itself is not rotated — you keep using the same one until it expires or you log out. The response contains only a new accessToken and its expiresIn.
curl -X POST https://gulfpay-production.up.railway.app/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "b7c1e2..." }'POST /auth/logout revokes the refresh token / clears the cookie.
Accounts are provisioned, not self-registered
Protect your credentials