The hardest part of building a multi-category API platform isn’t any individual endpoint. It’s making all 90+ of them feel like they came from the same mind.
Left to their own devices, APIs accumulate inconsistencies. One endpoint returns dates as Unix timestamps. Another uses ISO 8601 strings. A third returns a dateTime field. One uses success: true as a status wrapper. Another returns HTTP 200 with an error key. These inconsistencies are death by a thousand cuts — every integration requires a new mental model, every edge case is handled differently, every error requires separate handling logic.
Microwave has 90+ endpoints across 18 categories: finance, geography, text utilities, identity, time, color, units, and more. Getting them to feel unified required treating consistency as a first-class engineering concern from day one.
The envelope
Every Microwave response has the same shape:
{
"data": { ... },
"meta": {
"request_id": "req_01jq4...",
"latency_ms": 12,
"tokens_consumed": 3
}
}
data contains the result. meta contains operational metadata. Always. No endpoint returns a bare object. No endpoint returns an array at the top level. The envelope is non-negotiable.
This sounds obvious, but it took discipline to hold the line. A few endpoints have naturally list-shaped responses (timezone city search, for example). It’s tempting to return an array directly — it’s “simpler.” We don’t. The result is always { "data": [...] }. The consistency is worth the extra nesting.
Errors
Errors follow the same pattern:
{
"error": {
"code": "validation_error",
"message": "The 'from' parameter must be a valid IANA timezone identifier.",
"param": "from"
}
}
code is a machine-readable string in snake_case. message is human-readable. param is present when the error is tied to a specific input field. HTTP status code maps consistently: 400 for validation errors, 401 for auth failures, 429 for rate limiting, 500 for server errors.
We don’t return 200 with an error body. We don’t return opaque error codes like ERR_4291. The error codes are documented, stable, and map to specific conditions.
Naming conventions
Everything is snake_case. Input parameters, output fields, error codes. Not camelCase, not PascalCase, not kebab-case. Snake_case.
Fields are named for what they are, not what they represent in the implementation. We don’t expose internal naming (src_tz, dst_tz) — we name for the caller’s mental model (from, to). Where a field has an industry-standard name, we use it: postal_code not zip, country in ISO 3166-1 alpha-2, currency in ISO 4217.
Date-times are always ISO 8601 with timezone offset included where the value is timezone-sensitive. Durations are always in the unit implied by the field name: latency_ms is milliseconds, distance_km is kilometers. We don’t return “km” in a field called distance and trust the caller to remember.
The request ID
Every response includes a request_id in meta. It’s prefixed req_ and is a ULID — lexicographically sortable, time-ordered, globally unique. If something goes wrong, you hand us the request ID and we can pull the full trace within seconds.
This is directly inspired by Stripe. Their API’s ch_ prefixed charge IDs and evt_ prefixed event IDs are the most developer-friendly ID scheme we’ve seen. We adopted the pattern wholesale: req_ for requests. When we add webhooks, those will have their own prefix too.
How we enforce it
Consistency doesn’t happen by convention — it has to be enforced. We have three mechanisms:
Response validation in tests. Every endpoint’s test suite validates the response envelope shape, not just the data content. A test that only asserts data.rate === 1.62 will pass even if we accidentally return the data without the wrapper. Tests that assert the full envelope shape won’t.
A shared response builder. All endpoints use the same internal function to construct their response. You can’t accidentally return a bare object because the builder doesn’t accept one. The meta block is populated by the framework, not by individual endpoint implementations — so request_id and latency_ms are always present and always accurate.
A pre-merge checklist. New endpoints go through a design review against a checklist before merging: envelope shape, error codes, field naming, status codes, documentation. It’s a one-page doc. The review takes 10 minutes. It’s caught several naming inconsistencies that would have shipped otherwise.
The payoff
The payoff shows up in integration. When you’ve used one Microwave endpoint, you’ve used all of them — not literally, but the mental model transfers completely. The error handling code you wrote for currency conversion works unchanged for address parsing. The logging you built around request_id works for every call.
We’ve had developers tell us they integrated five endpoints in an afternoon and only read the docs for the first one. That’s the goal: radical transferability of knowledge across the API surface.
The conventions aren’t clever. Most of them are lifted directly from Stripe, who figured them out a decade ago. But there’s real discipline in maintaining them across 90+ endpoints and 18 categories. The temptation to make exceptions is constant. So far, we haven’t.