Guías

Errores

Los errores son documentos de problema RFC 9457. Ramifica según el code estable, nunca según el texto, y usa la tabla de reintentos para decidir si volver a intentarlo.

Un error de la API tiene el tipo de contenido application/problem+json. code es estable y está pensado para el control de flujo; type es el mismo código como URI, https://docs.filmo.io/errors/<code>, que lleva a su página de documentación, y title es su nombre corto en inglés. Para un código dado, type, title y code nunca cambian. Ramifica según code, no según la URI.

Un validation_error con dos errores de campo
{
  "type": "https://docs.filmo.io/errors/validation_error",
  "title": "Validation error",
  "status": 400,
  "code": "validation_error",
  "detail": "2 validation errors",
  "errors": [
    { "path": "lines[0].itemId", "code": "not_found", "message": "The lines[0].itemId identifier does not exist or does not belong to this account" },
    { "path": "endDate", "code": "must_be_after", "message": "endDate must be after startDate" }
  ],
  "requestId": "req_5f1c2f4e3a5b4f6d8e7a0b1c2d3e4f5a"
}
PropiedadSignificado
type, titleEl código como URI, y su nombre corto en inglés.
statusEl estado HTTP de la respuesta.
codeEl código de error estable. Mapéalo en tu cliente.
detailUna explicación de esta ocurrencia, para personas. No la uses para el control de flujo.
requestIdEl identificador de la petición; también la cabecera X-Request-Id. Cítalo cuando contactes con soporte.
errorsEn validation_error, cada campo incorrecto; en contact_in_use, address_in_use y insufficient_stock, lo que bloquea la operación; en conflict, la causa en errors[0].code.
requiredScopeEn insufficient_scope: el permiso (scope) que necesita la clave.
requiredAddonEn addon_required: el add-on que necesita el módulo.
retryAfterEn rate_limited: los segundos que hay que esperar antes de reintentar, el mismo valor que la cabecera Retry-After.
EstadoCódigoSignificado
401unauthorizedLa petición no lleva ninguna credencial Authorization: Bearer.
401api_key_invalidLa clave no se reconoce.
401api_key_revokedLa clave fue revocada.
401api_key_expiredLa clave ha caducado.
401api_key_owner_inactiveEl administrador propietario de la clave salió de la cuenta o fue desactivado.
401api_key_owner_not_adminEl propietario de la clave ya no es administrador.
403insufficient_scopeLa clave no tiene el scope que exige la operación.
403forbiddenEl propietario de la clave carece del módulo, la organización o el permiso de administrador que exige la operación.
402addon_requiredLa cuenta no ha contratado, ha dejado caducar o ha perdido con el fin de la prueba gratuita el add-on de ese módulo.
400validation_errorLa consulta o el cuerpo no es válido. errors[] lista cada campo erróneo.
422idempotency_key_reusedLa Idempotency-Key ya se usó con un cuerpo distinto.
404not_foundEl recurso no existe o queda fuera del aislamiento de la clave. No se pueden distinguir.
409conflictUna petición con esa clave sigue en curso (in_progress), una escritura condicional retuvo el recurso más de 5 segundos (lock_timeout), o el estado del recurso impide la operación (invalid_state). La causa está en errors[0].code.
409contact_in_useOtros registros siguen usando el contacto; errors[] los cuenta por tipo.
409address_in_useLos pedidos siguen usando la dirección como dirección de facturación o de envío.
409insufficient_stockNo hay stock suficiente para la petición; errors[] tiene una entrada por cada artículo que falta.
412precondition_failedLa etiqueta If-Match ya no coincide: el recurso cambió desde que lo leíste.
429rate_limitedSe superó un límite de peticiones; Retry-After, repetido en retryAfter, indica cuánto esperar.
500internal_errorUn error interno. También es la respuesta si falla el almacén que cuenta los límites de peticiones: la API no se salta el límite en ese caso, así que reintenta con backoff.
503service_unavailableLa función está desactivada para la cuenta. Reintentar no ayuda hasta que se active.

Los códigos 401 se comprueban en un orden fijo: sin credencial bearer (unauthorized), después una clave no reconocida (api_key_invalid), luego una revocada, una caducada y, por último, un propietario inactivo o que ya no es administrador. Un 401 y un 403 insufficient_scope no consumen cuota del límite de peticiones; el resto de respuestas de una clave autenticada sí.

Cada código tiene su propia página con las operaciones que lo devuelven y qué hacer. Los códigos de autenticación también se tratan en Autenticación.

Tres rechazos distintos se parecen y se arreglan en tres sitios distintos. Mandarlos todos al mismo sitio te lleva a depurar en el equivocado.

Estado y códigoTrata deQué fallaQuién lo arregla
403 insufficient_scopeLa claveLa clave no tiene el scope que exige el endpoint; requiredScope lo nombra.Vuelve a emitir la clave con ese permiso (scope).
403 forbiddenEl propietario de la claveLa clave es correcta, pero su propietario carece del módulo, la organización o el permiso de administrador que exige la operación.Un administrador de la cuenta concede el permiso.
402 addon_requiredEl add-onLa cuenta no ha contratado, o ha dejado caducar, el add-on del módulo; requiredAddon lo nombra. Ningún rol lo evita. La API y el MCP no son un add-on.Quien gestione la suscripción.

Artículos del Centro de ayuda: Scopes y permisos de la API, Roles y permisos y Tu suscripción. Consulta también Permisos (scopes).

errors[] lista todos los campos incorrectos, no solo el primero. path usa notación de puntos y corchetes (lines[0].itemId) y está vacío cuando el problema es todo el cuerpo. El campo code es estable. Estos son los códigos vistos hasta ahora; la lista no es exhaustiva, así que trata uno desconocido como un campo no válido genérico:

  • required, must_not_be_null
  • invalid_type, invalid_value, invalid_format
  • too_small, too_big
  • unknown_field
  • not_found
  • must_be_after

Un cuerpo que hace referencia a un identificador fuera de tu aislamiento, como el contacto de un pedido, es un 400 que nombra el campo, y no se escribe nada. El mensaje es el mismo tanto si el identificador no existe como si pertenece a otra cuenta, así que no sirve para sondear ninguno de los dos casos. Leer un recurso fuera de tu aislamiento es un 404 idéntico a que no exista.

Un 409 conflict indica el motivo en errors[0].code. La causa decide si esperar sirve de algo:

errors[0].codeCausaQué hacer
in_progressOtra petición con el mismo Idempotency-Key sigue ejecutándose.Espera y reintenta con la misma clave. No envíes un segundo intento en paralelo.
lock_timeoutOtra escritura condicional (If-Match) retuvo el recurso más de 5 segundos.Reintenta con espera. Si sigue ocurriendo, lee el recurso.
invalid_stateEl estado del recurso prohíbe la operación, o la escritura rompió una regla de unicidad. Una línea de un lote atómico también lo notifica cuando la línea está en un estado incorrecto; una línea que falta o una cantidad no válida notifican not_found o validation_error.No reintentes: esperar no cambia el estado. Lee el recurso y decide.

Reintenta in_progress y lock_timeout como máximo 3 veces con espera exponencial y la misma clave, y después lee el recurso. Trata un conflicto 409 con cualquier otro errors[0].code, o sin ninguno, como invalid_state tras esos 3 reintentos.

errors[] tiene una entrada por tipo de registro que usa el contacto o la dirección: path es el tipo y count cuántos registros de ese tipo lo usan. Los valores de path salen de un conjunto cerrado, que figura en el x-path-values de la propiedad path en la especificación: para contact_in_use, orders, projects, quotes, invoices, proformas, creditNotes, simpleCreditNotes, salesReceipts, supplierInvoices, contracts, subrents, purchaseOrders, sepaMandates, receipts y consignments; para address_in_use, orders. El conjunto puede crecer, así que tolera un valor que no conozcas. Cuando algo usa el contacto, no se borra nada.

Una entrada por artículo, o por espacio, que se queda corto. path es items o spaces, y cada entrada tiene itemId, requested, available y shortfall.

detail y cada errors[].message llegan en el idioma negociado con la cabecera Accept-Language. es-ES y en-US están garantizados; cualquier otro idioma admitido recurre al inglés. Sin la cabecera, una petición hecha con una clave que existe usa el idioma preferido del propietario de la clave, también cuando el error es un 401 por una clave revocada o caducada o un 403 insufficient_scope. Una petición sin credencial, o con una clave que Filmo no reconoce, no tiene propietario al que preguntar: su 401 llega en español salvo que envíes Accept-Language. Envía siempre la cabecera. Content-Language te dice cuál usó la respuesta. No escribas nunca lógica que dependa de esos textos. Consulta Idiomas.

Algunos fallos ocurren antes de que la petición llegue a la API o después de que salga de ella: una función que agota el tiempo de espera (504), un cuerpo de petición mayor de lo que acepta la plataforma (413), un bad gateway (502) o un desafío de cortafuegos. Llegan sin documento de problema, así que no hay code ni requestId. Los reconoces porque el tipo de contenido no es application/problem+json. No intentes leer code en ellos: decide por el estado HTTP.

  • 502 y 504: el resultado es desconocido. Reintenta con la misma Idempotency-Key; en una escritura sin clave, lee primero el recurso.
  • 413: el cuerpo es demasiado grande. Reintentar no sirve; envía menos datos en cada petición.
  • La plataforma genera 413, 502 y 504; la API nunca los envía como documento de problema. No dependas de ninguna cabecera específica de la plataforma para reconocer estas respuestas: cualquier 5xx sin application/problem+json es una de ellas.
  • Timeouts: POST /api/v1/ai/ask puede tardar hasta 300 segundos, así que da a esa llamada un timeout de cliente largo. Para cualquier otra ruta, Filmo no documenta ningún límite; define tus propios timeouts de cliente y gestiona un 504.

Una política por estado no basta. El mismo 409 puede significar espera y reintenta o no reintentes nunca, un 500 puede repetirse durante 24 horas, y un 207 no se arregla reenviando la misma petición. Ante un 409 conflict, lee primero la causa en errors[0].code (ver arriba). Después busca la primera fila que coincida con tu respuesta.

EstadoIdempotency-KeyIdempotent-Replay: trueQué hacer
409 conflict · in_progressSíNoerrors[0].code es in_progress: otra petición con la misma Idempotency-Key sigue en curso. Espera y reintenta con la misma clave (espera exponencial, hasta 3 veces): recibirás su respuesta guardada cuando termine.
409 conflict · lock_timeout—Noerrors[0].code es lock_timeout: otra escritura condicional retuvo el recurso más de 5 s. Reintenta con la misma clave y espera exponencial, hasta 3 veces; si persiste, vuelve a leer el recurso.
409 conflict · invalid_state——errors[0].code es invalid_state: el estado del recurso impide la operación (una violación de unicidad, una línea de un lote atómico). No reintentes: esperar no lo arregla nunca. Vuelve a leer el recurso y decide según su estado actual.
409 contact_in_use · address_in_use · insufficient_stock——No reintentes. errors[] dice qué bloquea la operación: corrígelo primero.
412 precondition_failed——Vuelve a leer el recurso, rehaz tu cambio sobre él y envía el ETag nuevo.
422 idempotency_key_reusedSí—Es un fallo del cliente: la clave se usó con otro cuerpo. Usa una clave nueva solo si de verdad es otra operación.
429 rate_limited——Espera los segundos de Retry-After y reintenta con la misma clave.
500 internal_errorSíSíPara. El fallo ocurrió después del commit y se guarda y se repite durante 24 h con esa clave: lee el recurso para ver dónde quedó.
500 internal_errorSíNoReintenta con espera exponencial y la misma clave.
500 internal_errorNo—Lee antes de reintentar una escritura: sin clave nada te protege de un duplicado.
503 service_unavailable——No reintentes: la función está desactivada en la cuenta.
207 (returnOrderLineItems con atomic: false)Sí—Las líneas que fallaron no se devolvieron y el resto sí. Reenvía solo las fallidas con una clave nueva; repetir la clave devolvería el mismo 207.
413 · 502 · 504 (no es application/problem+json)Sí—Respuesta de la plataforma, no de la API: decide por el estado y reintenta con la misma clave. Un 413 no se arregla reintentando (el cuerpo es demasiado grande).
Cualquier 4xx de validación——Corrige la petición y reenvíala; nunca en bucle.
401 · 402 · 403 (clave, propietario o plan)——No reintentes: reenviar la misma petición vuelve a fallar. Actúa sobre la clave, su propietario o el plan: usa una clave válida con el permiso adecuado, pide a un administrador el permiso que falta o contrata el add-on.

La clave es lo que hace seguros los reintentos: consulta Idempotencia. Para las escrituras que usan If-Match, consulta Concurrencia.

Este helper envuelve cualquier llamada y aplica las decisiones anteriores. Cópialo, cambia el nombre de las variables de entorno si guardas tu clave en otro sitio, y llámalo en lugar de fetch, requests o curl.

  • Toda escritura que declara la cabecera Idempotency-Key lleva una: la que pasas o un UUID nuevo. La misma clave se envía en cada reintento de esa llamada. El ayudante no envía ninguna en los POST de consulta (las comprobaciones de disponibilidad, la búsqueda de contactos y POST /api/v1/ai/ask), que no declaran la cabecera y la ignoran.
  • 429: espera Retry-After segundos y reintenta, hasta 5 veces.
  • 409 conflict: con in_progress y lock_timeout, hasta 3 reintentos con espera exponencial; después se detiene y te indica que leas el recurso; con invalid_state se detiene de inmediato y te dice lo mismo.
  • 500 con Idempotent-Replay: true: se detiene de inmediato y te indica que leas el recurso.
  • 500 sin esa cabecera: hasta 3 reintentos con espera exponencial y la misma clave. En una escritura enviada sin clave, se detiene y te indica que leas el recurso.
  • 502, 503 y 504 sin documento de problema, y fallos de red: se tratan como un 500 sin la cabecera de repetición.
  • Todo lo demás falla de inmediato, con el estado y el code, y una action que dice qué hacer a continuación.
#!/usr/bin/env bash
# filmo-request.sh: one function for any call to the Filmo REST API.
# Needs curl and jq, and FILMO_API_KEY; FILMO_BASE_URL is optional.
#
#   filmo_request METHOD PATH [JSON_BODY]      (put the query string in PATH)
#
# Writes get an Idempotency-Key: IDEMPOTENCY_KEY if set, else a fresh UUID. The
# query POSTs (availability checks, contacts lookup, ai ask) do not declare the header, so they get none.
# Set NO_IDEMPOTENCY_KEY=1 to send none. The body goes to stdout when the call succeeds.
# Optional: ACCEPT_LANGUAGE (e.g. en) sets the language of error messages; IF_MATCH sends an ETag.
# Exit code: 0 done; 10 fix the request; 11 read the resource before anything else;
#            12 read again and redo the change (412); 13 give up.
FILMO_MAX_RETRIES=3

filmo_request() {
  local method="$1" path="$2" body="$3"
  local base="${FILMO_BASE_URL:-https://app.filmo.io}"
  local is_write=0 key="" retries=0 waits=0
  [ "$method" != "GET" ] && is_write=1
  case "${path%%[?]*}" in
    /api/v1/availability/check | /api/v1/staff/availability | /api/v1/vehicles/availability | /api/v1/contacts/lookup | /api/v1/ai/ask) is_write=0 ;;
  esac
  if [ "$is_write" = 1 ] && [ "$NO_IDEMPOTENCY_KEY" != 1 ]; then
    key="${IDEMPOTENCY_KEY:-$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)}"
  fi
  local hdr out
  hdr="$(mktemp)"
  out="$(mktemp)"

  while :; do
    set -- -sS -X "$method" -D "$hdr" -o "$out" -w '%{http_code}' \
      -H "Authorization: Bearer $FILMO_API_KEY" -H "Accept: application/json"
    [ -n "$key" ] && set -- "$@" -H "Idempotency-Key: $key" # the same key on every retry
    [ -n "$ACCEPT_LANGUAGE" ] && set -- "$@" -H "Accept-Language: $ACCEPT_LANGUAGE"
    [ -n "$IF_MATCH" ] && set -- "$@" -H "If-Match: $IF_MATCH"
    [ -n "$body" ] && set -- "$@" -H "Content-Type: application/json" --data "$body"
    local status
    status="$(curl "$@" "$base$path")" || status=000 # network failure: it may or may not have arrived

    local problem=0 code="" cause="" replay="" wait=""
    if [ "$status" != 000 ]; then
      grep -qi '^content-type:.*application/problem+json' "$hdr" && problem=1
      if [ "$problem" = 1 ]; then
        code="$(jq -r '.code // empty' "$out" 2>/dev/null)"
        cause="$(jq -r '.errors[0].code // empty' "$out" 2>/dev/null)" # why a 409 happened
      fi
      grep -qi '^idempotent-replay: true' "$hdr" && replay=true
      wait="$(grep -i '^retry-after:' "$hdr" | tail -1 | tr -d '\r' | awk '{print $2}')"
    fi

    if [ "$status" != 000 ] && [ "$status" -lt 400 ]; then
      cat "$out"
      rm -f "$hdr" "$out"
      return 0
    fi

    if [ "$status" = 429 ]; then
      # Wait as long as the server says, then retry with the same key.
      [ "$waits" -ge 5 ] && { _filmo_fail "$status" "$code" "$out" "$hdr"; return 13; }
      waits=$((waits + 1))
      sleep "$([ "${wait:-0}" -gt 0 ] 2>/dev/null && echo "$wait" || echo 1)"
      continue
    fi
    if [ "$status" = 409 ] && [ "$code" = conflict ]; then
      # errors[0].code says why. in_progress and lock_timeout clear by waiting;
      # invalid_state (the state of the resource forbids the call) never does.
      if [ "$cause" = invalid_state ] || [ "$retries" -ge "$FILMO_MAX_RETRIES" ]; then
        _filmo_fail "$status" "$code" "$out" "$hdr"
        return 11
      fi
      sleep $((1 << retries)) # 1 s, 2 s, 4 s
      retries=$((retries + 1))
      continue
    fi
    # A 500 from the API, or a platform failure (network error, or a 502/503/504 that is not a problem document).
    if [ "$status" = 500 ] || [ "$status" = 000 ] ||
      { [ "$problem" = 0 ] && { [ "$status" = 502 ] || [ "$status" = 503 ] || [ "$status" = 504 ]; }; }; then
      # The failure happened after the commit and is stored for 24 h: retrying returns it again.
      if [ "$replay" = true ] || { [ "$is_write" = 1 ] && [ -z "$key" ]; }; then
        _filmo_fail "$status" "$code" "$out" "$hdr"
        return 11
      fi
      if [ "$retries" -ge "$FILMO_MAX_RETRIES" ]; then
        _filmo_fail "$status" "$code" "$out" "$hdr"
        return 13
      fi
      sleep $((1 << retries))
      retries=$((retries + 1))
      continue
    fi
    _filmo_fail "$status" "$code" "$out" "$hdr"
    if [ "$status" = 412 ]; then return 12; fi
    if [ "$status" -ge 500 ]; then return 13; fi
    return 10
  done
}

_filmo_fail() { # status code body-file header-file
  local rid
  rid="$(grep -i '^x-request-id:' "$4" 2>/dev/null | tr -d '\r' | awk '{print $2}')"
  echo "filmo: HTTP $1 ${2:+($2) }request $rid" >&2
  cat "$3" >&2 2>/dev/null
  rm -f "$3" "$4"
}

# 207 (returnOrderLineItems with atomic false): resend only the lines that failed,
# each round with a NEW key, because the same key would replay the same 207.
# Usage: return_lines '[{"lineItemId":"...","checkInQuantity":1,"returnCondition":"good"}]'
return_lines() {
  local pending="$1" round res
  for round in 1 2 3; do
    [ "$(echo "$pending" | jq 'length')" = 0 ] && break
    res="$(IDEMPOTENCY_KEY="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)" \
      filmo_request POST /api/v1/orders/line-items/returns \
      "$(jq -c '{items: ., atomic: false}' <<<"$pending")")" || return $?
    # Keep only the elements that failed (their index in what we sent).
    pending="$(jq -c --argjson sent "$pending" \
      '[.data[] | select(.status == "failed") | $sent[.index]]' <<<"$res")"
  done
  echo "$pending" # the lines that still failed; [] when everything was returned
}
action (código de salida en bash)Significado
fix-request (10)La petición es incorrecta. Corrígela; no la reintentes tal cual.
read-resource (11)El resultado es desconocido o está almacenado. Lee el recurso para ver en qué punto está antes de hacer nada más.
reread-and-redo (12)412: vuelve a leer el recurso y rehaz el cambio con el nuevo ETag.
give-up (13)El helper agotó los reintentos, o el fallo no se resolverá reintentando.

Para reanudar una escritura tras un reinicio, guarda la clave que generaste y pásala de nuevo: una clave nueva crearía un segundo recurso. El caso 207 solo se aplica a POST /api/v1/orders/line-items/returns enviado con atomic: false; returnLines (return_lines en bash y Python) reenvía las líneas fallidas con una clave nueva en cada ronda.

Una herramienta MCP informa de un fallo como un resultado con isError: true, y del mismo problema RFC 9457 como texto JSON en content[0].text. No hay structuredContent en un error. Analiza el texto y ramifica según code como haces con REST. El problema 429 lleva retryAfter. Lo que REST envía como cabeceras, un resultado de herramienta lo lleva en meta: meta.idempotencyKey, meta.idempotentReplay y meta.rateLimit (limit, remaining, reset).

Cuando contactes con soporte, incluye el requestId de la respuesta que falla. Consulta Soporte o el artículo del Centro de ayuda Contactar con soporte.

  • Guías

    Idempotencia y reintentos seguros

    Envía una cabecera Idempotency-Key con cada escritura y reutiliza el mismo valor cuando reintentes esa operación: Filmo devuelve el resultado almacenado del primer intento en lugar de escribir dos veces.

  • Guías

    Límites de peticiones

    Cada clave de API tiene 120 peticiones por minuto, y las tres comprobaciones de disponibilidad comparten su propia cuota de 30 por minuto. Al pasar un límite, la API responde 429 con Retry-After.

  • Guías

    Concurrencia y bloqueo optimista

    Cada lectura de detalle devuelve un ETag. Envíalo de vuelta en la cabecera If-Match de una escritura y Filmo rechaza la escritura con 412 si el registro ha cambiado desde que lo leíste, de modo que dos integraciones no puedan sobrescribirse en silencio.

  • Guías

    Lista de comprobación de salida a producción

    Repasa estas ocho comprobaciones antes de que tu integración funcione con datos de producción. Cada una remite a la guía que la explica.

¿Te ha resultado útil esta página?