Manual de Integración — Partner API
Guía para empresas externas que integran sus propios sistemas con IPSmart, máquina-a-máquina, mediante una API REST autenticada. Conserve sus sistemas y sincronícelos con IPSmart.
1Introducción
La API de Integración de IPSmart permite que una empresa externa —un ERP, una pasarela de pagos, un integrador— lea y escriba datos en IPSmart desde sus propios sistemas, sin abandonarlos.
Es una integración máquina-a-máquina: no hay sesión humana ni navegación por la interfaz de IPSmart. Su servidor habla directamente con el nuestro mediante peticiones HTTPS autenticadas.
¿Para quién es? Para empresas que conservan sus sistemas actuales y necesitan sincronizarlos con IPSmart: consultar clientes, suscripciones, facturas, pagos y planes; o dar de alta y actualizar clientes de forma programática.
- REST sobre HTTPS, JSON en peticiones y respuestas.
- Autenticación por API key (
ipk_…) o por JWT OAuth 2.0 client-credentials. - Autorización por scopes (
recurso:acción): la key solo hace lo que se le otorgó. - Aislamiento por tenant duro: cada key pertenece a un único tenant y jamás cruza a otro.
- PII enmascarada por defecto; verla en claro requiere un scope específico.
- Lecturas y escrituras: clientes, pagos y suscripciones; consulta de facturas y planes.
- Webhooks salientes firmados (HMAC-SHA256): IPSmart notifica a su sistema los eventos de dominio.
- Rate limiting por credencial, idempotencia en escrituras y auditoría de cada llamada.
Base URL y versionado
| Entorno | Base URL |
|---|---|
| Producción | https://api.ipsmart.app/api/v1/partner |
| Local / pruebas | http://localhost:3009/api/v1/partner |
El prefijo de versión (/api/v1/) es estable. Un cambio incompatible se publicará bajo /api/v2/… sin romper a los partners existentes. Documentación interactiva (Swagger): GET /api/v1/partner/docs.
2Primeros pasos
2.1 Obtener una API key
La API key la emite el administrador de IPSmart para su empresa. Usted no la genera: la solicita indicando el uso previsto y los scopes que necesita. Al emitirla, IPSmart le entrega:
- El secreto en claro (
ipk_…) — se muestra una sola vez. - El prefijo público (
ipk_<prefijo>) — identifica la key en paneles y logs. - Los scopes concedidos y el límite de tasa (
rate_limit_per_min).
2.2 Formato del secreto
ipk_<prefijo8>_<aleatorio48hex>
Ejemplo (placeholder, no real):
ipk_a1b2c3d4_0f1e2d3c4b5a69788796a5b4c3d2e1f00112233445566778
ipk— prefijo fijo del producto (IPSmart Partner Key).<prefijo8>— 8 caracteres hex públicos que identifican la key.<aleatorio48hex>— 48 caracteres hex secretos.
En la base de datos de IPSmart solo se guardan el prefijo público y el hash SHA-256 del secreto completo. El secreto en claro nunca se persiste ni se registra.
2.3 Guardar el secreto
2.4 Primera llamada
curl -H "Authorization: Bearer ipk_ejemplo_0f1e2d3c..." \
"https://api.ipsmart.app/api/v1/partner/planes?page=0&size=10"
3Autenticación
Toda petición debe llevar la API key en una de estas dos cabeceras (equivalentes).
Authorization: Bearer ipk_<prefijo>_<secreto>
# — o bien —
X-API-Key: ipk_<prefijo>_<secreto>
- Sin key válida → 401 (
unauthorized). - Key inactiva (
suspended/revoked), expirada o inexistente → 401. - Key válida pero sin el scope del endpoint → 403 (
forbidden).
La API resuelve la key a un contexto de partner (tenant + scopes + límite de tasa) que gobierna toda la petición. El tenant siempre proviene de la key: usted no puede indicarlo ni cambiarlo.
OAuth 2.0 client-credentials
Como alternativa a enviar la API key en cada llamada, la API ofrece el grant estándar OAuth 2.0 client-credentials: su servidor intercambia la key (ipk_…) por un JWT de vida corta con los mismos scopes de la key, y lo usa como Authorization: Bearer <jwt> en el resto de la API. El endpoint del token es público (las credenciales viajan en el cuerpo).
curl -X POST "https://api.ipsmart.app/api/v1/partner/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "ipk_ejemplo",
"client_secret": "ipk_ejemplo_0f1e2d3c..."
}'
grant_type— obligatorio, debe serclient_credentials.client_secret— obligatorio, el secreto completo de la key.client_id— opcional; si lo envía, debe ser el prefijo público de esa key o recibirá 401.
Respuesta 200
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "clientes:read facturas:read"
}
expires_in: vida del token en segundos (por defecto 3600). scope: heredado de la key (no se puede ampliar en la petición). Uso del token:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
"https://api.ipsmart.app/api/v1/partner/planes?page=0&size=10"
4Scopes (permisos)
Un scope tiene el formato recurso:acción. La key solo ejecuta un endpoint si tiene todos los scopes que exige (semántica AND). Si falta alguno → 403.
Scopes disponibles (implementados)
| Scope | Habilita |
|---|---|
| clientes:read | Leer/listar clientes (PII enmascarada). |
| clientes:write | Crear / actualizar clientes. |
| clientes:pii | Ver PII (cédula/teléfono/email) sin enmascarar. |
| suscripciones:read | Listar suscripciones. |
| suscripciones:write | Crear suscripciones y cancelarlas. |
| facturas:read | Listar facturas. |
| pagos:read | Listar pagos. |
| pagos:write | Registrar pagos. |
| planes:read | Listar planes/productos del catálogo. |
| webhooks:manage | Registrar y administrar webhooks salientes. |
clientes:pii es aditivo: se combina con clientes:read/clientes:write para recibir la PII en claro en esas mismas respuestas. Por sí solo no da acceso a ningún endpoint.
5Convenciones transversales
5.1 Paginación
| Parámetro | Tipo | Por defecto | Rango |
|---|---|---|---|
page | entero (base 0) | 0 | ≥ 0 |
size | entero | 25 | 1..100 (se recorta) |
Respuesta de todo listado:
{ "data": [ /* ... */ ], "page": 0, "size": 25, "total": 143860 }
5.2 Idempotency-Key (escrituras)
Para operaciones de escritura (POST), envíe la cabecera Idempotency-Key: <valor-único>.
- Misma clave + mismo cuerpo → devuelve la respuesta original almacenada, sin re-ejecutar el efecto.
- Misma clave + cuerpo distinto → 409 (
conflict). - Sin registro previo → ejecuta y almacena la respuesta.
- TTL 24 horas: pasado ese tiempo, un reintento con la misma clave vuelve a ejecutar.
La clave es única por credencial. Se recomienda usar un UUID por operación de negocio.
5.3 Envelope de error y correlación
{
"error": {
"code": "forbidden",
"message": "La API key no tiene el/los scope(s) requerido(s): clientes:write.",
"details": [ "..." ],
"requestId": "c1a2b3d4-e5f6-7890-abcd-ef0123456789"
}
}
Además, toda respuesta incluye la cabecera X-Request-Id. Cite ese requestId al reportar un problema: cada llamada queda auditada con ese identificador.
5.4 Rate limiting
El límite es por credencial (no por IP: su empresa puede llamar desde varios servidores) y usa el rate_limit_per_min propio de su key, sobre una ventana de 60 segundos.
- Excedido el límite → 429 (
rate_limited). - Incluye la cabecera
Retry-After(segundos) y las cabeceras de cuotaX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset.
Aplique backoff exponencial ante un 429 y respete Retry-After.
5.5 Enmascarado de PII
Por defecto, la API enmascara la información personal en las respuestas de clientes:
- Documentos y teléfonos: se ocultan salvo los últimos 2 dígitos (p. ej.
*******23,*********95). - Correos: se conserva la inicial y el dominio (p. ej.
j****@dominio.com).
Campos afectados: doc, document_number, tel, telefono, phone, email, value. Para recibirlos en claro, la key debe tener el scope clientes:pii. El enmascarado es no destructivo: los datos siguen íntegros en IPSmart.
5.6 Aislamiento por tenant
Cada key pertenece a un único tenant (operadora/ISP). Todas las lecturas y escrituras quedan acotadas a ese tenant; no es posible indicar otro tenantId ni cruzar tenants. Si el cuerpo de una creación incluye tenantId, se ignora: prevalece el de la key.
5.7 Formato de dinero
Los importes son numéricos de alta precisión y siempre van acompañados de su moneda (currency_code, ISO 4217). Nunca interprete un importe sin su moneda.
6Referencia de endpoints
Base: https://api.ipsmart.app/api/v1/partner (producción) o http://localhost:3009/api/v1/partner (local). Los ejemplos usan ipk_ejemplo_… como placeholder — sustitúyalo por su secreto real (o por su JWT OAuth).
| Método | Ruta | Scope | Descripción |
|---|---|---|---|
| POST | /oauth/token | público | Intercambia la key por un JWT de vida corta. |
| GET | /clientes | clientes:read | Lista/busca clientes. |
| GET | /clientes/:id | clientes:read | Ficha de un cliente. |
| POST | /clientes | clientes:write | Crea cliente (dedup, idempotente). |
| PATCH | /clientes/:id | clientes:write | Actualiza un cliente. |
| GET | /suscripciones | suscripciones:read | Lista suscripciones. |
| POST | /suscripciones | suscripciones:write | Crea una suscripción (idempotente). |
| PATCH | /suscripciones/:id | suscripciones:write | Actualiza (cancelación). |
| GET | /facturas | facturas:read | Lista facturas. |
| GET | /pagos | pagos:read | Lista pagos. |
| POST | /pagos | pagos:write | Registra un pago (idempotente). |
| GET | /planes | planes:read | Lista planes/productos. |
| POST | /webhooks | webhooks:manage | Registra un webhook. |
| GET | /webhooks | webhooks:manage | Lista los webhooks del tenant. |
| PATCH | /webhooks/:id | webhooks:manage | Pausa/reactiva o cambia eventos. |
| DELETE | /webhooks/:id | webhooks:manage | Da de baja un webhook. |
6.1 Clientes
Lista/busca clientes del tenant (paginado, filtros).
| Query | Tipo | Descripción |
|---|---|---|
q | string | Texto libre o número de documento. |
tipo | enum | natural | juridico. |
estado | string | active | inactive | suspended. |
page, size | int | Paginación (base 0; size 1..100). |
curl -H "Authorization: Bearer ipk_ejemplo_..." \
"https://api.ipsmart.app/api/v1/partner/clientes?q=perez&tipo=natural&page=0&size=25"
Respuesta 200 (PII enmascarada — key sin clientes:pii)
{
"data": [
{
"id": "0192f6a1-7b3c-7def-9012-3456789abcde",
"code": "CUST-000123",
"nombre": "Juan Pérez",
"party_type": "PERSON",
"doc": "*******78",
"document_type": "cedula",
"franquicia": "Fibex Telecom",
"tel": "*********67"
}
],
"page": 0, "size": 25, "total": 143860
}
Con el scope clientes:pii, doc y tel llegan en claro (V12345678, 04141234567).
Ficha del cliente (:id = party.id). 404 si no existe en el tenant de la key.
curl -H "Authorization: Bearer ipk_ejemplo_..." \
"https://api.ipsmart.app/api/v1/partner/clientes/0192f6a1-7b3c-7def-9012-3456789abcde"
Respuesta 200 (extracto; PII enmascarada salvo clientes:pii)
{
"id": "0192f6a1-7b3c-7def-9012-3456789abcde",
"nombre": "Juan Pérez",
"party_type": "PERSON",
"franquicia": "Fibex Telecom",
"code": "CUST-000123",
"estado": "active",
"doc": "*******78",
"document_type": "cedula",
"first_name": "Juan",
"last_name": "Pérez",
"saldo": 0,
"nro_contrato": "CT-0099",
"contactos": [ { "tipo": "mobile", "valor": "*********67" } ]
}
Crea un cliente. Dedup por hash de documento: si ya existe uno con ese documento en el tenant → 400. Soporta Idempotency-Key. El tenantId lo fija la key (si viene en el cuerpo, se ignora).
El cuerpo acepta el contrato rico (recomendado) o el plano legacy. Mínimo para persona natural:
curl -X POST "https://api.ipsmart.app/api/v1/partner/clientes" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f9619ff-8b86-d011-b42d-00cf4fc964ff" \
-d '{
"partyType": "PERSON",
"firstName": "Juan",
"lastName": "Pérez",
"documents": [
{ "documentType": "cedula", "documentNumber": "V12345678", "issuingCountry": "VE" }
],
"contacts": [
{ "contactType": "mobile", "value": "04141234567", "isPrimary": true }
]
}'
Respuesta 201
{ "partyId": "0192f6a1-7b3c-7def-9012-3456789abcde" }
Campos del cuerpo (contrato rico, los más usados)
| Campo | Tipo | Notas |
|---|---|---|
partyType | enum | PERSON | ORGANIZATION. |
firstName, lastName | string | Persona natural. |
legalName, tradeName | string | Persona jurídica (organization). |
status | enum | active | inactive | suspended. |
documents[] | array | { documentType, documentNumber, issuingCountry, issuedAt?, expiresAt? } |
contacts[] | array | { contactType (phone|mobile|whatsapp|email|social), value, isPrimary? } |
addresses[] | array | { countryCode, cityId, line1, addressType (fiscal|service|billing|shipping|home), … } |
profile | objeto | { segment (residential|sme|corporate|government|vip), acquisitionChannel? } |
Persona jurídica: partyType: "ORGANIZATION" con organization: { legalName, tradeName? } y documentType: "rif" (u otro) en documents[].
Actualiza un cliente del tenant. Cualquier sección provista se actualiza; las colecciones ricas (documents, contacts, addresses) son reemplazo total (soft-delete + insert).
curl -X PATCH "https://api.ipsmart.app/api/v1/partner/clientes/0192f6a1-...-abcde" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-d '{
"displayName": "Juan A. Pérez",
"status": "active",
"contacts": [
{ "contactType": "email", "value": "juan.perez@dominio.com", "isPrimary": true }
]
}'
Respuesta 200
{ "partyId": "0192f6a1-7b3c-7def-9012-3456789abcde" }
6.2 Suscripciones
Lista las suscripciones del tenant (paginado). Query: page, size.
curl -H "Authorization: Bearer ipk_ejemplo_..." \
"https://api.ipsmart.app/api/v1/partner/suscripciones?page=0&size=25"
{
"data": [
{
"id": "0192f6b0-1111-7abc-8def-000000000001",
"contract_id": "0192f6b0-2222-...",
"product_id": "0192f6b0-3333-...",
"service_address_id": "0192f6b0-4444-...",
"zone_id": "0192f6b0-5555-...",
"status": "active",
"activated_at": "2026-01-15T14:30:00.000Z",
"terminated_at": null,
"billing_day": 15,
"created_at": "2026-01-15T14:00:00.000Z",
"updated_at": "2026-01-15T14:30:00.000Z"
}
],
"page": 0, "size": 25, "total": 5210
}
Suscribe a un cliente (partyId) a un plan/producto de internet (productId). Soporta Idempotency-Key; el tenantId lo fija la key. Al crearse, dispara el webhook suscripcion.created.
| Campo | Tipo | Notas |
|---|---|---|
partyId | uuid | Obligatorio. Cliente del tenant. |
productId | uuid | Obligatorio. Plan/producto del catálogo. |
startDate | YYYY-MM-DD | Opcional. Si no viene, se activa hoy. |
curl -X POST "https://api.ipsmart.app/api/v1/partner/suscripciones" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" \
-d '{
"partyId": "0192f6a1-7b3c-7def-9012-3456789abcde",
"productId": "0192f6e0-1111-7abc-8def-000000000030",
"startDate": "2026-03-01"
}'
Respuesta 201 (extracto)
{
"id": "0192f6b0-1111-7abc-8def-000000000001",
"status": "active",
"activated_at": "2026-03-01T00:00:00.000Z"
}
Actualiza una suscripción del tenant. Único estado soportado hoy: cancelled → termina la suscripción. 404 si no existe en el tenant de la key.
curl -X PATCH "https://api.ipsmart.app/api/v1/partner/suscripciones/0192f6b0-1111-...-000000000001" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-d '{ "status": "cancelled" }'
6.3 Facturas
Lista las facturas del tenant (paginado). Query: page, size.
{
"data": [
{
"id": "0192f6c0-1111-7abc-8def-000000000010",
"subscription_id": "0192f6b0-1111-...",
"invoice_number": "FAC-2026-000123",
"issued_at": "2026-02-01T00:00:00.000Z",
"due_date": "2026-02-15",
"currency_code": "USD",
"subtotal_amount": "25.000000",
"tax_amount": "4.000000",
"total_amount": "29.000000",
"balance_amount": "0.000000",
"status": "paid",
"created_at": "2026-02-01T00:05:00.000Z",
"updated_at": "2026-02-03T10:00:00.000Z"
}
],
"page": 0, "size": 25, "total": 98432
}
Importes en numeric(20,6) con su currency_code (ISO 4217).
6.4 Pagos
Lista los pagos del tenant (paginado). Query: page, size.
{
"data": [
{
"id": "0192f6d0-1111-7abc-8def-000000000020",
"payment_method_id": "0192f6d0-2222-...",
"amount": "29.000000",
"currency_code": "USD",
"fx_rate_value": "36.500000",
"reference": "TRX-0099887766",
"received_at": "2026-02-03T09:58:00.000Z",
"gateway_transaction_id": "gw_abc123",
"status": "confirmed",
"created_at": "2026-02-03T09:59:00.000Z",
"updated_at": "2026-02-03T10:00:00.000Z"
}
],
"page": 0, "size": 25, "total": 76211
}
Registra un pago del cliente partyId. Si incluye invoiceId, el pago se aplica contra esa factura de forma transaccional. Soporta Idempotency-Key; el tenantId lo fija la key. Al registrarse, dispara el webhook pago.recibido.
| Campo | Tipo | Notas |
|---|---|---|
partyId | uuid | Obligatorio. Cliente que paga. |
monto | number | Obligatorio. Importe (hasta 2 decimales, positivo). |
invoiceId | uuid | Opcional. Si viene, el pago se aplica a esa factura. |
metodoCode | string | Opcional. TRANSFER/PAGOMOVIL/EFECTIVO… (por defecto TRANSFER). |
referencia | string | Opcional. Referencia de la transacción. |
fecha | ISO 8601 | Opcional. Momento de recepción; si no viene, el del registro. |
curl -X POST "https://api.ipsmart.app/api/v1/partner/pagos" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9f8e7d6c-5b4a-3210-fedc-ba9876543210" \
-d '{
"partyId": "0192f6a1-7b3c-7def-9012-3456789abcde",
"invoiceId": "0192f6c0-1111-7abc-8def-000000000010",
"monto": 29.00,
"metodoCode": "TRANSFER",
"referencia": "TRX-0099887766"
}'
Respuesta 201 (extracto)
{
"id": "0192f6d0-1111-7abc-8def-000000000020",
"amount": "29.000000",
"currency_code": "USD",
"status": "confirmed"
}
6.5 Planes
Lista los planes/productos del catálogo del tenant (paginado). Query: page, size.
{
"data": [
{
"id": "0192f6e0-1111-7abc-8def-000000000030",
"category_id": "0192f6e0-2222-...",
"sku": "FIBRA-100",
"name": "Fibra 100 Mbps",
"description": "Plan residencial de fibra óptica simétrica.",
"product_type": "service",
"is_recurring": true,
"status": "active",
"created_at": "2025-11-01T00:00:00.000Z",
"updated_at": "2025-11-01T00:00:00.000Z"
}
],
"page": 0, "size": 25, "total": 42
}
/api/admin/partner-keys (JWT + permiso security.admin), el administrador de IPSmart emite (POST, devuelve el secreto una vez), lista (GET), rota (POST /:id/rotate — emite una sucesora con gracia configurable, por defecto 24 h), revoca (POST /:id/revoke — baja inmediata, incluidos los JWT OAuth de esa key) y actualiza (PATCH /:id — scopes, rate limit, estado). No forman parte de la API del partner ni de su Swagger. Desde su óptica: al rotar su key recibe el secreto nuevo y un plazo de gracia sobre el anterior — migre antes de que venza.7Webhooks
Los webhooks salientes permiten que IPSmart notifique a su sistema cuando ocurre un evento de dominio, sin que usted tenga que sondear la API. IPSmart hace un POST firmado (HMAC-SHA256) a la URL que registre, con reintentos y bitácora. Toda la gestión requiere el scope webhooks:manage y queda acotada al tenant de su key.
7.1 Registrar un webhook — POST /webhooks
Devuelve el signingSecret en claro una sola vez (guárdelo: sirve para verificar la firma).
| Campo | Tipo | Notas |
|---|---|---|
targetUrl | string | Obligatorio. URL http(s) de su receptor. |
eventTypes | string[] | Obligatorio, no vacío. Eventos a suscribir (§7.2). |
description | string | Opcional. Nota libre. |
curl -X POST "https://api.ipsmart.app/api/v1/partner/webhooks" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-d '{
"targetUrl": "https://su-sistema.com/hooks/ipsmart",
"eventTypes": ["cliente.created", "pago.recibido"],
"description": "sincroniza con ERP Contoso"
}'
Respuesta 201
{
"id": "0192f7a0-1111-7abc-8def-000000000040",
"targetUrl": "https://su-sistema.com/hooks/ipsmart",
"eventTypes": ["cliente.created", "pago.recibido"],
"status": "active",
"secretLast4": "9f3c",
"consecutiveFailures": 0,
"createdAt": "2026-03-01T12:00:00.000Z",
"signingSecret": "whsec_2b7c...e1a9"
}
signingSecret (prefijo whsec_) solo aparece aquí. IPSmart lo guarda cifrado y no lo vuelve a mostrar; secretLast4 le permite identificar cuál tiene. Si lo pierde, borre el webhook y regístrelo de nuevo.7.1.1 Gestión — listar, actualizar, borrar
GET /webhooks— lista los webhooks del tenant (sin el secreto).PATCH /webhooks/:id— actualiza{ status?, eventTypes?, description? }.status:active|paused|disabled.DELETE /webhooks/:id— da de baja el webhook (devuelve{ "deleted": true }).
# Pausar un webhook temporalmente
curl -X PATCH "https://api.ipsmart.app/api/v1/partner/webhooks/0192f7a0-1111-...-000000000040" \
-H "Authorization: Bearer ipk_ejemplo_..." \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'
7.2 Tipos de evento
| Evento | Se dispara cuando |
|---|---|
cliente.created | Se crea un cliente. |
cliente.updated | Se actualiza un cliente. |
pago.recibido | Se registra un pago. |
factura.emitida | Se emite una factura. |
suscripcion.created | Se crea una suscripción. |
7.3 Formato de entrega
IPSmart hace POST application/json al targetUrl. La firma va en hex, sin prefijo sha256=. Cabeceras y cuerpo (el cuerpo son exactamente los bytes que se firman):
POST /hooks/ipsmart HTTP/1.1
Content-Type: application/json
X-IPSmart-Signature: 5b2e...c9a1 # HMAC-SHA256 hex, SIN prefijo
X-IPSmart-Event: pago.recibido
X-IPSmart-Delivery: 0192f7b0-2222-7abc-8def-000000000050
X-IPSmart-Timestamp: 1772539140000
{
"id": "0192f6d0-1111-7abc-8def-000000000020",
"type": "pago.recibido",
"tenantId": "0192f5a0-0000-7abc-8def-000000000000",
"occurredAt": "2026-02-03T09:59:00.000Z",
"data": { "id": "0192f6d0-1111-...", "amount": "29.000000", "currency_code": "USD" }
}
id— id del evento; correlaciona con el recurso origen. Úselo para descartar duplicados.type— tipo de evento (§7.2).tenantId— el de su key.occurredAt— ISO 8601.data— el recurso afectado (misma forma que la API de lectura correspondiente).X-IPSmart-Delivery— id único de esta entrega;X-IPSmart-Timestamp— ms epoch del intento.
7.4 Verificación de la firma HMAC-SHA256
La firma es HMAC-SHA256(signingSecret, cuerpo_crudo) en hex, enviada tal cual en X-IPSmart-Signature (sin el prefijo sha256=). Verifíquela sobre el cuerpo crudo (bytes exactos recibidos, no un JSON re-serializado), antes de parsear, con comparación de tiempo constante.
Node.js
const crypto = require('crypto');
function verificarFirmaIPSmart(rawBody, signatureHeader, signingSecret) {
// rawBody: Buffer/string EXACTO recibido (no re-serializado).
const esperado = crypto
.createHmac('sha256', signingSecret)
.update(rawBody)
.digest('hex');
const recibido = String(signatureHeader || ''); // hex puro, sin prefijo
const a = Buffer.from(esperado, 'hex');
const b = Buffer.from(recibido, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: capture el cuerpo crudo con express.raw().
app.post('/hooks/ipsmart', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verificarFirmaIPSmart(
req.body, // Buffer crudo
req.header('X-IPSmart-Signature'),
process.env.IPSMART_SIGNING_SECRET, // el whsec_... de este webhook
);
if (!ok) return res.status(401).send('firma inválida');
const evento = JSON.parse(req.body.toString('utf8'));
// ... procese el evento ...
res.status(200).send('ok');
});
PHP
<?php
function verificar_firma_ipsmart(string $rawBody, string $signatureHeader, string $signingSecret): bool {
$esperado = hash_hmac('sha256', $rawBody, $signingSecret); // hex
return hash_equals($esperado, $signatureHeader); // sin prefijo, tiempo constante
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_IPSMART_SIGNATURE'] ?? '';
$secret = getenv('IPSMART_SIGNING_SECRET');
if (!verificar_firma_ipsmart($rawBody, $signature, $secret)) {
http_response_code(401); exit('firma inválida');
}
$evento = json_decode($rawBody, true);
// ... procese el evento ...
http_response_code(200); echo 'ok';
Python
import hashlib, hmac
def verificar_firma_ipsmart(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
esperado = hmac.new(signing_secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, signature_header or "") # hex puro, tiempo constante
7.5 Reintentos y entrega
- Reintentos: hasta 3 intentos por entrega, con backoff inmediato → 1 s → 5 s. Cada
POSTtiene timeout de 8 s. Solo cuenta como entregado una respuesta 2xx. - Agotamiento: si los 3 intentos fallan, la entrega queda como
exhaustedy se registra en la bitácora de IPSmart (código HTTP + hash del payload, sin PII). - Salud del webhook: IPSmart lleva
consecutiveFailures,lastSuccessAtylastFailureAtpor webhook (visibles enGET /webhooks). - Handler idempotente: descarte duplicados por el
iddel evento y responda2xxen cuanto reciba (procese en segundo plano). - Los fallos de entrega no afectan la respuesta de la API a la operación que originó el evento (despacho fire-and-forget).
8Códigos de error
Todo error usa el envelope { error: { code, message, details?, requestId } }.
| HTTP | code | Significado | Causa típica |
|---|---|---|---|
| 400 | bad_request | Petición inválida. | Documento duplicado (dedup), cuerpo malformado. |
| 401 | unauthorized | No autenticado. | Falta la API key, o es inválida/inactiva/expirada. |
| 403 | forbidden | Sin permiso. | La key no tiene el scope requerido. |
| 404 | not_found | No encontrado. | El recurso no existe en el tenant de la key. |
| 409 | conflict | Conflicto. | Idempotency-Key reusada con cuerpo distinto. |
| 422 | unprocessable_entity | No procesable. | Validación semántica fallida. |
| 429 | rate_limited | Límite de tasa excedido. | Más peticiones que rate_limit_per_min. |
| 500 | internal_error | Error interno. | Fallo del servidor (sin exponer detalles). |
Ejemplo (401 — sin key)
{
"error": {
"code": "unauthorized",
"message": "Falta la API key (Authorization: Bearer ipk_... o X-API-Key).",
"requestId": "c1a2b3d4-e5f6-7890-abcd-ef0123456789"
}
}
Ejemplo (403 — falta scope)
{
"error": {
"code": "forbidden",
"message": "La API key no tiene el/los scope(s) requerido(s): facturas:read.",
"requestId": "d2b3c4e5-f6a7-8901-bcde-f01234567890"
}
}
9Buenas prácticas de seguridad
- TLS siempre. Llame únicamente por HTTPS. Nunca envíe la key por canales sin cifrar.
- No exponga el secreto. Guárdelo en un gestor de secretos o variables de entorno; nunca en código cliente, repositorios, logs ni URLs.
- Rote las keys periódicamente. Solicite una nueva, migre su sistema y pida revocar la anterior. Use
expires_atcuando aplique. - Mínimo privilegio. Pida solo los scopes que necesita.
clientes:piisolo si su caso de uso realmente requiere PII en claro. - Idempotencia en escrituras. Envíe siempre
Idempotency-KeyenPOST. - Maneje 429 con backoff. Respete
Retry-Aftery aplique backoff exponencial. - Valide la firma de los webhooks sobre el cuerpo crudo, con comparación de tiempo constante; descarte duplicados por
idde evento. - Prefiera OAuth para servidores de terceros: un JWT de vida corta limita la exposición frente a enviar la API key completa en cada llamada.
- Correlación. Registre el
X-Request-Idde cada llamada; facilita soporte y auditoría. - Ante sospecha de filtración de una key, solicite su revocación inmediata al administrador de IPSmart.
10Changelog y versionado
- Versionado por ruta. El contrato vive bajo
/api/v1/partner. Cambios retrocompatibles (campos opcionales, nuevos endpoints) dentro dev1; cambios incompatibles bajo/api/v2/…, manteniendov1en transición. - Recomendación: ignore campos desconocidos en las respuestas (tolerancia hacia adelante).
| Versión | Estado | Contenido |
|---|---|---|
| v1 · Fase 1 | Disponible | API key (ipk_…), scopes, aislamiento por tenant, rate limiting por credencial, idempotencia, auditoría, enmascarado de PII. Clientes (list/get/create/patch); lectura de suscripciones, facturas, pagos y planes. |
| v1 · Fase 2 | Disponible | OAuth 2.0 client-credentials (JWT de vida corta); webhooks salientes firmados (HMAC-SHA256, cabecera X-IPSmart-Signature, con reintentos y bitácora); escrituras de pagos (POST /pagos) y suscripciones (POST/PATCH /suscripciones); rotación y revocación de keys. Todo dentro de v1 (retrocompatible). |
| Fase 3 (previsto) | Próximamente | Rate limiting distribuido (Redis) para escalado multi-proceso y panel de autogestión de credenciales para el partner. |