API REST — Facturación electrónica Costa Rica
Para devs integrando facturación electrónica v4.4 desde un ERP, POS, backoffice o e-commerce. Auth por bearer token, payload JSON, errores con códigos específicos (402 para billing, 403 para permisos, etc.), docs OpenAPI en /v1/docs. Ejemplos en curl y TypeScript.
Conceptos de entrada
- Organización. El espacio de cuentas. Típicamente una por empresa dueña del producto, o una por despacho contable manejando múltiples clientes.
- Empresa (
Company). Entidad legal con cédula jurídica/física, certificado digital y actividad económica. Una organización puede tener N empresas. - Documento (
Document). Un CE — FE, TE, NC, ND, FEE, etc. Se crea en estadoDRAFT, pasa porSIGNED→DISPATCHED→ACCEPTEDoREJECTED. - Tenant scope. Toda request pasa por un guard que lee el header
x-organization-id. Los tokens pueden pertenecer a un usuario con múltiples memberships; el header selecciona cuál aplica.
Autenticación
Dos flujos soportados — cookie (para la web) y bearer token (para server-to-server e integrations mobile). Para integración vía API usá bearer.
1. Sign-in para obtener el token
curl -X POST https://www.facturitica.com/v1/auth/sign-in/email \
-H "Content-Type: application/json" \
-H "Origin: https://www.facturitica.com" \
-d '{"email":"tu@correo.com","password":"secret"}'
// response
{
"token": "xvBkmbwqwlV5FPwSeSZyyoavvukdcfcy",
"user": { "id": "...", "email": "tu@correo.com", "name": "..." }
}El header Origin es requerido (CSRF check de better-auth). Usá tu dominio si integrás desde el browser, o el dominio donde tengas configurado el origin trusted si es server-to-server.
2. Usá el token en todas las requests
curl https://www.facturitica.com/v1/me \
-H "Authorization: Bearer xvBkmbwqwlV5FPwSeSZyyoavvukdcfcy"
// response
{
"user": { "id": "...", "email": "..." },
"memberships": [
{
"organizationId": "fd821846-...",
"role": "OWNER",
"organizationName": "Mi Empresa"
}
]
}3. Organización activa: header x-organization-id
Todos los endpoints tenant-scoped requieren el header x-organization-id. Si no lo mandás, usa el primer membership por createdAt (no recomendado para integrations con múltiples orgs).
Flujo core: emitir un comprobante
Una emisión completa va: POST /documents (síncrono) → trabajador firma XAdES + envía a Hacienda (async vía BullMQ) → GET /documents/:id para polling de estado.
Crear el documento
curl -X POST https://www.facturitica.com/v1/companies/4e0244e1-.../documents \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: fd821846-..." \
-H "Content-Type: application/json" \
-d '{
"condicionVenta": "01",
"type": "01",
"receptor": {
"nombre": "Cliente SA",
"tipoIdentificacion": "02",
"numeroIdentificacion": "3101000001",
"correoElectronico": "cliente@dominio.cr"
},
"lineas": [{
"codigoCabys": "8399000000000",
"cantidad": 1,
"unidadMedida": "Sp",
"detalle": "Consultoría abril 2026",
"precioUnitario": 50000,
"vatRate": 13
}],
"mediosPago": [{ "tipo": "01" }]
}'
// response 201
{
"documentId": "8a4e6de4-...",
"clave": "50619042600310100000100100001040000000003132639311",
"status": "DRAFT",
"type": "01"
}Campos clave:
type:01FE,02ND,03NC,04TE,08FE Compra,09FE Exportación. Por defecto01.condicionVenta: catálogo oficial DGT.01= contado.vatRate: 0, 1, 2, 4 o 13 (tarifas IVA CR). Mapeo interno alcodigoTarifaIVAv4.4.codigoCabys: 13 dígitos, catálogo oficial.receptor: opcional para TE (04), requerido para FE (01) según reglas DGT.referencias: obligatorio para NC/ND (type02/03) — array con la clave de 50 dígitos del documento original + motivo.
Polling de estado
curl https://www.facturitica.com/v1/documents/8a4e6de4-... \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: ORG_ID"
// response (una vez que Hacienda respondió)
{
"id": "8a4e6de4-...",
"status": "ACCEPTED",
"claveNumerica": "50619042600310100000100100001040000000003132639311",
"finalizedAt": "2026-04-19T02:15:33.000Z",
"events": [
{ "type": "created", "createdAt": "...", "payload": {...} },
{ "type": "signed", "createdAt": "...", "payload": {...} },
{ "type": "dispatched", "createdAt": "...", "payload": {...} },
{ "type": "mh_state_check", "createdAt": "...", "payload": {"state": "aceptado", ...} }
],
"mhResponse": { "state": "aceptado", "respuestaXml": "..." }
}Estados posibles: DRAFT, SIGNED, DISPATCHED, ACCEPTED, REJECTED, PARTIALLY_ACCEPTED, OFFLINE. Los tres últimos son terminales. En dev+sandbox, el tiempo típico DRAFT → ACCEPTED es 12-18 segundos (dominado por RTT con Hacienda).
Descargar XML firmado y PDF
curl https://www.facturitica.com/v1/documents/8a4e6de4-.../xml \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: ORG_ID" \
-o signed.xml
curl https://www.facturitica.com/v1/documents/8a4e6de4-.../pdf \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: ORG_ID" \
-o comprobante.pdfListar + filtrar documentos
GET /v1/documents?status=ACCEPTED&companyId=...&type=01&limit=50
// query params
// status: DRAFT | SIGNED | DISPATCHED | ACCEPTED | REJECTED | ...
// type: 01 | 02 | 03 | 04 | 08 | 09
// companyId: uuid (para scoped por empresa dentro del org)
// limit: default 20, max 100Carga masiva (CSV)
curl -X POST https://www.facturitica.com/v1/companies/COMPANY_ID/documents/bulk \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: ORG_ID" \
-F "csv=@emisiones-abril.csv"
// response
{
"totalRows": 20,
"created": [
{ "row": 1, "documentId": "...", "clave": "..." },
...
],
"errors": []
}Hasta 500 filas por request, 2 MB de archivo. Validación atómica: si cualquier fila falla parseo, ninguna se emite. Columnas: detalle, cantidad, precioUnitario, vatRate requeridas; codigoCabys, unidadMedida, tipoIdentificacionReceptor, numeroIdentificacionReceptor, nombreReceptor, correoReceptor, type opcionales.
D-104 (IVA mensual) por API
curl "https://www.facturitica.com/v1/reporting/d104?companyId=...&period=2026-04" \
-H "Authorization: Bearer TOKEN" \
-H "x-organization-id: ORG_ID"
// response
{
"companyId": "...",
"company": { "legalName": "...", "identificationNumber": "..." },
"period": "2026-04",
"debito": {
"byRate": {
"08": { "base": 450000, "impuesto": 58500, "docsCount": 12 },
...
},
"totalVenta": 450000,
"totalImpuesto": 58500,
"docsCount": 12
},
"credito": { ... },
"netoAPagar": 58500
}
// PDF version
GET /v1/reporting/d104.pdf?companyId=...&period=YYYY-MMTypeScript client (mini)
class FEClient {
constructor(
private baseUrl: string,
private token: string,
private organizationId: string,
) {}
private async req<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${this.baseUrl}/v1${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
'x-organization-id': this.organizationId,
...(init.headers ?? {}),
},
});
const body = await res.json();
if (!res.ok) throw new ApiError(res.status, body);
return body as T;
}
createDocument(companyId: string, payload: CreateDocInput) {
return this.req<{ documentId: string; clave: string; status: string }>(
`/companies/${companyId}/documents`,
{ method: 'POST', body: JSON.stringify(payload) },
);
}
getDocument(id: string) {
return this.req<DocumentDetail>(`/documents/${id}`);
}
async waitUntilFinal(id: string, timeoutMs = 30_000): Promise<DocumentDetail> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const d = await this.getDocument(id);
if (d.status === 'ACCEPTED' || d.status === 'REJECTED' ||
d.status === 'PARTIALLY_ACCEPTED') return d;
await new Promise(r => setTimeout(r, 1500));
}
throw new Error('timeout waiting for MH state');
}
}Errores y códigos
| HTTP | Código | Significa |
|---|---|---|
| 401 | Unauthorized | Token inválido o expirado. |
| 402 | QuotaExceeded | Cuota mensual alcanzada en el plan actual. |
| 402 | SubscriptionUnpaid | Stripe agotó reintentos; actualizar método de pago. |
| 403 | Forbidden | El rol del user no permite la operación. |
| 400 | BadRequest | Payload inválido; mensaje detalla el campo. |
| 429 | TooManyRequests | Rate limit: 600 req/min por IP default. |
| 404 | NotFound | Recurso no existe o no pertenece al tenant. |
Rate limits
- Default: 600 req/min por IP. Suficiente para una integración POS típica (10 req/sec en burst).
- Endpoints exentos:
/v1/health,/v1/docs,/v1/catalogs/*,/v1/documents/:id/(xml|pdf)(streaming de archivos). - Si tu integración legítima necesita más, contactanos con tu IP de salida y la agregamos al allowlist.
Webhooks (salientes) — próximamente
Outbound webhooks (para notificarte a tu endpoint cuando un CE cambia de estado) están en roadmap. Por ahora hacé polling de GET /v1/documents/:id con el helper waitUntilFinal del ejemplo arriba, o usá waitUntilFinal con exponential backoff.
El webhook inbound de Stripe (para sync de subscription state) ya está soportado internamente — no lo exponemos a clientes porque depende de tu cuenta de Stripe, no la nuestra.
OpenAPI
El spec completo vive en https://www.facturitica.com/v1/docs — UI interactiva con try-it-out. El JSON crudo está en https://www.facturitica.com/v1/docs-json para generación de clientes (openapi-generator, orval, etc.).
Planes con acceso API
- Free (10 docs/mes): acceso de lectura y emisión via API está incluido pero limitado por quota.
- Pyme Pro ($49/mes, 2,000 docs): recomendado para la primera integración real — webhooks (cuando estén), docs OpenAPI, soporte prioritario.
- Contador/Empresarial: mismo acceso API, más volumen.