Backend Integration
Esta guΓa describe cΓ³mo integrar tu servicio con nuestra API RESTful. DiseΓ±ada para ser intuitiva y predecible, nuestra API utiliza caracterΓsticas estΓ‘ndar de HTTP como verbos, cΓ³digos de respuesta y autenticaciΓ³n basada en tokens. EncontrarΓ‘s ejemplos prΓ‘cticos de cURL, Python y JavaScript listos para copiar y pegar en tu dΓa a dΓa.
Authentication Required
La API soporta multiples esquemas de autenticacion. El metodo principal es Bearer Token via el header
Authorization, pero tambien puedes usar API Keys y OAuth2 segun el caso de uso.
Bearer Token (JWT)
Envia tu token en cada peticion con el prefijo Bearer:
curl https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Content-Type: application/json" Flujo OAuth2 (Authorization Code)
Para integraciones de terceros que necesitan actuar en nombre de un usuario, usa el flujo OAuth2 Authorization Code:
Flujo OAuth2 Authorization Code
ββββββββββββ ββββββββββββββββ ββββββββββββ
β Usuario β β Tu App β β API β
βββββββ¬βββββ ββββββββ¬ββββββββ ββββββ¬ββββββ
β 1. Click "Conectar" β β
βββββββββββββββββββββββββββββββββββββββββββ>β β
β β 2. Redirect a /oauth/authorize β
β<βββββββββββββββββββββββββββββββββββββββββββ β
β 3. Login + Autoriza permisos β β
βββββββββββββββββββββββββββββββββββββββββββ>β β
β β 4. Callback con ?code=abc123 β
β β<βββββββββββββββββββββββββββββββββββββ
β β 5. POST /oauth/token {code} β
β βββββββββββββββββββββββββββββββββββββ>β
β β 6. {access_token, refresh_token} β
β β<βββββββββββββββββββββββββββββββββββββ
β 7. Acceso a recursos protegidos β β
β<βββββββββββββββββββββββββββββββββββββββββββ β # Paso 5: Intercambiar el cΓ³digo por tokens
curl -X POST https://api.cookbooks.dev/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=abc123" \
-d "client_id=tu_client_id" \
-d "client_secret=tu_client_secret" \
-d "redirect_uri=https://tuapp.com/callback" {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "projects:read projects:write"
} Refresh de tokens
Cuando un access token expira, usa el refresh token para obtener uno nuevo sin pedir credenciales al usuario:
curl -X POST https://api.cookbooks.dev/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=rt_dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..." \
-d "client_id=tu_client_id" \
-d "client_secret=tu_client_secret" import requests
import time
class TokenManager:
"""Gestiona tokens con refresh automatico."""
def __init__(self, client_id, client_secret, refresh_token):
self.client_id = client_id
self.client_secret = client_secret
self.refresh_token = refresh_token
self.access_token = None
self.expires_at = 0
def get_token(self):
if time.time() >= self.expires_at - 60: # Renovar 60s antes
self._refresh()
return self.access_token
def _refresh(self):
resp = requests.post("https://api.cookbooks.dev/oauth/token", data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": self.client_id,
"client_secret": self.client_secret,
})
resp.raise_for_status()
data = resp.json()
self.access_token = data["access_token"]
self.expires_at = time.time() + data["expires_in"]
if "refresh_token" in data:
self.refresh_token = data["refresh_token"]
# Uso
token_mgr = TokenManager("id", "secret", "rt_...")
headers = {"Authorization": f"Bearer {token_mgr.get_token()}"} Comparacion de esquemas de autenticacion
| Esquema | Uso recomendado | Expiracion | Revocable |
|---|---|---|---|
API Key | Scripts, CI/CD, servidor a servidor | No expira (rotacion manual) | Si β panel de control |
Bearer Token (JWT) | Sesiones de usuario, SPAs | 30 min - 1 hora | Si β via refresh revocation |
OAuth2 Code | Integraciones de terceros | 1 hora (refresh: 30 dias) | Si β revocar autorizacion |
OAuth2 Client Credentials | Comunicacion machine-to-machine | 1 hora | Si β revocar client |
Gestion de API Keys
Puedes crear y administrar tus API Keys desde el panel de control o via la propia API:
# Crear una nueva API Key con scope limitado
curl -X POST https://api.cookbooks.dev/v1/api-keys \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Pipeline",
"scopes": ["projects:read", "deployments:write"],
"expires_at": "2026-12-31T23:59:59Z"
}' {
"id": "key_8fKz3mNpQr",
"name": "CI/CD Pipeline",
"key": "sk_live_nuevaClave4821...",
"scopes": ["projects:read", "deployments:write"],
"expires_at": "2026-12-31T23:59:59Z",
"created_at": "2026-02-22T10:00:00Z"
} Seguridad de las claves
La clave completa solo se muestra una vez al crearla. Guardala de forma segura en un gestor de secretos (como Vault, AWS Secrets Manager o variables de entorno cifradas). Si sospechas que una clave ha sido comprometida, rotala inmediatamente desde el panel de control. Las claves antiguas expiraran tras 24 horas de gracia.
Endpoints & CRUD Core
La API sigue convenciones REST estandar. Todos los endpoints estan bajo el prefijo
/v1 y usan los verbos HTTP apropiados para cada operacion.
Referencia de recursos
| Metodo | Endpoint | Descripcion | Status |
|---|---|---|---|
| GET | /v1/projects | Listar proyectos con paginacion | 200 |
| POST | /v1/projects | Crear un nuevo proyecto | 201 |
| GET | /v1/projects/{id} | Obtener detalle de un proyecto | 200 |
| PATCH | /v1/projects/{id} | Actualizar campos de un proyecto | 200 |
| DELETE | /v1/projects/{id} | Eliminar un proyecto | 204 |
| GET | /v1/deployments | Historial de builds y estados | 200 |
| POST | /v1/deployments | Iniciar un nuevo despliegue | 201 |
| GET | /v1/teams | Listar equipos y miembros | 200 |
| PUT | /v1/teams/{id} | Actualizar un equipo completo | 200 |
| DELETE | /v1/teams/{id} | Eliminar un equipo | 204 |
| GET | /v1/webhooks | Listar webhooks configurados | 200 |
| POST | /v1/webhooks | Registrar un nuevo webhook | 201 |
| DELETE | /v1/webhooks/{id} | Eliminar un webhook | 204 |
GET β Listar recursos
# Listar proyectos con filtros y paginacion
curl https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." \
-G \
-d "status=active" \
-d "sort=-created_at" \
-d "per_page=10" \
-d "page=1" {
"data": [
{
"id": "proj_a1b2c3",
"name": "docs-website",
"status": "active",
"framework": "astro",
"created_at": "2026-01-15T08:30:00Z",
"updated_at": "2026-02-20T14:22:00Z",
"owner": {
"id": "usr_x9y8z7",
"email": "dev@cookbooks.dev"
}
}
],
"meta": {
"page": 1,
"per_page": 10,
"total": 42,
"total_pages": 5
}
} POST β Crear un recurso
# Crear un nuevo proyecto
curl -X POST https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Content-Type: application/json" \
-d '{
"name": "mi-nuevo-proyecto",
"framework": "astro",
"description": "DocumentaciΓ³n tΓ©cnica del equipo",
"visibility": "private",
"settings": {
"build_command": "npm run build",
"output_dir": "dist",
"node_version": "20"
}
}' {
"id": "proj_d4e5f6",
"name": "mi-nuevo-proyecto",
"framework": "astro",
"description": "DocumentaciΓ³n tΓ©cnica del equipo",
"visibility": "private",
"status": "initializing",
"settings": {
"build_command": "npm run build",
"output_dir": "dist",
"node_version": "20"
},
"created_at": "2026-02-22T10:00:00Z",
"updated_at": "2026-02-22T10:00:00Z"
} PATCH β Actualizar parcialmente
# Actualizar solo el nombre y la descripcion
curl -X PATCH https://api.cookbooks.dev/v1/projects/proj_d4e5f6 \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Content-Type: application/json" \
-d '{
"name": "mi-proyecto-renombrado",
"description": "DescripciΓ³n actualizada"
}' PATCH vs PUT
Usa PATCH para enviar solo los campos que cambian. Con PUT debes
enviar el recurso completo. La mayoria de nuestros endpoints aceptan PATCH
para actualizaciones parciales, que es lo mas comun en integraciones.
DELETE β Eliminar un recurso
# Eliminar un proyecto (devuelve 204 sin body)
curl -X DELETE https://api.cookbooks.dev/v1/projects/proj_d4e5f6 \
-H "Authorization: Bearer sk_test_51Mz..." Eliminacion irreversible
Las operaciones DELETE son permanentes. Para recursos criticos como proyectos con
deployments activos, la API requiere confirmacion via el header
X-Confirm-Delete: true.
Query parameters para filtrado y ordenamiento
| Parametro | Tipo | Ejemplo | Descripcion |
|---|---|---|---|
page | integer | ?page=2 | Numero de pagina (desde 1) |
per_page | integer | ?per_page=25 | Elementos por pagina (max 100) |
sort | string | ?sort=-created_at | Ordenar por campo. Prefijo - para descendente |
status | string | ?status=active | Filtrar por estado |
search | string | ?search=docs | Busqueda full-text en nombre y descripcion |
created_after | ISO 8601 | ?created_after=2026-01-01 | Filtrar por fecha de creacion |
fields | string | ?fields=id,name,status | Seleccionar campos especificos (sparse fieldsets) |
Request & Response Format Standard
Todas las respuestas de la API siguen un formato envolvente (envelope) consistente. Esto facilita el parseo y la gestion de errores desde cualquier lenguaje.
Formato JSON envolvente
Para respuestas de listado (colecciones):
{
"data": [
{ "id": "proj_a1b2c3", "name": "docs-website", "status": "active" },
{ "id": "proj_d4e5f6", "name": "api-service", "status": "deploying" }
],
"meta": {
"page": 1,
"per_page": 25,
"total": 42,
"total_pages": 2
},
"links": {
"self": "/v1/projects?page=1&per_page=25",
"next": "/v1/projects?page=2&per_page=25",
"last": "/v1/projects?page=2&per_page=25"
}
} Para respuestas de recurso individual:
{
"data": {
"id": "proj_a1b2c3",
"name": "docs-website",
"status": "active",
"framework": "astro",
"created_at": "2026-01-15T08:30:00Z",
"updated_at": "2026-02-20T14:22:00Z"
}
} Para respuestas de error (ver seccion Error Handling para detalle completo):
{
"error": {
"code": "validation_error",
"message": "El campo 'name' es obligatorio",
"status": 422,
"details": [
{ "field": "name", "message": "Este campo no puede estar vacΓo" }
],
"request_id": "req_7f8g9h0i"
}
} Content Negotiation
La API acepta y devuelve JSON por defecto. Debes incluir los headers apropiados:
| Header | Valor | Descripcion |
|---|---|---|
Content-Type | application/json | Formato del body en requests POST/PATCH/PUT |
Accept | application/json | Formato deseado de respuesta (defecto si se omite) |
Accept-Encoding | gzip, deflate | Habilitar compresion en la respuesta |
Authorization | Bearer {token} | Token de autenticacion |
X-Request-Id | req_custom123 | ID de correlacion personalizado (opcional) |
X-Idempotency-Key | idem_abc456 | Clave de idempotencia para POST (opcional) |
Compresion Gzip
La API soporta compresion gzip para reducir el tamano de las respuestas. Las respuestas comprimidas pueden ser hasta un 80% mas pequenas:
# Peticion con compresion gzip
curl https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Accept-Encoding: gzip" \
--compressed import requests
# requests descomprime gzip automaticamente
response = requests.get(
"https://api.cookbooks.dev/v1/projects",
headers={
"Authorization": "Bearer sk_test_51Mz...",
"Accept-Encoding": "gzip",
},
)
# response.json() ya esta descomprimido
print(f"TamaΓ±o recibido: {len(response.content)} bytes") Headers de respuesta estandar
Cada respuesta incluye headers utiles para debugging y paginacion:
| Header | Ejemplo | Descripcion |
|---|---|---|
X-Request-Id | req_7f8g9h0i | ID unico de la peticion para soporte |
X-RateLimit-Limit | 1000 | Peticiones permitidas en la ventana |
X-RateLimit-Remaining | 994 | Peticiones restantes |
X-RateLimit-Reset | 1708617600 | Timestamp Unix cuando se reinicia el limite |
X-Total-Count | 42 | Total de elementos (en listados) |
Link | </v1/projects?page=2>; rel=βnextβ | Links de navegacion (en listados) |
X-Request-Id para soporte
Siempre guarda el header X-Request-Id en tus logs. Cuando contactes a soporte,
proporciona este ID para que podamos rastrear tu peticion en nuestros sistemas de forma inmediata.
Error Handling Critical
La API usa codigos de estado HTTP estandar. Todo codigo en el rango
2xx indica exito,
4xx indica un error del cliente, y
5xx indica un error del servidor.
Referencia completa de codigos HTTP
| Codigo | Nombre | Descripcion | Accion recomendada |
|---|---|---|---|
200 | OK | Peticion exitosa | Procesar la respuesta normalmente |
201 | Created | Recurso creado exitosamente | Leer el recurso creado en el body |
204 | No Content | Operacion exitosa, sin body | No esperar body (ej. DELETE) |
400 | Bad Request | JSON malformado o parametros invalidos | Revisar el formato del request |
401 | Unauthorized | Token ausente, invalido o expirado | Verificar credenciales o renovar token |
403 | Forbidden | Sin permisos para este recurso | Verificar scopes del token y permisos del equipo |
404 | Not Found | Recurso no encontrado | Verificar ID del recurso y la URL |
409 | Conflict | Conflicto de estado (ej. duplicado) | Resolver el conflicto antes de reintentar |
422 | Unprocessable Entity | Validacion fallida en los datos enviados | Revisar los campos indicados en details |
429 | Too Many Requests | Limite de rate limit excedido | Esperar segun el header Retry-After |
500 | Internal Server Error | Error inesperado del servidor | Reintentar con backoff exponencial |
503 | Service Unavailable | Servicio temporalmente no disponible | Reintentar tras Retry-After segundos |
Formato de error estandar
Todos los errores siguen la misma estructura JSON:
{
"error": {
"code": "validation_error",
"message": "Los datos enviados contienen errores de validaciΓ³n",
"status": 422,
"request_id": "req_7f8g9h0i",
"details": [
{
"field": "email",
"message": "Formato de email invΓ‘lido",
"value": "no-es-un-email"
},
{
"field": "name",
"message": "Debe tener entre 3 y 100 caracteres",
"value": "ab"
}
]
}
} Estrategia de reintentos con backoff exponencial
Para errores transitorios (429, 500, 503), implementa reintentos con backoff exponencial y jitter:
import time
import random
import requests
def request_with_retry(method, url, max_retries=5, **kwargs):
"""
Realiza una peticion HTTP con reintentos automaticos
usando backoff exponencial + jitter.
"""
retryable_codes = {429, 500, 502, 503, 504}
for attempt in range(max_retries):
response = requests.request(method, url, **kwargs)
if response.status_code not in retryable_codes:
return response
# Respetar Retry-After si viene en la respuesta
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = int(retry_after)
else:
# Backoff exponencial: 1s, 2s, 4s, 8s, 16s + jitter
wait_time = min(2 ** attempt + random.uniform(0, 1), 30)
print(f"[Retry {attempt + 1}/{max_retries}] "
f"Status {response.status_code}. "
f"Esperando {wait_time:.1f}s...")
time.sleep(wait_time)
# Retornar la ultima respuesta si se agotan los reintentos
return response
# Uso
response = request_with_retry(
"GET",
"https://api.cookbooks.dev/v1/projects",
headers={"Authorization": "Bearer sk_test_51Mz..."},
) // Implementacion en JavaScript/Node.js
async function fetchWithRetry(url, options = {}, maxRetries = 5) {
const retryableCodes = new Set([429, 500, 502, 503, 504]);
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (!retryableCodes.has(response.status)) {
return response;
}
const retryAfter = response.headers.get('Retry-After');
const waitTime = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(2 ** attempt * 1000 + Math.random() * 1000, 30000);
console.log(
`[Retry ${attempt + 1}/${maxRetries}] ` +
`Status ${response.status}. Esperando ${waitTime}ms...`
);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
throw new Error(`Reintentos agotados tras ${maxRetries} intentos`);
}
// Uso
const response = await fetchWithRetry(
'https://api.cookbooks.dev/v1/projects',
{ headers: { Authorization: 'Bearer sk_test_51Mz...' } }
); Idempotencia en reintentos
Solo reintenta peticiones que sean seguras de repetir. Los GET siempre son
seguros. Para POST, usa el header
X-Idempotency-Key para evitar crear recursos duplicados al reintentar.
Pagination & Filtering
La API soporta dos modos de paginacion: offset-based (por defecto) y cursor-based (recomendado para datasets grandes). Ambos modos estan disponibles en todos los endpoints de listado.
Comparacion de estrategias de paginacion
| Caracteristica | Offset (page/per_page) | Cursor (after/before) |
|---|---|---|
| Salta a pagina arbitraria | Si | No |
| Rendimiento con datos grandes | Degradado (OFFSET lento) | Constante (O(1)) |
| Datos consistentes si hay inserciones | No (puede saltar/duplicar) | Si |
| Total de elementos | Incluido en meta | Requiere has_more flag |
| Caso de uso ideal | UIs con numeros de pagina | Scroll infinito, exports, webhooks |
Paginacion offset-based
# Pagina 1, 25 elementos por pagina
curl https://api.cookbooks.dev/v1/projects?page=1&per_page=25 \
-H "Authorization: Bearer sk_test_51Mz..."
# Pagina 3
curl https://api.cookbooks.dev/v1/projects?page=3&per_page=25 \
-H "Authorization: Bearer sk_test_51Mz..." Respuesta con metadatos de paginacion:
{
"data": [ "..." ],
"meta": {
"page": 1,
"per_page": 25,
"total": 142,
"total_pages": 6
},
"links": {
"self": "/v1/projects?page=1&per_page=25",
"first": "/v1/projects?page=1&per_page=25",
"next": "/v1/projects?page=2&per_page=25",
"last": "/v1/projects?page=6&per_page=25"
}
} Paginacion cursor-based
Para datasets grandes o cuando necesitas consistencia, usa paginacion por cursor:
# Primera peticion (sin cursor)
curl https://api.cookbooks.dev/v1/projects?limit=25&sort=-created_at \
-H "Authorization: Bearer sk_test_51Mz..."
# Siguiente pagina usando el cursor devuelto
curl https://api.cookbooks.dev/v1/projects?limit=25&after=cur_eyJpZCI6MTAwfQ \
-H "Authorization: Bearer sk_test_51Mz..." {
"data": [ "..." ],
"meta": {
"has_more": true,
"next_cursor": "cur_eyJpZCI6NzV9",
"previous_cursor": "cur_eyJpZCI6MTAwfQ"
}
} Iterar todas las paginas con Python
import requests
def iter_all_projects(token):
"""Itera todos los proyectos usando paginacion por cursor."""
url = "https://api.cookbooks.dev/v1/projects"
params = {"limit": 100, "sort": "-created_at"}
headers = {"Authorization": f"Bearer {token}"}
while True:
resp = requests.get(url, headers=headers, params=params)
resp.raise_for_status()
body = resp.json()
for project in body["data"]:
yield project
if not body["meta"].get("has_more"):
break
params["after"] = body["meta"]["next_cursor"]
# Uso
for project in iter_all_projects("sk_test_51Mz..."):
print(f"{project['id']}: {project['name']}") Navegacion con el header Link
Los endpoints de listado incluyen el header
Link siguiendo el estandar RFC 8288 para facilitar la navegacion:
Link: <https://api.cookbooks.dev/v1/projects?page=2&per_page=25>; rel="next",
<https://api.cookbooks.dev/v1/projects?page=6&per_page=25>; rel="last",
<https://api.cookbooks.dev/v1/projects?page=1&per_page=25>; rel="first" // Parsear el header Link en JavaScript
function parseLinkHeader(header) {
if (!header) return {};
return header.split(',').reduce((acc, part) => {
const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
if (match) acc[match[2]] = match[1];
return acc;
}, {});
}
// Uso
const links = parseLinkHeader(response.headers.get('Link'));
if (links.next) {
const nextPage = await fetch(links.next, { headers });
} Filtrado avanzado
Combina multiples filtros en una sola peticion:
# Proyectos activos creados en 2026, framework Astro, ordenados por nombre
curl -G https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." \
-d "status=active" \
-d "framework=astro" \
-d "created_after=2026-01-01T00:00:00Z" \
-d "created_before=2026-12-31T23:59:59Z" \
-d "sort=name" \
-d "fields=id,name,status,created_at" \
-d "per_page=50" Sparse fieldsets
Usa el parametro fields para recibir solo los campos que necesitas.
Esto reduce el tamano de la respuesta y mejora los tiempos de transferencia,
especialmente util para listados grandes.
Rate Limiting Important
Limitamos las peticiones para asegurar la estabilidad del sistema y una experiencia justa para todos los usuarios. Usamos un algoritmo de token bucket que permite rafagas cortas por encima del limite promedio.
Niveles de rate limiting
| Plan | Limite por minuto | Burst maximo | Limite diario | Concurrencia maxima |
|---|---|---|---|---|
Free | 100 req/min | 20 req/s | 10,000 req/dia | 5 conexiones |
Pro | 1,000 req/min | 100 req/s | 100,000 req/dia | 25 conexiones |
Business | 5,000 req/min | 500 req/s | 1,000,000 req/dia | 100 conexiones |
Enterprise | Personalizado | Personalizado | Sin limite | Personalizado |
Como funciona el Token Bucket
El algoritmo de token bucket permite rafagas controladas. Imagina un cubo que se llena con tokens a una tasa constante:
Algoritmo Token Bucket
Tokens se agregan a tasa constante (ej. 1000/min)
β
βΌ
βββββββββββββββββ
β β β β β β β β β Bucket (capacidad maxima = burst)
β β β β β β β β
β β β β β β β Cada peticion consume 1 token
βββββββββ¬ββββββββ
β
βΌ
Si hay tokens β Peticion permitida (200)
Si no hay β Peticion rechazada (429) Headers de rate limiting
Cada respuesta incluye informacion sobre tu cuota actual:
# Inspeccionar headers de rate limit
curl -I https://api.cookbooks.dev/v1/projects \
-H "Authorization: Bearer sk_test_51Mz..." HTTP/2 200
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 994
X-RateLimit-Reset: 1708617600
Retry-After: 42 Manejar rate limiting correctamente
Si excedes el limite, recibiras un codigo de estado
429 Too Many Requests.
La respuesta incluira el header
Retry-After indicando cuantos segundos esperar:
class APIClient {
constructor(token) {
this.token = token;
this.baseURL = 'https://api.cookbooks.dev/v1';
}
async request(method, path, body = null) {
const url = `${this.baseURL}${path}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(url, options);
// Logging de cuota para monitoreo
const remaining = response.headers.get('X-RateLimit-Remaining');
const limit = response.headers.get('X-RateLimit-Limit');
console.log(`Rate limit: ${remaining}/${limit}`);
// Manejar rate limit
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get('Retry-After') || '5', 10
);
console.warn(`Rate limited. Reintentando en ${retryAfter}s...`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
return this.request(method, path, body); // Reintentar
}
return response;
}
} Patron Circuit Breaker
Para proteger tu aplicacion cuando la API tiene problemas sostenidos, implementa un circuit breaker:
import time
class CircuitBreaker:
"""
Patron Circuit Breaker para proteger contra fallos en cascada.
Estados:
- CLOSED: Todo funciona, las peticiones pasan normalmente.
- OPEN: Demasiados fallos, las peticiones se rechazan inmediatamente.
- HALF_OPEN: Periodo de prueba, se permite una peticion para verificar.
"""
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = self.CLOSED
self.failure_count = 0
self.last_failure_time = None
def can_execute(self):
if self.state == self.CLOSED:
return True
if self.state == self.OPEN:
if time.time() - self.last_failure_time >= self.recovery_timeout:
self.state = self.HALF_OPEN
return True
return False
# HALF_OPEN: permitir una peticion de prueba
return True
def record_success(self):
self.failure_count = 0
self.state = self.CLOSED
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = self.OPEN
# Uso con la API
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=60)
def safe_api_call(url, headers):
if not breaker.can_execute():
raise Exception("Circuit breaker OPEN: API no disponible")
try:
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code >= 500:
breaker.record_failure()
raise Exception(f"Server error: {resp.status_code}")
breaker.record_success()
return resp
except requests.exceptions.Timeout:
breaker.record_failure()
raise Monitorea tu cuota
Configura alertas cuando X-RateLimit-Remaining caiga por debajo del 20%
de tu limite. Esto te dara tiempo de reaccionar antes de empezar a recibir errores 429.
Webhooks Events
Los webhooks te permiten recibir notificaciones en tiempo real cuando ocurren eventos en tu cuenta. En vez de hacer polling, la API envia un
POST a tu URL con los detalles del evento.
Registrar un webhook
curl -X POST https://api.cookbooks.dev/v1/webhooks \
-H "Authorization: Bearer sk_test_51Mz..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://tuapp.com/api/webhooks/cookbooks",
"events": [
"project.created",
"project.updated",
"deployment.completed",
"deployment.failed"
],
"secret": "whsec_tu_secreto_para_verificar_firmas"
}' {
"id": "wh_k1l2m3n4",
"url": "https://tuapp.com/api/webhooks/cookbooks",
"events": [
"project.created",
"project.updated",
"deployment.completed",
"deployment.failed"
],
"status": "active",
"created_at": "2026-02-22T10:00:00Z"
} Eventos disponibles
| Evento | Descripcion | Payload clave |
|---|---|---|
project.created | Nuevo proyecto creado | project object |
project.updated | Proyecto modificado | project object + changes |
project.deleted | Proyecto eliminado | project id |
deployment.started | Despliegue iniciado | deployment object |
deployment.completed | Despliegue exitoso | deployment object + url |
deployment.failed | Despliegue fallido | deployment object + error |
team.member_added | Nuevo miembro en el equipo | team id + user object |
team.member_removed | Miembro eliminado del equipo | team id + user id |
Formato del payload
Cada webhook envia un POST con el siguiente formato JSON:
{
"id": "evt_p5q6r7s8",
"type": "deployment.completed",
"created_at": "2026-02-22T10:05:00Z",
"data": {
"id": "dpl_t9u0v1w2",
"project_id": "proj_a1b2c3",
"status": "success",
"url": "https://docs-website.cookbooks.dev",
"commit_sha": "a1b2c3d4",
"duration_seconds": 45
}
} Verificacion de firma con HMAC-SHA256
Cada peticion de webhook incluye el header
X-Cookbooks-Signature que debes verificar para asegurar que la peticion proviene de nuestra API y no ha sido alterada:
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_tu_secreto_para_verificar_firmas"
def verify_signature(payload_body, signature_header):
"""Verifica la firma HMAC-SHA256 del webhook."""
expected = hmac.new(
key=WEBHOOK_SECRET.encode("utf-8"),
msg=payload_body,
digestmod=hashlib.sha256,
).hexdigest()
received = signature_header.replace("sha256=", "")
return hmac.compare_digest(expected, received)
@app.route("/api/webhooks/cookbooks", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Cookbooks-Signature", "")
if not verify_signature(request.data, signature):
abort(401, "Firma invalida")
event = request.json
event_type = event["type"]
# Procesar segun el tipo de evento
if event_type == "deployment.completed":
handle_deployment_completed(event["data"])
elif event_type == "deployment.failed":
handle_deployment_failed(event["data"])
elif event_type == "project.created":
handle_project_created(event["data"])
# Responder 200 rapidamente para confirmar recepcion
return {"received": True}, 200 // Verificacion en Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'whsec_tu_secreto_para_verificar_firmas';
app.post('/api/webhooks/cookbooks',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-cookbooks-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).json({ error: 'Firma invalida' });
}
const event = JSON.parse(req.body);
console.log(`Evento recibido: ${event.type}`);
// Procesar de forma asincrona (no bloquear la respuesta)
processEvent(event).catch(console.error);
res.json({ received: true });
}
); Politica de reintentos de webhooks
Si tu endpoint no responde con un codigo
2xx dentro de 10 segundos, reintentaremos la entrega con el siguiente calendario:
| Intento | Espera | Tiempo acumulado |
|---|---|---|
| 1 (original) | Inmediato | 0 min |
| 2 | 1 minuto | 1 min |
| 3 | 5 minutos | 6 min |
| 4 | 30 minutos | 36 min |
| 5 | 2 horas | ~2.5 horas |
| 6 (ultimo) | 8 horas | ~10.5 horas |
Idempotencia en webhooks
Cada evento incluye un
id unico. Dado que un mismo evento puede entregarse mas de una vez (por reintentos), tu handler debe ser idempotente:
# Ejemplo: usar Redis para deduplicar eventos
import redis
r = redis.Redis()
def handle_webhook(event):
event_id = event["id"]
# Verificar si ya procesamos este evento
if r.get(f"webhook:processed:{event_id}"):
print(f"Evento {event_id} ya procesado, ignorando duplicado")
return {"received": True}
# Procesar el evento
process_event(event)
# Marcar como procesado (TTL de 7 dias)
r.set(f"webhook:processed:{event_id}", "1", ex=604800)
return {"received": True} Responde rapido, procesa despues
Tu endpoint de webhook debe responder con 200 lo mas rapido posible
(idealmente en menos de 5 segundos). Si necesitas procesamiento pesado,
encola el evento y procesalo en un worker en segundo plano.
SDK & Client Generation Tools
Nuestra API publica una especificacion OpenAPI 3.1 completa que puedes usar para generar clientes automaticamente en cualquier lenguaje. Tambien proporcionamos SDKs oficiales para Python y TypeScript.
Especificacion OpenAPI
La especificacion esta disponible en formato JSON y YAML:
# Descargar la especificacion OpenAPI
curl https://api.cookbooks.dev/v1/openapi.json -o openapi.json
# O en formato YAML
curl https://api.cookbooks.dev/v1/openapi.yaml -o openapi.yaml
# Validar la especificacion localmente
npx @redocly/cli lint openapi.json SDK oficial de Python
pip install cookbooks-sdk from cookbooks import CookbooksClient
# Inicializar el cliente
client = CookbooksClient(
api_key="sk_test_51Mz...",
base_url="https://api.cookbooks.dev/v1", # Opcional
timeout=30, # Timeout en segundos
max_retries=3, # Reintentos automaticos
)
# --- Proyectos ---
# Listar proyectos con filtros
projects = client.projects.list(
status="active",
sort="-created_at",
per_page=50,
)
for project in projects:
print(f"{project.id}: {project.name}")
# Crear un proyecto
new_project = client.projects.create(
name="api-docs",
framework="astro",
visibility="private",
settings={
"build_command": "npm run build",
"output_dir": "dist",
},
)
print(f"Proyecto creado: {new_project.id}")
# Actualizar un proyecto
updated = client.projects.update(
"proj_a1b2c3",
name="api-docs-v2",
description="Documentacion actualizada",
)
# Eliminar un proyecto
client.projects.delete("proj_a1b2c3")
# --- Deployments ---
# Listar deployments de un proyecto
deployments = client.deployments.list(project_id="proj_a1b2c3")
# Iniciar un nuevo deployment
deploy = client.deployments.create(
project_id="proj_a1b2c3",
branch="main",
)
print(f"Deployment: {deploy.id} β Status: {deploy.status}")
# --- Manejo de errores ---
from cookbooks.exceptions import (
NotFoundError,
RateLimitError,
ValidationError,
)
try:
project = client.projects.get("proj_inexistente")
except NotFoundError:
print("Proyecto no encontrado")
except RateLimitError as e:
print(f"Rate limited. Reintentar en {e.retry_after}s")
except ValidationError as e:
print(f"Error de validacion: {e.details}") SDK oficial de TypeScript/JavaScript
npm install @cookbooks/sdk import { CookbooksClient } from '@cookbooks/sdk';
// Inicializar el cliente
const client = new CookbooksClient({
apiKey: 'sk_test_51Mz...',
timeout: 30000, // Timeout en ms
maxRetries: 3, // Reintentos automaticos
});
// Listar proyectos
const { data: projects, meta } = await client.projects.list({
status: 'active',
sort: '-created_at',
perPage: 50,
});
console.log(`Total: ${meta.total} proyectos`);
projects.forEach(p => console.log(`${p.id}: ${p.name}`));
// Crear un proyecto
const newProject = await client.projects.create({
name: 'api-docs',
framework: 'astro',
visibility: 'private',
settings: {
buildCommand: 'npm run build',
outputDir: 'dist',
},
});
// Actualizar un proyecto
const updated = await client.projects.update('proj_a1b2c3', {
name: 'api-docs-v2',
});
// Eliminar un proyecto
await client.projects.delete('proj_a1b2c3');
// Manejo de errores con tipos
import {
NotFoundError,
RateLimitError,
ValidationError,
} from '@cookbooks/sdk';
try {
const project = await client.projects.get('proj_inexistente');
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Proyecto no encontrado');
} else if (error instanceof RateLimitError) {
console.log(`Reintentar en ${error.retryAfter}s`);
} else if (error instanceof ValidationError) {
console.log('Errores:', error.details);
}
} Generar clientes con OpenAPI Generator
Si prefieres otro lenguaje, genera un cliente automaticamente desde la especificacion OpenAPI:
# Instalar OpenAPI Generator
npm install -g @openapitools/openapi-generator-cli
# Generar cliente en Go
openapi-generator-cli generate \
-i https://api.cookbooks.dev/v1/openapi.json \
-g go \
-o ./cookbooks-go-client \
--additional-properties=packageName=cookbooks
# Generar cliente en Java
openapi-generator-cli generate \
-i https://api.cookbooks.dev/v1/openapi.json \
-g java \
-o ./cookbooks-java-client \
--additional-properties=artifactId=cookbooks-client
# Generar cliente en Rust
openapi-generator-cli generate \
-i https://api.cookbooks.dev/v1/openapi.json \
-g rust \
-o ./cookbooks-rust-client Coleccion de Postman
Importa nuestra coleccion de Postman para explorar y probar la API de forma interactiva:
# Descargar la coleccion de Postman
curl https://api.cookbooks.dev/v1/postman-collection.json \
-o cookbooks-postman-collection.json Tambien puedes importarla directamente en Postman usando esta URL:
https://api.cookbooks.dev/v1/postman-collection.json La coleccion incluye:
- Todos los endpoints organizados por recurso
- Variables de entorno preconfiguradas (
base_url,api_key) - Ejemplos de request y response para cada operacion
- Scripts de pre-request para autenticacion automatica
- Tests automaticos para validar respuestas
Documentacion interactiva
Ademas de Postman, puedes explorar la API directamente en tu navegador visitando
https://api.cookbooks.dev/docs (Swagger UI) o
https://api.cookbooks.dev/redoc (ReDoc).
Ambas interfaces se generan automaticamente desde la especificacion OpenAPI.