QannasAPI

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.

SectionAPI fundamentals
Actions usedPOST /v1/data/contacts/find-email
Credits5 (worst case)
Test-key safeYes — runs on qk_test_ for zero credits
Time to complete~10 min
PrerequisitesA 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.

three outcomes
# 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
# < 503

Do 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.py
# 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.

idempotent.sh
# 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.

retry.py
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

PatternHow
Found200, data populated, X-Credits-Charged > 0
Miss200, data: null, X-Credits-Charged: 0 — do not retry
Failure4xx / 5xx — retry 5xx and timeouts only
Safe retryIdempotency-Key header, stable per logical operation
Bad request422 — fix the body, never resend as-is
BackoffExponential 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.