Forward-deployed / Learning zone
Technical product sensea standalone module
Lesson 02

APIs & contracts

TL;DR

An API is the contract by which one piece of software talks to another: your app to your server, your server to a payment provider, your product to a model. The contract defines the request (what you send), the response (what you get), and the promises around it: idempotency (safe to retry), versioning (changes don't break callers), and error behavior. PMs don't design the wire format. But the contract is the product surface for anything integrated, and getting it wrong is expensive to undo, because other people build on it.

🎯 For the AI PM

Why it matters — Model providers are APIs. Their contract governs your AI feature's reliability: rate limits, timeouts, streaming, token limits, and what happens on a 429 or 500. Your own AI feature is also an API others may call.

What it changes in your decisions — You treat "what happens when the model API is slow, rate-limited, or errors?" as a first-class requirement. You design idempotency so a retried request doesn't charge the user twice or fire an action twice.

Ask yourself — "If this call fails or is retried, what does the contract guarantee — and is it safe?"

Risk if ignored — Duplicate charges, double-sent messages, or a feature that falls over the first time an upstream API returns an error you didn't plan for.

A request, end to end

The clearest way to see an API is to follow one call between components:

A request, end to end

Every arrow is a promise · idempotency makes a retry harmless

The clearest way to see an API is to follow one call between components.

Client
API layer
Service
Database
1Client → API: POST /orders (Idempotency-Key: abc)
2API: authenticate + validate
3API → Service: create order
4Service → Database: INSERT order
5Database → Service: ok
6Service → API: order created
7API → Client: 201 Created {order_id}
A retry with the same idempotency key returns the same order — no duplicate. One header field is the difference between "resilient" and "refund queue."

Each arrow is a promise. The client promises a well-formed request with auth. The API promises to validate, do the work, and return a status code (2xx success, 4xx "you messed up," 5xx "we messed up") plus a body.

The parts of the contract

Idempotency — the retry-safety promise

Networks fail mid-request, so clients retry. If a retried "create payment" runs twice, the user is charged twice. Idempotency means doing the same operation twice has the same effect as doing it once — usually through an idempotency key the server remembers. Reads are naturally idempotent; writes are the danger. Whenever a duplicate would be harmful — payments, messages, actions — the contract must be idempotent. This is one of the highest-leverage technical questions a PM can ask.

Versioning — changing without breaking

Once others call your API, you can't freely change it — a rename breaks their code. Teams handle change with versioning (/v1/, /v2/) and by making only backward-compatible changes to a live version: add optional fields, and never remove or repurpose existing ones. The product consequence is that API changes are slow and deliberate, because every caller is a dependency. Design the contract as if you'll live with it for years, because you will.

REST isn't the only shape

Everything above describes REST — a resource per URL, one call per round trip — because it's still the default: it powers the large majority of public APIs, and it's what most third-party integrations speak. But two other shapes come up often enough in real engineering conversations that recognizing them by name matters:

The product-sense takeaway isn't picking the protocol — engineering does that. It's recognizing that "the contract" question (what's sent, what's returned, what happens on failure) applies identically regardless of which of the three you're looking at, so a GraphQL or gRPC conversation doesn't need a different mental model than a REST one.

Webhooks — the API that calls you

Sometimes you need to know when something happens elsewhere: a payment cleared, a job finished. Polling ("is it done yet?") is wasteful, so a webhook flips it — the other system POSTs to your endpoint when the event occurs. Webhooks power most integrations, and they bring their own contract concerns. They can arrive out of order, arrive more than once (idempotency again), or not arrive at all — so you still need a reconciliation fallback.

A worked pass: the double charge

Here is the classic contract failure, end to end. A user taps "Pay," and the request reaches the payment service. The charge succeeds, but the response is lost to a network blip. The app sees a timeout and retries. Without idempotency, the second request is a second charge: the user pays twice, support gets a furious ticket, and finance spends a week on refunds. With an idempotency key — the app sends a unique ID per payment attempt, and the server returns the original result for a repeated key — the retry is harmless. One header field is the difference between "resilient" and "refund queue."

The PM's contract review fits in five questions. What happens if this call is retried? That's idempotency. What does the caller see when it fails? That's the error contract — is "card declined" distinguishable from "try again"? Who else consumes this, and what breaks if we rename a field? That's versioning: additive changes are safe, renames and removals are breaking. How does the other side learn something happened? That's webhook vs. polling, and what happens when the webhook receiver is down. What are the rate limits, and what do we do when we hit them? You don't need to design the wire format to ask all five. Every one of them is a user-visible product behavior wearing an engineering costume.

Failure modes

Practitioner checklist