Authentication and rate limits
This article covers how to authenticate API calls, what error responses mean, and how to stay inside the rate-limit envelope.
Authentication
Every BizRnR API call is authenticated with an API key sent as a Bearer token:
GET /v1/conversations HTTP/1.1
Host: api.bizrnr.com
Authorization: Bearer sk_live_...Keys come in two flavors:
sk_live_...— production secret keys. Used for real traffic.sk_test_...— test mode keys. Hits a sandbox tenant; no real calls placed, no Stripe charges. Useful for local dev + CI.
You can have both at once — production code reads sk_live_* from your environment, tests read sk_test_* from .env.test.
Status codes you'll see
| Code | Meaning | What to do | |---|---|---| | 200 / 201 | Success | Parse the body | | 400 | Bad request — malformed JSON or invalid params | Read the error.message field; fix the request | | 401 | Auth failed — key missing, wrong, or revoked | Check your env var; regenerate if needed | | 403 | Auth ok, but key lacks scope (e.g. read-only key on a write endpoint) | Use a key with the right scope | | 404 | Resource not found, OR your key can see it but it doesn't exist | Verify the ID; remember tenant scoping | | 409 | Conflict — usually a duplicate idempotency key with a different body | See the idempotency section below | | 422 | Validation error — the request was understood but a field is invalid | error.fields lists which fields and why | | 429 | Rate limit hit | Read Retry-After; back off | | 500 / 502 / 503 | Our problem | Retry with exponential backoff (1s, 2s, 4s, 8s, give up) |
Every error response has the same shape:
{
"error": {
"type": "validation_error",
"message": "phone is not a valid E.164 number",
"fields": { "phone": "must start with + and country code" },
"request_id": "req_..."
}
}Always log request_id — when you ping support, it's the fastest way for us to find your call in our logs.
Rate limits
Limits are per-tenant, sliding-window over 60 seconds:
| Tier | Requests/min | Burst | |---|---|---| | AI Receptionist | Not included by default | Not included by default | | Enterprise/API-enabled | Contracted | Contracted |
The "burst" column is what you get for one minute every five — useful for batch jobs.
When you hit the limit, the response is 429 with these headers:
Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714153200Wait Retry-After seconds before retrying. The official SDK handles this for you with exponential backoff capped at 30s.
Idempotency
Any POST or PUT accepts an Idempotency-Key header. Use a UUID v4 per logical operation:
curl -X POST https://api.bizrnr.com/v1/contacts \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: 8a1f3c5e-..." \
-H "Content-Type: application/json" \
-d '{"phone":"+15555551234","name":"Acme Corp"}'Replays of the same key within 24 hours return the original response, so a network blip mid-write doesn't create duplicates. If you replay the key with a *different* body, you get 409 conflict — that's a bug in your code, not a flake.
Idempotency keys cost nothing on the limit — they're a header, not a separate call. Use them on every write, especially anything that creates a contact or sends a message.
Webhook signing
Outgoing webhooks (the ones we call when something happens in your tenant) are signed with HMAC-SHA256 over the raw request body using the secret in Settings → Webhooks → Secret.
We send three headers:
BizRnR-Signature: t=<timestamp>,v1=<hex-hmac>BizRnR-Event: <event-type>BizRnR-Delivery: <unique-delivery-id>
Verify before processing:
import crypto from 'node:crypto';
function verifyWebhook(body: string, header: string, secret: string): boolean {
const [tPart, sigPart] = header.split(',');
const t = tPart!.split('=')[1]!;
const sig = sigPart!.split('=')[1]!;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${body}`)
.digest('hex');
// Reject anything older than 5 minutes — guards against replay.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}If verification fails, return 400 and we'll retry with backoff. If your endpoint stays down, we exponentially back off for 24 hours then give up — failed deliveries are visible in Settings → Webhooks → Deliveries for 30 days so you can replay them.
What's next
If you're building a public integration that other BizRnR customers will install, look at the OAuth section of the API docs. OAuth lets you act on a user's behalf without them ever sharing their API key.