Handle misses and retries without paying twice
What you'll build: a call wrapper that distinguishes three outcomes — found, not found, and failed — and retries only the third, with idempotency so a retry can never double-charge.
| Section | API fundamentals |
|---|---|
| Actions used | POST /v1/data/contacts/find-email |
| Credits | 5 (worst case) |
| Test-key safe | Yes — runs on qk_test_ for zero credits |
| Time to complete | ~10 min |
| Prerequisites | A key, and a client that can set request headers. |
Three outcomes, not two
A found result is HTTP 200 with data populated and a non-zero charge. A miss is HTTP 200 with data: null and X-Credits-Charged: 0 — the lookup ran honestly and found nothing, so nothing was billed. A failure is a 4xx or 5xx. Only the third is worth retrying.
# 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
# < 503Do not retry a miss
A miss is a real answer about the world, not a transient error. Retrying it will not conjure a result; it just adds latency. Cache the miss for a sensible window and move on.
# 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"]Retry failures with idempotency
Data endpoints accept an Idempotency-Key header. Send a stable key per logical operation and a retry after a timeout resolves to the original result rather than a second billed call — which matters most when you never saw the first response.
# 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"}'Back off, and never retry a 4xx
Retry 5xx and timeouts with exponential backoff and jitter. A 422 means the request body is wrong: fix it rather than resending. Upstream failures are never billed, so a retry after a genuine failure costs you nothing 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")Summary
| Pattern | How |
|---|---|
| Found | 200, data populated, X-Credits-Charged > 0 |
| Miss | 200, data: null, X-Credits-Charged: 0 — do not retry |
| Failure | 4xx / 5xx — retry 5xx and timeouts only |
| Safe retry | Idempotency-Key header, stable per logical operation |
| Bad request | 422 — fix the body, never resend as-is |
| Backoff | Exponential with jitter, capped attempts |
Production checklist
- Branch on data == null before you branch on the status code.
- Send an Idempotency-Key on anything you might retry.
- Retry 5xx and timeouts; never retry 422.
- Cache misses so you do not re-ask the same question all day.
Next steps
Run the estimate first. It costs nothing and it is the same arithmetic the meter will apply.