Every endpoint answers with the same three top-level keys. Only data changes shape, so one
response handler covers your whole integration no matter how many endpoints you call.
{
"status": "ok",
"error": null,
"data": {
"joke": "I only know 25 letters of the alphabet. I don't know y."
}
}| Key | On success | On failure |
|---|---|---|
status | "ok" | "error" |
error | null | One sentence naming what to change |
data | The result | null |
{
"status": "error",
"error": "The 'email' parameter is required.",
"data": null
}Check status rather than inspecting data for emptiness. A successful call that legitimately
found nothing still returns status: "ok" with an empty result inside data — that is a valid
answer, not a failure.
Response formats
JSON is the default. Four other serialisations are available through the Accept header, and
they carry the same envelope rather than a different shape:
| Format | Accept header | Good for |
|---|---|---|
| JSON | application/json (default) | Everything |
| XML | application/xml | Legacy integrations and enterprise middleware |
| YAML | application/x-yaml | Config pipelines and human-readable fixtures |
| CSV | text/csv | Dropping a result straight into a spreadsheet |
| Markdown | text/markdown | Piping a result into a document or an LLM prompt |
curl 'https://api.apiverve.com/v1/dadjokes' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Accept: text/markdown'| Field | Value |
| --- | --- |
| status | ok |
| data.joke | I only know 25 letters of the alphabet. I don't know y. |
Accept: application/yaml is not recognised. It does not error — it falls through to the
default and returns JSON with a application/json content type, which is easy to miss if you
are not checking. Use application/x-yaml or application/x-yml.
An unrecognised Accept value behaves the same way: JSON, no warning. If you are not getting
the format you asked for, check the response's own Content-Type header first — it always
reports what you actually received.
Nested data
data is endpoint-specific. Some endpoints return a flat object, some return nested objects,
some return an array of results under a named key:
{
"status": "ok",
"error": null,
"data": {
"location": { "city": "London", "country": "GB", "lat": 51.5, "lon": -0.13 },
"current": { "temperature": 14.2, "humidity": 77, "condition": "Cloudy" }
}
}Each endpoint's reference page lists every field it can return, with its type,
an example value, and its full dot-path — so location.city in the table is exactly what you index
in code.
Premium fields are absent, not empty
This is the one rule worth reading twice, because getting it wrong produces a bug that only appears when a plan changes.
Fields your plan does not include are omitted from the response entirely. They are not
null, not 0, not an empty string.
riskScore on email validator is the canonical example — free plans get
the verdict fields, paid plans also get the score:
// Wrong — treats a premium field you don't have as a real zero
if (data.riskScore === 0) approve();
// Right — presence is the test
if ('riskScore' in data && data.riskScore === 0) approve();
if (data.riskScore == null) { /* not included on this plan */ }The reasoning: a zeroed field is indistinguishable from a genuine zero, and a fraud score of 0
means "definitely safe" — exactly the wrong default to invent for a customer who is not paying
for that field. Absence is unambiguous.
Premium fields are marked on every reference page, so you can see before you call which parts of a response your plan will actually receive — and plans covers which tier includes what.
Field types
Types are stable per field: a field documented as a number is always a number, never a numeric
string, and never switches to null to signal absence — an absent value is an absent key.
Two conventions are worth knowing:
- Dates are ISO 8601 strings (
2026-08-18T05:30:00Z) unless the reference page says otherwise. Cycle timestamps in headers, likex-api-renewal, are Unix seconds. - Empty collections are
[]or{}, notnull. A missing collection means the key was not returned at all.
Handling every response the same way
async function call(endpoint, params, key) {
const url = new URL(`https://api.apiverve.com/v1/${endpoint}`);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url, { headers: { 'x-api-key': key } });
const body = await res.json();
if (body.status !== 'ok') throw new Error(body.error);
return body.data;
}That function is complete for every endpoint in the catalog. The only thing that varies is what
you do with data.
Next
The headers that select a format are in request headers, and making requests covers parsing and caching what comes back. Any term above that is new is in the glossary.
Status codes and which failures are worth retrying are in error handling. For what a single response costs and how to read your balance from its headers, see rate limits.