Innbocks Partner API
Build your CRM and billing system on top of Innbocks operations.
Use `/api/v1` to provision invited customers, map external CRM IDs, read compliance and mail state, request mail actions, inspect webhooks, and reconcile active mailbox usage.
Tooling
Authentication
Create scoped keys in Dashboard Settings, then send them as `Authorization: Bearer ib_live_...`.
Idempotency
Send `Idempotency-Key` on customer creation, updates, deactivation, and mail-action requests to safely retry CRM jobs.
Errors
Errors use `"error": { "code", "message", "request_id" }. Every response includes `X-Request-Id`.
Inbound Mail Operations
Partners with `mail:write` can create inbound mail/package records for their own customers. The API requires `customer_id`, `type`, at least one photo URL, and `performed_by`. Innbocks verifies that the customer belongs to the partner, has active billing/prepay, and is approved for mailbox receipt before emitting `mail.arrived` or `package.arrived`. Reassignment is intentionally not exposed; delete a partner-created mail record and recreate it if your system assigned it incorrectly.
MVP Endpoints
Scopes
Create Customer
curl -X POST https://app.innbocks.com/api/v1/customers \
-H "Authorization: Bearer ib_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm_customer_123_create" \
-d '{
"external_customer_id": "crm_customer_123",
"email": "jane@example.com",
"name": "Jane Smith",
"phone": "+15555555555",
"prepay": true
}'Create Inbound Mail
curl -X POST https://app.innbocks.com/api/v1/mail \
-H "Authorization: Bearer ib_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inbound_mail_789" \
-d '{
"customer_id": "cus_123",
"type": "package",
"photos": ["https://example.com/package-front.jpg"],
"performed_by": "warehouse_user_42",
"tracking_number": "1Z9999999999999999"
}'Webhook Verification
import crypto from "crypto";
export function verifyInnbocksWebhook(rawBody, timestamp, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const received = signatureHeader
?.split(",")
.map((part) => part.trim())
.find((part) => part.startsWith("v1="))
?.slice(3);
return Boolean(received) &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}