Authentication
Every /api/v1 data endpoint requires authentication. The one exception is the health check, GET /api/v1/health, which is public and useful for connectivity tests. Unauthenticated requests to protected endpoints receive 401 UNAUTHORIZED.
The scheme: Bearer JWT
Section titled “The scheme: Bearer JWT”Qeychain authenticates requests with a JSON Web Token (JWT) session access token sent in the standard Authorization header:
GET /api/v1/wallets/{address}/dashboard HTTP/1.1Host: api.qeychain.xyzAuthorization: Bearer <access_token>The middleware reads the token from the Authorization: Bearer header. A few details worth knowing:
- The token is a signed session token, not an API key. It encodes the user identity and an expiry.
- Access tokens are short-lived — they expire 15 minutes after issue.
- A longer-lived refresh token (valid 7 days) is issued alongside it so you can obtain a new access token without re-entering credentials.
Get a token
Section titled “Get a token”1. Create an account
Section titled “1. Create an account”Sign up with an email and password. Signup creates the account and sends a verification email — you must verify your email before you can log in.
curl -X POST https://api.qeychain.xyz/api/v1/auth/signup \ -H "Content-Type: application/json" \ -d '{ "email": "advisor@example.com", "password": "a-strong-password", "full_name": "Jordan Advisor", "accepted_terms": true }'2. Log in
Section titled “2. Log in”Once your email is verified, log in to receive your tokens. The access and refresh tokens are returned in the response body (and also set as cookies).
curl -X POST https://api.qeychain.xyz/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "advisor@example.com", "password": "a-strong-password" }'{ "success": true, "data": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "advisor@example.com", "full_name": "Jordan Advisor", "email_verified": true, "plan_name": "professional", "plan_expired": false, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }}Make an authenticated request
Section titled “Make an authenticated request”Send the access_token value as a Bearer credential:
curl https://api.qeychain.xyz/api/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/dashboard \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."To confirm which identity a token belongs to, call:
curl https://api.qeychain.xyz/api/v1/auth/me \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Refreshing an expired token
Section titled “Refreshing an expired token”When the 15-minute access token expires, requests return 401 UNAUTHORIZED. Obtain a fresh access token from the refresh endpoint, which reads the refresh_token from a cookie:
curl -X POST https://api.qeychain.xyz/api/v1/auth/refresh \ --cookie "refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."The endpoint issues a new access token as an access_token cookie. Clients that cannot use cookies can simply log in again to mint a new token pair.
Partner integrations
Section titled “Partner integrations”For server-to-server access, Qeychain issues partner keys. Present a partner key in the X-Partner-Key header (it may also be passed as a partner_key query parameter):
curl https://api.qeychain.xyz/api/v1/wallets/0xd8dA...6045/positions \ -H "X-Partner-Key: <your-partner-key>"Partner keys are provisioned by Qeychain — contact us to enroll. Partner applications can also sign users in through a one-time magic-link SSO flow, which returns the same JWT tokens described above.
Scoped API keys
Section titled “Scoped API keys”Beyond session tokens, Qeychain supports database-backed API keys for programmatic access with granular, SOC 2-friendly permissions. You create and manage them under /api/v1/api-keys using your Bearer token.
Create a key (the full key value is returned only once — store it securely):
curl -X POST https://api.qeychain.xyz/api/v1/api-keys \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "name": "reporting-service", "permissions": ["read:holdings", "read:tax-reports"] }'{ "success": true, "data": { "id": "a1b2c3d4-0000-0000-0000-000000000000", "key_prefix": "qk_1a2b3c4d", "name": "reporting-service", "permissions": ["read:holdings", "read:tax-reports"], "is_active": true, "total_requests": 0, "key": "qk_1a2b3c4d...full-value-shown-once..." }}Generated keys carry a qk_ prefix and are presented in the X-API-Key header. Available permission strings are:
| Permission | Grants |
|---|---|
read:holdings |
Read wallet holdings and positions |
read:transactions |
Read transaction history |
read:lots |
Read tax lot details |
read:tax-reports |
Read tax reports and Form 8949 |
write:portfolios |
Create and manage portfolios |
write:webhooks |
Create and manage webhooks |
admin:api-keys |
Manage API keys |
List, inspect, and revoke keys with the same collection:
# List keys (values are never returned again — only prefixes and metadata)curl https://api.qeychain.xyz/api/v1/api-keys \ -H "Authorization: Bearer eyJhbGci..."
# Revoke a key permanentlycurl -X DELETE "https://api.qeychain.xyz/api/v1/api-keys/a1b2c3d4-0000-0000-0000-000000000000?reason=rotated" \ -H "Authorization: Bearer eyJhbGci..."A 401 looks like this
Section titled “A 401 looks like this”{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Authentication required" }, "meta": { "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "timestamp": "2026-01-15T09:24:11.482Z", "version": "1.0.0" }}See Response format for the full error contract.
