API

API reference

Accounts

Log in, sign up, tokens, users, passwords

Base URL https://api.ghostunnel.com Auth Bearer token

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

bash
curl -s http://localhost:3000/v1/auth \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"email":"[email protected]","password":"secret123"}'

200

json
{ "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

bash
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

json
{ "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

bash
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

bash
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

bash
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 :id path segment is ignored — the controller always returns the authenticated user (so GET/v1/users/me works just as well).

Request

bash
curl -s http://localhost:3000/v1/users/me \
  -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOi...'

200

json
{
  "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 :id is ignored (updates the authenticated user). Permitted params: email, password, password_confirmation, name, username. A new email is not applied straight away: we email a confirmation link to it (valid 24 hours) and it shows as unconfirmed_email until the user opens the link. An address already in use is a 422.

Request

bash
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 :id is ignored): account, plan, data_usage, devices, active_tunnels, invoices. No secrets (password hash, tunnel private keys) are included.

Request

bash
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 :id is 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

bash
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

bash
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)

Request (authenticated — change your own password)

bash
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)

bash
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

bash
curl -s 'http://localhost:3000/v1/emails/ada%40example.com' -H 'Accept: application/json'

200

json
{ "messages": [], "available": false, "registered": true }