Using the API
Idempotency
Send an Idempotency-Key on every call that spends credits so a retry returns the first answer instead of charging twice.
Networks fail. If a reveal times out you cannot know whether it was charged. An Idempotency-Key makes the retry safe: Funnelfeedr remembers the first successful answer for that key and returns it again instead of running the call twice.
POST /external/v1/contacts/reveal HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json
Idempotency-Key: 9a3e7c1d-2b4f-4d8e-a6c0-5f1b3e9d7a28
When to send one
| Call | Idempotency-Key |
|---|---|
POST /contacts/reveal, dry runs included | Required. Without it: 400 idempotency_key_required. |
POST /organizations/match, POST /people/match or POST /organizations/switchboards/match that reveals (reveal set, dryRun false) | Required. Without it: 400 idempotency_key_required. |
POST /organizations/match, POST /people/match or POST /organizations/switchboards/match without a reveal, or as a dry run | Optional. Honoured when sent. |
Every other call: the GETs | Ignored. |
The key
- Any 1 to 255 visible ASCII characters; anything else is
422 validation_failed. Use a new UUID for each logical request. - Generate it once, before the first attempt, and reuse it for every retry of that request.
- Idempotency keys are kept per API key: two integrations cannot collide.
What happens
| Situation | Result |
|---|---|
| First request with the key | Runs normally. A successful (2xx) answer is stored for 24 hours. |
| Same key, same request, after it succeeded | The stored status and body, byte for byte, with the header Idempotent-Replayed: true. Nothing runs and nothing is charged again. |
| Same key while the first request is still running | 409 idempotency_key_in_use (retryable: true). Wait a moment and send it again to get the first request's answer. |
| Same key, different request | 422 idempotency_key_reused. Use a new key for a new request. |
| Same key, after the first attempt failed | Runs again. Only successful (2xx) answers are stored, and a failed reveal charged nothing, so this is safe — and it is what you want after topping up following a 402. |
| After 24 hours | The key is forgotten; a repeat runs as a new request. |
"Same request" means the same method, path, query string and body bytes, checked per API key. Re-serialising your JSON with a different key order or spacing makes it a different request. Keep the body you sent and resend exactly that.
A retry loop
import os, time, uuid, requests
def reveal(items, info_types, max_credits):
key = str(uuid.uuid4()) # one key for every attempt of this logical request
body = {"items": items, "infoTypes": info_types, "maxCredits": max_credits}
for attempt in range(5):
response = requests.post(
"https://api.funnelfeedr.com/external/v1/contacts/reveal",
headers={
"Authorization": f"Bearer {os.environ['FUNNELFEEDR_API_KEY']}",
"Idempotency-Key": key,
},
json=body,
timeout=60,
)
if response.ok:
return response.json()
problem = response.json()
if not problem.get("retryable"):
raise RuntimeError(f"{problem['code']}: {problem['detail']}")
time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))
raise RuntimeError("Gave up after 5 attempts")
requests serialises the same body dict identically on every attempt, so the retries match the first request.