To ship a public API your first customers will actually use, start from three to five jobs real customers already asked for, expose only the resources those jobs need, and make the boring parts dependable: prefixed API keys with read or write scopes, one consistent error format, clear rate limits with a 429 and a Retry-After header, idempotency keys on anything that creates data, and a quickstart that gets someone from new key to first successful call in five minutes. A small API that never surprises anyone beats a big one that mirrors your whole database. Then watch three numbers: keys created, first successful call, and weekly active keys.
Below, how to build a public API step by step: when you need one, how to scope v1, keys, errors, limits, retries, versioning, docs, adoption, a made-up example and a 14-day plan.
Do you actually need a public API yet?
Most small products hear "do you have an API?" long before they need one. The question is cheap. Supporting an API forever is not. So look at what is behind the request.
Signals that a public API for SaaS is worth it now:
The same job keeps coming up. Several customers want to push data in or pull it out on a schedule.
People already script around you. Someone scrapes your pages or exports a CSV by hand every morning.
A deal depends on it. A B2B buyer needs to connect you to internal tools, and "no API" ends the conversation.
Glue tools come up. Users ask for Zapier, Make or n8n, which would need an API from you anyway.
Signals that something smaller will do:
One customer wants one report. A scheduled CSV export solves it this week.
Everyone asking wants the same tool. One native connection may serve them better; see choosing your first integration when users ask for everything.
Nobody can say what they'd do with it. "It would be nice to have an API" is not a job.
My rule: if I can name three customers and the exact job each wants to automate, I build the API. If not, I ship the export and keep a list.
What should the first version of your API do?
Scope v1 from real jobs, not database tables. Write each job as one sentence in the customer's words, then list the calls it needs.
Collect 3 to 5 jobs. "When an order ships in our system, create a record in yours." "Every night, pull yesterday's entries into our warehouse." "Let our support tool look up a customer's status."
Map each job to calls. Usually a list, a get-by-ID and maybe one create. Write them as lines like
GET /v1/batchesandPOST /v1/batches.Cut everything else. No delete unless a job needs it. No admin settings, no billing.
Check with the customers who asked. Send the lines and ask, "Could you do your job with only these?" That's your spec review.
Six endpoints covering four real jobs is a good v1. Every endpoint is a promise: adding one later is easy, taking one away means emailing everyone who built on it.
How should you shape resources and responses?
You don't need a style guide on day one, but you need a few rules you never break. Customers forgive a small API, not one where every endpoint behaves differently.
Use nouns for resources.
/v1/batches,/v1/batches/{id}. The HTTP method carries the verb.Same JSON shape everywhere. Pick snake_case or camelCase and keep it. Lists always return a
dataarray plus a "more items" flag.Stable IDs. String IDs that never change. A type prefix like
bat_makes logs and support emails easier to read.Timestamps in one format, in UTC. RFC 3339 defines a timestamp format for internet protocols that is a profile of ISO 8601, which means values like
2026-10-26T09:30:00Z.Cursor pagination from the start. Return a page of items plus a cursor for the next page, instead of page numbers. Stripe's list endpoints, for example, take a
limitand astarting_afterobject ID, and returnhas_more. Cursors don't skip or repeat rows when new data arrives, which is exactly what nightly sync jobs hit.Don't leak internals. No column names you might rename, no fields you're unsure about. Adding a field later is easy; removing one breaks people.
How should API keys work?
API keys best practices come down to one idea: assume a key will leak one day, and make that day boring. Here's what I'd ship in v1.
Prefixed keys. Put a readable prefix on every key, such as
hl_live_andhl_test_. GitHub explained why it moved to this pattern in its post on new authentication token formats: prefixes likeghp_make tokens identifiable, which helps secret scanning, and the underscore separator isn't a Base64 character, so random strings don't look like tokens by accident.Show the key once. Display it at creation, let the user copy it, then never show it again. Store only a hash plus the last four characters, so the dashboard can say "key ending in 7f2a".
Scopes, even if there are only two. Read and write is enough for v1. Stripe's authentication docs describe restricted keys with a subset of permissions, which "limits the damage a bad actor could cause if they obtained the key". A reporting script should never hold a key that can delete things.
Many named keys. One key per script or integration, so revoking a leaked one breaks nothing else.
Rotate and revoke in the UI. Revoke is immediate. Rotation means create, swap, then revoke, so two keys must work at once.
Header only, never in URLs. Accept keys in the
Authorizationheader. URLs end up in logs and browser history.Last used, on every key. It helps customers clean up, and it's the cheapest usage data you'll collect.
What should your API errors look like?
Use HTTP status codes honestly, and wrap every error in the same body. The codes tell a program what class of thing went wrong. The body tells the developer what to fix.
Status | Use it when | What the body should say |
|---|---|---|
400 | The request is malformed or a field is invalid | Which field, and what a valid value looks like |
401 | No key, or the key is wrong or revoked | "Missing or invalid API key", nothing more |
403 | The key is valid but lacks the scope | Which scope the call needs |
404 | The object doesn't exist for this account | The ID that wasn't found |
409 | A conflict, such as a reused idempotency key with different data | What conflicted |
429 | Too many requests | The limit, plus when to retry |
500 | Your bug | A request ID they can send you |
For the body, you don't have to invent a format. RFC 9457, Problem Details for HTTP APIs, defines a standard JSON error object served as application/problem+json, with members called type, status, title, detail and instance, and room for your own extension members. It replaced the older RFC 7807.
Add a stable error code customers can switch on, and a request ID on every response. Half of API support is "can you look up this request?"
How do you rate limit without annoying good customers?
API rate limiting protects you from one runaway loop. It shouldn't punish a customer syncing normal amounts of data.
Limit per key, not only per IP. Jobs often run from shared cloud IPs; per-key limits point at the script that misbehaves.
Start generous. Set the first limit a few times above your heaviest real customer's need, then tighten with data. Rule of thumb, not a standard.
Answer with 429 and Retry-After. MDN's page on 429 Too Many Requests describes it as the status for a client that "has sent too many requests in a given amount of time", and notes a Retry-After header may be included. The Retry-After header takes either an HTTP date or a number of seconds, so
Retry-After: 30tells a well-written client exactly how long to wait.Show the budget and document it. Return remaining-requests headers so clients slow down before the wall, and put the limits in the docs. Surprise limits feel like outages.
How do you make retries safe?
Networks fail halfway. A script sends "create this record", the connection drops, and it doesn't know if it worked. If the retry creates a second record, you've made a mess they must clean up.
The fix is an idempotency key. The client sends a unique value with every create; if it arrives again, you return the first result. Stripe's idempotent requests docs are a good model to copy: the client generates the key (they suggest V4 UUIDs), sends it in an Idempotency-Key header, keys are up to 255 characters, and for their v1 API all POST requests accept one. Retries with the same key return the saved response, Stripe compares incoming parameters to the original and errors if they differ, and keys can be pruned after at least 24 hours.
For a small API: accept Idempotency-Key on every POST, store key, request fingerprint and response for a day, replay the stored response on a repeat, and return 409 if the key returns with different data.
How should you handle API versioning?
Keep API versioning boring. A version in the path, like /v1/, is easy to see and route. Dated versions pinned per account are more flexible and more work; Stripe documents that model on its API versioning page. For a first API, /v1/ is enough.
The bigger habit is additive change: new fields, endpoints and optional parameters. Tell clients to ignore fields they don't know. Renaming, removing or changing the meaning of a field waits for v2.
If you ever do retire something, two HTTP headers exist for saying so in machine-readable form: the Deprecation header in RFC 9745, and the Sunset header in RFC 8594, which carries the date a resource is expected to become unresponsive. The email to the people using that endpoint matters more.
Where do webhooks fit in a first API?
Webhooks mean you call customers when something happens instead of them polling. Add them once a job really is "tell me when X happens": a few events, signed payloads, retries on failure. For how receivers get it wrong, see this piece on why a Stripe webhook that works in test mode breaks in production. Polling with cursors is a fine start.
What API documentation does a first API need?
Good API documentation is the difference between "we have an API" and "people use it". V1 needs four pieces, none of them a big portal.
An OpenAPI file. The OpenAPI Specification defines a standard, programming language-agnostic description for HTTP APIs that both humans and computers can read. One file gives you reference docs, mocks and generated clients. Keep it next to the code.
A five-minute quickstart. Create a key, make one call with curl, see real data come back. Something like
curl -H "Authorization: Bearer hl_test_..." [your API host]/v1/batches, then the exact response. If it takes you longer than five minutes on a clean account, fix the product.Copy-paste examples per job. One short page per v1 job, with real requests and responses.
A changelog. Every new field and fix, dated. API users read changelogs closely. Here's how to write a changelog that brings silent users back.
Docs can also bring search traffic; that's a separate project, covered in this guide to getting organic signups from product docs without a blog.
How do you know anyone is using your API?
Hearing nothing after launch is normal. Track three numbers to see where people stop:
Keys created. Low means nobody knows the API exists or key creation is hidden.
First successful call. Keys with at least one 2xx. A gap here means the quickstart is broken.
Weekly active keys. Any 2xx in the last 7 days. A drop means the API doesn't cover the job yet.
Also log top error codes per key. A key that makes fifty 400s and goes quiet is a customer who gave up. Email them the exact fix.
A made-up example: Hopsledger opens its API
Hopsledger is a made-up product for this post: a batch log for small craft breweries. Every number below is invented to show the method, not real data.
After a year of support requests, the founder wrote down the jobs and built only what they needed.
Customer job (made up) | Calls in v1 | Decision |
|---|---|---|
Push finished batches into our accounting sheet every night | GET list of batches, filtered by updated time | Build |
Create a batch from our recipe tool when brewing starts | POST batch, with idempotency key | Build |
Show tank status on a screen in the taproom | GET batch by ID | Build |
Let our distributor see stock levels | GET list of batches with a read-only key | Build (read scope covers it) |
Change team members and permissions from a script | Admin endpoints | Not in v1, one request only |
Bulk delete old batches | DELETE batches | Not in v1, CSV cleanup instead |
That's three endpoints, two scopes, test and live keys with prefixes, problem-details errors, a per-key limit with 429 and Retry-After, and one quickstart. In this made-up story, the funnel after the first month showed plenty of keys created but few first calls; the quickstart assumed a field customers didn't have yet. One fix to the docs, no new endpoints.
What traps should you avoid?
Mirroring your database. An endpoint per table locks your internal schema into a public contract. Design around jobs.
One master key per account. No scopes, no names, no way to revoke one integration without breaking the rest.
Inconsistent errors. A string here, an object there, a 200 with "error" inside. Every client must special-case you.
Silent breaking changes. Renaming a field "because nobody uses it" is how you find out who does.
Docs that drift. Hand-written reference pages fall behind the code within weeks. Keep the spec next to the code.
A 14-day plan to ship your first public API
Rule of thumb for a solo maker or a pair, alongside normal work. Stretch it, don't skip steps.
Days | Do this | Done when |
|---|---|---|
1 to 2 | Collect 3 to 5 customer jobs, map them to calls | A list of 4 to 8 endpoint lines |
3 | Send the list to the customers who asked | At least two say "yes, that covers it" |
4 to 5 | Write the OpenAPI file, decide naming, IDs, timestamps, pagination | Spec reviewed against your own rules |
6 to 7 | Keys: prefixes, hashing, scopes, names, revoke, last used | Create, use and revoke a key end to end |
8 to 9 | Build the endpoints with one error format and request IDs | Every endpoint returns the same shapes |
10 | Rate limits with 429 and Retry-After; idempotency on POST | A retry test creates exactly one record |
11 | Quickstart, job examples, limits page, changelog entry | A friend gets a first call in 5 minutes |
12 | Usage logging: keys created, first call, weekly active keys, top errors | One query shows the funnel |
13 | Private release to the customers who asked | Each one made a successful call |
14 | Fix what they hit, then announce it in the changelog | API linked from the app and the docs |
FAQ
REST or GraphQL for a small team?
For a first public API, I'd pick plain REST with JSON. It works with curl and every no-code tool, and caching and rate limits stay simple. GraphQL makes sense when clients need many shapes of the same data and you can handle query cost limits. Most small products aren't there yet.
Should the API be free?
Including API access on paid plans is common, and for v1 that's usually enough. Don't design pricing around the API before you know how people use it. Watch the usage funnel for a few months, then decide.
How do I stop people abusing the API?
Require a key for every call, give new accounts lower limits, make most keys read-only, rate limit and log per key, and revoke fast when something looks wrong.
Do I need an SDK?
Not at first. Curl examples and an OpenAPI file get most developers going. Write an SDK when one language keeps showing up in your logs and you're ready to maintain it.
Build the small API your customers asked for
A first public API doesn't need to be big. It needs to do a few real jobs reliably, fail in ways people understand, and come with a working quickstart. Let the next endpoint come from the next real request. When it's live, you can put your product in front of more makers on MakerHunt.