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

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:

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.pdf

Listar + 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 100

Carga 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-MM

TypeScript 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

HTTPCódigoSignifica
401UnauthorizedToken inválido o expirado.
402QuotaExceededCuota mensual alcanzada en el plan actual.
402SubscriptionUnpaidStripe agotó reintentos; actualizar método de pago.
403ForbiddenEl rol del user no permite la operación.
400BadRequestPayload inválido; mensaje detalla el campo.
429TooManyRequestsRate limit: 600 req/min por IP default.
404NotFoundRecurso no existe o no pertenece al tenant.

Rate limits

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


Integrar ahora

Cuenta Free en 30 segundos, API en la primera hora, primera emisión en la segunda. Si algo falta en esta doc, escribínos con la request específica — la agregamos.