Skip to main content

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

With an API key you do not send X-Project-Id

The 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.

Panel (user session)

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 once

The 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:

ValueEffect
omitted86400 (24 hours) — the default
0revokes the old one immediately (only do this if you swap the key everywhere at once)
up to 2592000a 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)​

  1. Rotate with a grace period that fits your deploy window (24 h is generous). Store the new api_key in your vault.
  2. 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.
  3. Check that nobody uses the old one any more: GET /keys shows each key's last_used_at. Once it stops moving, the migration is done.
  4. 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:

FieldWhat it says
expires_atwhen the rotated key stops working. null when it was never rotated
rotated_tothe 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.

Partner keys rotate through another route

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:

RoleCan
Administratoreverything: billing, account consumption, managing users and projects
Membereverything 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.