Using the API
Errors
Every error is RFC 9457 problem+json with a stable code and a retryable flag. The full catalog, with what to do about each code.
When a request fails as a whole, the response has an HTTP error status and a Content-Type: application/problem+json body (RFC 9457). That holds for every error under /external/v1, including a path that does not exist (404), the wrong HTTP method (405) and a body that is not JSON (415):
{
"type": "https://funnelfeedr.com/developers/errors/insufficient_credits",
"title": "Not enough credits",
"status": 402,
"detail": "This request needs 3.4 credits; the account has 1.2. Nothing was revealed or charged. Buy credits in Funnelfeedr under Settings → Subscription, then retry.",
"code": "insufficient_credits",
"retryable": false,
"creditsRequired": 3.4,
"creditsRemaining": 1.2,
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
| Field | Meaning |
|---|---|
code | The stable, machine-readable error. Branch on this. |
retryable | true when the same request may succeed if you send it again later. |
status | The HTTP status, repeated. |
title | A short summary; always the same for a given code. |
detail | What went wrong this time, for humans. It may change; don't parse it. |
type | A link to this code's row in the table below. |
traceId | Quote it when you contact support. |
errors | validation_failed only: the messages for each invalid field, keyed by the field's JSON path (maxCredits, items, items[3].contactId), by $ for the body as a whole, or by the header's name (Idempotency-Key). |
creditsRequired | credit_cap_exceeded and insufficient_credits: what the request would have cost. Nothing was spent. |
maxCredits | credit_cap_exceeded only: the maxCredits the request carried. |
creditsRemaining | insufficient_credits only: the account's balance when the request ran. |
retryAfterSeconds | rate_limited only: seconds until a retry can succeed, as in Retry-After. |
maxItems | too_many_items only: the most items a request may carry (25). |
A single bad item in a batch is not an error response: it comes back inside a 200 as "status": "invalid" with an error object, and the rest of the batch runs. See Batching.
Catalog
| code | Status | Retryable | What to do |
|---|---|---|---|
invalid_api_keyInvalid API key | 401 | no | The Authorization header is missing, malformed or holds a key that does not exist. Check the FUNNELFEEDR_API_KEY value; keys start with ff_live_ and are 46 characters long. |
api_key_revokedAPI key revoked | 401 | no | An admin revoked the key. Ask for a new key; do not retry. |
api_key_expiredAPI key expired | 401 | no | The key was rolled and its overlap period has ended. Switch to the replacement key. |
feature_not_enabledThe External API is not enabled for this account | 403 | no | API access has not been granted to the account, or has been withdrawn. Request access from Settings → API keys. |
not_foundNot found | 404 | no | No endpoint has this path. Check the path against the API reference. A record that does not exist is not this error: lookups answer not_found per item instead. |
method_not_allowedMethod not allowed | 405 | no | The path exists but not for this HTTP method (a GET on a POST endpoint). detail and the Allow header name the method to use. |
unsupported_media_typeThe request body must be JSON | 415 | no | Send the body as JSON with Content-Type: application/json. |
validation_failedThe request is invalid | 422 | no | The body, a parameter or the Idempotency-Key header is invalid as a whole. errors maps each field's JSON path to its messages. Fix the request; an unusable single item comes back as status: "invalid" instead. |
too_many_itemsToo many items in one request | 422 | no | Send at most 25 items per request (maxItems in the body). Split the batch. |
credit_cap_exceededThe request would cost more than maxCredits | 422 | no | Nothing was spent. creditsRequired in the body is what the request would cost, maxCredits the cap it carried. Ask the user before raising maxCredits. |
idempotency_key_requiredAn Idempotency-Key header is required | 400 | no | The call spends credits (contacts/reveal always, dry runs included; organizations/match, people/match and organizations/switchboards/match when they reveal). Send an Idempotency-Key header with a new UUID for each logical request. |
idempotency_key_reusedIdempotency-Key was already used with a different request | 422 | no | The key was used in the last 24 hours for a different method, path or body. Generate a new key for a new request. |
idempotency_key_in_useA request with this Idempotency-Key is still in progress | 409 | yes | A request with the same key is still running. Wait a moment and retry with the same key and body to get its response. |
insufficient_creditsNot enough credits | 402 | no | Nothing was spent. creditsRequired and creditsRemaining in the body say how far short the account is. Stop and tell the user; credits are bought in the app under Settings → Subscription. Do not retry until they have been. |
rate_limitedRate limit exceeded | 429 | yes | Wait the number of seconds in Retry-After (also retryAfterSeconds in the body), then retry. |
internal_errorInternal error | 500 | yes | Retry with backoff. For a spend call, retry with the same Idempotency-Key so it cannot charge twice. Quote traceId if you contact support. |
New codes may be added. Treat an unknown code by its status and retryable.
Validation errors
A request that is invalid as a whole is 422 validation_failed, with each problem under errors, keyed by field. That includes a body that is not valid JSON ("$": ["The body is not valid JSON (line 1, position 12)."]), a value of the wrong type ("items[0].contactId": ["Must be a UUID."], "infoTypes[0]": ["Must be one of: email, phone, linkedIn."]) and a malformed Idempotency-Key header:
{
"type": "https://funnelfeedr.com/developers/errors/validation_failed",
"title": "The request is invalid",
"status": 422,
"detail": "maxCredits is required when revealing. Set it to the most this request may spend, or send dryRun: true to get the price first.",
"code": "validation_failed",
"retryable": false,
"errors": {
"maxCredits": [
"maxCredits is required when revealing. Set it to the most this request may spend, or send dryRun: true to get the price first."
]
},
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
Retrying
- Retry only when
retryableistrue:rate_limited(afterRetry-After),idempotency_key_in_useandinternal_error. - Back off between attempts, and give up after a few.
- On a call that spends, retry with the same
Idempotency-Keyand the same body, so a retry cannot charge twice. See Idempotency. - Never retry
insufficient_creditsin a loop. Stop and tell the user.