IP
IPSmart · Partner API

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.

api.ipsmart.app/api/v1/partner REST · JSON · HTTPS Versión v1 Auth: API key ipk_… · OAuth Webhooks firmados

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

EntornoBase URL
Producciónhttps://api.ipsmart.app/api/v1/partner
Local / pruebashttp://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

Importante: el secreto se muestra una única vez al emitirse. Guárdelo de inmediato en un gestor de secretos (vault, variables de entorno). Si lo pierde, no se recupera: hay que emitir una nueva key y revocar la anterior.

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 ser client_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"
Revocación en vivo: cada petición con el JWT re-valida el estado actual de la key. Si se revoca, suspende o expira, el JWT deja de aceptarse de inmediato aunque no haya caducado. El JWT de partner se firma con un secreto dedicado y no vale como sesión de usuario. Usar la API key directamente sigue siendo válido; OAuth es opcional.

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)

ScopeHabilita
clientes:readLeer/listar clientes (PII enmascarada).
clientes:writeCrear / actualizar clientes.
clientes:piiVer PII (cédula/teléfono/email) sin enmascarar.
suscripciones:readListar suscripciones.
suscripciones:writeCrear suscripciones y cancelarlas.
facturas:readListar facturas.
pagos:readListar pagos.
pagos:writeRegistrar pagos.
planes:readListar planes/productos del catálogo.
webhooks:manageRegistrar 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.

Todos los scopes de la tabla están implementados (lecturas, escrituras de clientes, pagos y suscripciones, y gestión de webhooks). Su key recibe solo los scopes que su contrato con IPSmart otorgue; pida únicamente los que necesite.

5Convenciones transversales

5.1 Paginación

ParámetroTipoPor defectoRango
pageentero (base 0)0≥ 0
sizeentero251..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 cuota X-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étodoRutaScopeDescripción
POST/oauth/tokenpúblicoIntercambia la key por un JWT de vida corta.
GET/clientesclientes:readLista/busca clientes.
GET/clientes/:idclientes:readFicha de un cliente.
POST/clientesclientes:writeCrea cliente (dedup, idempotente).
PATCH/clientes/:idclientes:writeActualiza un cliente.
GET/suscripcionessuscripciones:readLista suscripciones.
POST/suscripcionessuscripciones:writeCrea una suscripción (idempotente).
PATCH/suscripciones/:idsuscripciones:writeActualiza (cancelación).
GET/facturasfacturas:readLista facturas.
GET/pagospagos:readLista pagos.
POST/pagospagos:writeRegistra un pago (idempotente).
GET/planesplanes:readLista planes/productos.
POST/webhookswebhooks:manageRegistra un webhook.
GET/webhookswebhooks:manageLista los webhooks del tenant.
PATCH/webhooks/:idwebhooks:managePausa/reactiva o cambia eventos.
DELETE/webhooks/:idwebhooks:manageDa de baja un webhook.

6.1 Clientes

GET/clientesscope clientes:read

Lista/busca clientes del tenant (paginado, filtros).

QueryTipoDescripción
qstringTexto libre o número de documento.
tipoenumnatural | juridico.
estadostringactive | inactive | suspended.
page, sizeintPaginació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).

GET/clientes/:idscope clientes:read

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" } ]
}
POST/clientesscope clientes:write

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)

CampoTipoNotas
partyTypeenumPERSON | ORGANIZATION.
firstName, lastNamestringPersona natural.
legalName, tradeNamestringPersona jurídica (organization).
statusenumactive | 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), … }
profileobjeto{ segment (residential|sme|corporate|government|vip), acquisitionChannel? }

Persona jurídica: partyType: "ORGANIZATION" con organization: { legalName, tradeName? } y documentType: "rif" (u otro) en documents[].

PATCH/clientes/:idscope clientes:write

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

GET/suscripcionesscope suscripciones:read

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
}
POST/suscripcionesscope suscripciones:write

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.

CampoTipoNotas
partyIduuidObligatorio. Cliente del tenant.
productIduuidObligatorio. Plan/producto del catálogo.
startDateYYYY-MM-DDOpcional. 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"
}
PATCH/suscripciones/:idscope suscripciones:write

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

GET/facturasscope facturas:read

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

GET/pagosscope pagos:read

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
}
POST/pagosscope pagos:write

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.

CampoTipoNotas
partyIduuidObligatorio. Cliente que paga.
montonumberObligatorio. Importe (hasta 2 decimales, positivo).
invoiceIduuidOpcional. Si viene, el pago se aplica a esa factura.
metodoCodestringOpcional. TRANSFER/PAGOMOVIL/EFECTIVO… (por defecto TRANSFER).
referenciastringOpcional. Referencia de la transacción.
fechaISO 8601Opcional. 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

GET/planesscope planes:read

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
}
Administración de credenciales (backoffice de IPSmart). Bajo /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).

CampoTipoNotas
targetUrlstringObligatorio. URL http(s) de su receptor.
eventTypesstring[]Obligatorio, no vacío. Eventos a suscribir (§7.2).
descriptionstringOpcional. 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"
}
El 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

EventoSe dispara cuando
cliente.createdSe crea un cliente.
cliente.updatedSe actualiza un cliente.
pago.recibidoSe registra un pago.
factura.emitidaSe emite una factura.
suscripcion.createdSe 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 POST tiene timeout de 8 s. Solo cuenta como entregado una respuesta 2xx.
  • Agotamiento: si los 3 intentos fallan, la entrega queda como exhausted y se registra en la bitácora de IPSmart (código HTTP + hash del payload, sin PII).
  • Salud del webhook: IPSmart lleva consecutiveFailures, lastSuccessAt y lastFailureAt por webhook (visibles en GET /webhooks).
  • Handler idempotente: descarte duplicados por el id del evento y responda 2xx en 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 } }.

HTTPcodeSignificadoCausa típica
400bad_requestPetición inválida.Documento duplicado (dedup), cuerpo malformado.
401unauthorizedNo autenticado.Falta la API key, o es inválida/inactiva/expirada.
403forbiddenSin permiso.La key no tiene el scope requerido.
404not_foundNo encontrado.El recurso no existe en el tenant de la key.
409conflictConflicto.Idempotency-Key reusada con cuerpo distinto.
422unprocessable_entityNo procesable.Validación semántica fallida.
429rate_limitedLímite de tasa excedido.Más peticiones que rate_limit_per_min.
500internal_errorError 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_at cuando aplique.
  • Mínimo privilegio. Pida solo los scopes que necesita. clientes:pii solo si su caso de uso realmente requiere PII en claro.
  • Idempotencia en escrituras. Envíe siempre Idempotency-Key en POST.
  • Maneje 429 con backoff. Respete Retry-After y aplique backoff exponencial.
  • Valide la firma de los webhooks sobre el cuerpo crudo, con comparación de tiempo constante; descarte duplicados por id de 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-Id de 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 de v1; cambios incompatibles bajo /api/v2/…, manteniendo v1 en transición.
  • Recomendación: ignore campos desconocidos en las respuestas (tolerancia hacia adelante).
VersiónEstadoContenido
v1 · Fase 1DisponibleAPI 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 2DisponibleOAuth 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óximamenteRate limiting distribuido (Redis) para escalado multi-proceso y panel de autogestión de credenciales para el partner.