Public API
API HTTP estable y versionada para que integres tu Nxar con apps propias — backends de portales de cliente, ecommerce, dispositivos IoT, conectores no-oficiales con Zapier / n8n, etc.
Dos namespaces:
/public/v1/data/...— CRUD sobre tus records (Contacts, Opportunities, custom entities). Reusa exactamente los permisos y sharing que ya configuraste en la UI./public/v1/custom/...— endpoints custom que vos definís apuntando a un Logic Component o a una automation, con API key o con firma HMAC (webhooks).
Quickstart
1. Habilitar el Public API
Desde la UI: Configuración → Integraciones y API → Endpoints → API Config y activá Habilitar API Pública para este tenant.
(Necesitás el permiso manage_endpoints — viene en el permission set
“System Admin” por default.)
2. Crear una API key
En la misma pantalla, API Keys → Crear API key. Te pide:
- Name: para identificar la key en la lista (ej. “Customer portal backend”).
- Rate limit: requests/minuto que aceptás de esta key. Default 1000.
- CORS allowed origins: si la key se va a usar desde un browser, listá
los origins (uno por línea, ej.
https://app.cliente.com). Vacío = sólo server-to-server. - IP allowlist: CIDRs IPv4 (uno por línea, ej.
1.2.3.0/24). Vacío = cualquier IP.
Al crear, el token se muestra una sola vez en un modal con un botón Copy. Guardalo ya en tu secrets manager — no se puede recuperar después (si lo perdés, hay que borrar la key y crear una nueva).
3. Hacer tu primer request
curl -H "Authorization: Bearer nxar_api_..." \
https://tu-tenant.nx-ar.com/public/v1/meSi todo está bien, recibís:
{
"data": {
"userId": "...",
"email": "...",
"name": "...",
"isOwner": true,
"abilities": [...]
},
"meta": {}
}URL scheme
GET /public/v1/me → identidad del API key
GET /public/v1/data/{entity} → list (cursor pagination)
GET /public/v1/data/{entity}/{id} → read
POST /public/v1/data/{entity} → create
PATCH /public/v1/data/{entity}/{id} → partial update (merge JSONB)
DELETE /public/v1/data/{entity}/{id} → delete
POST /public/v1/custom/{slug} → corre una automation (trigger HTTP) o un Logic Component{entity} es el API name del entity type, en singular y snake_case
(contact, opportunity, account: el que aparece debajo de la etiqueta en
Configuración → Entidades). Un plural (contacts) da 404.
Authentication
Authorization: Bearer nxar_api_<hex>Cada request lleva una API key. La key resuelve a un user de tu tenant — las queries y mutations corren con los permisos de ese user (sharing rules, field-level permissions, todo aplica igual que en el web UI).
Response envelope
Todas las respuestas tienen el shape:
// éxito:
{ "data": ..., "meta": {} }
// error:
{ "error": { "code": "VALIDATION", "message": "...", "details": {...} } }Códigos de error comunes:
| Code | HTTP | Cuándo |
|---|---|---|
UNAUTHORIZED | 401 | Token inválido / revoked / expirado |
FORBIDDEN | 403 | El user de la key no tiene permiso sobre el recurso |
CORS_DENIED | 403 | El Origin del browser no está en cors_allowed_origins |
IP_DENIED | 403 | El client IP no está en ip_allowlist |
NOT_FOUND | 404 | Entity o record inexistente |
RATE_LIMITED | 429 | Excediste el rate limit. Header Retry-After indica los segundos |
VALIDATION | 400 | Body inválido |
API_DISABLED | 503 | El admin apagó el toggle global |
Rate limiting
Cada API key tiene su propio rate_limit_per_minute (default 1000).
Cuando excedés, recibís 429 con headers:
Retry-After: 47
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 47Los mismos headers X-RateLimit-* se incluyen en todas las respuestas para
que sepas cuánto budget te queda sin tener que esperar al 429.
Pagination
GET /public/v1/data/{entity} paginar con cursor:
curl ".../public/v1/data/contacts?limit=50"
# response: { data: [...], meta: { count: 50, hasMore: true, nextCursor: "eyJ0cyI6IjI..." } }
curl ".../public/v1/data/contacts?limit=50&cursor=eyJ0cyI6IjI..."
# siguiente páginaEl nextCursor es un string opaco (base64url) — tratalo como un token,
no parsees el contenido. Pasalo tal cual al siguiente request. Cuando
hasMore es false, no hay más páginas y nextCursor viene null.
limit máximo: 200.
Por qué cursor (en lugar de ?page=2):
- Consistente con escrituras concurrentes. Si entre páginas alguien inserta records, no se mezclan en tu paginado — los nuevos quedan delante del cursor (los ves en una página posterior, no duplicados).
- Performance. Sin offset scan; cada página es index seek O(log n)
contra
(entity_type_id, created_at DESC, id DESC).
Internamente el cursor es compuesto (created_at, id) para romper
empates determinísticamente cuando dos records tienen el mismo timestamp
(puede pasar en bulk inserts).
Custom endpoints
Para exponer lógica custom (cálculo de envío, lookup contra un sistema externo, flow de creación de tickets, etc.) hay dos targets posibles:
Logic Component
Una función JS sandboxed. Más liviano — el component define un handler con input/output schemas, vos lo registrás como endpoint en Configuración → Integraciones y API → Endpoints → Endpoints y webhooks.
- Crear el component en Configuración → Constructor de app → Logic Components.
- Crear el endpoint con target = “Logic Component” y elegir el component.
curl -X POST \
-H "Authorization: Bearer nxar_api_..." \
-H "Content-Type: application/json" \
-d '{"weight_kg": 2.5, "destination_zip": "1414"}' \
https://tu-tenant.nx-ar.com/public/v1/custom/calculate-shippingEl body se pasa como inputs al component. La response es el return value
del handler.
Automation
Un flow de automation declarativo (mismo builder visual que las automations
de records). Más potente para lógica multi-paso que combine create_record,
call_integration, condition, loop, send_email, etc.
- Crear la automation en Configuración → Automatización → Procesos → Nueva
con el tipo Endpoint de API. Eso fija el trigger en
http.requesty muestra los editores de Inputs y Outputs (también editables después desde Configuración dentro del builder). - Definí los inputs (lo que esperás en el body) y outputs (las variables
que el flow va a setear y querés devolver). Cada uno con
name+type(text|number|boolean|json/ objeto / lista) +required?. Declará como JSON / objeto lo que llega como estructura: si lo declarástext, el flow recibe el objeto convertido a texto y los nodos que lo leen saltean en silencio. - Diseñá el flow. Los inputs/outputs viven bajo dos namespaces fijos para
no mezclarse con las variables del propio flow:
- Inputs: referenciables como
{{inputs.<name>}}. Cualquier nodo puede leerlos:condition,set_value,get_record,send_email, etc. El autocomplete los sugiere mientras tipeás. - Outputs: writables vía
set_valuecon target =outputsy fieldPath =<name>(el engine soporta dot-path en object vars). También podés llenarlos desde el return de uncustom_componentmapeando sus fields aoutputs.<name>conoutputMappings. El bagoutputsarranca vacío{}, así que se puede escribir desde el primer nodo sin estado previo. Cuando el flow termina, el handler leeoutputs.<name>para cada field declarado en el schema y los devuelve al cliente comoresponse.data.
- Inputs: referenciables como
- Crear el endpoint en Public API → Endpoints y webhooks y elegir la automation que acabás de crear en ¿Qué corre?.
curl -X POST \
-H "Authorization: Bearer nxar_api_..." \
-H "Content-Type: application/json" \
-d '{"subject": "Sin internet", "contact_email": "[email protected]"}' \
https://tu-tenant.nx-ar.com/public/v1/custom/create-support-caseResponse:
{
"data": {
"case_id": "abc-123-...", // variable seteada por el flow
"ticket_number": "CASE-0042" // idem
},
"meta": {
"nodesExecuted": 4,
"durationMs": 137
}
}Reglas de validación: el body se valida contra input_schema antes de
correr el flow — campos required faltantes o tipos incorrectos devuelven
422 EXECUTION_ERROR con el mensaje Input validation failed: [...] (la lista
dice qué campo falló). Outputs que el flow no
setea aparecen como null en la response.
Identity (run as)
Por default, el custom endpoint corre con la identidad del dueño del API key que hizo el request. Eso significa que CASL (sharing, field permissions) filtra con esos permisos.
Si querés que el endpoint corra siempre con la identidad de un user específico
(típico para service accounts cuando múltiples API keys deben converger en un
mismo owner / sharing), configurá Correr como usuario (en Opciones avanzadas del diálogo
del endpoint). La identidad efectiva pasa a ser ese user. El executionContext del
component o automation sigue mandando si declara system o specific_user.
Webhooks con token secreto
Es el camino corto para recibir un webhook: probarlo con curl, conectar una
herramienta no-code (Zapier, Make, n8n, un Apps Script) o un sistema propio que
no sabe firmar requests.
- En Endpoints y webhooks → Nuevo endpoint, elegí el nombre en la URL, qué corre (una automation con trigger HTTP o un logic component) y dejá Token secreto en ¿Quién puede llamarlo?.
- Al crear, Nxar muestra una sola vez la URL, el token y un request de ejemplo. Copialos: después sólo vas a poder generar un token nuevo (que invalida el anterior en el acto).
- Quien llama manda el token en el header
X-Webhook-Token(o comoAuthorization: Bearer …) y el cuerpo en JSON oapplication/x-www-form-urlencoded.
curl -X POST "https://tu-tenant.nx-ar.com/public/v1/custom/nuevo-lead" \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: nxar_whk_…" \
-d '{"event":"lead.created","email":"[email protected]"}'El cuerpo llega como los inputs de la automation ({{inputs.email}}) o del
logic component. Un token ausente o equivocado devuelve 401 y no ejecuta nada.
Qué contesta lo elegís vos, en ¿Qué contesta? al crear o editar el endpoint:
- El resultado de lo que corre —
200con lo que devolvió la automation (lo que hayas escrito enoutputs.*) o el logic component. Es la opción para un endpoint que responde una consulta. - Sólo el acuse de recibo —
202con{ received: true, event_id }. Es lo que espera un proveedor que reintenta: confirma que el aviso llegó, sin devolver datos.
Si algo falla, las dos opciones devuelven el error del flujo (por ejemplo 422),
para que quien llama sepa que tiene que reintentar.
Reintentos: si quien llama puede reenviar la misma entrega, que mande un
header Idempotency-Key con un identificador propio. Con ese header, el segundo
recibo devuelve 200 con { duplicate: true } sin volver a correr nada; sin él,
cada request es un evento nuevo. ⚠️ Ese segundo recibo no trae el resultado
del primero (no se guarda), así que un cliente que necesita la respuesta no
debería mandar esa cabecera.
Ver qué llegó: dejá prendido Registrar cada request (viene activado al
crear) y mirá Audit Logs: cada entrega aparece con el cuerpo que mandó el
emisor y la respuesta, etiquetada TOKEN.
Webhooks inbound firmados (HMAC)
Para los casos donde un servicio externo (un payment processor, un proveedor de comms, un sistema legacy del cliente) tiene que mandar eventos sin tener una API key tuya, el custom endpoint soporta autenticación HMAC: el provider firma cada request con un shared secret y Nxar valida la firma antes de disparar el component / automation.
En el diálogo Nuevo endpoint, en ¿Quién puede llamarlo? elegí Firmado por el proveedor (HMAC). Aparecen dos campos extra:
- Logic component verificador: el Logic Component que valida la firma del provider (ver abajo). Los connectors instalables traen el suyo.
- Secret del webhook: el secret que vas a compartir con el provider. Pegalo desde la UI del provider. Se guarda en el config del endpoint y queda redacted en la UI después de crear.
El endpoint queda en la misma URL que un endpoint normal — POST /public/v1/custom/<slug> — pero sin Authorization header. La auth es
la firma del provider:
El formato exacto del header de firma depende del verifier — cada provider tiene su scheme. Ejemplo con un verifier simple que valida HMAC-SHA256-sobre-body (patrón común — Brevo, Postmark, GitHub, …):
SECRET="el-secret-que-pegaste-en-la-UI"
BODY='{"event":"payment.confirmed","payment_id":"abc123","amount":1000}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST "https://tu-tenant.nx-ar.com/public/v1/custom/my-webhook" \
-H "content-type: application/json" \
-H "x-signature: $SIG" \
-d "$BODY"Para providers con scheme más complejo (ej. MercadoPago usa ts=<unix>,v1=<hex> con signed manifest), el verifier Logic Component implementa la lógica específica. Usá el connector oficial cuando exista.
Idempotency está incorporada: el verifier extrae un provider_event_id
del body y Nxar dedupea por (endpoint, dedup_key) en la DB. Si el provider
reenvía el mismo evento por retry, el segundo recibo devuelve 200 con
{ duplicate: true } sin volver a disparar el component / automation. Cero
double-processing garantizado a nivel de constraint, no de cache.
En éxito el endpoint responde 202 Accepted con { received: true, event_id }
— semánticamente correcto para webhooks (recibido + processing OK). En fallo
del dispatch, propaga 4xx / 5xx para que el provider retry-ee; la próxima
retry exitosa pasa por idempotency y no duplica.
Verifier como Logic Component: el campo Logic component verificador del diálogo lista los Logic Components de tu workspace. Un verifier recibe { rawBody, headers, config }, valida la firma con ctx.crypto.* (hmacSha256, safeCompare, checkTimestampFresh) y devuelve { providerEventId, eventKind, canonicalEvent } con ctx.success(...), o rechaza con ctx.fail(...). Si tu provider no tiene connector oficial, escribís el verifier en Constructor de app → Logic Components y lo elegís acá. Algunos providers no firman sus notificaciones: el patrón entonces es un verifier que valida la forma del payload y un hydrate que vuelve a consultar el recurso al provider antes de confiar.
Connectors instalables: para providers comunes (MercadoPago, y próximamente Mobbex / Pagos360), instalá el connector desde el Marketplace. Trae el Logic Component verifier, el custom endpoint configurado y la automation que normaliza y despacha los eventos. Solo pegás el webhook_secret, configurás la URL del webhook en el panel del provider y elegís qué procesa cada evento (Integraciones y connectors).
Audit log
El admin puede activar el audit log desde Integraciones y API → Endpoints → API Config. Dos toggles:
- “Log requests to standard endpoints” (global del tenant) cubre
/public/v1/data/*y/public/v1/mey también todos los early failures (401, 403, 429, 503) sin importar el endpoint. Útil para detectar probing / brute-force / clientes mal escritos. - Per-endpoint toggle en cada custom endpoint cubre los success de
/public/v1/custom/<slug>específicamente.
Si prendés el global y dejás el per-endpoint off, igual se loguean los 4xx/5xx del custom endpoint — el admin que prendió “audit standard” no espera perderse errores en otros endpoints.
Los requests persistidos se conservan 30 días y son visibles desde
Integraciones y API → Endpoints → Audit Logs, con filtros por usuario,
endpoint (/data/*, /me, /custom/* o un endpoint específico) y estado
(todos / solo errores), con el request y la response de cada uno.
Independientemente del toggle, cada request siempre emite un log
estructurado (event: public_api.request) a stdout — el equipo de Nxar
los ingestía para troubleshooting incluso si vos tenés el audit log
apagado. Incluye también el caso donde el token presentado no existe
(api_key_id: null + api_key_prefix = primeros 12 chars del token —
útil para forensia sin exponer el secret completo).
Limitaciones de M1
- Solo IPv4 en el IP allowlist (IPv6 retorna false). Si necesitás IPv6, dejá la lista vacía.
- Rate limit es in-memory por proceso. Si Nxar escala a múltiples nodes, cada uno enforcea su propio budget — el efectivo puede exceder el configurado proporcionalmente.
- No hay OpenAPI spec auto-generada todavía. Post-MVP.
- No hay SDKs (TS / Python). Usá cualquier cliente HTTP.