Funnelfeedr Logo

Using the API

Credits & reveal

What spends credits, how to get an exact quote with dryRun, how maxCredits caps a request, how reveals are charged all-or-nothing and only for what they unmask, and what an organization's details cost.

A contact's email address and phone number are masked until your account reveals them; name, job title and LinkedIn URL never are. An organization's basics are free, and its details are held back until your account reveals them. Revealing spends the account's credits, the same credits a reveal in the app spends. Everything else in the API is free.

What spends credits

Exactly four things:

  • POST /contacts/reveal, for contacts you already have ids for, unless dryRun is true.
  • POST /people/match with reveal set and dryRun not true.
  • POST /organizations/switchboards/match with reveal set and dryRun not true.
  • POST /organizations/match with reveal: ["details"] and dryRun not true. See Organization details.

Every API key can make all four calls. The maxCredits cap on each request is what keeps a reveal within what you approved.

Info typeDefault price per contact
email0.2 credits
phone1.0 credits
linkedInFree. LinkedIn URLs are never masked; asking for one is accepted and reported as already revealed, or unavailable when the contact has none.

Email and phone together cost the sum. A switchboard — an organization's main line, a contact that is not a person — is priced at the app's rates for a contact that is not a person: by default 1.0 credits for its phone and 0.2 for its email, the same charge as revealing them in the app. An account group can have its own prices, and GET /credits shows the balance, not prices — so don't hard-code them: ask with dryRun. The OpenAPI spec states the defaults and rules in an x-credit-cost extension on each of the four operations.

A request is charged only for what it actually unmasks:

  • Only for matched items. A not_found, ambiguous or invalid item costs nothing.
  • Only what exists. A contact with no phone number on record is not charged for a phone, and a contact with none of the requested info types is skipped entirely.
  • Never twice. Info types your account has revealed within the last year cost nothing again, whoever revealed them — a user in the app or another key. A reveal by a key unmasks the contact for everyone in the account.

Quote with dryRun

Send the reveal with "dryRun": true. Nothing is revealed or spent and the contacts stay masked. Each result's reveal shows what that contact would cost, and creditsQuoted is the total:

{
  "items": [
    { "ref": "anna", "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90" },
    { "ref": "johan", "contactId": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2" }
  ],
  "infoTypes": ["email", "phone"],
  "dryRun": true
}
{
  "results": [
    {
      "ref": "anna",
      "status": "quoted",
      "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
      "reveal": {
        "creditCost": 1.2,
        "newInfoTypes": ["email", "phone"],
        "alreadyRevealedInfoTypes": [],
        "unavailableInfoTypes": []
      },
      "contact": {
        "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
        "name": "Anna Svensson-Berg",
        "jobTitle": "Head of Procurement",
        "isExecutive": false,
        "isDecisionMaker": true,
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "email": "XXXXX@nordviklogistik.se",
        "emailIsMasked": true,
        "phone": "+46701XXXXXX",
        "phoneIsMasked": true,
        "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
      }
    },
    {
      "ref": "johan",
      "status": "quoted",
      "contactId": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2",
      "reveal": {
        "creditCost": 0.2,
        "newInfoTypes": ["email"],
        "alreadyRevealedInfoTypes": [],
        "unavailableInfoTypes": ["phone"]
      },
      "contact": {
        "id": "d41d8cd9-8f00-4b20-9e98-0ecf8427e1b2",
        "name": "Johan Lindqvist",
        "jobTitle": "CFO",
        "isExecutive": true,
        "isDecisionMaker": true,
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "email": "XXXXX@nordviklogistik.se",
        "emailIsMasked": true,
        "phoneIsMasked": false
      }
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 1.4,
  "creditsRemaining": 412.4,
  "dryRun": true
}

Johan has no phone number on record, so only his email is priced. The quote and the charge come from the same calculation, so the real call charges exactly creditsQuoted — or less, if someone in your account revealed some of the same details in between. A contact that appears more than once in a request is charged once, but each of its results shows its creditCost; add up creditsQuoted, not the results.

On organizations/match, people/match and organizations/switchboards/match a dry run needs no Idempotency-Key and counts against the search rate-limit bucket only. POST /contacts/reveal needs an Idempotency-Key even to quote, and always counts against the spend bucket.

Cap with maxCredits

A call that spends must say how much it may spend. maxCredits is required unless dryRun is true; without it the request fails with 422 validation_failed. If the reveal would cost more, the request fails with 422 credit_cap_exceeded, nothing is spent, and the body has the real cost in creditsRequired:

{
  "type": "https://funnelfeedr.com/developers/errors/credit_cap_exceeded",
  "title": "The request would cost more than maxCredits",
  "status": 422,
  "detail": "This reveal costs 3.4 credits, more than maxCredits (2). Nothing was revealed or charged. Raise maxCredits or reveal fewer contacts or info types.",
  "code": "credit_cap_exceeded",
  "retryable": false,
  "creditsRequired": 3.4,
  "maxCredits": 2,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Set maxCredits to the quote the user approved — not to a large number "to be safe". The cap is what stops a bug or a prompt-injected agent from emptying the account. A dry run is never capped.

Reveal

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
{
  "items": [
    { "ref": "anna", "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90" },
    { "ref": "missing", "contactId": "e3b0c442-98fc-4c14-9afb-f4c8996fb924" }
  ],
  "infoTypes": ["email", "phone"],
  "maxCredits": 1.2,
  "dryRun": false
}

Each found contact has the status revealed, with reveal saying what this request unmasked (newInfoTypes) and what was already revealed or unavailable, and contact holding the unmasked details. A contact id the key cannot see is not_found; an item without a contactId is invalid. Here Anna's details had been revealed before, so they come back unmasked and cost nothing again:

{
  "results": [
    {
      "ref": "anna",
      "status": "revealed",
      "contactId": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
      "reveal": {
        "creditCost": 0,
        "newInfoTypes": [],
        "alreadyRevealedInfoTypes": ["email", "phone"],
        "unavailableInfoTypes": []
      },
      "contact": {
        "id": "c0a8012e-5b1f-4e3d-9a7c-2f6b8d4e1a90",
        "name": "Anna Svensson-Berg",
        "jobTitle": "Head of Procurement",
        "isExecutive": false,
        "isDecisionMaker": true,
        "organizationId": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "organizationName": "Nordvik Logistik AB",
        "email": "anna.svensson-berg@nordviklogistik.se",
        "emailIsMasked": false,
        "phone": "+46701234567",
        "phoneIsMasked": false,
        "linkedInUrl": "https://linkedin.com/in/anna-svensson-berg"
      }
    },
    {
      "ref": "missing",
      "status": "not_found",
      "contactId": "e3b0c442-98fc-4c14-9afb-f4c8996fb924"
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0,
  "creditsRemaining": 411.2,
  "dryRun": false
}

infoTypes is required. A reveal needs an Idempotency-Key header, and counts against the spend rate limit bucket.

All or nothing

A reveal request is charged as one unit. Either every reveal in it goes through and is charged, or — when the account cannot pay for all of it — none does:

{
  "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"
}

You never get half a batch revealed and have to work out which half. On organizations/match, people/match and organizations/switchboards/match a failed reveal fails the whole request, matches included. After a top-up, send the same request again; the same Idempotency-Key is fine because only successful responses are stored.

Organization details

POST /organizations/match is free and returns an organization's basics. Its details cost credits, like a contact's email or phone.

Free on every matchDetails, revealed for credits
id, url, name, countryCode, legalForm, city, foundedYear, and the identifier you matched by: organizationNumber for an orgNumber match, domain (the one you sent, normalized) for a domain match, linkedInUrl for a linkedInUrl match. An id match returns nothing extra.organizationNumber, domain and linkedInUrl when you did not match by them, website, employeesMin and employeesMax, revenueMin, revenueMax and revenueCurrency, oneSentenceDescription, description (English), technologies, keywords, tags, and the rest of the address: addressLine1, addressLine2, postalCode, stateOrProvince.

A Swedish sole trader's organization number is a personal identity number and stays masked (850101-XXXX), revealed or not.

Price. 0.2 credits per organization by default, one price for all of its details. An account group can have its own price, so ask with dryRun rather than hard-coding it. The x-credit-cost extension on match_organizations in the OpenAPI spec states the default.

detailsRevealed. Every organization object carries it. It is true when the object holds every detail Funnelfeedr has: your account revealed them within the last year, or there are none beyond the basics. When it is false the details are held back.

Valid for a year. Once your account has revealed an organization's details, every organizations/match returns them for a year, free and without reveal. A reveal by one key unlocks them for every key on the account.

Revealing. Add "reveal": ["details"] to an organizations/match request. The rules are the same as for a people reveal: a dry run spends nothing; a real reveal needs maxCredits and an Idempotency-Key header, counts against the spend rate-limit bucket as well as search, and is charged all-or-nothing. If it would cost more than maxCredits it fails with 422 credit_cap_exceeded, and if the account cannot pay with 402 insufficient_credits; either way nothing is spent and no matches are returned. Only matched organizations whose details the account has not revealed are charged. not_found and invalid items, and an organization with no details beyond the basics, cost nothing.

With reveal set, each matched result has a creditCost: what revealing that organization costs (on a dry run) or cost, 0 when the account already revealed it or there is nothing to reveal. An organization that appears more than once in a request is charged once, but every one of its results shows its price, so creditsQuoted, not the sum of creditCost, is what the request costs. A dry run:

{
  "items": [
    { "ref": "crm-1001", "domain": "nordviklogistik.se" }
  ],
  "reveal": ["details"],
  "dryRun": true
}
{
  "results": [
    {
      "ref": "crm-1001",
      "status": "matched",
      "matchedBy": "domain",
      "organization": {
        "id": "6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21",
        "name": "Nordvik Logistik AB",
        "detailsRevealed": false,
        "countryCode": "SE",
        "domain": "nordviklogistik.se",
        "city": "Göteborg",
        "legalForm": "Aktiebolag",
        "foundedYear": 2004,
        "url": "https://app.funnelfeedr.com/organizations/6f9d2f1e-6f37-4a2f-9a3e-1b5c0f4d7e21"
      },
      "creditCost": 0.2
    }
  ],
  "creditsCharged": 0,
  "creditsQuoted": 0.2,
  "creditsRemaining": 412.4,
  "dryRun": true
}

The real reveal and its response are on the Matching page. organizations/switchboards/match and organizations/contacts/match return the same organization object — the basics, plus the details only if your account has already revealed them — and never reveal organization details themselves.

Knowing the balance

  • GET /credits returns included (the plan's recurring allowance, spent first), purchased (does not reset) and total. It is free.
  • Every response that can spend has creditsCharged, creditsQuoted, creditsRemaining and dryRun in the body, and the balance in the Funnelfeedr-Credits-Remaining header, so you rarely need to ask separately. creditsRemaining is null, and the header absent, when the account has no credit subscription.
  • Credits are bought in the app under Settings → Subscription; the API cannot buy credits.