Amba

Developer Auth

Signup, login, sessions, token refresh, logout, OAuth grants, and current-developer lookup.

Developer credentials are owned by Amba's Amba API. Passwords are hashed with bcrypt (10-round salt). Refresh tokens are stored as sha256 hashes so a DB dump can't replay them.

Sessions

Every sign-in (signup, login, emailed code, GitHub) starts a session and returns its access token (24 hours) and refresh token (30 days). An access token works only while its session is live:

  • POST /auth/developer/logout ends the session. Its refresh token and every access token it issued stop working within 30 seconds.
  • POST /auth/developer/refresh replaces the refresh token. Access tokens issued earlier in the same session keep working until they expire.
  • A refresh token presented again after it was replaced ends the whole session (401 REFRESH_TOKEN_REUSED), for whoever copied it and for the client that refreshed. Two processes sharing one refresh token must refresh one at a time; the CLI does this with a lock file.
  • An access token without a session (issued before 2026-10-15) answers 401 INVALID_DEVELOPER_TOKEN. Refresh it, or sign in again.

Rate limits

Per-IP, two-tier (fast + daily):

RoutePer minutePer day
POST /auth/developer/signup550
POST /auth/developer/waitlist550
POST /auth/developer/login10100
POST /auth/developer/otp/request550
POST /auth/developer/otp/verify20200
POST /auth/developer/refresh30—

otp/request is additionally capped per address (5 per hour, 20 per day). /logout and /me are not rate-limited beyond the global gateway defaults.

POST /auth/developer/signup

Create a new developer and return an access + refresh token pair.

Signup is open, so invite_code is optional. If signups become invite-only, a call without invite_code answers 403 INVITE_REQUIRED. See Getting access for the signup modes and how to request a code.

Request

POST /auth/developer/signup
Content-Type: application/json
FieldTypeRequiredDescription
emailstringyesUnique email address.
passwordstringyesMinimum 8 characters. Bcrypt-hashed server-side.
namestringnoDisplay name.
invite_codestringif invite-onlyInvite code from the invite email (AMBA- plus four groups of four letters and digits, such as AMBA-7KQ2-M9XD-4TNB-HR3W; case, spaces, and dashes are ignored). Optional when signups are open. A code that is sent must be valid in every mode.

Response 201

{
  "data": {
    "access_token": "eyJ…",
    "refresh_token": "eyJ…",
    "developer": {
      "id": "…",
      "email": "…",
      "name": "…",
      "oauth_providers": [],
      "created_at": "…",
      "updated_at": "…"
    }
  }
}

Errors

  • 400 INVALID_INPUT — missing or malformed email, or missing password.
  • 400 WEAK_PASSWORD — password is shorter than 8 characters.
  • 400 DISPOSABLE_EMAIL — the address is on a disposable / temporary inbox domain. Use a permanent address.
  • 403 INVITE_REQUIRED — signups are invite-only and no invite_code was sent.
  • 403 INVITE_INVALID — the invite_code is mistyped, expired, revoked, or already used.
  • 403 SIGNUPS_CLOSED — new accounts are closed for now; invite codes are refused too.
  • 409 EMAIL_EXISTS — the email, or an alias of the same mailbox, is already registered. Aliases are matched case-insensitively with any +tag removed, and with dots ignored for Gmail addresses (j.doe+work@gmail.com and jdoe@gmail.com are the same mailbox). A sent invite code stays unspent.
  • 429 — rate limited.
  • 500 CREATE_FAILED — database error. Inspect server logs.

The three 403 refusals carry where to request access:

{
  "error": {
    "code": "INVITE_REQUIRED",
    "message": "Amba signups are invite-only right now. Sign up again with an invite code (invite_code), or request access at https://app.amba.dev/request-access.",
    "details": {
      "request_access_url": "https://app.amba.dev/request-access",
      "request_access_endpoint": "POST /v1/auth/developer/waitlist"
    }
  }
}

Try it:

POST/auth/developer/signup
public auth
curl -X POST 'https://api.amba.dev/v1/auth/developer/signup' \
  -H 'Content-Type: application/json' \
  -d '{
  "email": "you@example.com",
  "password": "example-password"
}'

Curl:

curl -X POST "${BASE_URL}/auth/developer/signup" \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"example-password"}'

POST /auth/developer/redeem-invite

For an account that already exists: redeems an invite code so the account can create projects while access is limited. New accounts pass invite_code at signup instead. Requires the developer Bearer (session token or PAT).

POST /auth/developer/redeem-invite
Authorization: Bearer <token>
Content-Type: application/json
 
{ "invite_code": "<code>" }

Response 200: { "data": { "can_provision": true, "redeemed": true } }. An account that can already create projects gets "redeemed": false and the code stays unspent.

Errors: 403 INVITE_INVALID (mistyped, expired, revoked, or already used), 403 SIGNUPS_CLOSED (codes are refused while signups are closed), 403 PROVISIONING_REVOKED (project creation was turned off for the account; contact support), 401 without a valid Bearer, 429 when rate-limited (10 per minute per IP).

POST /auth/developer/waitlist

Request access when signups are invite-only or closed. Public, no authentication. When access is granted, Amba emails the address an invite code, a signup link with the code filled in, and a one-line prompt to paste into a coding agent. The console form at app.amba.dev/request-access calls this endpoint.

Request

POST /auth/developer/waitlist
Content-Type: application/json
FieldTypeRequiredDescription
emailstringyesWhere the invite code is sent.
notestringnoWhat you're building. The first 500 characters are kept.

Response 200

{ "data": { "ok": true } }

Every well-formed email gets this same response, whether or not the address is already listed or already has an account, so the endpoint cannot be used to enumerate accounts. Asking again for the same address is safe; the note from the first request is the one kept.

Errors

  • 400 INVALID_INPUT — body is not JSON, email is missing or implausible, or note is not a string.
  • 429 — rate limited (5 per minute, 50 per day per IP).
  • 429 WAITLIST_FULL — the day's list of new addresses is full. Try again tomorrow.
  • 503 WAITLIST_UNAVAILABLE — the request was not recorded. Try again in a few minutes.

Curl:

curl -X POST "${BASE_URL}/auth/developer/waitlist" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "you@company.com", "note": "Building a habit tracker in Expo" }'

POST /auth/developer/login

Exchange email + password for tokens.

Request

POST /auth/developer/login
Content-Type: application/json
FieldTypeRequired
emailstringyes
passwordstringyes

Response 200

{
  "data": {
    "access_token": "eyJ…",
    "refresh_token": "eyJ…",
    "developer": {
      "id": "…",
      "email": "…",
      "name": "…",
      "oauth_providers": [],
      "created_at": "…",
      "updated_at": "…"
    }
  }
}

Errors

  • 400 INVALID_INPUT — missing email or password.
  • 401 INVALID_CREDENTIALS — wrong email or password. (Generic on purpose — never leak whether the email exists.)
  • 429 RATE_LIMIT_EXCEEDED — too many attempts from your address (10/min, 100/day), or too many failed password attempts for this account (20/hour, 100/day, counted across every Amba sign-in surface). Retry-After says when to retry; an emailed sign-in code still works in the meantime.
  • 500 LOGIN_FAILED.

Try it:

POST/auth/developer/login
public auth
curl -X POST 'https://api.amba.dev/v1/auth/developer/login' \
  -H 'Content-Type: application/json' \
  -d '{
  "email": "you@example.com",
  "password": "example-password"
}'

Curl:

curl -X POST "${BASE_URL}/auth/developer/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"example-password"}'

POST /auth/developer/otp/request

Email a 6-digit sign-in code to an existing developer account. This is the passwordless path: an account that never set a password (for example one created by an agent and later bound to your address) signs in with the code instead. The console's Email me a sign-in code option and amba login use it.

Request

POST /auth/developer/otp/request
Content-Type: application/json
FieldTypeRequired
emailstringyes

Response 200

{ "data": { "ok": true } }

The response is the same whether or not an account exists for the address, so it cannot be used to enumerate accounts. Codes expire after 10 minutes, are single-use, lock after 5 wrong attempts, and a new request replaces any code still outstanding.

Errors

  • 400 INVALID_INPUT — missing or implausible email.
  • 429 RATE_LIMIT_EXCEEDED — per-IP or per-address limit hit (Retry-After is set).
  • 500 OTP_REQUEST_FAILED.

POST /auth/developer/otp/verify

Redeem the emailed code for a session. Returns the same envelope as POST /auth/developer/login, and marks the account's email verified (a sandbox account is promoted to verified_free).

Request

POST /auth/developer/otp/verify
Content-Type: application/json
FieldTypeRequiredNotes
emailstringyesThe address the code was sent to.
codestringyesThe 6 digits from the email (spaces are fine).

Response 200

Identical to POST /auth/developer/login: access_token, refresh_token, developer.

Errors

  • 400 INVALID_INPUT — missing email, or a code that is not 6 digits.
  • 401 OTP_INVALID — wrong, expired, already-used, or locked code, or no account for the address. (Generic on purpose.)
  • 429 — rate limited.
  • 500 OTP_VERIFY_FAILED.

Curl:

curl -X POST "${BASE_URL}/auth/developer/otp/request" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "you@company.com" }'
 
curl -X POST "${BASE_URL}/auth/developer/otp/verify" \
  -H 'Content-Type: application/json' \
  -d '{ "email": "you@company.com", "code": "123456" }'

POST /auth/developer/refresh

Rotate the refresh token. In one step, the presented refresh token is revoked and a new access + refresh pair is issued in the same session. A refresh token presented after it was already replaced ends the whole session: that is how a copied token is detected, and both the copy and the client that refreshed must sign in again.

Request

POST /auth/developer/refresh
Content-Type: application/json
FieldTypeRequired
refresh_tokenstringyes

Response 200

{ "data": { "access_token": "eyJ…", "refresh_token": "eyJ…" } }

Errors

  • 400 INVALID_INPUT.
  • 401 INVALID_TOKEN — token signature failed, session not found, expired, or the session already ended.
  • 401 REFRESH_TOKEN_REUSED — the token was already replaced; its session has now ended.
  • 429 — rate limited.
  • 500 REFRESH_FAILED.

Try it:

POST/auth/developer/refresh
public auth
curl -X POST 'https://api.amba.dev/v1/auth/developer/refresh' \
  -H 'Content-Type: application/json' \
  -d '{
  "refresh_token": "<refresh_token>"
}'

Curl:

curl -X POST "${BASE_URL}/auth/developer/refresh" \
  -H 'Content-Type: application/json' \
  -d '{"refresh_token":"<refresh_token>"}'

POST /auth/developer/logout

End the session the refresh token belongs to: its refresh token and every access token it issued stop working within 30 seconds. Other sign-ins of the same account stay signed in, including a CLI that signed in through the console (amba login gets a session of its own). Idempotent — an already-invalid token returns success. amba logout calls this for the CLI's stored session.

Request

POST /auth/developer/logout
Content-Type: application/json
FieldTypeRequired
refresh_tokenstringyes

Response 200

{ "data": { "success": true } }

Errors

  • 400 INVALID_INPUT.
  • 500 LOGOUT_FAILED.

Try it:

POST/auth/developer/logout
developer auth
curl -X POST 'https://api.amba.dev/v1/auth/developer/logout' \
  -H 'Content-Type: application/json' \
  -d '{
  "refresh_token": "<refresh_token>"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/auth/developer/logout" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"refresh_token":"<refresh_token>"}'

GET /auth/developer/oauth-grants

List the apps connected to the account through OAuth: each MCP client (Claude, Cursor and others) you approved on the MCP server's sign-in page. The console shows them on the Connected apps page. Authenticate with a session token or the PAT; a connected app's own OAuth token gets 403 OAUTH_TOKEN_CANNOT_MANAGE_GRANTS here and on DELETE (an app ends its own connection with the MCP server's /revoke).

Request

GET /auth/developer/oauth-grants
Authorization: Bearer <developer-access-token>

Response 200

{
  "data": [
    {
      "grant_id": "6f2b8a0e-…",
      "client_id": "…",
      "client_name": "Claude",
      "redirect_hosts": ["claude.ai"],
      "scope": "amba",
      "created_at": "2026-10-12T09:30:00.000Z",
      "last_used_at": "2026-10-15T08:05:00.000Z",
      "expires_at": "2026-11-14T08:05:00.000Z",
      "client_revoked": false
    }
  ]
}

client_name is chosen by the app when it registers; treat it as a claim and check redirect_hosts. expires_at is when the connection ends unless the app refreshes before then.

DELETE /auth/developer/oauth-grants/:grantId

Disconnect one app: revoke every refresh token of the grant. The MCP server refuses the app's access tokens within 30 seconds, and the app has to be approved again to reconnect. Replacing the personal access token (POST /auth/developer/pat/rotate) disconnects every app at once and reports how many in oauth_grants_revoked.

Response 200

{ "data": { "grant_id": "6f2b8a0e-…", "revoked": true } }

revoked is false when the grant had already ended.

Errors

  • 404 OAUTH_GRANT_NOT_FOUND — no grant with that id belongs to the caller.

GET /auth/developer/me

Return the current developer profile. Requires a valid access token.

Request

GET /auth/developer/me
Authorization: Bearer <developer-access-token>

Response 200

{
  "data": {
    "id": "…",
    "email": "…",
    "name": "…",
    "oauth_providers": [],
    "created_at": "…",
    "updated_at": "…"
  }
}

Errors

  • 401 UNAUTHORIZED — missing or invalid access token.
  • 404 NOT_FOUND — the developer the token references no longer exists.
  • 500 FETCH_FAILED.

Try it:

GET/auth/developer/me
developer auth
curl -X GET 'https://api.amba.dev/v1/auth/developer/me'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/auth/developer/me" \
  -H "Authorization: Bearer ${DEV_TOKEN}"