Every API is a user interface. The users just happen to be developers instead of end customers, and that distinction gets forgotten more often than it should. I've integrated more third-party APIs than I can count, across insurance, remittance, and ticketing platforms, and the difference between a good one and a painful one is rarely about raw functionality. It's about whether someone designing the API stopped to think about the person consuming it.
Here's what I've learned separates an API developers tolerate from one they actually enjoy working with.
Resource naming should be boring and predictable
/api/policies/{id}/claims tells you exactly what you're getting. /api/getPolicyClaimsData does not, and it breaks the moment you need a different HTTP verb against the same resource. Nouns for resources, HTTP verbs for actions. It's not a creative exercise, and that's the point. The less a consumer has to think about your naming conventions, the more they can focus on their actual integration.
Consistency matters more than any individual naming choice. If /policies returns a paginated list with data and meta keys, every other collection endpoint should follow that same shape. I've worked with APIs where half the endpoints paginated one way and the rest did something different, and every integration against that API carried extra defensive code because of it.
Status codes are a contract, not decoration
A 200 with an error message buried in the response body is one of the more common ways to make an API frustrating to consume. If something failed, return a 4xx or 5xx and let HTTP do the job it was designed for. Consumers should be able to branch on status code alone without parsing your response body to find out whether something actually worked.
csharp
if (application is null)
return Results.NotFound(new ProblemDetails { Title = "Application not found" });
return Results.Ok(application);
Pair this with ProblemDetails so error responses have a consistent, predictable shape across your entire API, not a different structure per endpoint depending on who wrote it.
I've seen more than one integration break because an endpoint that used to return twenty records started returning twenty thousand as the underlying data grew. Build pagination in from the start, even when the initial dataset is small, because you will not remember to add it later, and neither will the team maintaining it after you.
csharp
app.MapGet("/api/claims", async (int page = 1, int pageSize = 20, IClaimService service) =>
{
var result = await service.GetPagedAsync(page, pageSize);
return Results.Ok(result);
});
Cursor-based pagination is worth the extra complexity for high-volume, frequently-changing datasets. Offset-based pagination is fine for most everything else. Pick deliberately, don't default blindly.
Versioning before you need it, not during the incident
The first time you need to make a breaking change to a live API is a bad time to discover you have no versioning strategy. URL-based versioning (/api/v1/policies) is the simplest for consumers to reason about, even if header-based versioning is arguably more "correct" in a purist sense. On systems where third parties integrate against you, predictability wins over elegance every time.
Idempotency for anything that touches money or state
If a consumer's request times out and they retry, does your API create a duplicate transaction? On a remittance platform, this isn't a theoretical concern, it's the difference between a minor inconvenience and a customer being charged twice. Idempotency keys, where the client supplies a unique key per logical operation and the server recognizes a repeat, solve this cleanly.
csharp
app.MapPost("/api/transfers", async (TransferRequest request, string idempotencyKey, ITransferService service) =>
{
var existing = await service.GetByIdempotencyKeyAsync(idempotencyKey);
if (existing is not null) return Results.Ok(existing);
var result = await service.ProcessAsync(request, idempotencyKey);
return Results.Ok(result);
});
Documentation that matches reality
Nothing erodes trust in an API faster than documentation that describes a version of the API that no longer exists. I treat OpenAPI specs as a build artifact generated from the actual code, not a document maintained separately by hand, because the two will inevitably drift apart otherwise.
The real takeaway
None of this is complicated in isolation. What makes it hard is doing all of it consistently, on every endpoint, over the life of a growing API, without someone eventually cutting corners under deadline pressure. The APIs I've enjoyed integrating against the most were never the cleverest ones. They were the ones where someone clearly thought about what it would feel like to be on the other end of the request.