What is the number that decides how much to invest in API documentation?
Up to 1 day a description generated from the code is enough; between 2 and 3 days you need complete documentation with examples and checks; above 3 days the problem is the API and it must be fixed first. In a company of 40 people, at 3 days, the right path cost about 32,940 euros over three years, against 88,560 euros of doing nothing.

A company that makes packaging machinery runs an ERP in .NET and has exposed its APIs for four years to resellers, carriers and one large customer. Every time a new partner wants to connect, the same script plays out: a phone call, three emails, an Excel file with field names, two weeks of questions and an internal developer pulled off whatever he was doing.
The IT manager thinks the partner is the problem, "they just don't get it". The truth is that nobody ever wrote down how those APIs are used, and every integration pays for that silence all over again.
This article tells you what it costs to have no API documentation, which routes exist to get it (and what they cost over three years), how to build it with OpenAPI in ASP.NET Core without writing it twice, and what it must contain for an integrator to actually use it.
You'll find the cost of the silence, the number that decides how much to invest, a comparison of five routes with costs in euros, the C# code that compiles to describe the endpoints, the test that stops you breaking the contract without noticing, the thirty-day plan and the mistakes I see most often.
A premise. I have been designing software for companies since 1999, almost always in .NET, and I have seen excellent APIs buried by useless documents and mediocre APIs saved by one well-written page. I'm not here to sell you a portal: I'll recommend the cheapest route that holds up, and it is often much smaller than the one you were quoted.
The case I use is a typical company, built from orders of magnitude and industry averages. It is not a client, it is a method: you can redo it with your own numbers in an afternoon.
What is API documentation and why does almost nobody write it?
API documentation is everything that lets someone who did not write the program use it without asking you anything: which addresses exist, what to send, what comes back, what each error means, how to authenticate and what will change in the coming months.
The definition is simple. The reason it doesn't get written is just as simple, and it isn't laziness.
- Whoever writes the API already knows how it works. The knowledge feels so obvious that writing it down seems pointless. For the integrator it is the one thing missing.
- It has no deadline. The API release has a date, the documentation doesn't. When time gets tight, it is the first thing cut from the plan.
- It goes stale on its own. A hand-written document is right on the day you write it. After three releases it describes an API that no longer exists, and a wrong document is worse than no document.
- The cost of not having it is invisible. It is scattered across phone calls, emails and half-days. Nobody invoices it and nobody adds it up.
The last point is what turns everything else into a money problem rather than a matter of good manners. If the cost of the silence were a line in the budget, the documentation would already be written.
There is also something new. People who connect to your APIs today increasingly ask an AI assistant to write the integration code. The assistant works well if you give it a clean OpenAPI description, and badly if you give it a PDF full of screenshots. Machine-readable documentation is no longer a luxury for big companies: it decides whether your partner gets a first successful call in an hour or in two weeks.
What does the typical company look like and what numbers do we start from?
The typical company makes packaging machinery and has 40 people. The ERP is in ASP.NET Core and has exposed REST APIs for four years. Five parties use them: a sales portal for resellers, the carrier, an app for service technicians, a large customer with its own order system and the accountant for invoices.
Here are the numbers, the ones you can collect in half a day by looking at emails, the support request log and the hours your developers report.
| Measure | Value | How to find it |
|---|---|---|
| Parties using the APIs | 5 active, 8 new integrations a year | List of issued access keys and projects started in the last year |
| API support requests | 14 a month, 1.5 hours each | Shared mailbox and tickets, words like "field", "error", "what is it called" |
| Hand-holding for each new integration | 3 days of an internal developer | Hours reported on integration projects over the last year |
| Changes that broke a partner | 3 a year, 2 days to fix each | Team memory and incident log |
| Public endpoints | 76, in 11 groups | List of the application's routes |
| Existing documentation | An 18-page PDF from 2022 and an Excel sheet | Ask "where do you send a new partner?" |
| Cost of one hour of internal developer time | 60 euros | Labor cost divided by hours worked |
| Cost of one development day | 480 euros | Reference rate, 8 hours at 60 euros |
Two notes on the numbers. First: 14 requests a month sound like few. That is 21 hours, almost three full days of skilled work that produces not one line of new code. And it doesn't count the call where the developer loses the thread of what he was doing.
Second: the 2022 PDF is not documentation, it is a relic. It describes 52 endpoints out of 76, mentions two fields that no longer exist and never says what a 422 error means. The partner reads it, doesn't trust it, and calls.
How much does it cost not to have documentation?

The cost is real, but it sits in three lines that nobody has ever added up.
- Support requests. Fourteen a month at 1.5 hours is 21 hours, or 252 hours over twelve months. At 60 euros that is 15,120 euros a year.
- Hand-holding. Eight new integrations, three days of support each, 480 euros a day: 11,520 euros. The developer explains out loud what the partner should have been able to read.
- Breakages. Three times a year a change removes or renames a field and a partner grinds to a halt. Two days to understand, fix and apologize: 2,880 euros.
The total is 29,520 euros a year, about 88,560 euros over three years. For 40 people and 76 endpoints it is a figure that appears in no budget, and it is the same figure you should compare against any developer portal quote.
Deliberately left out are the heaviest and hardest-to-estimate items.
- The partner who gives up: the large customer who, after two weeks without an answer, picks a supplier with a documented API. You never find out, because they don't call to tell you.
- The new developer who takes a month to understand the system because the only source is the person who wrote it.
- Technical reputation: people who integrate and lose time tell others that your APIs are an ordeal.
If you want one exercise to do today, open the shared mailbox and count, for the last month, the emails that start with "a question about the API". Multiply by half an hour and by 60 euros. It takes ten minutes, and the figure is usually higher than you expect.
What is the number that decides how much to invest?

People split into two camps. Those who say "documentation is essential, let's build a portal". And those who say "the code explains itself". Both are right and both are wrong, because they mix up two different problems.
The first problem is missing information: the partner doesn't know which field to send, what an error means, how to authenticate. You solve it by writing, and writing well.
The second problem is API quality: names change from one endpoint to another, errors say nothing, the same concept comes in three shapes. Documentation doesn't solve this. You solve it by fixing the API, and then writing.
The number that tells the two cases apart is simple: how many days of hand-holding each new integration needs, on average. The typical company spends three, which is 11,520 euros across eight integrations.
The rule I use:
- Up to one day: the API is already clear enough. A description generated from the code and published, with a couple of examples, is enough. Spending more is waste.
- Between two and three days: you need real documentation, with descriptions, examples, an error catalog and change control. This is the zone where the return is fastest.
- Above three days: documentation is not enough. Every integrator trips over the same inconsistencies, and no portal hides them. Fix the errors and the names first, then document.
The typical company is at three days, right on the edge. The conclusion is not what either the portal advocates or the "self-documenting code" camp expected: first you do a two or three day cleanup of the worst inconsistencies, then you document with tools that keep themselves up to date.
One more check, which takes twenty minutes. Ask the developer who does the hand-holding to note down every question he receives for a month. Tag each with a letter: I if the answer was somewhere the partner didn't find it (information), Q if the question comes from an inconsistency in the API (quality), A if it is about access, keys and environments. If you have more Qs than Is, documenting is not enough.
Which routes exist, and what do they cost over three years?

There are four routes, plus doing nothing. The prices are orders of magnitude I have at hand, to be checked with a quote. The sum is always the same: switching cost, three years of maintenance, and the cost of what stays as it was.
| Route | What you do | Switching cost | Maintenance and fees over 3 years | Cost that remains over 3 years | Total over 3 years |
|---|---|---|---|---|---|
| A. No documentation | Nothing, you answer the phone | 0 | 0 | support 45,360 + hand-holding 34,560 + breakages 8,640 | 88,560 |
| B. Hand-written document | Word or wiki, written by a developer | 3,840 (8 days) | 8,640 (6 days a year) | support 29,484 + hand-holding 23,040 + breakages 8,640 | 73,644 |
| C. Description from the code, as is | OpenAPI generated and published, nothing else | 1,920 (4 days) | 2,880 (2 days a year) | support 20,412 + hand-holding 23,040 + breakages 8,640 | 56,892 |
| D. Purchased portal | Subscription service with pages, live try-out and written content | 4,800 (10 days) | 7,200 in fees + 5,760 for content | support 15,876 + hand-holding 17,280 + breakages 8,640 | 59,556 |
| E. From the code, with examples and control | Curated OpenAPI, examples, listed errors, change control in the pipeline | 4,320 (9 days) | 2,880 (2 days a year) | support 11,340 + hand-holding 11,520 + breakages 2,880 | 32,940 |
How to read the table. The cheapest route over three years is E, the most expensive after silence is the hand-written document, and the purchased portal costs almost as much as the plain generated description, because the fee does not remove the work of writing the content.
Why does E beat C, which already looks "fully automatic"? Two reasons. First: the generated description gives the shape (fields, types, addresses) but not the why, nor what to do when an error comes back. That is the part that generates support requests. Second: without change control, the three breakages a year stay three. With E they drop to one.
Why does E beat the portal? Because the portal is a container, and your problem is the content. A portal with empty descriptions costs 7,200 euros in fees and leaves the integrator exactly where he was. It makes sense when you have hundreds of external partners, a developer program with self-service registration and keys. For eight integrations a year, no.
And the hand-written document? It looks like the prudent route and costs more than the generated description, because it ages. The saving on support is 35 percent in the first year and less in the second, because people stop trusting it. The cost I used is a conservative estimate.
Three caveats on the numbers.
- The reduction percentages (55 percent of support with the generated description, 75 percent with the curated one, 67 percent of hand-holding with E) are orders of magnitude I have seen in projects. Yours should be measured after the first quarter.
- The portal fee I used, 2,400 euros a year, is an order of magnitude. Price lists change often and many services charge more as users grow.
- E does not include the cost of fixing the API's inconsistencies. They are real, and they are the reason the "above three days" zone exists. Count them separately: two or three days of cleanup is a good starting point.
How do you build the OpenAPI description in ASP.NET Core without writing it twice?
The rule that holds everything together is one: documentation is not written next to the code, it is derived from the code. If the endpoint description lives in a separate file, within three releases it describes a different API.
OpenAPI is the standard that describes a REST API in a file readable by people and programs: addresses, parameters, request and response shapes, errors, authentication. From that file you generate reference pages, clients in various languages, contract tests and files for Postman.
In ASP.NET Core generation is built in since .NET 9, in the Microsoft.AspNetCore.OpenApi package. You no longer need third-party tools to get the file. For a browsable page with live try-out you can add a viewer such as Scalar, which is a separate package.
Here is a minimal application that describes an endpoint properly. The code compiles with .NET 10, after installing the Microsoft.AspNetCore.OpenApi and Scalar.AspNetCore packages.
using Microsoft.AspNetCore.Mvc;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IRegistroOrdini, RegistroOrdini>();
builder.Services.AddOpenApi(opzioni =>
{
opzioni.AddDocumentTransformer((documento, contesto, token) =>
{
documento.Info.Title = "API Ordini";
documento.Info.Version = "v1";
documento.Info.Description =
"Crea e consulta gli ordini. Autenticazione con chiave nell'intestazione X-Api-Key. " +
"Gli errori seguono il formato Problem Details (RFC 9457).";
return Task.CompletedTask;
});
});
var app = builder.Build();
app.MapOpenApi(); // pubblica /openapi/v1.json
if (app.Environment.IsDevelopment())
{
app.MapScalarApiReference(); // pagina di consultazione con prova in linea
}
var ordini = app.MapGroup("/ordini").WithTags("Ordini");
ordini.MapGet("/{id:guid}", (Guid id, IRegistroOrdini registro) =>
{
var ordine = registro.Trova(id);
return ordine is null
? Results.Problem(
title: "Ordine non trovato",
detail: $"Nessun ordine con identificativo {id}.",
statusCode: StatusCodes.Status404NotFound)
: Results.Ok(ordine);
})
.WithName("LeggiOrdine")
.WithSummary("Legge un ordine")
.WithDescription("Restituisce l'ordine con righe e stato. Lo stato cambia solo in avanti.")
.Produces<OrdineDto>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound);
ordini.MapPost("/", (
[FromHeader(Name = "Idempotency-Key")] string? chiave,
NuovoOrdineDto richiesta,
IRegistroOrdini registro) =>
{
if (string.IsNullOrWhiteSpace(chiave))
{
return Results.Problem(
title: "Manca l'intestazione Idempotency-Key",
detail: "Senza chiave una ripetizione della chiamata creerebbe un ordine doppio.",
statusCode: StatusCodes.Status400BadRequest);
}
var creato = registro.Crea(chiave, richiesta);
return Results.Created($"/ordini/{creato.Id}", creato);
})
.WithName("CreaOrdine")
.WithSummary("Crea un ordine")
.WithDescription("Se ripeti la chiamata con la stessa Idempotency-Key ottieni lo stesso ordine, non un secondo.")
.Produces<OrdineDto>(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.Run();
public record RigaDto(string Codice, int Quantita);
public record NuovoOrdineDto(string CodiceCliente, List<RigaDto> Righe);
public record OrdineDto(Guid Id, string CodiceCliente, string Stato, List<RigaDto> Righe);
public interface IRegistroOrdini
{
OrdineDto? Trova(Guid id);
OrdineDto Crea(string chiave, NuovoOrdineDto richiesta);
}
public class RegistroOrdini : IRegistroOrdini
{
private readonly Dictionary<Guid, OrdineDto> _ordini = new();
private readonly Dictionary<string, Guid> _chiavi = new();
public OrdineDto? Trova(Guid id) => _ordini.GetValueOrDefault(id);
public OrdineDto Crea(string chiave, NuovoOrdineDto richiesta)
{
lock (_ordini)
{
if (_chiavi.TryGetValue(chiave, out var esistente))
{
return _ordini[esistente];
}
var ordine = new OrdineDto(Guid.NewGuid(), richiesta.CodiceCliente, "Ricevuto", richiesta.Righe);
_ordini[ordine.Id] = ordine;
_chiavi[chiave] = ordine.Id;
return ordine;
}
}
}
public partial class Program { }What does the generated file do, without you writing a line of separate documentation? It lists the two endpoints with name, summary and description, the shapes of OrdineDto and NuovoOrdineDto, the mandatory idempotency header, and the possible response codes, including the error format.
Three choices in this code matter more than the code itself.
- Every endpoint has an operation name.
WithNamebecomes the identifier that client generators use. If you leave it empty, the name is invented by the tooling and changes every time you rename the method. - Errors are declared.
ProducesProblemputs the failure cases of the endpoint into the file. A partner who sees only the 200 writes an integration that breaks on the first error. - The description states a rule, it doesn't repeat the name. "The status only moves forward" is information. "Reads an order" is just a title.
A note on long descriptions. Don't write them inside code strings if they run past two sentences. From .NET 10 the generator can also read XML comments (///) from your methods, provided you enable documentation file generation in the project. For APIs built with controllers, XML comments are the natural route; for minimal APIs, the WithSummary and WithDescription calls are more direct and show up in review.
What must the documentation contain for integrators to actually use it?

The OpenAPI file is the reference: it says what exists. It is not enough. An integrator has five needs, and the file covers one and a half.
- The reference. Every endpoint, every field, every type. The code generates it. It is the one part that must never be written by hand.
- The first call in ten minutes. A page that takes you from the access key to a successful response, with a ready-to-paste request in
curl. It is the most read page and the most neglected. If it's missing, the partner asks for it by phone. - Scenario guides. "How do I create an order and track its status", "how do I handle a retry after a timeout". One page for everything the integrator really wants to do, in the order they do it.
- The error catalog. For each error: what it means, whose fault it is, what to do. This is where most support requests die.
- The change log. What changed, when, and what will be removed. Without it, the integrator has to find out alone, usually at midnight.
Points 2, 3, 4 and 5 cannot be derived from the code. You write them once, in a few text files in the same repository, and review them together with the code in the same pull request. It is about four days of work for the first draft, and little after that.
For each of the five parts, here is what works in practice.
The first call
A complete, copyable request, with a test value that works in the test environment. Not "replace with your key" in a three-line text: the whole request, and the expected response below it.
curl -X POST https://api.esempio.it/ordini \
-H "X-Api-Key: CHIAVE_DI_PROVA" \
-H "Idempotency-Key: 6f2c1d9e-prova-0001" \
-H "Content-Type: application/json" \
-d '{"codiceCliente":"C-1042","righe":[{"codice":"NASTRO-45","quantita":12}]}'Below it, the response with a 201 code and the full body. If the partner sees the same thing on their screen within ten minutes, you have already saved yourself the phone call.
Errors
The error format must be one, for the whole API. The standard is Problem Details, described by RFC 9457, which ASP.NET Core already knows how to produce. An error has a type, a title, a status code, a detail and, when needed, the list of wrong fields.
The error catalog is a table like this:
| Code | When it happens | Whose fault | What to do |
|---|---|---|---|
| 400 | Idempotency-Key is missing or the body is unreadable | The caller | Add the header; check the body format |
| 401 | Key missing, expired or revoked | The caller | Request a new key; do not retry automatically |
| 404 | The order does not exist or belongs to another customer | The caller | Check the identifier; this is not an error to retry |
| 409 | Same Idempotency-Key with a different body | The caller | Use a new key for a new order |
| 422 | The body is readable but a line is not valid (unknown code, zero quantity) | The caller | Fix the field named in the error list |
| 429 | Too many requests in a short time | The caller | Wait the seconds given in Retry-After and retry |
| 503 | The service is under maintenance or under load | Us | Retry with increasing backoff; check the status page |
The "whose fault" column is the one that saves the most phone calls. If it's the integrator's, they know they have to fix something. If it's ours, they know they have to wait, and they don't open a ticket.
Retries
The most expensive question in the world is "did the call go through?". It happens when the network drops halfway: the partner doesn't know whether the order was created. If they retry without protection, they risk a duplicate; if they don't retry, they risk losing it.
The solution is idempotency: the integrator sends a unique key for each operation, and the service, if it sees it twice, returns the same result. In the code above the header is Idempotency-Key, by now a widespread convention. Document it in the "what to do after a timeout" guide: it is the page that saves you from duplicate orders.
Changes and deadlines
An API changes. The promise you can make is that changes that break a partner don't arrive without notice. Keeping it takes two things. A version number in the address or in a header, and a standard way to announce that something is about to disappear.
The standard exists. The Deprecation header (RFC 9745) says an endpoint is deprecated; the Sunset header (RFC 8594) gives the day it will be switched off. Put in the responses, they let integrators notice from their own logs, without reading your newsletter.
app.Use(async (contesto, successivo) =>
{
if (contesto.Request.Path.StartsWithSegments("/v1/spedizioni-vecchie"))
{
contesto.Response.Headers["Deprecation"] = "true";
contesto.Response.Headers["Sunset"] = "Fri, 30 Apr 2027 23:59:59 GMT";
contesto.Response.Headers["Link"] = "<https://api.esempio.it/docs/migrazione-spedizioni>; rel=\"deprecation\"";
}
await successivo();
});In this example the date is written in plain text for simplicity. In a real project you read it from a deprecations table, the same one that feeds the change log, so the two can never drift apart.
How do you stop breaking a partner without noticing?
The typical company's three yearly breakages have the same origin: a developer tidies up a name, removes a field that "isn't needed" or makes a parameter mandatory, and nobody checks who uses it. The remedy is not caution, it is an automated check that fails when the contract changes.
The check works on two levels.
The first is a contract test. The approved OpenAPI file lives in the repository. On every build, the test starts the application, reads the file it generates now and compares it with the approved one. If they differ, the test fails, and whoever made the change must update the approved file in the same pull request: the change becomes visible to the reviewer.
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class ContrattoApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _fabbrica;
public ContrattoApiTests(WebApplicationFactory<Program> fabbrica)
{
_fabbrica = fabbrica;
}
[Fact]
public async Task Il_contratto_non_cambia_senza_che_qualcuno_lo_approvi()
{
var client = _fabbrica.CreateClient();
var attuale = await client.GetStringAsync("/openapi/v1.json");
var approvato = await File.ReadAllTextAsync("Contratto/openapi.v1.json");
Assert.Equal(Normalizza(approvato), Normalizza(attuale));
}
private static string Normalizza(string json) => JsonNode.Parse(json)!.ToJsonString();
}The test needs the Microsoft.AspNetCore.Mvc.Testing package, and the public partial class Program { } line at the bottom of the application, which the previous example already contains. You create the approved file the first time by saving what the application generates and reviewing it by hand.
The second level tells harmless changes apart from those that break. Adding an optional field to a response hurts nobody. Removing a field, changing its type or making a previously optional parameter mandatory does. The free tool oasdiff compares two OpenAPI files and classifies the differences. In the pipeline a single step is enough:
oasdiff breaking Contratto/openapi.v1.json openapi-attuale.json --fail-on ERRIf the comparison finds a breaking change, the command exits with an error and the pipeline stops. The change can still be made, but only in a new version of the API, with notice and with a shutdown date for the old one.
How much does all this cost? The test and the pipeline step are about one day of work. The benefit is not in the test, it is in the conversation it forces: every change to the contract goes through a review where someone asks "what about the partners?". In the typical company the breakages drop from three to one a year, and the one that remains is the one you chose to make.
How do you publish it, and who needs the live try-out?
With the OpenAPI file in hand, there are three ways to publish it, from simplest to richest.
- Just the file. You put it at a public address or send it to the partner. Experienced developers load it into their own tool. It's the minimum, and it costs zero.
- A reference page. A viewer (Scalar, Redoc, Swagger UI) reads the file and renders the pages with search and live try-out. It costs half a day.
- A documentation site. The hand-written pages (first call, guides, errors, changes) and the reference page sit together, with the same look. It costs two to four days, and it is published from the pipeline on every release.
The live try-out, the button that fires a real request from the browser, is very popular and should be used with judgment. On a test environment with fake data it is a gift. On production it is a risk: whoever tries it can create a real order. Simple rule: live try-out exists only in the test environment, with test keys.
The test environment is, after the first call, the thing the integrator appreciates most. A separate address, fake but realistic data, keys they can get on their own, no risk of polluting production. For a serious partner the difference between "I have an environment where I can get things wrong" and "I test in production" is the difference between a week and a month.
The OpenAPI file also gives you other things almost for free.
- Ready-made clients. Tools like Kiota (from Microsoft) generate a client for C#, TypeScript, Python and other languages. A partner on .NET writes
cliente.Ordini.PostAsync(...)instead of building requests by hand. - The collection for Postman or other tools. You import the file and the collection is ready, with every endpoint.
- Data for coding assistants. A consistent OpenAPI file is the best input for anyone asking an assistant to write the integration. The more curated the file, the less the assistant makes up.
What happens if the API is inconsistent?
Back to the "above three days" zone, because it is where most projects fail. If your count says each integration needs four or five days of hand-holding, documentation will fix nothing: it will only make the inconsistencies visible, in an orderly way.
The inconsistencies I see most often, in the order they cost:
- Different names for the same thing.
codiceClientein one endpoint,customerIdin another,idClientein a third. The integrator writes three mappings and suspects they are different things. - Silent errors. A generic 500, or a 400 with the text "Error". The integrator doesn't know whether to fix or retry, and calls.
- Dates and numbers in mixed formats. One date in Italian format, one in ISO 8601, one as a millisecond count. Amounts as text with a comma. These are the quietest bugs.
- Different pagination in every list. Page and size, or a cursor, or nothing and it returns everything.
- The same status in different words. "Annullato", "cancellato", "stornato" across three endpoints for the same concept.
- Fields that change meaning. A
statofield that used to be 0 or 1 and is now a text value, with no announcement.
None of these is solved by a portal. They are solved by one day of cleanup for each group of endpoints, picking a convention and applying it. The cost is two to three days for the typical company, and it pays back in under a year on hand-holding alone.
The right way to do it is to leave the old routes alone. You create the corrected version as a new version, announce it with Deprecation and Sunset and leave the old one running for six to twelve months. During that period the old one is documented as "being phased out" and the new one is the one in the quickstart. It is the slowest and least dangerous cleanup there is.
How do you go from silence to documentation in thirty days?
The transition doesn't need a six-month project. For the typical company the plan is thirty working days at most, with a developer at half time and one hour a week from whoever receives the support requests.
- Days 1-3: the count. Open the shared mailbox, count last month's questions and tag each with I, Q or A. Add up the hand-holding days of the last year. Write down the deciding number.
- Days 4-8: the OpenAPI file. Turn on generation, give every endpoint an operation name, declare the error codes. Publish the file as is, at an internal address.
- Days 9-12: the endpoint descriptions. For the ten most used endpoints, a summary, a description that states the rule and an example. The other 66 stay as they are. Usually most questions concern a small share of the endpoints.
- Days 13-16: the first call. One page, one copyable request, one expected response, one test environment. Have someone who doesn't know the APIs try it.
- Days 17-20: the error catalog. The table with code, cause, fault and remedy. Replace silent errors with Problem Details on the ten most used endpoints.
- Days 21-24: change control. The approved file in the repository, the contract test, the oasdiff step in the pipeline.
- Days 25-28: the guides. The two or three guides that match the most frequent questions: creating an order, tracking its status, handling a timeout.
- Days 29-30: publication. The documentation site, the address, the change log. Send the link to the five parties already using the APIs, with one line: "from today the documentation is here, and it updates with every release".
The rollback plan costs little: everything is in text files in the repository and the API's behavior doesn't change. If the plan stops at day 12, you already have a generated file and ten described endpoints, which is route C in the table.
A note on people, which weighs more than the code. Before you start, ask the two developers who answer partners most often to write down, freehand, the five questions they hear most. That is the table of contents of the documentation, and it is usually better than what you would have designed at a desk. Then, three months after release, redo the count: if requests haven't dropped by at least half, something wasn't written where people look.
Which mistakes should you avoid when documenting an API?
The mistakes I see most often, and that cost the most, are eight.
- Writing the document by hand, separate from the code. After three releases it is wrong, and people stop trusting even the correct parts.
- Documenting everything at the same level. Seventy-six endpoints described with the same care means ten described badly. Focus on the small part that generates most of the questions.
- Descriptions that repeat the name. "CreaOrdine: creates an order". It adds nothing. Write the rule, the constraint, the odd case.
- Not declaring errors. The file shows only success, and the integrator discovers the failure cases in production.
- Examples that don't work. An example copied from an old environment, with a field that no longer exists, is worse than no example. Examples should be tested in the pipeline like any other code.
- Breaking the contract in secret. Removing a field "because nobody uses it" without checking. You check with a test, not with memory.
- Exposing live try-out on production. A curious partner creates a real order. Test environment only, test keys only.
- Buying a portal before you have the content. The fee starts immediately, the pages stay empty, and the integrator finds an elegant interface with no answers.
One rule sums them all up: documentation is part of the release, not an activity on the side. If a change to the API goes to production without touching the documentation, the pipeline should stop it. It is easier than it sounds, and after the first month nobody thinks about it anymore.
When is it better not to document?
It happens often, and it is good news. There are three situations where spending on complete documentation doesn't pay, and I'll tell you plainly.
- The API is internal to a single team. Whoever writes it also uses it, and the user sits in the next room. A generated file and a README are enough here, and they're free.
- A single integration, already in production, that doesn't change. The cost of silence is close to zero. One page with the authentication rules and the errors saves a lot of trouble.
- The API is about to disappear. If you will replace it with a product within twelve months, invest in the shutdown plan and nothing else.
In each of these cases there is a minimum worth doing anyway, and it costs a few hours: the generated and published OpenAPI file, a written list of issued keys and who uses them, and a page with the test environment address. That is 8-10 hours of work, and it removes the worst risk, having an API that only one person can explain.
If you've read this far, you probably have a portal quote in mind, or a partner who has been waiting two weeks for an answer. Before deciding, take your numbers: requests per month, hand-holding days, breakages per year. Half a day, and you know which zone of the rule you are in.
If you want a second opinion on your case, the route is software consulting. And when the right solution is a system built around the way you work, with APIs designed from day one for whoever will have to integrate them, you'll find it explained on the page about custom software. If your problem is connecting the ERP to an online store, I cover it in the guide to ERP and e-commerce integration. On the topic of architecture choices you pay for over years, there is when you really need the CQRS pattern.
Frequently asked questions
For a company of 40 people, 76 endpoints and 8 new integrations a year, about 29,520 euros a year: 15,120 euros of support requests, 11,520 euros of hand-holding partners and 2,880 euros for changes that break an integration. Over three years that is about 88,560 euros.
It is the standard that describes a REST API in a file readable by people and programs: addresses, parameters, request and response shapes, errors and authentication. From that file you get reference pages, clients in several languages, contract tests and Postman collections.
Since .NET 9 yes: the Microsoft.AspNetCore.OpenApi package produces the file from the application, with AddOpenApi and MapOpenApi. It does not describe why the rules exist or the first call: those parts must be written and kept in the same repository as the code.
For 8 integrations a year, no: in the typical company it costs about 59,556 euros over three years against 32,940 for the path from the code with examples and checks, because the subscription does not remove the work of writing the content. It makes sense with hundreds of external partners and keys issued on their own.
With a test that compares the generated OpenAPI file with the approved one in the repository, and with a tool like oasdiff that flags incompatible changes and stops the pipeline. Breaking changes go into a new version, with the Deprecation and Sunset headers.
When it is internal to one team, when there is a single stable integration, or when it will be replaced within twelve months. The minimum to do anyway is to publish the generated OpenAPI file, keep a list of issued keys and point to the test environment.
