Gestiona resultados vacíos y reintentos sin pagar dos veces
Lo que vas a construir: un envoltorio de llamada que distingue tres desenlaces — encontrado, no encontrado y fallo — y reintenta solo el tercero, con idempotencia para que un reintento nunca pueda cobrar dos veces.
| Sección | Fundamentos de la API |
|---|---|
| Acciones usadas | POST /v1/data/contacts/find-email |
| Créditos | 5 (peor caso) |
| Compatible con clave de prueba | Sí — se ejecuta con qk_test_ por cero créditos |
| Tiempo estimado | ~10 min |
| Requisitos previos | Una clave y un cliente capaz de fijar cabeceras de petición. |
Tres desenlaces, no dos
Un resultado encontrado es HTTP 200 con data poblada y un cargo distinto de cero. Un resultado vacío es HTTP 200 con data: null y X-Credits-Charged: 0 — la búsqueda se hizo con honestidad y no encontró nada, así que no se cobró nada. Un fallo es un 4xx o un 5xx. Solo el tercero merece reintento.
# FOUND — data populated, charged
# < 200 X-Credits-Charged: 5
{ "status": "OK", "data": { /* … */ } }
# MISS — ran honestly, found nothing, billed zero
# < 200 X-Credits-Charged: 0
{ "status": "OK", "data": null }
# FAILURE — nothing billed, safe to retry
# < 503No reintentes un resultado vacío
Un resultado vacío es una respuesta real sobre el mundo, no un error transitorio. Reintentarlo no hará aparecer un resultado; solo añade latencia. Cachéalo durante una ventana razonable y sigue adelante.
# Branch on the payload BEFORE the status code:
# a miss and a hit are both HTTP 200.
if res.status_code >= 500:
return retry_later() # transient
if res.json()["data"] is None:
cache_miss(query, ttl=86_400) # a real answer — do not retry
return None
return res.json()["data"]Reintenta los fallos con idempotencia
Los endpoints de datos aceptan la cabecera Idempotency-Key. Envía una clave estable por operación lógica y un reintento tras un timeout se resuelve al resultado original en vez de a una segunda llamada cobrada — lo que más importa cuando nunca llegaste a ver la primera respuesta.
# A stable key per logical operation. If you never saw the first
# response, the retry resolves to it instead of billing twice.
curl -X POST https://api.qannasapi.com/v1/data/contacts/find-email \
-H "Authorization: Bearer $QANNAS_API_KEY" \
-H "Idempotency-Key: signup-8f21c4" \
-H "Content-Type: application/json" \
-d '{"query": "Jane Doe, Acme Logistics"}'Espera entre intentos y nunca reintentes un 4xx
Reintenta 5xx y timeouts con retroceso exponencial y jitter. Un 422 significa que el cuerpo de la petición es incorrecto: corrígelo en lugar de reenviarlo. Los fallos de origen nunca se cobran, así que reintentar tras un fallo real no te cuesta nada extra.
import random, time
def call_with_retry(send, *, max_attempts=5):
for attempt in range(max_attempts):
res = send()
if res.status_code == 422:
raise ValueError(res.text) # malformed — never resend
if res.status_code < 500:
return res
# 5xx: upstream failures are never billed, so retrying is free
time.sleep(0.5 * 2 ** attempt + random.uniform(0, 0.5))
raise RuntimeError("exhausted retries")Resumen
| Patrón | Cómo |
|---|---|
| Encontrado | 200, data poblada, X-Credits-Charged > 0 |
| Vacío | 200, data: null, X-Credits-Charged: 0 — no reintentar |
| Fallo | 4xx / 5xx — reintenta solo 5xx y timeouts |
| Reintento seguro | Cabecera Idempotency-Key, estable por operación lógica |
| Petición inválida | 422 — corrige el cuerpo, nunca lo reenvíes igual |
| Retroceso | Exponencial con jitter y número de intentos limitado |
Lista para producción
- Bifurca por data == null antes de bifurcar por el código de estado.
- Envía una Idempotency-Key en todo lo que puedas llegar a reintentar.
- Reintenta 5xx y timeouts; nunca reintentes un 422.
- Cachea los resultados vacíos para no repetir la misma pregunta todo el día.
Siguientes pasos
Ejecuta primero la estimación. No cuesta nada y aplica exactamente la misma aritmética que el contador.