Accounts API — /v1/*
Authentication, the current user, password flows and email availability. See README.md for conventions (base URL, auth header, error shape, pagination).
Authentication
POST/v1/auth — log in Public
Authenticate with email + password and receive a token pair. Also supports social providers via provider.
| Param | In | Notes |
|---|---|---|
email |
body | required for the default (email) provider |
password |
body | required for the default provider |
provider |
body | apple → also send apple_identity_token; google → also send google_access_token; otherwise email |
otp_code |
body | required when the account has two-factor authentication on — the current 6-digit authenticator code, or a backup code |
Request
curl -s http://localhost:3000/v1/auth \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"email":"[email protected]","password":"secret123"}'
200
{ "access_token": "eyJhbGciOi...", "refresh_token": "eyJhbGciOi..." }
401 — wrong password / unknown account / missing params (standard error shape; message like "password incorrect", "account not found", "invalid parameters").
With two-factor authentication on and no (or a wrong) otp_code: 401 with "two_factor_required": true and
message "two-factor code required" / "two-factor code is incorrect" — ask the user for their code and retry with it.
POST/v1/registrations — sign up Public
Create a user and receive a token pair.
| Param | In | Notes |
|---|---|---|
email |
body | required (blank → 400). Must be a valid format and not already used. |
name |
body | optional — defaults to the email local-part, titleized |
password |
body | optional — a random one is generated if blank |
referral_code |
body | optional — the code from a friend's invite link (/r/<code>); unknown codes are ignored |
Request
curl -s http://localhost:3000/v1/registrations \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"email":"[email protected]","name":"Grace Hopper","password":"secret123"}'
200
{ "access_token": "eyJhbGciOi...", "refresh_token": "eyJhbGciOi..." }
400 — { "message": "email cannot be empty", ... }
422 — invalid email format / "email has been used" (standard error shape).
GET/v1/tokens/validate — check a token Token
Verifies the Authorization: Bearer token.
Request
curl -s http://localhost:3000/v1/tokens/validate \
-H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOi...'
200 → { "valid": true }. 401 — missing / malformed / expired / revoked token, a refresh token, or the token's
user no longer exists.
POST/v1/tokens/refresh — refresh Public
| Param | In | Notes |
|---|---|---|
token |
body | the (still-valid) refresh token — an access token is refused |
Request
curl -s http://localhost:3000/v1/tokens/refresh \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"token":"eyJhbGciOi..."}'
200 → { "access_token": "eyJhbGciOi...", "refresh_token": "eyJhbGciOi..." } — for an app install the refresh
token rotates: keep the new one, the old one now fails. 401 — token_expired / invalid_token (bad, revoked,
or an access token), or refresh_token_reused: an already-exchanged refresh token came back, so the device was
signed out.
POST/v1/tokens/revoke — sign out everywhere Token
Invalidates every access and refresh token issued to the user so far (including the one sending the request).
Request
curl -s -X POST http://localhost:3000/v1/tokens/revoke \
-H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOi...'
200 → { "success": true }. 401 — no/invalid token.
Users
GET/v1/users/:id — current user Token
⚠️ The
:idpath segment is ignored — the controller always returns the authenticated user (soGET/v1/users/meworks just as well).
Request
curl -s http://localhost:3000/v1/users/me \
-H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOi...'
200
{
"id": 1, "name": "Ada", "email": "[email protected]", "phone": null, "username": "ada",
"confirmed": false, "confirmed_at": null, "confirmation_sent_at": null, "unconfirmed_email": null,
"created_at": "2026-01-01T10:00:00.000Z", "updated_at": "2026-01-01T10:00:00.000Z"
}
401 — no/invalid token.
PUT/v1/users/:id — update current user Token
The
:idis ignored (updates the authenticated user). Permitted params:password,password_confirmation,name,username. A newunconfirmed_emailuntil the user opens the link. An address already in use is a 422.
Request
curl -s -X PUT http://localhost:3000/v1/users/me \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-H 'Authorization: Bearer eyJhbGciOi...' \
-d '{"name":"Ada Lovelace","username":"ada_l"}'
200 — same shape as GET; if password changed, also access_token + refresh_token (the old tokens
are revoked). 422 — validation failed (e.g. username taken). 401 — no token.
GET/v1/users/:id/export — download your data Token
Everything we hold about the authenticated user (the
:idis ignored):account,plan,data_usage,devices,active_tunnels,invoices. No secrets (password hash, tunnel private keys) are included.
Request
curl -s http://localhost:3000/v1/users/me/export \
-H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOi...'
200 — the export object. 401 — no token.
DELETE/v1/users/:id — delete your account Token
Deletes the authenticated user for good (the
:idis ignored): cancels a Stripe subscription immediately, releases their tunnels, signs out every device. Irreversible.
| Param | In | Notes |
|---|---|---|
confirmation |
body | required — the account's email address (case-insensitive). |
Request
curl -s -X DELETE http://localhost:3000/v1/users/me \
-H 'Content-Type: application/json' -H 'Authorization: Bearer eyJhbGciOi...' \
-d '{"confirmation":"[email protected]"}'
204 — deleted. 422 — confirmation doesn't match. 502 — the payment provider couldn't cancel the
subscription; nothing was deleted. 401 — no token.
Passwords
POST/v1/passwords — request a reset Public
| Param | In | Notes |
|---|---|---|
email |
body | required (blank → 400). Sends Devise reset instructions if the email exists. |
Request
curl -s http://localhost:3000/v1/passwords \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"email":"[email protected]"}'
200 → { "success": true } — the same whether or not the address has an account, so the endpoint can't be
used to find out who does. 400 — "email cannot be blank".
POST/v1/passwords/reset — alias of the above (deprecated) Public
Same as POST/v1/passwords. Use POST/v1/passwords instead.
POST/v1/passwords/change — set a new password Optional token
Two modes, chosen automatically:
| Param | In | Notes |
|---|---|---|
password, password_confirmation |
body | required (blank → 400) |
current_password |
body | required when authenticated — must match the current password (else 422) |
reset_password_token or reset_password_code |
body | required when not authenticated (else 400) |
- Authenticated (
Authorization: Bearer ...): changes the current user's password after checkingcurrent_password. - Unauthenticated: resets via
reset_password_token(orreset_password_codefor the OTP flow).
Request (authenticated — change your own password)
curl -s http://localhost:3000/v1/passwords/change \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-H 'Authorization: Bearer eyJhbGciOi...' \
-d '{"current_password":"secret123","password":"newsecret456","password_confirmation":"newsecret456"}'
Request (unauthenticated — reset via the emailed token)
curl -s http://localhost:3000/v1/passwords/change \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"reset_password_token":"abc123...","password":"newsecret456","password_confirmation":"newsecret456"}'
200 → { "success": true }; for a signed-in change also access_token + refresh_token — the new password
revoked all earlier tokens, including the one used for this request. 400 — missing password / missing token.
422 — wrong current password, or invalid/expired reset token.
Email availability
GET/v1/emails/:id — is this email taken? Public
:id is the address, URL-encoded. (The older ?email= query parameter still works and wins when given.)
Request
curl -s 'http://localhost:3000/v1/emails/ada%40example.com' -H 'Accept: application/json'
200
{ "messages": [], "available": false, "registered": true }