# Convenciones de la API --- - [Convenciones de la API](#convenciones) - [URL base y versión](#base) - [Formato de las peticiones](#peticiones) - [Paginación](#paginacion) - [Fechas y horas](#fechas) - [Errores](#errores) - [Metadata](#metadata) - [Montos y monedas](#montos) - [Identificadores](#ids) - [Idempotencia](#idempotencia) - [Límites, tiempos y reintentos](#limites) - [Lo que nunca devolvemos](#nunca) ## URL base y versión [#base] ``` https://sag.efipay.co/api/v1 ``` La versión va en la ruta. Hoy solo existe `v1`; cuando exista otra, `v1` seguirá respondiendo. Dentro de una versión **solo agregamos**: campos nuevos en las respuestas, parámetros opcionales nuevos, valores nuevos en las enumeraciones. Nunca quitamos un campo ni cambiamos el significado de uno existente. :::caution Por eso tu código no debe romperse si aparece un campo que no conocías, ni si una enumeración trae un valor nuevo. Ignora lo que no uses. ::: ## Formato de las peticiones [#peticiones] - Cuerpo en **JSON**, con `Content-Type: application/json`. - Header `Accept: application/json`. Sin él, un error de validación puede llegarte como HTML en lugar de JSON. - Autenticación con `Authorization: Bearer`. Ver [Autenticación](/authentication). - Nombres de campo en **`snake_case`** en lo que envías. La única excepción es [crear una sucursal](/commercio#create) con logo o RUT, que va como `multipart/form-data` porque lleva archivos. :::note En las **respuestas** verás dos estilos: `snake_case` en la mayoría de los endpoints y `camelCase` en algunos de suscripciones. Cada página muestra el suyo; no lo adivines. ::: ## Paginación [#paginacion] Los listados vienen paginados con el sobre estándar de Laravel: ```json { "data": [ "..." ], "links": { "first": "https://sag.efipay.co/api/v1/...?page=1", "last": "https://sag.efipay.co/api/v1/...?page=8", "prev": null, "next": "https://sag.efipay.co/api/v1/...?page=2" }, "meta": { "current_page": 1, "from": 1, "last_page": 8, "per_page": 15, "to": 15, "total": 118 } } ``` | Parámetro | Qué hace | Por defecto | | - | - | - | | page | Página a traer | 1 | | per_page | Cuántos elementos por página | 15 en la mayoría de los endpoints | :::danger Dos excepciones que muerden: - [Listar planes](/plan#get-active-plans) trae **2** por página. Si no envías `per_page` parecerá que tienes dos planes. - [Reservas de cupo](/mit-pre-authorization#consultar) trae 25. ::: Para recorrer todo, sigue `links.next` hasta que sea `null`. No calcules las URLs a mano. ## Fechas y horas [#fechas] | Contexto | Formato | Ejemplo | | - | - | - | | Filtros de fecha que envías | `Y-m-d` | `2026-07-31` | | Vencimiento de tarjeta | `Y-m` | `2030-12` | | Fecha tope de un plan | `Y-m-d H:i:s` | `2026-12-31 23:59:59` | | Fechas que devolvemos (`created_at`, `approved_at`…) | ISO 8601 en UTC | `2026-07-31T14:32:10.000000Z` | | Fechas en los webhooks | Hora de Colombia | `2026-07-31 09:32:10` | | Fechas legadas de suscripciones (`ends_at`, `canceled_at`…) | `Y-m-d H:i:s`, hora de Colombia | `2026-10-28 11:28:00` | | Día de cobro de suscripciones (`next_transaction`, `next_renew_at`) | `Y-m-d`, día en Colombia | `2026-10-28` | | Instantes nuevos de suscripciones (`ends_at_iso`, `next_charge_at`, `charge_scheduled_at`, `cancel_at`) | ISO 8601 con offset explícito | `2026-10-28T19:00:00-05:00` | :::caution La zona horaria de la operación es **America/Bogotá** (UTC−5). Un cobro de las 8 p.m. del 31 de julio en Colombia es `2026-08-01T01:00:00Z` en UTC: si conviertes mal, te cambia de mes en la conciliación. ::: :::note En [suscripciones](/subscription#fechas) los campos legados conservan su formato para no romper integraciones, y junto a ellos hay campos ISO 8601 **con offset explícito** (`-05:00`). Si vas a comparar instantes, usa los ISO: no tienes que adivinar la zona. ::: ## Errores [#errores] Todo error de la API v1 (`4xx`) trae, además de `message` (y `errors` en un `422`), un objeto `error` con la misma forma: ```json { "message": "Este cobro ya tiene una transacción y no permite reintentos", "error": { "type": "invalid_request_error", "code": "payment_already_used", "message": "Este cobro ya tiene una transacción y no permite reintentos", "param": "payment.id" } } ``` Decide con **`error.code`**, que es estable; el texto de `message` puede cambiar. El catálogo de tipos y códigos está en [Códigos de error](/error-codes#sobre). ## Metadata [#metadata] [Suscripciones](/subscription#metadata), [suscriptores](/subscriptor#metadata), [planes](/plan#metadata) y [cobros generados](/generate-transaction#metadata) aceptan un objeto `metadata` de pares clave-valor para guardar tus propias referencias, al estilo de Stripe: hasta 50 claves de 1 a 40 caracteres (`[A-Za-z0-9_-]`) y valores de texto o número de hasta 500 caracteres, que se guardan como texto. Te lo devolvemos en las consultas y en los webhooks. ## Montos y monedas [#montos] Los montos van como **número decimal en la unidad principal de la moneda**, no en centavos: `120000` son ciento veinte mil pesos, y `1200.50` son mil doscientos pesos con cincuenta centavos. | Moneda | Código | Notas | | - | - | - | | Peso colombiano | `COP` | La moneda de operación. Máximo 999.999.999.999 | | Dólar | `USD` | Se convierte a COP con la TRM del día. Máximo 200.000.000 | | Euro | `EUR` | Igual que USD | En una transacción en moneda extranjera verás `amount` (moneda original), `value_cop` (el equivalente en pesos) y `currency_rate_conversion` con la tasa aplicada. **Concilia con `value_cop`**: es lo que efectivamente se movió. :::note Las [reservas de cupo](/mit-pre-authorization) solo operan en COP. ::: ## Identificadores [#ids] | Tipo | Aspecto | Dónde | | - | - | - | | UUID | `9b0f2a13-2c44-4a1a-9f2e-1d3b4c5d6e7f` | Cobros, transacciones, planes, suscriptores, cupones, reservas, eventos de webhook (`meta.event_id`) | | Consecutivo | `20481` | `transaction_id`, el número que ve tu operador | | Entero | `1` | Sucursales | Una transacción tiene **los dos**: `id` (UUID) y `transaction_id` (consecutivo). Los endpoints de consulta aceptan cualquiera de los dos; guarda el que le vayas a mostrar a una persona. :::caution Los ids no se comparten entre ambientes. Un id de prueba da `404` con token de producción, y al revés. ::: ## Idempotencia [#idempotencia] Las escrituras de **suscripciones**, **pagos** y **reservas de cupo** aceptan el header `Idempotency-Key`. Si repites la misma llamada con la misma clave y los mismos parámetros, te devolvemos la respuesta original con el header `Idempotency-Replayed: true`, **sin volver a ejecutar el cobro**. | Rutas que la aceptan | | | - | - | | `/api/v1/subscriptions/*` | Crear, cancelar, cambiar de plan, actualizar la tarjeta, pausar, reanudar | | `/api/v1/payment/generate-payment` | Generar el cobro | | `/api/v1/payment/transaction-checkout/*` | Checkout por API: tarjeta, efectivo, PSE, Bre-B | | `/api/v1/payment/refund-transaction` | Devoluciones | | `/api/v1/mit/pre-authorizations/*` | Crear, autorizar, cobrar y liberar reservas | :::note **Las consultas no la aceptan**, aunque vayan por `POST`: `transaction-status`, `transaction/{id}`, `all-transaction-status` y el `sync` de una reserva. Ahí la clave no tendría sentido: se pide justamente el estado más reciente. Los pasos de 3DS tampoco, porque su resultado cambia entre llamadas. ::: ```bash curl -X POST \ '/api/v1/subscriptions/subscription' \ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H 'Content-type: application/json' \ -H 'Idempotency-Key: 9b1f0e34-6b2a-4a7e-9d0a-uno-por-operacion' \ -d '{ "...": "..." }' ``` - Usa una clave **distinta por operación** (un UUID sirve). No reutilices la misma clave para dos cobros diferentes. - La clave vale **24 horas** y su alcance es tu comercio y el usuario del token. - La misma clave con parámetros distintos responde `409`. - La misma clave mientras la primera petición sigue en curso responde `409`. - Los errores `5xx` no se memorizan, para que puedas reintentar un fallo transitorio. Detalle en [Suscripciones](/subscription#idempotencia). ## Límites, tiempos y reintentos [#limites] ### Límite de peticiones **No publicamos hoy un límite de peticiones por minuto.** Lo que sí existe es un candado de concurrencia por cobro: no se procesan dos pagos del mismo cobro a la vez. | Situación | Respuesta | Ventana | | - | - | - | | Otro pago del mismo cobro en curso (tarjeta, PSE, Bre-B) | `429`, `error.code: payment_in_progress` | 10 segundos | | Otro pago en efectivo del mismo cobro en curso | `429`, `error.code: payment_in_progress` | 5 segundos | | Otra operación con la misma `Idempotency-Key` en curso | `409` | Hasta que la primera termine | :::caution Un `429` **no significa que hayas excedido una cuota**: significa que esa misma operación ya se está procesando. Espera el resultado y consúltalo; no la reenvíes. ::: ### Tiempos de respuesta | Operación | Espera razonable | Nuestro tope contra la red | | - | - | - | | Catálogos y consultas | Menos de 1 s | — | | Pago con tarjeta (sin 3DS) | 3 a 10 s | 45 s | | Pago con tarjeta (con 3DS) | Depende del banco y del cliente | 45 s por llamada | | Reserva de cupo | 3 a 10 s | 45 s | Pon en tu cliente un timeout **mayor** a 45 segundos para las operaciones de cobro. Si cortas antes, no cancelas nada: la operación sigue su curso en la red y te quedas sin saber cómo terminó. ### Qué es seguro reintentar | Respuesta | ¿Reintentar? | | - | - | | `5xx`, `error.retryable: true` | Sí, con espera creciente | | `error.retryable: false` (fondos, tarjeta vencida, fraude) | No. El resultado será el mismo | | `202`, `T01`, o un timeout de tu lado | **No a ciegas.** Estado indeterminado: consulta primero | | `409` de idempotencia | No: ya hay una operación con esa clave | | `422` de validación | Solo después de corregir la petición | :::danger La regla que evita cobros dobles: **ante la duda, consulta antes de reenviar**. Con [estado de transacción](/status-transaction) o con la misma `Idempotency-Key`, que te devuelve la respuesta original en vez de cobrar de nuevo. ::: ## Lo que nunca devolvemos [#nunca] - **El número completo de una tarjeta.** Siempre enmascarado: `491617******1313`. - **El CVV.** No se guarda. - **Tokens en claro.** El token de un cobro en modalidad `api` y el token de una tarjeta se devuelven **una sola vez**, al crearse. De ahí en adelante solo guardamos su hash. :::danger Si necesitas volver a mostrar «con qué tarjeta pagó», usa el BIN y los últimos cuatro dígitos que ya te devolvemos. No hay forma de recuperar el número completo, y eso es deliberado. :::