Account, projects, and users
bZapper organizes everything into three levels:
Account (company)
├── Users (admin / member) ← billing and the team live on the account
├── Contacts (shared) ← visible across every project, filterable
└── Projects
├── Project A
│ ├── Numbers (instances) + rotation
│ ├── Inbox (conversations/messages)
│ ├── API keys
│ ├── Statistics
│ └── Number identity (profile/"About")
└── Project B … (isolated from A)
What is a project?
A project is an isolated environment inside your account. Unlike other tools where "instance = one number", here a project groups several numbers that take turns among themselves (redundancy). Each project isolates:
- Numbers (instances) and the rotation among them;
- Inbox — conversations and messages;
- API keys;
- Statistics (consumption);
- Number identity (profile/"About").
Use projects to separate clients, brands, environments (production/test), or teams — without mixing numbers, conversations, or billing.
API keys are per project
Each API key belongs to exactly one project. The key already carries the context: every call authenticated with it operates only on the numbers, inbox, and statistics of that project. You don't need to send anything else.
# This key belongs to "Project A" → it only sees Project A's numbers/inbox.
curl https://api.bzapper.com.br/instances -H "Authorization: Bearer bz_live_forProjectA..."
To operate on another project, generate a key in that project (in the panel, switch the project in the top selector and create the key under Keys).
X-Project-IdThe key already carries its project. The X-Project-Id header is only for the panel
session (JWT), where the active project comes from the selector — on a call authenticated
by API key it is simply ignored. To slice data by project, use the ?project_id=
parameter on the endpoints that accept it.
In the panel, the active project is chosen in the header selector and sent on
each request via the X-Project-Id header. The screens (Numbers, Inbox, Keys,
Statistics) reflect the active project. Switching the project switches the entire
context.
API key rotation
The raw key (bz_live_...) is shown only once. If it leaked, if a dev left the team,
or if you simply want to swap keys for hygiene, do not delete it and create another:
rotate it. POST /keys/{id}/rotate creates a new key that inherits the old one's
role, scopes, project, and name, and keeps the old one working for a grace
period — your integration does not go down mid-deploy.
curl -X POST https://api.bzapper.com.br/keys/$KEY_ID/rotate \
-H "Authorization: Bearer $BZ_KEY" \
-H "Content-Type: application/json" \
-d '{ "revoke_in_seconds": 86400 }'
Response 200:
{
"api_key": "bz_live_newRawKey...",
"key": { "id": "9b1c…", "name": "production", "role": "admin", "project_id": "4d2e…" },
"previous_key": { "id": "3f2a…", "expires_at": "2026-07-02T12:00:00Z", "rotated_to": "9b1c…" },
"old_key_expires_at": "2026-07-02T12:00:00Z"
}
api_key is shown only onceThe api_key field in the response is the new raw key — it is not recoverable
afterwards. Store it in your secret vault before you close the request.
The grace period (revoke_in_seconds)
revoke_in_seconds is how long the old key stays valid:
| Value | Effect |
|---|---|
| omitted | 86400 (24 hours) — the default |
0 | revokes the old one immediately (only do this if you swap the key everywhere at once) |
up to 2592000 | a maximum of 30 days |
Past the deadline, the old key answers 401 key_expired — a code distinct from
key_revoked, so your logs tell you a rotation expired rather than a key being revoked.
The safe procedure (no downtime)
- Rotate with a grace period that fits your deploy window (24 h is generous). Store
the new
api_keyin your vault. - Ship the new key wherever the integration runs (environment variables, secret manager) and deploy. Both keys are valid at the same time — no request fails.
- Check that nobody uses the old one any more:
GET /keysshows each key'slast_used_at. Once it stops moving, the migration is done. - Let the old one expire on its own at the end of the grace period — or hurry it
along with
DELETE /keys/{id}once step 3 is confirmed.
Do not invert the order: rotating after the deploy breaks the integration in between.
In the panel, the Keys screen has a Rotate button with the grace period in three options (1 day, 1 week, or now) — the new key is revealed once, with a notice of when the old one stops working.
GET /keys during a rotation
The listing gains two fields that tell each key's story:
| Field | What it says |
|---|---|
expires_at | when the rotated key stops working. null when it was never rotated |
rotated_to | the id of the key that replaced it — the trail of what came next |
Who can, and what fails
Rotation is admin only (an agent key or a member user gets 403 admin_required). A
non-existent key is 404; a key that was already revoked or that already expired
answers 409 with key_already_revoked / key_already_expired — there is nothing left
to rotate.
A key issued to a partner application through bZapper Connect does not rotate here:
use POST /partner/connections/{id}/rotate-key. See the
Connect reference.
In the official SDKs
// Node / TypeScript
const { api_key, old_key_expires_at } = await bz.rotateKey(keyId, { revoke_in_seconds: 3600 });
# Python
rotated = bz.rotate_key(key_id, revoke_in_seconds=3600)
print(rotated["api_key"]) # store it now; it is never shown again
// PHP
$rotated = $bz->rotateKey($keyId, 3600);
// Go
grace := 3600
rot, err := bz.RotateKey(ctx, keyID, bzapper.RotateKeyParams{RevokeInSeconds: &grace})
// Java
ApiKeyRotated rotated = bz.rotateKey(keyId, 3600);
// .NET (C#)
var rotated = await bz.RotateMyKeyAsync(keyId, new RotateApiKey { RevokeInSeconds = 3600 });
# Ruby
rotated = bz.accounts.rotate_my_key(key_id, revoke_in_seconds: 3600)
Contacts belong to the account (shared)
The contact base belongs to the account — the same client is recognized in any project. You can filter contacts by project:
GET /contacts # every contact in the account
GET /contacts?project_id=<id> # only those who had a conversation in that project
GET /contacts?project_id=current # only those of your key's/session's project
Users and roles
Users belong to the account and can see every project. There are two roles:
| Role | Can |
|---|---|
| Administrator | everything: billing, account consumption, managing users and projects |
| Member | everything except billing and the account page |
An admin invites users under Account → Team (by email; the invitee receives a link to set their password). The account always keeps at least one administrator.
Account billing (Pro plan)
Billing belongs to the account (not the project): the account has one plan (Free or Pro) and one currency, and the resources of every project count against the account's limits. Free is free forever; Pro is a monthly recurring subscription (unlimited messages + a bundle of resources), with add-ons to extend it. Details in Billing.
The Billing page (admin) shows the plan, the invoices, the cards (with a primary card + recurrence), and the aggregated consumption per project (numbers, sent, received, total).
GET /me/entitlements # the account's effective limits (plan, add-ons, allowances, usage)
GET /me/subscription # plan state (status, due date, recurrence, currency)
GET /me/invoices # the account's invoice history
GET /account/usage # admin: { account: {…}, projects: [{ name, numbers, total, … }] }
Summary
- Account = company (users, billing/plan, contacts).
- Project = isolated environment (numbers, inbox, keys, stats, identity).
- API key = always belongs to one project; swap it with rotation
(
POST /keys/{id}/rotate), never by deleting and recreating. - Contacts = shared, filterable per project.
- Plan = Free or Pro (recurring + add-ons); billing lives on the account.
- Members see everything except billing.