Recetario cURL
Autenticación, operaciones con suscriptores, consentimiento y errores desde la terminal con cURL.
Estos ejemplos usan la API pública actual de suscriptores. Necesitas Bash, cURL, jq y una clave live de la marca correcta.
Peticiones sobre datos reales
Las lecturas requieren subscribers:read y las escrituras subscribers:write (o permisos que los incluyan). Una clave sin permisos suficientes recibe 403 missing_scope. Las claves test reciben 403 test_mode_unavailable: no existe un sandbox operativo. Usa contactos propios y consentimiento verificable.
Preparación
Exporta la clave una vez por sesión. No la guardes en el repositorio ni la publiques en registros:
export FLUYIO_API_KEY="fly_live_xxx"
export FLUYIO_API_VERSION="2026-05-15"Este ayudante añade las cabeceras comunes:
fluyio() {
local method="$1"; shift
local path="$1"; shift
curl --silent --show-error --globoff \
-X "$method" "https://fluyio.com/api/v1${path}" \
-H "Authorization: Bearer ${FLUYIO_API_KEY}" \
-H "Fluyio-Version: ${FLUYIO_API_VERSION}" \
-H "Content-Type: application/json" \
"$@"
}Así puedes ejecutar fluyio GET /subscribers. La opción --globoff conserva los corchetes de filter[...].
Añade | jq para leer el JSON con formato. Los identificadores y las respuestas de esta página son ejemplos.
Comprobar la autenticación
No hay un endpoint /me. Una consulta de un elemento permite comprobar una clave con permiso de lectura:
curl --silent --show-error -i "https://fluyio.com/api/v1/subscribers?limit=1" \
-H "Authorization: Bearer ${FLUYIO_API_KEY}" \
-H "Fluyio-Version: 2026-05-15" | head -20HTTP/2 200
fluyio-request-id: req_3f9a1c7e5b2d8f4a6c0e9b1d3f
fluyio-version: 2026-05-15
fluyio-mode: live
ratelimit-limit: 150
ratelimit-remaining: 149
ratelimit-reset: 1715800230
ratelimit-policy: 150;w=10, 600;w=60Suscriptores
Crear
El ejemplo declara consentimiento explícito para email y SMS. Úsalo solo si dispones de ese consentimiento para ambos canales. Si omites el consentimiento de email, se crea como never_subscribed (estado interno pendiente), no como suscriptor activo.
fluyio POST /subscribers \
-H "Idempotency-Key: signup-2026-05-15-ada" \
-d '{
"email": "[email protected]",
"phone": "+34600000000",
"first_name": "Ada",
"last_name": "Lovelace",
"external_id": "user_12345",
"properties": { "plan": "pro", "company": "Analytical Engines" },
"consent": { "email": "subscribed", "sms": "subscribed" }
}' | jq{
"id": "sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8",
"object": "subscriber",
"email": "[email protected]",
"phone": "+34600000000",
"first_name": "Ada",
"last_name": "Lovelace",
"external_id": "user_12345",
"consent": { "email": "subscribed", "sms": "subscribed", "whatsapp": "never_subscribed" },
"properties": { "plan": "pro", "company": "Analytical Engines" },
"lists": [],
"tags": [],
"created_at": "2026-05-15T12:00:00.000Z",
"updated_at": "2026-05-15T12:00:00.000Z",
"last_active_at": null
}Repetir la creación con el mismo email y otra operación puede devolver 409 resource_already_exists. La misma clave de idempotencia y la misma petición reproducen un resultado completado dentro de su ventana; un resultado incierto tiene un tratamiento distinto, descrito en idempotencia.
También se admite consent.whatsapp: "subscribed" al crear, con teléfono E.164 y consentimiento explícito para WhatsApp. Omitirlo no genera un alta.
Recuperar
fluyio GET /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8 | jqBuscar por email o por tu identificador
fluyio GET "/subscribers?filter[email][email protected]&limit=1" | jq '.data[0]'
fluyio GET "/subscribers?filter[external_id]=user_12345&limit=1" | jq '.data[0].id'Listar con paginación
Primera página, siguiente página y ejemplo de filtros:
# First page, newest first
fluyio GET "/subscribers?limit=100" | jq '{count: (.data | length), has_more, next_cursor}'
# Next page
fluyio GET "/subscribers?limit=100&cursor=MjAyNi0wNS0xMVQxMjozNDo1Ni4wMDBafDlm..." | jq '.data[].email'
# Everyone created in May 2026 who can receive email, oldest first
fluyio GET "/subscribers?filter[created_at_gte]=2026-05-01T00:00:00Z&filter[created_at_lte]=2026-05-31T23:59:59Z&filter[has_consent]=email&sort=created_at" | jq '.data[].email'Para recorrer una colección:
cursor=""
while :; do
page=$(fluyio GET "/subscribers?limit=100${cursor:+&cursor=$cursor}")
echo "$page" | jq -r '.data[] | [.id, .email, .consent.email] | @tsv'
[ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
cursor=$(echo "$page" | jq -r '.next_cursor')
doneDetén el proceso ante errores HTTP o de análisis JSON; este bucle ilustra la paginación, no un importador con recuperación completa.
Actualizar
fluyio PATCH /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8 \
-d '{ "first_name": "Ada", "properties": { "plan": "enterprise" } }' | jq '.properties'properties se combina por clave y conserva las propiedades no enviadas. Las claves que empiezan por __ se rechazan. Cambiar email o teléfono no transfiere el consentimiento al nuevo destino; consulta actualización.
Consentimiento
Registra únicamente hechos de consentimiento que puedas acreditar. El valor source describe el origen; no ejecuta un proceso de doble confirmación por sí solo.
# Opt in to WhatsApp (the subscriber needs a phone)
fluyio POST /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8/subscribe \
-d '{ "channel": "whatsapp", "source": "double_opt_in" }' | jq '.consent'
# Opt out of email
fluyio POST /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8/unsubscribe \
-d '{ "channel": "email", "reason": "user_request" }' | jq '.consent'
# Opt out of everything
fluyio POST /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8/unsubscribe \
-d '{ "channel": "all", "reason": "gdpr_request" }' | jq '.consent'Las bajas conservan las supresiones existentes. SMS devuelve unsubscribed tras una baja. El alta no elimina estados como email bounced/complained o WhatsApp blocked/bounced: devuelve 409 channel_unavailable.
Eliminar
La eliminación es permanente; comprueba el identificador antes de ejecutar:
fluyio DELETE /subscribers/sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8 | jq{ "id": "sub_9f1c2b3a4d5e6f708192a3b4c5d6e7f8", "object": "subscriber", "deleted": true }Buscar y crear o actualizar desde la terminal
No existe un endpoint de upsert atómico. Este ejemplo busca y después crea o actualiza; otras escrituras simultáneas aún pueden provocar conflictos. Proporciona una clave estable por operación lógica, sin email ni datos personales dentro de la clave:
upsert() {
local email="$1" body="$2" operation_key="$3"
[ -n "$operation_key" ] || return 2
local existing
existing=$(fluyio GET /subscribers --fail-with-body --get \
--data-urlencode "filter[email]=$email" --data-urlencode "limit=1" \
| jq -er 'if .object == "list" then (.data[0].id // "") else error("lookup failed") end') || return
if [ -n "$existing" ]; then
fluyio PATCH "/subscribers/${existing}" -H "Idempotency-Key: ${operation_key}-update" -d "$body"
else
fluyio POST /subscribers -H "Idempotency-Key: ${operation_key}-create" -d "$body"
fi
}
upsert [email protected] '{ "email": "[email protected]", "properties": { "plan": "pro" } }' 'ada-profile-v1' | jq '.id'El ejemplo solo sincroniza el perfil: si crea un contacto sin opt-in de email, queda never_subscribed. No vuelve a suscribir contactos existentes ni sustituye la gestión de bajas. Si falla una escritura, reconcilia su resultado antes de volver a ejecutar toda la función: la búsqueda podría elegir otra rama. No cambies la clave para forzar una operación cuyo resultado sea incierto.
Gestionar errores
Los errores de los endpoints publicados usan este formato:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_param",
"message": "Either \"email\" or \"phone\" must be provided.",
"request_id": "req_3f9a1c7e5b2d8f4a6c0e9b1d3f"
}
}Añade -i para ver el estado HTTP y las cabeceras:
fluyio POST /subscribers -i -d '{}'Con --fail-with-body, cURL devuelve un código de salida distinto de cero ante errores HTTP y conserva el cuerpo. Guarda el request_id de los errores inesperados para que soporte pueda localizarlos.
Límites de peticiones
Las cabeceras indican la ventana más restrictiva:
Ratelimit-Limit: 150
Ratelimit-Remaining: 149
Ratelimit-Reset: 1715800230
Ratelimit-Policy: 150;w=10, 600;w=60Un 429 Too Many Requests incluye Retry-After. Este ayudante reintenta solo 429, respeta los segundos indicados y se detiene si falta una cabecera válida, si falla el transporte o tras cinco reintentos:
fluyio_retry() {
local attempt=0 out status delay headers
headers=$(mktemp) || return
while :; do
out=$(fluyio "$@" --dump-header "$headers" -w $'\n%{http_code}') || {
rm -f "$headers"
return 1
}
status=${out##*$'\n'}
if [ "$status" != "429" ] || [ "$attempt" -ge 5 ]; then
printf '%s\n' "${out%$'\n'*}"
rm -f "$headers"
case "$status" in 2??) return 0 ;; *) return 1 ;; esac
fi
delay=$(awk 'tolower($1) == "retry-after:" {gsub("\r", "", $2); value=$2} END {print value}' "$headers")
case "$delay" in
''|*[!0-9]*)
printf '%s\n' "${out%$'\n'*}"
rm -f "$headers"
return 1
;;
esac
sleep "$delay"
attempt=$(( attempt + 1 ))
done
}En escrituras, pasa siempre la misma Idempotency-Key durante los reintentos de la misma operación. Un 5xx o una pérdida de respuesta puede dejar un resultado incierto reservado durante 24 horas: este ayudante no lo reintenta automáticamente. Reconcilia el estado y consulta idempotencia. No uses otra clave para eludir esa reserva.
Un 503 idempotency_unavailable indica que no se inició la operación; no garantiza una reserva creada. Reintenta la misma operación con la misma clave, sin generar otra para eludir la protección.
Consulta límites de peticiones para los detalles.
También puede interesarte
- Referencia de suscriptores: parámetros y errores.
- Paginación: cursores y filtros.
- Idempotencia: resultados completados e inciertos.
- Sincronizar suscriptores: importación y sincronización de perfiles.