Timbre

Contribuyentes

Dar de alta a quien emite, dejarlo listo para facturar y darlo de baja.

Un contribuyente es la persona o la empresa a cuyo nombre se emite: su cédula, su certificado de firma, sus credenciales de Hacienda, sus sucursales y sus terminales. Toda emisión lleva el issuerId de uno.

Una cuenta puede tener varios. Cuando tiene uno solo podés omitir el issuerId y las rutas de configuración lo resuelven solas; en cuanto hay dos, omitirlo es 400 con issuer_id_required.

Todo lo de esta página —alta, certificado, credenciales, sucursales, terminales, régimen, marca, baja— también se hace desde el panel, en Contribuyentes → el que quieras configurar. Lo de abajo es para cuando lo vas a manejar por código, o para automatizar el alta de muchos.

Darlo de alta

Petición
curl -X POST https://api.timbre.cr/internal/v1/issuers \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{
    "identification": "3101123456",
    "tradeName": "Ferretería OSA",
    "taxRegime": "normal",
    "certificateBase64": "MIIK…",
    "certificatePassword": "…"
  }'

El .p12 viaja en base64 y su PIN aparte. taxRegime es normal, simplificado o zona_franca, y por defecto normal.

Responde 201 con el perfil: su id —el issuerId que vas a mandar de ahí en adelante—, su active, y los metadatos que Timbre leyó del certificado (subject, issuer, serialNumber, fingerprint, notBefore, notAfter). La llave privada no vuelve nunca.

El certificado se valida al recibirlo, no al firmar el primer documento: un .p12 que no abre con ese PIN es certificate_password_invalid, uno vencido es certificate_expired, uno que todavía no entra en vigor es certificate_not_yet_valid, y uno cuya cédula no es la que declaraste es certificate_identification_mismatch. Todos son 400.

Leerlo de vuelta

Petición
curl https://api.timbre.cr/internal/v1/issuers \
  -H "Authorization: Bearer timbre_sk_…"

El mismo perfil que devolvió el alta, con sus metadatos de certificado. Sirve para una cuenta con un solo contribuyente —si tiene varios, hay que decir cuál con issuerId como parámetro de consulta, y omitirlo es issuer_id_required. Para ver todos a la vez, sin el certificado, es GET /list de la siguiente sección.

Un contribuyente desactivado se lee igual que uno activo: por eso active importa más que si la ruta respondió. Que exista el perfil no dice que pueda facturar.

Cuántos podés tener

La cuenta tiene un tope de contribuyentes activos. Al alcanzarlo, dar de alta otro responde 409 con issuer_limit_reached.

Petición
curl https://api.timbre.cr/internal/v1/issuers/list \
  -H "Authorization: Bearer timbre_sk_…"
Respuesta
{
  "issuers": [
    {
      "id": "…",
      "identification": "3101123456",
      "tradeName": "Ferretería OSA",
      "taxRegime": "normal",
      "active": true,
      "hasTerminal": true,
      "hasCredentials": true,
      "credentialStatus": "accepted"
    }
  ],
  "maxIssuers": 25
}

maxIssuers viaja en cada respuesta a propósito: el tope lo guarda Timbre y se puede subir en cualquier momento, así que leelo de acá en vez de tenerlo escrito en tu lado.

Acepta environment como parámetro de consulta —sandbox o production, y sandbox si lo omitís— porque dos de los campos dependen del ambiente:

CampoQué es
activeSi el contribuyente puede emitir del todo
hasTerminalSi tiene al menos un terminal, en cualquier sucursal
hasCredentialsSi hay credenciales guardadas para ese ambiente
credentialStatusQué dijo Hacienda de esas credenciales

Las sucursales y las terminales son compartidas entre ambientes; las credenciales no. Por eso un contribuyente listo en sandbox no está listo en producción por herencia: le faltan las credenciales del otro ambiente. Eso es Producción.

Credenciales de Hacienda

Son el usuario y la contraseña del ATV con los que se presenta el comprobante. Se guardan por ambiente.

Petición
curl -X PUT https://api.timbre.cr/internal/v1/issuers/environments/sandbox/credentials \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{
    "haciendaUsername": "cpf-00-0000-0000",
    "haciendaPassword": "…"
  }'

Guardar no es solo guardar: Timbre le pide un token al proveedor de identidad de Hacienda con esas credenciales, sin caché, y te devuelve qué contestó en verification.outcome.

outcomeQué pasó
acceptedHacienda entregó un token
rejectedHacienda las rechazó con su propio invalid_grant
unreachableNo hubo veredicto

unreachable es un hecho sobre ese intento, no sobre las credenciales: se guardaron igual y no queda archivado ningún veredicto. Cubre dos cosas: que no hayamos podido preguntar —la red, el proveedor de identidad caído—, y que Hacienda haya contestado algo que no es un juicio sobre estas credenciales, como una mala configuración de nuestro lado.

Volvé a guardarlas: si era lo primero, se vuelve a preguntar y el veredicto aparece. Si después de reintentar credentialStatus sigue en unverified, el problema puede estar de nuestro lado y reintentar no lo va a mover; escribinos.

Los cuatro estados guardados

credentialStatus, en la lista, es lo que quedó archivado:

EstadoQué significa
absentNo hay credenciales guardadas para ese ambiente
unverifiedHay, pero Hacienda nunca dio un veredicto sobre ellas
acceptedEste contribuyente puede presentar ahora mismo
rejectedHacienda las rechazó

accepted no quiere decir «la contraseña se acaba de comprobar». Quiere decir que se consiguió un token, y ese token puede venir de uno en caché o renovado. Si necesitás fechar el veredicto, la lectura del ambiente —GET /internal/v1/issuers/environments/{environment}/credentials— lo trae con su verifiedAt, o null si todavía no hay ninguno.

Solo rejected es un veredicto en contra. unverified es incertidumbre nuestra —normalmente el proveedor de identidad no respondió—, no una falta del contribuyente, y tratarlo como un rechazo deja sin facturar a alguien cuyas credenciales están bien.

Sucursales y terminales

branchCode y terminalCode no se inventan en la emisión: se registran antes, y el terminal es lo que provisiona la numeración. Sin al menos uno no hay consecutivo, y sin consecutivo no hay documento.

Petición
curl -X POST https://api.timbre.cr/internal/v1/issuers/branches \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{ "code": "001", "name": "Casa matriz" }'

La respuesta trae el id de la sucursal, que es lo que necesita el terminal:

Petición
curl -X POST https://api.timbre.cr/internal/v1/issuers/branches/{branchId}/terminals \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{ "code": "00001", "name": "Caja 1" }'

Podés registrar cuantas quieras: GET /internal/v1/issuers/branches lista las sucursales del contribuyente y GET /internal/v1/issuers/branches/{branchId}/terminals las cajas de una de ellas.

Repetir un código de sucursal en el mismo contribuyente es 409 con duplicate_branch_code, y repetir uno de terminal en la misma sucursal es 409 con duplicate_terminal_code. El mismo código de terminal en otra sucursal sí vale: la numeración es por sucursal y terminal, no por terminal a secas.

Rotar el certificado

Los certificados de Hacienda vencen. Cargá el nuevo y desde ese momento se firma con él; la cédula, el nombre comercial, las sucursales y la numeración no se tocan.

Petición
curl -X PUT https://api.timbre.cr/internal/v1/issuers/certificate \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{ "certificateBase64": "MIIK…", "certificatePassword": "…" }'

El nuevo se valida igual que el primero, así que un .p12 equivocado se rechaza con 400 y el anterior sigue firmando. Los documentos ya firmados no cambian: lo que se presentó quedó firmado con el certificado de ese momento.

Si Timbre no logra leer el .p12 guardado, el perfil vuelve con certificateUnreadable: true y sin metadatos de certificado. Todo lo que ese contribuyente emita va a fallar al firmarse; cargá uno nuevo.

Cambiar de régimen

Entrar o salir del régimen simplificado es un evento normal, no una re-inscripción: no hace falta dar de alta al contribuyente de nuevo.

Petición
curl -X PUT https://api.timbre.cr/internal/v1/issuers/tax-regime \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{ "taxRegime": "simplificado" }'

El cambio queda con su fecha (taxRegimeUpdatedAt), pero no es retroactivo: los documentos ya emitidos conservan el régimen que tenían congelado en su propia procedencia, aunque el contribuyente haya cambiado de régimen después.

Marca

El logo, el color de acento y los datos de contacto que aparecen en la representación en PDF son del contribuyente, no del proyecto.

Petición
curl -X PUT https://api.timbre.cr/internal/v1/issuers/branding \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{
    "logoBase64": "iVBORw0KGgo…",
    "accentColor": "#1B4D3E",
    "contactEmail": "facturas@ferreteriaosa.cr"
  }'

El logo es PNG o JPEG, hasta 512 KB, en base64. Cada campo tiene tres estados: omitilo y queda como estaba, mandalo y lo reemplaza, mandá null y lo borra —así podés corregir un logo equivocado sin escribirle a nadie.

GET /internal/v1/issuers/branding nunca devuelve los bytes del logo, solo logoSha256 y logoContentType: alcanza para saber si hay uno sin descargarlo en cada carga de página.

Desactivar y reactivar

Cuando un cliente deja de facturar con vos, desactivalo: deja de poder emitir y libera su espacio en el tope de la cuenta.

Petición
curl -X PUT https://api.timbre.cr/internal/v1/issuers/{issuerId}/active \
  -H "Authorization: Bearer timbre_sk_…" \
  -H 'Content-Type: application/json' \
  -d '{ "active": false }'

Acá el issuerId va en la ruta y es obligatorio aunque la cuenta tenga uno solo: esto cambia si alguien puede facturar o no, y no se hace sobre un contribuyente adivinado.

No se borra nada. El perfil, las sucursales, las terminales y —sobre todo— la numeración quedan como estaban, porque el consecutivo fiscal no tiene huecos ni marcha atrás: al reactivarlo sigue donde lo dejó, no vuelve a 1.

Un contribuyente desactivado sigue apareciendo en la lista, con active: false. Es la única forma de encontrarlo para traerlo de vuelta, y es lo que hace que la lista cuadre con maxIssuers. Emitir a su nombre responde 404 con issuer_not_found.

Reactivar vuelve a mirar el tope. Si el espacio que este liberó ya lo ocupa otro, la respuesta es 409 con issuer_limit_reached: desactivá uno que ya no uses y volvé a intentar.

Cuándo puede emitir

Tres cosas:

  1. Está activo

    active: true. Vale para toda la cuenta, no por ambiente.

  2. Tiene al menos un terminal

    En cualquiera de sus sucursales. Es lo que provisiona la numeración, y las sucursales y terminales son las mismas en los dos ambientes.

  3. Tiene credenciales que Hacienda no rechazó

    credentialStatus distinto de absent y de rejected en el ambiente desde el que vas a facturar. Es la única de las tres que no se hereda de un ambiente al otro.

Cumplidas las tres, ya podés cotizar y emitir.