Basely API
API reference
One key per business. Read the books, bank accounts, transactions, invoices and bills of a business, write to them with the owner's permission, and get signed webhooks when something changes.
Quick start
- The owner of the business opens API in the Basely panel and creates a key with the scopes your system needs.
- Put the key in your server's environment. It is shown once.
- Call /v1/me to check the connection, then the endpoints you need.
curl https://api.baselyapp.com/v1/me \ -H "Authorization: Bearer bsk_live_..." curl "https://api.baselyapp.com/v1/financial/summary?period=2026-09" \ -H "Authorization: Bearer bsk_live_..."
Authentication
Send the key as Authorization: Bearer bsk_live_... on every request.
- The business comes from the key. There is no business id in the request, and a key never reaches another business.
- Test keys (bsk_test_) read the real business and validate writes, but never save them.
- Only the owner of the business creates, rotates and revokes keys. A key stops working when the person who created it is no longer the owner.
- Rotating keeps the old key working for a grace period you choose, so your server can switch without downtime.
Scopes
Each key carries only the scopes it was given. An endpoint without its scope answers 403 SCOPE_DENIED.
| Scope | Service | Access |
|---|---|---|
| financial:read | financial | Reads |
| accounts:read | financial | Reads |
| transactions:read | financial | Reads |
| transactions:write | financial | Writes to the books |
| invoices:read | financial | Reads |
| invoices:write | financial | Writes to the books |
| bills:read | financial | Reads |
| bills:write | financial | Writes to the books |
| contacts:read | crm | Reads |
| contacts:write | crm | Writes |
| conversations:read | phone | Reads |
| texts:send | phone | Writes |
| inbox:write | phone | Writes |
| calls:create | phone | Writes |
| sessions:manage | phone | Writes |
| otp:send | phone | Writes |
| phone_contacts:write | phone | Writes |
| bank:refresh | banking | Coming later |
| email:send | Coming later | |
| sms:send | sms | Coming later |
| calendar:read | calendar | Coming later |
| calendar:write | calendar | Coming later |
| files:read | files | Coming later |
Endpoints
Base URL below. Every response has an x-request-id header.
Platform
| GET | /v1/me | any key | The business and the key behind this request. |
Financial
| GET | /v1/financial/summary | financial:read | Cash, inflows, outflows, expenses, recurring costs, burn and runway for a period. |
| GET | /v1/financial/cash-flow | financial:read | Cash flow month by month: operating, investing and financing. |
| GET | /v1/financial/reports/{report} | financial:read | A financial report: pnl, balance-sheet, expenses-by-category or trial-balance. |
| GET | /v1/accounts | accounts:read | Bank accounts connected to the books, with their ledger balance. |
| GET | /v1/transactions | transactions:read | Bank transactions, newest first. |
| GET | /v1/transactions/{id} | transactions:read | One bank transaction. |
| POST | /v1/transactions/{id}/categorize | transactions:write | Post a bank transaction to a category in the books. |
| GET | /v1/invoices | invoices:read | Invoices, newest first. |
| POST | /v1/invoices | invoices:write | Create an invoice. |
| GET | /v1/invoices/{id} | invoices:read | One invoice, with what is still open. |
| POST | /v1/invoices/{id}/payments | invoices:write | Record a payment received for an invoice. |
| GET | /v1/bills | bills:read | Bills to pay, newest first. |
| POST | /v1/bills | bills:write | Create a bill to pay. |
CRM
| GET | /v1/contacts | contacts:read | Contacts of the business. |
| POST | /v1/contacts | contacts:write | Create a contact, or update the one with the same email or phone. |
Phone and texts
| GET | /v1/businesses/{negocioId}/conversations | conversations:read | Messages and calls, masked. |
| POST | /v1/businesses/{negocioId}/texts | texts:send | Send a text from the business number. |
| POST | /v1/businesses/{negocioId}/emails | inbox:write | Hand a received email to the inbox. |
| POST | /v1/businesses/{negocioId}/calls | calls:create | Ring a team member. |
| POST | /v1/businesses/{negocioId}/contacts | phone_contacts:write | Declare a customer or a team member. |
| POST | /v1/businesses/{negocioId}/sessions | sessions:manage | Open a masked line. |
| DELETE | /v1/businesses/{negocioId}/sessions/{sessionId} | sessions:manage | Close a masked line. |
| POST | /v1/businesses/{negocioId}/otp | otp:send | Send a one-time code by text. |
| POST | /v1/businesses/{negocioId}/otp/verify | otp:send | Verify a one-time code. |
Conventions
- Money is in integer cents with a currency. Dates are ISO 8601.
- Lists return object: list, data, has_more. Use limit (up to 100) and from/to to narrow them.
- Balances come from the books, which the bank keeps in sync by itself. Reading never asks the bank directly.
- A manual refresh from the bank (Fresh) is a separate action with its own monthly quota. Reading the API never spends one.
- Rate limits per key per minute: 120 reads, 30 writes, 20 reports. Over the limit: 429 with retry-after.
Errors
Errors have a stable code, a message for people and the request id. Write errors add reason and field.
{ "error": { "code": "SCOPE_DENIED", "message": "...", "scope": "invoices:write", "request_id": "req_..." } }| MISSING_KEY | Send your API key as Authorization: Bearer <key>. |
| INVALID_KEY | This API key is not valid. |
| KEY_REVOKED | This API key was revoked or replaced. Use the current key of this business. |
| KEY_EXPIRED | This API key expired. Ask the owner of the business for a new one. |
| NOT_FOUND | Not found. |
| BUSINESS_FROZEN | This business is frozen. Calls resume when it is active again. |
| BUSINESS_CLOSED | This business is closed. |
| SCOPE_DENIED | This key does not have the scope this endpoint needs. |
| MODULE_NOT_ENABLED | Finance is not enabled for this business. |
| BOOKS_NOT_SET_UP | The books of this business are not set up yet. |
| RATE_LIMITED | Too many requests for this key. Wait and try again. |
| INVALID_REQUEST | The request is not valid. |
| INVALID_JSON | The body must be a JSON object. |
| TEST_MODE | Test keys validate writes but never save them. |
| CONFLICT | The books do not allow this change right now. See reason. |
| FORBIDDEN | The owner of this key can no longer do this in the business. |
| INTERNAL | Something failed on our side. Try again; if it keeps failing, send us the request_id. |
Webhooks
The owner adds the address of your system and picks the events. Deliveries retry for 24 hours.
Each delivery is signed with your endpoint secret in basely-signature, and carries basely-event-id so you can drop duplicates. Verify it like this:
import { createHmac, timingSafeEqual } from "node:crypto";
// header: basely-signature: t=<unix seconds>,v1=<hex>
export function isFromBasely(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}