Every system in this certification is built from other people's APIs. The decisions that matter most are made before the first line of integration code: which API answers which question, and what you will do when it cannot. Get those wrong and no amount of retry logic or caching saves the design. You end up paying 20 credits for a field you could have had for 6, joining two answers that define the same word differently, or discovering in production that a third of your inputs come back with the one field you needed set to null.
This lesson is about choosing deliberately. You will start from the job rather than the catalog, check each candidate against five properties that decide fit, run a proof of concept on real inputs, and score the options in a matrix that someone else can read and challenge.
Start from the decision, not the catalog
An API is never the goal. It feeds a decision: accept this signup, route this lead to sales, flag this domain for review. Write that decision down first, in one sentence, and then list the facts it needs. For a B2B signup flow it might look like this:
| Fact the decision needs | Why | Where it is used |
|---|---|---|
| The email can receive mail | We cannot onboard someone we cannot reach | Request path, blocks signup |
| The address is disposable | Throwaway identities abuse free trials | Request path, blocks signup |
| The email domain is a company domain | Company domains get routed to sales | Background, after signup |
| The domain is older than a few months | New domains correlate with abuse | Background, scoring |
Two columns in that table do most of the work. Where it is used tells you the latency you can afford: a request-path check has to answer inside the user's patience, while a background check can take seconds and retry. Why tells you what happens when the fact is missing, which you will need when you design failure handling in the next lesson.
Only now open the catalog. For each fact, find every API that could supply it. You will usually find more than one, and the overlaps are where good architecture starts.
Five properties that decide fit
Schema fit: does a field answer your question exactly?
Map each fact to a concrete JSON path, then read the field's definition, not just its name. Similar names hide different definitions.
Disposable detection is available from two APIs. Email Validator (emailvalidator, 20 credits) returns isDisposable along with MX resolution, SPF and DMARC presence, role-account and free-provider flags, and a normalizedEmail. Disposable Email Checker (emaildisposablechecker, 6 credits) returns isDisposable, email and domain, and nothing else.
curl "https://api.apiverve.com/v1/emailvalidator?email=support%40myspace.com" \
-H "x-api-key: YOUR_API_KEY"One call answers several facts from the table: canReceiveMail, isDisposable, isCompanyEmail, plus isRoleAccount for free.
If your decision only needs the disposable flag, the second API answers it for under a third of the price. If it needs deliverability too, calling both is the expensive mistake: you pay 26 credits for an answer one 20-credit call already contained. Overlap analysis runs in both directions.
Domain age is a subtler case. WHOIS Lookup (whoislookup, 8 credits) returns domainAgeDays and domainAgeYears derived from createdDate, plus registrar and status data. Domain Age (domainage, 10 credits) returns the same age figures and adds ageCategory and isNewlyRegistered. Both have a "new domain" flag, and they are not the same flag: isNewlyRegistered on Domain Age is true under 30 days, while isRecentlyRegistered on WHOIS Lookup means within the last 90 days, and it is a premium field. A rule written as "block new domains" means different things depending on which field you wired it to.
curl "https://api.apiverve.com/v1/domainage?domain=myspace.com" \
-H "x-api-key: YOUR_API_KEY"Compare ageCategory and isNewlyRegistered with what WHOIS Lookup returns for the same domain. The age figures agree; the flags are defined differently.
Premium fields deserve their own row in your mapping. On plans without access they come back as null, present in the response but empty. A riskScore you planned around that is null on the plan you will actually run in production is a schema mismatch, even though the field exists.
Coverage and freshness: how often is the answer there, and how old is it?
Schema tells you what an API can return. Coverage tells you how often it does, for your inputs. A geolocation answer for a mobile carrier IP may carry a country and nothing finer. A carrier name exists only for number ranges the underlying dataset knows about. You do not learn this from documentation; you learn it from a sample of your own data, which is what the proof of concept below is for.
Freshness is the age of the answer. Some APIs tell you directly: Website Tech Stack Detector returns fingerprintsUpdatedAt, the date its fingerprint database was last refreshed, and VPN Proxy Detector returns checked_on. Others compute everything live at call time, as Email Validator does with its DNS checks. Note which kind each candidate is, because it decides how long you may cache the result and how often you must re-check it later.
Cost: credits per call times calls per decision
Credits differ per API, set in each definition and shown on its reference page. The number that matters is not the price of one call but the price of one decision: the sum of every call the decision makes, times how often it runs. A signup check that calls Email Validator (20) and WHOIS Lookup (8) costs 28 credits per signup. If a quarter of signups are free-mail addresses where WHOIS adds nothing, skipping it for them is a design decision worth roughly 2 credits per signup on average. Write the per-decision cost into your evaluation; it is what the finance conversation will be about.
Latency: measure, do not read
The catalog shows a typical latency for each API, and it is a reasonable first filter: an API listed in the seconds is not a request-path candidate for a form submission. But the listed figure is an estimate. Your latency depends on your region, your inputs and the time of day, and the tail matters more than the middle. Measure p50 and p95 against your own sample before you commit a step to a request path.
Failure modes: what does "no answer" look like?
Every API has a shape for "I could not answer", and they differ. Learn each candidate's shape before you write code against it:
- Bad input is a
400whoseerrormay be a structure rather than a sentence. Sendingnotanemailto Email Validator returned"error": {"invalid": ["email: must be a valid email address"]}. Code that doeserr.error.toLowerCase()crashes on that. - Nothing exists can be a successful response. WHOIS Lookup for an unregistered domain returns
200withstatus: null, emptynameServers, and nocreatedDateat all. Your code has to handle a field that is absent, not just one that isnull. - Nothing found on some APIs is a
404, which is a valid answer for that input and not an outage. - Rate limit and exhausted credits both answer
429. You covered telling them apart in earlier courses; at architecture level the point is that a monthly cap is a shared failure across every API on the key.
curl "https://api.apiverve.com/v1/whoislookup?domain=example-not-registered-zzqx.com" \
-H "x-api-key: YOUR_API_KEY"A 200 with almost nothing in it. createdDate and domainAgeDays are missing entirely, not null.
Terms and licensing
The data behind an API comes from somewhere: public registries, licensed databases, DNS, crowd-sourced lists. Before an API becomes part of your product, read the terms that cover what you plan to do with its output. The questions to answer are practical. May you store results, and for how long? May you show them to your own customers? May you resell or redistribute them inside your product? Does the underlying source restrict commercial use? If you cannot answer one of these from the provider's terms, ask the provider in writing before you build, not after a customer asks.
Your signup check needs only one fact: whether the address uses a disposable email domain. Which choice fits best?
Run a proof of concept on real inputs
A decision matrix filled in from documentation is a guess with formatting. Fill it from a proof of concept: a script that sends a sample of real inputs through each candidate and records what came back.
The sample matters more than the script. Take a few hundred inputs from production or from the data you will actually process, including the ugly ones: typos, international formats, the oldest records, the inputs that broke the last system. Strip anything you are not allowed to send to a third party. Then measure four things per candidate: latency, coverage of each field you need, error rate by status, and credits spent.
// node poc.mjs < sample.txt (one input per line)
import { readFileSync } from 'node:fs';
const KEY = process.env.APIVERVE_API_KEY;
const CANDIDATES = {
// api id: { param name, credits per call, JSON paths the decision needs }
emailvalidator: { param: 'email', credits: 20, paths: ['isDisposable', 'canReceiveMail', 'isCompanyEmail'] },
emaildisposablechecker: { param: 'email', credits: 6, paths: ['isDisposable'] },
};
const get = (obj, path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
const pct = (xs, p) => xs.sort((a, b) => a - b)[Math.min(xs.length - 1, Math.floor((p / 100) * xs.length))];
const inputs = readFileSync(0, 'utf8').split('\n').map((s) => s.trim()).filter(Boolean);
for (const [id, c] of Object.entries(CANDIDATES)) {
const ms = [], statuses = {}, present = Object.fromEntries(c.paths.map((p) => [p, 0]));
let ok = 0;
for (const input of inputs) {
const url = new URL(`https://api.apiverve.com/v1/${id}`);
url.searchParams.set(c.param, input);
const t0 = performance.now();
const res = await fetch(url, { headers: { 'x-api-key': KEY }, signal: AbortSignal.timeout(15_000) })
.catch((e) => ({ status: e.name }));
ms.push(performance.now() - t0);
statuses[res.status] = (statuses[res.status] ?? 0) + 1;
const body = res.json ? await res.json().catch(() => null) : null;
if (body?.status !== 'ok') continue;
ok++;
for (const p of c.paths) if (get(body.data, p) != null) present[p]++;
}
console.log(id, {
p50: Math.round(pct(ms, 50)), p95: Math.round(pct(ms, 95)),
statuses,
coverage: Object.fromEntries(c.paths.map((p) => [p, `${Math.round((100 * present[p]) / Math.max(ok, 1))}%`])),
creditsSpent: ok * c.credits,
});
}A few details carry most of the value:
- Coverage counts
nulland absent together, using!= null. For a decision, a premium field that isnullon your plan and a field the API omitted for this input are the same problem: you do not have the fact. - Statuses are tallied, not thrown. A proof of concept that stops at the first
400tells you nothing about the other 299 inputs. Timeouts appear under the error's name, so they are counted too. - Credits are computed from successful calls only, because failed calls are not billed. Run this on your real plan's key, not an admin key, so premium gating matches what production will see.
- Calls run sequentially. This is a measurement, not a load test. Keep the sample small enough to finish well inside your rate limit, and remember that every successful call spends real credits.
Run it, then read the per-input results by hand for the twenty inputs where candidates disagree. Disagreement is where definitions differ, and you will often find that neither API is wrong: they are answering slightly different questions.
Score the options in a decision matrix
With measurements in hand, the matrix is short work. Two rules keep it honest.
Hard requirements are gates, not weights. If a fact is required and a candidate's coverage for it is 40%, no amount of speed or price compensates. Apply gates first and drop candidates that fail; only score the survivors. The same goes for terms you cannot comply with.
Weights are agreed before the scores are filled in. Choose them from the decision you wrote in the first section: a request-path check weighs latency heavily, a nightly enrichment barely cares. Writing weights after seeing the numbers is how a favourite wins.
| Criterion | Weight | Candidate A | Candidate B |
|---|---|---|---|
| Coverage of required fields | 0.35 | 5 | 5 |
| p95 latency for the request path | 0.25 | 3 | 4 |
| Credits per decision | 0.20 | 2 | 5 |
| Extra facts we will use later | 0.10 | 5 | 1 |
| Failure shapes easy to handle | 0.10 | 4 | 4 |
The scoring is mechanical, so make it code that lives next to the proof of concept and reruns when the measurements change:
export function scoreOptions(weights, options) {
const total = Object.values(weights).reduce((a, b) => a + b, 0);
if (Math.abs(total - 1) > 1e-9) throw new Error(`weights sum to ${total}, expected 1`);
return options
.filter((o) => o.gates.every(Boolean)) // hard requirements first
.map((o) => ({
name: o.name,
score: Object.entries(weights).reduce((s, [k, w]) => s + w * o.scores[k], 0),
}))
.sort((a, b) => b.score - a.score);
}
const weights = { coverage: 0.35, latency: 0.25, cost: 0.2, extras: 0.1, failures: 0.1 };
console.log(scoreOptions(weights, [
{ name: 'A', gates: [true], scores: { coverage: 5, latency: 3, cost: 2, extras: 5, failures: 4 } },
{ name: 'B', gates: [true], scores: { coverage: 5, latency: 4, cost: 5, extras: 1, failures: 4 } },
]));
// [ { name: 'B', score: 4.25 }, { name: 'A', score: 3.8 } ]The output is a recommendation, not a verdict. Read it next to the extras row: candidate A loses here, but if the "extra facts we will use later" become required next quarter, you would be calling it anyway, and the cheaper answer today becomes a second integration tomorrow. That is why the matrix is written down. When the job changes, you change a weight or a gate and rerun, and the reasoning is visible.
An option scores highest in your weighted matrix, but its coverage for a required field was 55% in the proof of concept. What is the right handling?
What to hand to the next step
Choosing APIs produces three artifacts you will use for the rest of this course: the fact-to-path mapping with each field's exact definition, the measured latency and coverage per candidate, and the scored matrix with its gates and weights. The mapping becomes the contract each workflow step promises. The latency numbers become the budget you split across steps. The coverage numbers tell you which steps can be optional, because a fact that is missing a third of the time cannot be one the workflow refuses to proceed without.
Next you will combine several chosen APIs into one workflow, as a dependency graph that runs independent calls at the same time and survives partial failure: Orchestrate multi-API workflows.