Sales Tools

    Data Enrichment APIs: Rate Limits, Credits, and What to Cache

    Two separate systems can stop an enrichment run, and a retry that rescues you from one will burn through the other. What breaks once the integration is live.

    Editorial illustration for Data Enrichment APIs
    August 27, 2026Updated August 14, 20268 min read
    Share:
    The short answer

    A data enrichment API appends attributes to a person or company record programmatically, one lookup at a time. Two independent limits govern it: a rate cap on how fast you may ask, and a credit balance on how many times you may ask at all. They fail differently and a single retry policy cannot handle both correctly.

    Key takeaways

    • A rate-limit error and a credit-exhaustion error need opposite responses: back off and retry the first, stop the run on the second, because retrying into an empty balance cannot succeed and on per-attempt billing it spends what is left.
    • Whether a provider bills per request submitted or per result returned changes the retry policy more than its headline rate does, and that clause sits in the terms rather than on the rate card.
    • Cache lookups on the person rather than on your row id, and cache the misses too with an expiry in months, because a negative result is information you already paid for.
    • Record throttles, misses and refusals distinguishably, since a throttled request that returns nothing is indistinguishable from a genuine miss and quietly understates that provider's coverage.

    Reviewed and updated August 14, 2026

    Two separate things can stop an enrichment API mid-run, and teams routinely build for only one of them. The first is the rate limit, which governs how fast you may ask. The second is the credit balance, which governs how many times you may ask at all. They are enforced by different parts of the vendor's stack, they fail with different status codes, and the retry logic that rescues you from one of them will burn through the other.

    Almost every write-up of enrichment APIs is a ranked list of providers. This is the other page: what actually breaks once the integration is live, and the handful of design decisions that determine what the same volume costs you.

    The two limit systems, and why they need separate handling

    A rate limit is a speed cap. Exceed it and you get a 429, usually with a Retry-After header, and the correct response is to wait and try again. Nothing has been consumed. The request never really happened.

    A credit balance is a budget. Exhaust it and you get a different error, often a 402 or a 403 with a quota message in the body, and retrying is worse than useless. The request will fail identically every time until somebody buys more credits or the monthly allowance rolls over.

    The reason this matters more than it sounds: a generic HTTP client treats both as transient failures. Standard exponential backoff sees a non-2xx response, sleeps, and tries again, which is exactly right for the first case and a loop that never terminates for the second. Worse is the variant where a retry on a credit error does succeed, because the vendor bills per attempt rather than per result, and each retry quietly draws down the balance you were already out of.

    Handle them apart. Parse the status code and the error body, back off on the rate limit, and fail the whole run loudly on the credit error. A run that stops with a clear message costs you an afternoon. A run that retries into an empty balance costs you the balance.

    1. Step 1Send the lookup

      One person or one domain, with the strongest key you hold

    2. Step 2Read the status, not just the body

      429 is a speed problem, 402 or 403 with a quota message is a budget problem

    3. Step 3Back off on 429 only

      Honour Retry-After where the vendor sends it, and retry the same request

    4. Step 4Stop the run on a quota error

      Retrying cannot succeed, and on per-attempt billing it spends what is left

    5. Step 5Record the outcome either way

      A miss, a throttle and a refusal must be distinguishable afterwards

    The branch most integrations are missing. A 429 and a quota error look alike to a generic HTTP client and need opposite responses.

    That last step is the one people skip, and it has a specific cost. A throttled request that returns nothing looks identical in your output to a lookup that genuinely found nobody. If you do not record which happened, the provider's apparent coverage on your list is understated by however much you throttled, and you will draw the wrong conclusion in the comparison you run next quarter.

    What you are billed for changes how you should retry

    Vendors bill enrichment lookups in one of two ways, and the difference is larger than the difference between their headline rates.

    Billed per request submitted. You pay whether or not the provider finds anything. Under this model a miss is a real cost, so it is worth spending effort upstream to avoid asking questions you can predict will fail. Sending a personal webmail domain to a B2B company-data endpoint is money gone.

    Billed per result returned. You pay only for hits. Misses are free, which makes the API safe to ask speculatively, and changes the economics of the whole integration. It also means the provider has a mild incentive to return something rather than nothing, which is worth remembering when you assess accuracy.

    Nothing about a provider's rate card tells you which model you are on. It is in the terms, and it is the single clause most worth finding before you write the retry logic, because per-request billing turns an aggressive retry policy into a line item.

    Idempotency, and the case for caching a negative

    Section illustration: Idempotency, and the case for caching a negative

    An enrichment lookup is a read. Asking twice for the same person should not cost twice, and in a well-built integration it does not, because the second question never reaches the provider.

    Key a persistent cache on the thing you asked about rather than on the row you asked from. The same person turns up in a list this month, a re-import next quarter and a colleague's campaign after that, and if the cache is keyed on your internal row id, all three pay. Keyed on the person, the second and third are free.

    The part that gets left out is caching the misses. A negative result is expensive information that you paid for, and it is exactly as reusable as a positive one. Without a negative cache, every list that happens to contain the same hard-to-find person re-buys the same failure, forever, and the cost is invisible because nothing in your reporting distinguishes a fresh miss from a repeated one.

    Negatives do need an expiry that positives do not. A person who had no findable work address in January may have one in June, because they changed jobs or their new employer's domain became crawlable. Give a cached miss a lifetime measured in months and re-ask after it, rather than treating it as permanent.

    No cacheEvery run pays
    • Each list re-buys people it has seen before
    • Cost scales with runs, not with distinct people
    • Spend looks like volume growth when it is repetition
    • Simplest to build and the most expensive to operate
    Positive cache onlyThe usual halfway house
    • Found addresses are reused, which is most of the saving
    • Misses are re-purchased on every list that contains them
    • Hard-to-find people are the ones you pay for repeatedly
    • Reporting cannot show you this is happening
    Positive and negativeWhat to build
    • Both outcomes stored, keyed on the person
    • Misses expire after months, positives after longer
    • A repeated miss costs one cache read
    • Makes provider comparison honest, because retries stop polluting it
    Three cache designs over the same lookups. The middle column is the common one and the right column is the one that stops re-buying failures.

    Where an API belongs relative to a waterfall

    An API is a delivery mechanism rather than a strategy. It answers the question of how a lookup reaches a provider; it says nothing about which provider should receive it, or in what order.

    That ordering question is settled by unit cost, and it is settled the same way whether you are uploading a CSV or calling an endpoint: run the cheap probabilistic step first and only pay the expensive definitive one for what survives. The waterfall enrichment guide sets out the arithmetic in full, including the published unit costs that make the ordering obvious.

    What the API form adds is the ability to run that sequence per record and in real time, which is genuinely useful and introduces one failure of its own. In a batch you can see the shape of the whole run before committing to it. Per record, each lookup makes its own decision, and a chain that escalates too eagerly will reach the expensive provider far more often than the same logic would in bulk. Set an explicit stopping rule in the code rather than letting the chain run to its end by default.

    The other structural choice is when the call fires. Enriching on record creation keeps data fresh at the moment somebody needs it and spreads spend across the month. Enriching on a schedule is easier to budget and produces values that begin ageing immediately. The trade is covered from the CRM side in CRM enrichment, and the answer is usually to run both: a trickle on creation, and a periodic re-verification pass over what you already hold.

    The join key is part of the API contract

    Every enrichment endpoint takes a key, and the strength of that key sets a ceiling on the quality of everything downstream. This is not a data-modelling nicety; it is the difference between a correct answer and a confident wrong one.

    Email is the strongest person-level key because a mailbox belongs to one identity. Company domain is the strongest company-level key for the same reason. Company name is the weakest by a wide margin, because trading names, legal entities, punctuation and suffixes all differ, and a great many real companies share a name with a larger one. A provider handed a name will usually return something, and you have no way to tell from the response whether it matched the company you meant.

    The measurement that follows from this is match rate, and the important thing about it is that a vendor's published figure was computed on the vendor's sample. The only number that means anything is the one you compute on your own records, with your own keys.

    Instrument the integration for the failure verification cannot see

    Section illustration: Instrument the integration for the failure verification cannot see

    The defect that survives every other check is a valid mailbox belonging to the wrong person. The address exists, it accepts mail, it delivers cleanly, and it reaches somebody's previous employer or a different person with a similar name. Verification cannot catch it because the mailbox is real. Bounce rate cannot catch it either.

    The cheap test is to compare the domain of each resolved address against the company on the row, and to read a sample of the mismatches by hand rather than trusting the rate. Do it per provider, because an overall mismatch rate tells you the list has a problem while a rate per provider tells you which stage to fix. Expect legitimate mismatches, since corporate parents and legacy domains are common and benign, which is why the mismatches need reading rather than rejecting.

    Enrichment API integration checklist
    • Yes: Rate-limit errors and quota errors take different code paths
    • Yes: You know whether the provider bills per request or per result
    • Yes: Lookups are cached on the person, and misses are cached too
    • Yes: Throttles, misses and refusals are distinguishable in your output
    • No: A generic backoff wrapper handles every non-2xx the same way
    • No: You match on company name because it is the field you happen to hold
    • Depends: Real-time enrichment on record creation, with a scheduled re-verify pass
    What to settle before an enrichment API goes into production, ordered by how much it will affect the bill.

    Concurrency belongs per provider, not per pipeline

    Providers tolerate very different request rates, and the tolerable rate is rarely documented prominently. A pipeline tuned to the fastest provider in the chain will hammer the most fragile one into throttling.

    Set concurrency per provider rather than globally, and treat a disappointing hit rate as a throttling question before you treat it as a coverage question. Rate-limited requests can fail in ways that resemble a miss, which understates that provider's coverage in exactly the comparison you would use to decide whether to keep paying for it.

    What we do and do not do with these APIs

    Section illustration: What we do and do not do with these APIs

    Our own documented stack is MillionVerifier brute-force first, then Prospeo, then Findymail, and it stops there. There is no fourth paid stage: past the third provider the marginal cost per additional address climbs while the quality of what is recovered falls, so the remainder is banked rather than bought and volume comes from fresh sourcing instead.

    Accept-all domains are the honest limit of the whole design. The mail server accepts everything, so verification cannot confirm that a specific mailbox exists and every candidate returns as catch-all. We bank that tail as a preserved artifact rather than sending to it, because an unverifiable address on shared sending infrastructure puts every other campaign on that infrastructure at risk. The mechanics are in catch-all domain, and the deliverability reasoning is in the cold email deliverability guide.

    For a worked example of a single vendor's endpoints, the Apollo.io API guide covers one provider in detail, and email finder tools covers how to compare providers on your own list rather than on their published claims.

    The short version

    Split rate-limit handling from credit-exhaustion handling, because a retry fixes one and compounds the other. Find out whether you are billed per request or per result before writing the retry policy. Cache on the person and cache the misses, with an expiry on the negatives. Match on email or domain rather than company name, measure match rate on your own records, and audit resolved domains against the row's company per provider, because the expensive defect is a valid mailbox that belongs to somebody else.

    If you would rather have a verified list and a campaign running than build and instrument this yourself, you can see what a campaign would look like for your market.

    Vendor terms verified against each provider's own published pages as of August 2026. Verify current terms with the vendor before relying on them.

    Questions

    Frequently asked questions.

    Frequently asked questions
    What is a data enrichment API?
    It is an endpoint that takes a key you already hold, usually an email address or a company domain, and returns attributes attached to it: job title, seniority, headcount, industry, sometimes a verified work email. The difference from a bulk upload is timing rather than substance, since it lets you enrich one record at the moment it is created instead of running a table on a schedule.
    Why did my enrichment API stop returning results?
    Check which failure you are seeing before retrying. A 429 means you are asking too fast and waiting will fix it. A 402 or a 403 carrying a quota message means the credit balance is gone, and retrying will fail identically every time. A generic backoff wrapper treats both as transient, which is how a run loops against an exhausted account.
    Should I cache enrichment results?
    Yes, and cache the misses as well as the hits. The same person recurs across lists, campaigns and quarters, so a cache keyed on the person rather than on your internal row id stops you buying the same record repeatedly. Give negatives an expiry measured in months, because somebody unfindable in January may be findable later after a job change.
    Does an API replace a waterfall?
    No. An API is how a lookup reaches a provider, and a waterfall is which provider receives it and in what order. The ordering argument comes from unit cost and holds either way. What the API form adds is per-record real-time execution, plus one hazard: a chain that escalates per record reaches the expensive provider more often than the same logic would in bulk unless you set a stopping rule.
    Data EnrichmentSales ToolsAPIRevOpsEmail Finding
    Byline

    About the author.

    RevenueFlow Team

    B2B cold email experts helping companies generate qualified leads through done-for-you outreach campaigns.

    RevenueFlow Team

    Your next move

    Ready to scale your outreach?

    We build GTM engines that book real meetings. See the receipts.

    Further reading

    Related articles.