Reglas comunes

Paginación, idempotencia, errores, ritmo y versionado. Lo mismo en las cuatro aplicaciones.

Wrap, Swarm, Nest y Flow comparten estas reglas. Aprenderlas una vez alcanza para las cuatro.

Versionado

La base siempre es /api/public/v1. Un cambio que rompa compatibilidad sube la versión en la ruta; nunca cambia el significado de lo que ya está publicado.

Esa base es el único contrato. Cualquier otra ruta que descubras cambia sin aviso, así que una integración que dependa de una se va a romper.

Paginación

Todas las listas usan cursor:

GET /api/public/v1/contacts?limit=50&cursor=<opaco>
{ "data": [ ... ], "next_cursor": "b2ZmOjUw" }
  • limit por defecto 50, máximo 100. Un valor raro no da error: se ajusta. Cero, negativo o texto vuelven a 50; 500 se convierte en 100.
  • next_cursor es null en la última página, así que no hay un viaje de más para descubrir que ya no queda nada.
  • El cursor es opaco. Se devuelve tal cual, no se interpreta ni se fabrica. Cada aplicación lo codifica a su manera y esa codificación puede cambiar.
  • Un cursor ilegible no da error: la lista empieza desde el principio. Vale la pena saberlo, porque un bucle que reintenta con un cursor corrupto se queda girando sobre la primera página en lugar de fallar.

No hay total ni has_more. Se pagina hasta que next_cursor sea null.

Idempotencia

Casi todo POST exige la cabecera Idempotency-Key. Todo el que cree o registre algo la pide; las dos POST que administran webhooks en Flow son la excepción. Sin ella recibes un 400 y no se crea nada:

{
  "error": {
    "code": "invalid_request",
    "message": "The Idempotency-Key header is required on this endpoint."
  }
}

Con ella:

Caso Qué pasa
Misma clave, mismo cuerpo Recibes la respuesta guardada tal cual, con su mismo estado. La operación no se repite
Misma clave, cuerpo distinto conflict, 409

Dos detalles que sorprenden:

  • La clave se recuerda para siempre. No caduca. Reusar una de hace seis meses devuelve aquella respuesta.
  • La clave es del espacio, no del endpoint. La unicidad es la pareja espacio y clave, así que usar la misma clave en dos endpoints distintos da un 409, no dos creaciones. Genera una clave por operación: un UUID sirve.

Se guarda incluso la respuesta de error, así que un reintento con la misma clave te devuelve el mismo error en lugar de intentarlo otra vez.

Solo POST. PATCH y DELETE no la piden y no la miran: una actualización parcial que manda el mismo cuerpo dos veces ya deja el registro igual, así que no hace falta protegerla.

Errores

Siempre la misma envoltura:

{ "error": { "code": "not_found", "message": "..." } }

Ocho códigos, y su estado no cambia:

Código Estado
unauthorized 401
forbidden 403
invalid_request 400
not_found 404
conflict 409
unprocessable 422
rate_limited 429
internal 500

Ramifica sobre error.code, nunca sobre el estado ni sobre el mensaje: los mensajes están en inglés y pueden cambiar.

404, no 403

Un registro que existe pero que tu credencial no puede ver responde 404, exactamente igual que un identificador inventado.

Los dos casos no se distinguen a propósito. Un 403 confirmaría que ese proyecto privado existe, y con el identificador casi siempre se puede deducir de quién es: sería filtrar justo lo que "privado" existe para proteger.

Ritmo

Cada permiso tiene una clase, y cada clase su presupuesto por minuto:

Clase Por minuto
Lectura 120
Escritura 30
Administración 30

Los cubos son por credencial y por permiso, así que leer y escribir a la vez no compiten entre sí.

Al pasarte recibes un 429 con Retry-After. No hay cabeceras X-RateLimit-* en esta superficie, así que no puedes saber cuánto te queda sin pedir y ver si falla. La única API de la suite que sí las manda es la de entrega de Zap.

Trata estos números como un piso, no como un techo. El límite efectivo puede ser más alto y no es predecible: diseña para respetar el número publicado y para tolerar un 429 de todos modos.

Importes

Los importes vienen como cadenas, no como números de punto flotante, para que un valor con decimales no pierda precisión al pasar por JSON. Conviértelos con la librería decimal de tu lenguaje, no con parseFloat.