fluyio.
HTTP y herramientas

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 -20
HTTP/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=60

Suscriptores

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 | jq

Buscar 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')
done

Deté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=60

Un 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

En esta página