Quickstart
This guide covers wallet sign-in, an authenticated API call, and sending a message, using curl. You need an Ethereum wallet capable of personal_sign and a running API server.
Base URLs
Section titled “Base URLs”http://localhost:3001Bring the stack up with Docker — see Run the stack locally. Verify the API is up:
curl http://localhost:3001/healthhttps://api.zentalk.chatAll current endpoints live under the /api/v1/ prefix. The examples below use the local base URL.
Authentication flow
Section titled “Authentication flow”Zentalk authenticates by wallet signature, then issues JWTs:
- The client constructs a login message that includes a millisecond timestamp.
- The wallet signs it (Ethereum
personal_sign, EIP-191). POST /api/v1/initializeverifies the signature and returns an access token (15-minute lifetime) and a refresh token (7 days).- Subsequent calls send
Authorization: Bearer <access_token>.
The server refuses re-used signatures (replay protection) and rejects Timestamp: lines older than 5 minutes — but currently only when the line is present, so always include it. The full model — phone auth, sessions, MFA — is in Authentication.
First message
Section titled “First message”-
Construct and sign the login message.
The message must contain a line of the form
Timestamp: <unix-ms>:Message to sign Login to ZentalkTimestamp: 1768387200000Sign it with your wallet’s
personal_sign(in a browser:ethereum.request({ method: 'personal_sign', params: [message, address] })). Keep the exact message string — the server re-hashes it during verification. -
Initialize a session.
New users must provide a
username; returning users can omit it.Login (wallet signature → JWT) curl -X POST http://localhost:3001/api/v1/initialize \-H "Content-Type: application/json" \-d '{"wallet_address": "0x1234567890abcdef1234567890abcdef12345678","username": "alice","message": "Login to Zentalk\nTimestamp: 1768387200000","signature": "0xabcdef..."}'Response {"success": true,"address": "0x1234567890abcdef1234567890abcdef12345678","username": "alice","message": "Client initialized successfully","access_token": "eyJhbGciOi...","refresh_token": "eyJhbGciOi...","expires_at": "2026-07-14T12:15:00Z","token_type": "Bearer"}Store both tokens. Export the access token for the next steps:
Terminal window export ZENTALK_TOKEN="eyJhbGciOi..." -
Call an authenticated endpoint.
List your one-to-one chats:
List chats curl http://localhost:3001/api/v1/chats \-H "Authorization: Bearer $ZENTALK_TOKEN"Response (truncated) {"success": true,"chats": [{"id": "chat-123","sender": {"name": "Bob","username": "@bob","online": true,"address": "5678..."},"messages": []}]} -
Send a message.
Send a direct message curl -X POST http://localhost:3001/api/v1/send \-H "Authorization: Bearer $ZENTALK_TOKEN" \-H "Content-Type: application/json" \-d '{"recipient_address": "0x5678000000000000000000000000000000005678","content": "Hello from the API"}'Response {"success": true,"message_id": "msg-12345","timestamp": 1768387200,"message": "Message sent successfully"}An optional client-generated
message_id(UUID) makes sends idempotent: retries with the same ID return the existing message instead of duplicating it. Production clients end-to-end encrypt content before sending — see Security overview. -
Refresh the access token when it expires.
Access tokens live 15 minutes. Exchange the refresh token for a new pair:
Refresh curl -X POST http://localhost:3001/api/v1/auth/refresh \-H "Content-Type: application/json" \-d '{"refresh_token": "eyJhbGciOi..."}'
Next steps
Section titled “Next steps”- Authentication — phone auth, sessions and devices, CSRF, MFA, WebSocket tickets
- Environments — ports, health checks, and base URLs for every service
- WebSocket — receive messages in real time instead of polling
- REST API reference — every endpoint, generated from the OpenAPI spec
- SDKs — typed clients that handle message signing and token refresh
