Get started
Guidance for AI agents
Rules for an AI agent that calls the Funnelfeedr API on someone's behalf — which calls spend credits, how to quote before revealing, and how to stop safely.
Every API key can spend credits, so an agent with a key can turn a customer's credits into organization and contact details. Credits are money, and a reveal cannot be undone. This page is the contract an agent should follow; the same rules are in /developers/llms.txt and in the snippet for your agent's prompt.
The rules
- Only reveals spend credits:
POST /contacts/reveal, andPOST /organizations/match,POST /people/matchorPOST /organizations/switchboards/matchwithrevealset anddryRunfalse. Matching, contacts, switchboards and the balance are free. - Before any reveal, send the same request with
dryRun: true, show the user the quoted cost (creditsQuoted), and reveal only after they approve it. - Always set
maxCreditson a reveal to the amount the user approved. A request that would cost more fails withcredit_cap_exceededand spends nothing. - Always send an
Idempotency-Keyheader (a new UUID per logical request) on a call that spends, and on everyPOST /contacts/reveal, dry runs included. Reuse the same key, with the same body, only to retry that same request. - Never loop reveals one item at a time and never reveal the same contact or organization twice. Batch up to 25 items in one request.
- On
402 insufficient_credits, stop and tell the user. Nothing was spent; do not retry until they have topped up. - On an
ambiguousmatch, ask the user which candidate they mean, or pick one byidfrom your own data. Never reveal every candidate. - On
429 rate_limited, wait the number of seconds inRetry-Afterbefore retrying. Retry other errors only when the body says"retryable": true. - Read the key from the
FUNNELFEEDR_API_KEYenvironment variable. Never print, log or commit it, and never send it from a browser or mobile app.
Which calls spend credits
| Call | Spends? |
|---|---|
POST /organizations/match without reveal, or with dryRun: true | Never. The basics come back; the details only if the account already revealed them. |
POST /organizations/match with reveal: ["details"] and without dryRun | Yes, for the organizations whose details it reveals |
POST /organizations/contacts/match | Never. Details come back masked unless the account already revealed them. |
POST /organizations/switchboards/match without reveal, or with dryRun: true | Never |
POST /organizations/switchboards/match with reveal and without dryRun | Yes, for the switchboard numbers and emails it unmasks |
POST /people/match without reveal, or with dryRun: true | Never |
POST /people/match with reveal and without dryRun | Yes, for the email and phone numbers it unmasks |
POST /contacts/reveal with dryRun: true | Never (the call still needs an Idempotency-Key) |
POST /contacts/reveal | Yes |
GET /credits | Never |
Only an organization's details (0.2 credits per organization by default), a contact's email (0.2) and phone (1.0) cost anything; a contact's LinkedIn URL is never masked and never charged. Details the account has already revealed cost nothing again, and a request that finds nothing charges nothing. The OpenAPI spec marks the operations that can spend with an x-credit-cost extension.
A safe reveal, step by step
- Find the people for free. Use
people/matchwithoutreveal, ororganizations/contacts/match(which lists only the people matching the account's target contact roles, for up to 25 organizations a call), to get contact ids. Keep only the people the user actually needs. To call an organization rather than a person, find its switchboard withorganizations/switchboards/match. - Quote. Send the reveal with
dryRun: true. The response'screditsQuotedis the exact price, and each result'sreveal.creditCostthat person's price. A person who appears twice is charged once but shows the price on both results, so show the usercreditsQuoted, not a sum of the results. - Ask. Show the user the number of people, what will be revealed and the price, and wait for a yes. A user's standing instruction ("reveal emails for up to 50 credits a day") counts — keep a running total against it.
- Reveal once. Send the same request with
dryRun: false,maxCreditsset to the approved amount and a newIdempotency-Key. - Check the answer. Read
creditsChargedandcreditsRemainingand report them. Do not reveal the same contacts again.
The reveal in step 4, as a POST /contacts/reveal body:
{
"items": [
{ "ref": "anna", "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90" },
{ "ref": "johan", "contactId": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2" }
],
"infoTypes": ["email", "phone"],
"maxCredits": 1.4,
"dryRun": false
}
Organization details follow the same steps. Match for free first and check detailsRevealed: an organization the account already revealed returns its details without a reveal. Quote the rest with organizations/match, reveal: ["details"] and dryRun: true (creditsQuoted is the total; each result's creditCost is that organization's price, shown on every result for an organization that appears more than once but charged once), ask, then reveal once with maxCredits and an Idempotency-Key.
When to stop
| You get | Do this |
|---|---|
402 insufficient_credits | Stop. Nothing was spent. Tell the user the balance is too low; they top up in the app. Do not retry in a loop. |
422 credit_cap_exceeded | Stop. Nothing was spent. The body's creditsRequired is the real cost; ask the user before raising maxCredits. |
429 rate_limited | Wait Retry-After seconds, then continue. |
409 idempotency_key_in_use | The same request is still running. Wait a moment, then retry with the same key to get its answer. |
5xx | Retry a few times with backoff. On a spend call, reuse the same Idempotency-Key so a retry cannot charge twice. |
Branch on the code field, never on the detail text: detail is for humans and may change.
Ambiguous and missing matches
ambiguousmeans more than one person fits. The response lists up to fivecandidateswith their id, name, job title and organization. Ask the user which one they mean, or pick byidif your own data settles it (a matching title, say). Never reveal all of them to be safe.not_foundis free and final for that input. Funnelfeedr does not guess: nicknames ("Kalle" for "Karl") do not match in v1. Try another identifier you have — an email or a LinkedIn URL — rather than variations of the name. An email only matches an address the account has already revealed or owns, so anot_foundemail says nothing about whether the address exists; try the name and organization or the LinkedIn URL.
See Matching for full examples of each.
Batch, don't loop
Every lookup takes up to 25 items. Send one request with 25 people or organizations rather than 25 requests with one, and use ref to line answers up with your rows. A reveal is all-or-nothing per request, so a batch is either fully charged or not charged at all. See Batching.