Solo Founder Product Engineering Handbook
API Design Brief
Turn one customer workflow into a narrow API contract with explicit authority, state, failure, retry, and compatibility boundaries.
Begin with the Work, Not the Route
An API turns part of your product into a promise that somebody else’s software can depend on. Once a customer has written an integration, an ambiguous status or casually renamed field becomes their incident and your support queue. A solo founder should therefore expose less than the application happens to contain, but describe the exposed behavior precisely.
Start the brief in the customer’s language:
Customer and job:
Trigger:
Outcome they need:
How they know the outcome happened:
What can still fail after the request is accepted:
Why an API is better than the current UI, import, automation, or manual service:
First known consumer:
Decision date:
If the first consumer and workflow are unknown, stop. A speculative API expands the product’s compatibility surface before it creates evidence. A design-partner request is a better starting point than a plan to be “API-first.”
Suppose an agency uses a reporting product to turn advertising exports into reviewed weekly digests. A design partner wants its internal workflow to deliver an approved digest without a person returning to the product UI. The useful operation is narrow: request delivery of one immutable digest version to one stored recipient through an allowed channel, then observe the delivery’s progress. It is not remote access to every screen or database object in the product.
Draw the Boundary Before Naming Endpoints
List the durable product objects involved and the facts the caller may learn or change:
Primary object created or changed:
Objects referenced:
Required state before the operation:
State transition requested:
State transitions that may occur later:
Facts deliberately kept internal:
For the agency workflow, delivery is the created resource. It refers to an approved digest, a specific digest_version, a stored recipient, and an agency account. The request may move the delivery from nonexistent to queued; a worker and provider may later move it to sending, accepted, or failed. Queue acceptance is not evidence that the provider accepted the message, and provider acceptance is not evidence that the recipient read it.
Keeping those claims separate prevents a convenient HTTP response from overstating the customer outcome. It also suggests two operations rather than a collection of action-shaped routes:
POST /v1/deliveries
GET /v1/deliveries/{delivery_id}
Do not create general-purpose update and delete operations merely for symmetry. Add an operation when a real workflow needs it and when its authorization, side effects, and failure semantics can be stated.
Write One Contract Completely
Copy this record for each operation:
Operation and purpose:
Caller and credential type:
Account and object scope:
Required permission:
Preconditions:
Request fields and limits:
Successful response and status:
State represented by that response:
Later states and how the caller observes them:
Safe retry rule:
Idempotency-key scope and retention:
Same key with changed input:
Failure code, HTTP status, and caller action for each expected failure:
Rate-limit boundary and retry signal:
Sensitive data accepted, returned, or logged:
Audit facts retained:
Compatibility promise:
Deprecation and removal trigger:
Acceptance examples:
Rejection and boundary examples:
Operational owner and support signal:
For the delivery operation, a successful request might be:
POST /v1/deliveries HTTP/1.1
Authorization: Bearer <account-scoped credential>
Idempotency-Key: "agency-job-8842"
Content-Type: application/json
{
"digest_id": "digest_661",
"digest_version": 3,
"recipient_id": "recipient_073",
"channel": "email"
}
The account comes from the credential, not from a caller-supplied account_id. The server verifies that the credential may create deliveries, that the digest and recipient belong to the same account, that the requested digest version is still approved, and that email is allowed for that recipient. Checking only the caller’s role is insufficient; authorization also depends on object ownership and current state.
If delivery is asynchronous, the response should say so without pretending the work has finished:
HTTP/1.1 202 Accepted
Location: /v1/deliveries/delivery_992
Content-Type: application/json
{
"id": "delivery_992",
"status": "queued",
"digest_id": "digest_661",
"digest_version": 3,
"recipient_id": "recipient_073",
"channel": "email",
"created_at": "2026-07-17T08:42:11Z"
}
The retrieval operation must define which states are terminal, whether failure is retryable, and how long the resource remains available. If completion time matters to the consumer, state the observed service expectation and the behavior when it is missed. Do not imply instant completion simply because the common case is quick.
Make Failure Useful to the Caller
“Something went wrong” transfers diagnosis to the customer and usually back to the founder. Define a small error envelope with a stable machine code, a safe human message, and a request identifier:
{
"error": {
"code": "digest_version_conflict",
"message": "The requested digest version is no longer approved.",
"request_id": "req_4f81"
}
}
Map each expected failure to an action. An invalid or missing credential asks the consumer to authenticate. A recognized caller without permission is forbidden. A resource outside the caller’s account should not reveal whether another account owns it. Malformed input needs field-level correction; a digest whose approval changed needs the caller to refresh state; rate limiting and temporary dependency failure need bounded retry guidance. Choose status codes consistently, but let the stable error code carry the product-specific meaning.
Keep internal exception text, provider payloads, credentials, customer content, and stack traces out of responses. Logs may link the safe request_id to restricted diagnostics. Record enough audit information to answer who requested the delivery, for which account and object, under which credential, and what state transition followed—without copying the digest or recipient address into every log line.
Design Retries as Part of the Operation
Any caller can lose a response after the server has accepted the request. Without a retry contract, the caller must choose between abandoning work and creating a duplicate side effect.
For POST /v1/deliveries, require an idempotency key. Scope it to the authenticated account, operation, and caller as appropriate; store a fingerprint of the meaningful request fields. A repeated key with the same fingerprint returns the original delivery and response semantics. The same key with different input returns a conflict instead of silently reusing or replacing work. State how long the key is retained and what the caller must do after that window.
Idempotent request handling does not by itself guarantee exactly-once delivery. The queue worker, database changes, and external provider each have their own retry boundaries. Give the created delivery a stable identity, persist the accepted request before starting the side effect, and make downstream attempts deduplicable where the provider permits it. When certainty is impossible, expose an honest state such as outcome_unknown and define the reconciliation path. Never report failure and then quietly continue an untraceable side effect.
Spend Compatibility Deliberately
Write the first compatibility promise in the brief:
Consumers may depend on:
Consumers must tolerate:
We will not change without a new version or migration:
We may add without a new version:
Deprecation notice and observation period:
Removal requires:
For a JSON API, adding an optional response field is often compatible when consumers are told to ignore fields they do not understand. Removing or renaming a field, changing its type, making an optional request field mandatory, or changing the meaning of accepted is not. New enum values can also break clients that assumed the original set was closed, so state whether a consumer must handle an unknown value.
Do not version in anticipation of every imaginable future. Preserve meanings, prefer additive changes, and introduce a new version or explicit migration when a real consumer cannot safely cross the change. Before removing old behavior, identify active consumers from credential or request data, give them a migration path, and observe that traffic has moved. A calendar date alone does not prove that a design partner updated its integration.
Test the Promise, Not Only the Happy Request
Turn every sentence that contains “must,” “may,” “same,” “later,” or “only” into an example or automated check. For the delivery contract, include at least these cases:
- an authorized credential creates one queued delivery for an approved digest version;
- a retry with the same key and input returns that delivery rather than creating another;
- the same key with changed input is rejected;
- a credential from another account cannot discover or deliver the digest;
- a caller with the wrong permission cannot create a delivery;
- an unapproved or stale digest version cannot enter the queue;
- malformed fields, oversized input, and unsupported channels have stable errors;
- rate limiting tells the caller when or how to retry;
- a worker or provider failure becomes an observable delivery state;
- additional response fields and previously unseen documented enum values do not break the reference consumer;
- logs and errors omit credentials, recipient addresses, and digest content.
Trace one request from edge to stored resource, queue, provider attempt, audit record, status lookup, and product event. Then trace a timeout at each boundary. The brief is ready when another engineer could implement the operation without inventing authority or state transitions, a consumer could recover from every documented outcome, and the founder can name the ongoing support and compatibility burden.
Keep the brief beside the API schema and the customer request that justified it. Update it when the contract changes in meaning, not whenever implementation details move. If the useful workflow can still be served safely through a manual export or narrow automation, that may remain the better product decision.
Continue reading
Full table of contents