Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

I Mostly Consume APIs. Here Is What I Wish Their Publishers Had Decided Before v1

Most of the APIs I deal with, I deal with from the wrong side of the door. I build VoiceDash, a white-label portal that turns voice agents running on Retell into branded client dashboards, and nearly every interesting li

Most of the APIs I deal with, I deal with from the wrong side of the door. I build VoiceDash, a white-label portal that turns voice agents running on Retell into branded client dashboards, and nearly every interesting line in that codebase exists because of a decision someone else made when they published their API.

So when a client asks me "should we ship a public API?", I end up answering from the consumer's chair. This is that answer, with the parts of my own code that taught it to me.

First: do you need one yet?

Not always. Once an API is public, you maintain it, and that is a long promise. The signals that it is time:

  • Customers keep asking for the same integration. Three accounts wanting your records in their CRM is one API plus webhooks, not three custom connectors.
  • Your own apps need it anyway. A mobile app, a second frontend or AI features that act on user data means you are already building an API. Building it to public standards now is cheaper than retrofitting.

Before committing, check the cheap exits. A scheduled CSV export covers a lot of reporting. Webhooks alone, with no read API, cover "tell my other system when X happens." A no-code connector covers many small customers.

Design around the integrations, not the tables

The fastest way to ship an API is to expose the data model: every table an endpoint, every column a field. It is also the fastest way to couple every partner to internals you will want to change next quarter.

Instead, write down the two or three integrations people will actually build. "Sync new bookings into the CRM." "Create a record from an external form." "Pull last month's activity into billing." Each one tells you the resources, fields, events and writes that matter. Ship those. Adding a field later is easy. Removing one is a negotiation.

Predictable shapes, or your consumers write this

Here is a real line from my Retell sync:

const data = await res.json();
return Array.isArray(data) ? data : data.calls || data.call_list || [];

Three shapes for one list endpoint. I wrote it that way because I was not sure which one I would get, and once a consumer is unsure, the cheapest safe thing is to accept all of them forever. Every consumer of an inconsistent API carries a line like this, and none of them will ever delete it.

The same file has a smaller version of the same habit. My webhook handler reads the event as body.event || body.call?.call_status, because a consumer that has been surprised once stops trusting any single field.

The fix on the producer side is boring and permanent: one envelope for every list, one date format, one error shape, and IDs that never change and are never reused. Partners will store your IDs in their database and match on them for years. If you have read my notes on merging two voice APIs into one table, you know how much of integration work is just matching records.

Give consumers "what changed", not just "what was created"

Most real integrations are syncs. Retell's list endpoint lets me filter calls by start time and page with a pagination_key, which is good. What I cannot ask is "which calls changed since my last sync." So my sync keeps a watermark per agent and still has to do this:

/**
 * Each sync re-reads this far behind the agent's watermark, for calls Retell
 * listed late or that were still in progress last time.
 */
const OVERLAP_MS = 2 * 60 * 60 * 1000;

Two hours of re-reading on every sync, because a call that started before my watermark can still change after it. Then a second pass on top: any call I stored mid-call with a duration of zero gets patched when the list later returns it finished.

That is the normal cost of a created-time filter on data that keeps changing after creation. Give consumers an updated_since filter or a change cursor, pair it with cursor pagination, and decide up front how deletions show up. A sync that never learns about deletions slowly drifts away from the truth, and nobody notices until a client does.

Make writes safe to retry

Networks fail halfway through requests. If a partner times out on "create invoice" and retries, your API should not create two invoices. Accept an idempotency key on writes, a unique value the client sends so a repeated request returns the original result instead of acting twice.

I learned the consumer half of this the hard way: you can retry a write, you cannot retry an email. Every non-idempotent endpoint you publish pushes that problem onto every partner.

While you are there, make errors actionable: a machine-readable code, a human message, the field that failed, and a clear difference between "you sent something wrong" and "try again later." Partner code handles those two very differently, and right now my sync can only throw Retell list-calls failed with status ${status} because a status code is the only part of a failure my code knows how to read.

Scope every key to one account

A public API is a new front door into your customers' data. API keys suit a customer connecting their own systems. OAuth suits third-party apps that many customers connect and want to revoke. Many products need both.

Either way, every credential maps to exactly one account, enforced on every request, job and webhook. I lean on this as a consumer more than I expected. A comment in my sync code says each workspace's API key "can list only calls its Retell account owns", and that guarantee is what lets me sync several workspaces without writing my own isolation layer on top. Add scopes for read-only keys, let customers rotate and revoke keys themselves, and log who called what.

Webhooks: allow more than one subscriber

Webhooks beat polling for everyone. Partners stop hammering you every few minutes, and they hear about changes in seconds. The usual list applies: signed payloads, retries with backoff, a way to see and replay failed deliveries, and no assumptions about order. My receiver upserts on (agentId, platformCallId) and leaves the transcript, duration and recording alone when an event arrives without them, because several events arrive per call and I cannot rely on their sequence. I wrote more about that in a webhook tells you something happened, not that you have everything.

The design choice that cost me the most code is a quieter one. In my setup, a Retell agent posts its call events to one webhook address. VoiceDash lets the same Retell agent be connected in more than one workspace, so only one of them can receive live events. The others get calls through the polling sync above. And when the VoiceDash agent that owns the webhook is deleted, I have a function whose whole job is to find another workspace whose key can manage that Retell agent, check the webhook still points at the deleted one, and repoint it.

If your API might have more than one integration per resource, and it will, let consumers register multiple subscriptions with their own URLs and their own event filters. A single URL field turns into a coordination problem between your customers' tools.

Decide the change policy before v1

This is what separates an API partners trust from one they dread. Additive changes, like new endpoints, optional fields and event types, ship without a new version, and consumers should ignore fields they do not recognise. Breaking changes get a new version, in the URL or a dated header, with the old one kept alive for a published window. Deprecations get a changelog, warning headers and direct notice to the accounts still calling them.

Set per-credential rate limits too, with headers showing remaining quota and reset time and a proper 429, so one runaway script cannot slow everyone down.

Docs are the API

For a partner, the documentation is the product. Keep one machine-readable spec, such as an OpenAPI file, as the source for reference docs, client libraries and tests so they cannot drift. Add a sandbox, copy-paste examples for each core integration, and a status page so people know whose failure it is. AI agents read those docs now too, so clear descriptions and explicit scopes matter twice.

The short version

  1. Design for the two or three integrations people will actually build.
  2. Stable IDs, one envelope, one error shape.
  3. An updated_since filter or change cursor, plus a story for deletions.
  4. Idempotency keys on every write.
  5. Every credential scoped to one account, with scopes, rotation and audit logs.
  6. Signed, retried, replayable webhooks, with more than one subscription per resource.
  7. A versioning and deprecation policy before v1.
  8. Rate limits with clear headers.
  9. One spec as the source for docs, tests and SDKs, plus a sandbox.

Several items on that list exist in my own code as consumer-side workarounds. That is the real cost of a sloppy public API: it does not show up on your side. It shows up as a two-hour overlap constant in someone else's code.

The longer, client-facing version of this guide lives on the Null Studio blog. If you publish an API, I am curious: what is the one decision you wish you had made before v1?

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.