Examples use fictional data. Every company, person, address and count in them is invented, and their domains use the reserved .example TLD.
Getting started
Authentication
Two credential types, one header each. Session tokens are for humans in a browser; API keys are for your servers. Everything else on this page is a consequence of that split.
Bearer JWT vs. x-api-key
A session token is a short-lived HS256 JWT carrying the user, workspace and role. An API key is a workspace credential with no user attached — it never expires on its own and is revoked explicitly. In this deployment session tokens stay inside the web app (httpOnly cookies); integrations use API keys. If both are present, the session is used.
| Bearer JWT | x-api-key | |
|---|---|---|
| Header | authorization: Bearer … | x-api-key: sk_live_… |
| Issued by | POST /v1/auth/login or /signup | POST /v1/api-keys |
| Lifetime | 30 minutes, refreshed with a 30-day refresh token | Until revoked |
| Identity | User + workspace + role | Workspace only, member role: cannot invite people or change workspace settings |
| Use for | Browser sessions, dashboards, anything a person clicks | Servers, cron jobs, data pipelines |
| Rate limit | 300 requests / minute | 300 requests / minute |
# server-to-server
curl "https://beamro.com/api/v1/me" -H "x-api-key: $SALVO_API_KEY"/v1/auth/signupCreate a workspace
Registers the user, creates their organisation, grants the starter sending wallet and returns a signed session in one round trip.
Body parameters
| Field | Type | Description |
|---|---|---|
| emailreq | string | Work email. Unique across Salvo. |
| passwordreq | string | Minimum 8 characters. Stored only as a bcrypt hash. |
| name | string | Display name of the person signing up. |
| org_name | string | Workspace name. Defaults to the email domain when omitted. |
| invite_token | string | The invite parameter of a join link. Joins that workspace instead of creating one; must match the invited email. |
curl -X POST "https://beamro.com/api/v1/auth/signup" \
-H "content-type: application/json" \
-d '{
"email": "priya@harlowmeadow.example",
"password": "a-long-passphrase",
"name": "Priya Whitcombe",
"org_name": "Harlow Meadow Events"
}'{
"user": {
"id": "usr_7d2m41ka9xq0zv3b8n",
"email": "priya@harlowmeadow.example",
"name": "Priya Whitcombe",
"avatar_url": null,
"email_verified": false,
"created_at": "2026-08-31T11:04:02.000Z"
},
"org": {
"id": "org_4k1p8w2ne6ts0ybc7j",
"name": "Harlow Meadow Events",
"slug": "harlow-meadow-events",
"plan": "free",
"credit_balance": 0,
"email_balance_cents": 3000,
"trial_granted": true,
"created_at": "2026-08-31T11:04:02.000Z"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfN2QybTQxa2E5eHEwenYzYjhuIiwib3JnIjoi…",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfN2QybTQxa2E5eHEwenYzYjhuIiwidHlwIjoi…"
}409 conflict. The starter grant of $30 lands on email_balance_cents, not on data credits — see credits and metering./v1/auth/loginExchange credentials for a session
Returns the same envelope as signup. Wrong credentials are a 401 with no hint about which half was wrong.
Body parameters
| Field | Type | Description |
|---|---|---|
| emailreq | string | Registered email address. |
| passwordreq | string | Plaintext password, compared against the stored hash. |
curl -X POST "https://beamro.com/api/v1/auth/login" \
-H "content-type: application/json" \
-d '{
"email": "priya@harlowmeadow.example",
"password": "a-long-passphrase"
}'{
"user": {
"id": "usr_7d2m41ka9xq0zv3b8n",
"email": "priya@harlowmeadow.example",
"name": "Priya Whitcombe",
"avatar_url": null,
"email_verified": false,
"created_at": "2026-08-31T11:04:02.000Z"
},
"org": {
"id": "org_4k1p8w2ne6ts0ybc7j",
"name": "Harlow Meadow Events",
"slug": "harlow-meadow-events",
"plan": "free",
"credit_balance": 0,
"email_balance_cents": 3000,
"trial_granted": true,
"created_at": "2026-08-31T11:04:02.000Z"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}/v1/auth/refreshRotate an expired access token
Trades a refresh token for a fresh pair. The old refresh token stops working the moment the new one is issued.
Body parameters
| Field | Type | Description |
|---|---|---|
| refresh_tokenreq | string | The refresh token from signup, login or a previous refresh. |
curl -X POST "https://beamro.com/api/v1/auth/refresh" \
-H "content-type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}'{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"
}/v1/auth/logoutEnd the current session
Invalidates the refresh token behind the presented session. API keys are unaffected — revoke those explicitly.
curl -X POST "https://beamro.com/api/v1/auth/logout" \
-H "authorization: Bearer $SALVO_ACCESS_TOKEN"{
"ok": true
}/v1/meWho am I
Resolves whichever credential you sent into a user, a workspace and a role. The cheapest way to check that a key still works.
curl "https://beamro.com/api/v1/me" \
-H "x-api-key: $SALVO_API_KEY"{
"user": {
"id": "usr_7d2m41ka9xq0zv3b8n",
"email": "priya@harlowmeadow.example",
"name": "Priya Whitcombe",
"avatar_url": null,
"email_verified": false,
"created_at": "2026-08-31T11:04:02.000Z"
},
"org": {
"id": "org_4k1p8w2ne6ts0ybc7j",
"name": "Harlow Meadow Events",
"slug": "harlow-meadow-events",
"plan": "growth",
"credit_balance": 38420,
"email_balance_cents": 4180,
"trial_granted": true,
"created_at": "2026-08-31T11:04:02.000Z"
},
"role": "owner"
}role: "owner" and a synthetic service user.Unauthenticated endpoints
A small public surface powers the marketing tools — company and people search, lookalike lookup, segment and geography stats. It takes no credential, returns capped result sets, never exposes contact details, and is limited to 30 requests an hour per IP. Everything under the public prefix mirrors an authenticated equivalent, so promoting a prototype to production is a base-path change and a header.
curl -X POST "https://beamro.com/api/v1/public/search/companies" \
-H "content-type: application/json" \
-d '{
"filters": {
"q": "boutique hotels in Lisbon",
"limit": 5
}
}'