Basely API
Referencia de la API
Una clave por negocio. Lee los libros, las cuentas del banco, las transacciones, las facturas y las cuentas por pagar de un negocio, escribe en ellos con el permiso del dueño y recibe webhooks firmados cuando algo cambia.
Inicio rápido
- El dueño del negocio abre API en el panel de Basely y crea una clave con los scopes que tu sistema necesita.
- Guarda la clave en el entorno de tu servidor. Se muestra una sola vez.
- Llama a /v1/me para probar la conexión y después a los endpoints que necesites.
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_..."
Autenticación
Envía la clave como Authorization: Bearer bsk_live_... en cada solicitud.
- El negocio sale de la clave. No hay id de negocio en la solicitud, y una clave nunca alcanza otro negocio.
- Las claves de prueba (bsk_test_) leen el negocio real y validan escrituras, pero nunca guardan.
- Solo el dueño del negocio crea, rota y revoca claves. La clave deja de funcionar cuando quien la creó ya no es el dueño.
- Rotar mantiene la clave anterior funcionando durante el plazo que elijas, para que tu servidor cambie sin caerse.
Scopes
Cada clave lleva solo los scopes que recibió. Un endpoint sin su scope responde 403 SCOPE_DENIED.
| Scope | Servicio | Acceso |
|---|---|---|
| financial:read | financial | Lee |
| accounts:read | financial | Lee |
| transactions:read | financial | Lee |
| transactions:write | financial | Escribe en los libros |
| invoices:read | financial | Lee |
| invoices:write | financial | Escribe en los libros |
| bills:read | financial | Lee |
| bills:write | financial | Escribe en los libros |
| contacts:read | crm | Lee |
| contacts:write | crm | Escribe |
| conversations:read | phone | Lee |
| texts:send | phone | Escribe |
| inbox:write | phone | Escribe |
| calls:create | phone | Escribe |
| sessions:manage | phone | Escribe |
| otp:send | phone | Escribe |
| phone_contacts:write | phone | Escribe |
| bank:refresh | banking | Próximamente |
| email:send | Próximamente | |
| sms:send | sms | Próximamente |
| calendar:read | calendar | Próximamente |
| calendar:write | calendar | Próximamente |
| files:read | files | Próximamente |
Endpoints
URL base abajo. Toda respuesta tiene el encabezado x-request-id.
Plataforma
| GET | /v1/me | any key | The business and the key behind this request. |
Finanzas
| 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. |
Teléfono y textos
| 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. |
Convenciones
- El dinero va en centavos enteros con la moneda. Las fechas en ISO 8601.
- Las listas devuelven object: list, data, has_more. Usa limit (hasta 100) y from/to para filtrar.
- Los saldos vienen de los libros, que el banco mantiene al día solo. Leer nunca le pregunta al banco directo.
- Actualizar ahora en el banco (Fresh) es una acción aparte, con su propia cuota mensual. Leer la API nunca gasta Fresh.
- Límite por clave por minuto: 120 lecturas, 30 escrituras, 20 reportes. Al pasarlo: 429 con retry-after.
Errores
El error tiene un código estable, un mensaje para personas y el id de la solicitud. Los errores de escritura agregan reason y 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
El dueño registra la dirección de tu sistema y elige los eventos. La entrega reintenta durante 24 horas.
Cada entrega va firmada con el secreto de la dirección en basely-signature y trae basely-event-id para descartar repeticiones. Verifícalo así:
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 ?? ""));
}