How to Structure OpenAPI 3.1 for Financial REST APIs
Learn how to structure OpenAPI 3.1 financial APIs for money precision, FAPI 2.0 security, idempotency, errors, versioning, and regulated review.

Team Docuwiz
Documentation Experts
Sign Up for Docuwiz
Experience the magic of collaborative documentation with Docs-As-Code Workflow
Introduction
A payments spec has to survive things a generic REST API never faces. An amount that loses a cent between your spec and a generated Java client becomes a reconciliation problem. A retry that creates a second payment becomes a customer complaint and an audit finding. And unlike most APIs, this one gets read by compliance and legal before it ships.
OpenAPI 3.1 handles all of that better than 3.0 did. Most teams upgraded the version number and kept writing 3.0-shaped specs anyway.
This guide covers the patterns that matter when an API moves money: how to type amounts so precision survives code generation, how to describe FAPI 2.0 security, how to document idempotency and errors, and what to check if you are documenting a spec somebody else designed.
Why does OpenAPI 3.1 matter for financial APIs?
OpenAPI 3.1 is the first release aligned with JSON Schema Draft 2020-12, which gives you a fuller and more consistent validation vocabulary than 3.0 offered. OpenAPI 3.1.0 landed in February 2021. Before it, OpenAPI used its own dialect, a subset of JSON Schema with custom extensions.
3.0 was not helpless. It had pattern, enum, numeric ranges, and oneOf, and you can build a serviceable financial schema with those alone. What it could not do was express conditional rules cleanly, and that is where most financial validation lives.
Three changes matter most.
Real JSON Schema. Inside schemas, you can now use if/then/else, dependentRequired, unevaluatedProperties, and $ref alongside sibling keywords. 3.0 either lacked all of these or silently ignored them. Conditional validation is everywhere in payments: a domestic transfer needs a sort code, an international one needs a BIC and IBAN. In 3.0 you approximated that with oneOf and a comment. In 3.1 you can express it directly.
PaymentInstruction:
type: object
required: [amount, currency, scheme]
properties:
scheme: { enum: [FASTER_PAYMENTS, SEPA_CREDIT_TRANSFER] }
if:
required: [scheme] # without this, an absent scheme passes `if`
properties: { scheme: { const: SEPA_CREDIT_TRANSFER } }
then:
required: [iban, bic]
else:
required: [sortCode, accountNumber]
Webhooks as a first-class element. Webhooks are a top-level object, a sibling of paths.
Before 3.1, there was nowhere in the spec to describe the events your API pushes out: payment status changes, settlement confirmations, mandate cancellations. OpenAPI 3.0 had callbacks, but those hang off a specific operation. They only describe an event triggered by a request the client just made. A payment status change that fires two hours later, to a URL configured in a dashboard, had no home in the spec at all. It lived in a PDF, a wiki page, or an onboarding email.
Now it is an entry in the document, with a schema and a response, exactly like an endpoint. A partner can mock the event and test their listener before you send a live one. Generator support is still thinner than for paths, so check your toolchain.
nullable is gone. OpenAPI 3.0's nullable: true was a non-standard bolt-on. In 3.1, you write type: [string, "null"], which is ordinary JSON Schema.
This is not cosmetic. Let's take an example.
Your API has a settlementDate field. While a payment is still waiting to settle, there is no date yet, so the API sends back null. That is correct, and your spec says so.
Now a client team generates their code from your spec. Some generators don't handle a two-type list properly. They read [string, "null"], keep the first one, and throw the null away. The client code now has a date field that refuses to accept null.
Nobody notices. On a normal working day, payments settle straight away, the date is always there, and every test passes.
Then a payment arrives on a bank holiday. Settlement can't happen until the next working day, so your API sends null, exactly as promised. The client code was never built to accept it, and it fails. Every client fails at once, on the day with the biggest payment queue and the fewest people around to fix it.
3.1 also adds jsonSchemaDialect at the document root, so tooling knows exactly which schema rules apply. That matters when the spec is an artifact in an audit trail.
One note on versions. The current 3.1 patch is 3.1.2; patch releases clarify wording without changing the feature set, so anything written for 3.1.0 still applies. OpenAPI 3.2.0 was published in September 2025 as a feature release, and existing 3.1 documents keep working. Nothing below is invalidated by either.
How do you model money, currencies, and dates in OpenAPI 3.1?
How should you represent money amounts?
Represent money as a constrained decimal string or as an integer in the currency's minor units. Never as type: number.
JSON itself allows arbitrary-precision numbers. Most parsers do not. The common default is to deserialize into an IEEE 754 double, and doubles cannot represent 0.1 exactly. Some parsers can be configured for exact decimals, but that is opt-in, and you do not control your consumers' configuration. A type: number amount means every client team has to get that setting right, and you will not find out which ones didn't until a reconciliation breaks.
Two representations work. A third turns up often enough to be worth recognizing.
Option 1, decimal string. The most common choice in banking APIs.
Amount:
type: string
pattern: '^-?\d{1,18}(\.\d{1,4})?$'
example: "1234.56"
The string forces the client to parse deliberately. Depending on the generator and configuration, Java typically gives you a String, which you convert to BigDecimal; C# maps to string or decimal; TypeScript keeps it a string until a decimal library touches it. Generate a client in each language you support and check what you actually got, rather than assuming the mapping.
The pattern is what does the work. It pins scale and range, and it survives into generated validation. Adding format: decimal alongside it documents intent to a human reader, but no validator enforces it, so the pattern has to stand on its own.
Option 2, integer minor units. Stripe's approach: type: integer with format: int64, where 5050 means USD 50.50. Exact, fast, and immune to float problems. The catch is that the minor-unit exponent varies: JPY has none, most currencies have two, and a few have three. The number is meaningless without the currency, so never document one without the other.
The third pattern is a structured object, Google's Money type, which splits the whole units from a fractional field into separate properties. It is precise, and you will meet it in specs with gRPC ancestry, but it is verbose and unfamiliar to most consumers of a banking API. Recognize it; don't reach for it.
Pick one of the first two and use it everywhere. Mixed representations in a single spec are worse than either choice on its own.
How do you model currency codes, account identifiers, and dates?
Constrain all three. Currency is a three-letter ISO 4217 code, so make it an enum listing the currencies you actually support, not every code in the standard. An unsupported currency should fail validation rather than reach a payment rail and get rejected there.
Account identifiers need a pattern, not a bare type: string. IBANs, sort codes, and routing numbers each have a defined shape, and a malformed one caught at the edge of your API is considerably cheaper than one caught downstream.
Be clear with yourself about where the line falls. A schema does structural validation: is this the right length, the right character set, the right shape. It cannot tell you that an IBAN's mod-97 checksum is valid, that a routing number is currently assigned, or that a currency in your enum is one your settlement provider will actually accept today. Those are application checks and maintained allowlists. The spec's job is to reject the obviously wrong before it costs anyone a round trip, not to be the last line of defense.
For dates, separate the business date from the timestamp. A settlement date is a calendar date; an event time is an instant, RFC 3339, always with an offset. Mark the difference with format: date and format: date-time, not because they validate, but because generators map them to different types and a reader can tell at a glance which one they are looking at. Conflating the two produces off-by-one-day errors around midnight and quarter ends, which is precisely where a reconciliation team will find them.
How do you describe FAPI 2.0 security in an OpenAPI 3.1 spec?
You can describe the OAuth 2.0 flows and mutual TLS natively. Everything else, including PAR, PKCE, DPoP, and token binding, has no OpenAPI vocabulary and belongs in the scheme description.
The FAPI 2.0 Security Profile was approved as a Final Specification by the OpenID Foundation in February 2025. Check which profile your ecosystem actually mandates before you write anything down. Several live ones, UK Open Banking among them, still certify against FAPI 1.0 Advanced.
The requirements that have no OpenAPI equivalent are the ones your description text has to carry: Pushed Authorization Requests (RFC 9126), PKCE with S256 (RFC 7636), sender-constrained access tokens via mutual TLS (RFC 8705) or DPoP (RFC 9449), and client authentication using tls_client_auth, self_signed_tls_client_auth, or private_key_jwt. Name them and cite them, because a consumer has no other way to learn them from your spec.
The one piece OpenAPI handles directly is mutualTLS, which became a security scheme type in 3.1. In 3.0 there was no way to declare client certificate authentication at all, so teams buried it in a description or an x- extension. It takes no additional fields. Its only job is to state that the transport requires a client certificate.
components:
securitySchemes:
oauth2:
type: oauth2
description: >
FAPI 2.0 Security Profile. Authorization requests MUST be sent to
the PAR endpoint (RFC 9126). PKCE with S256 is required. Access
tokens are sender-constrained via mTLS (RFC 8705).
flows:
authorizationCode:
# authorizationUrl, tokenUrl and scopes as normal
mtls:
type: mutualTLS
description: Client certificate required on all API and token endpoints.
security:
- oauth2: [payments] # both schemes in one array element
mtls: [] # means both are required
One line in that snippet does more work than it looks. Listing oauth2 and mtls inside a single array element means both are required, an AND. Split them across separate array elements, and you have said either one is sufficient, which quietly permits a caller to skip OAuth entirely. It is an easy mistake to make and an easy one to miss in review.
How do you document idempotency, errors, and rate limits?
How do you document idempotency for payment endpoints?
Declare Idempotency-Key as a required header parameter, and state the retention window and replay behavior in its description.
A client that times out on a payment POST has to retry, and a retry must not create a second payment. The convention is a client-supplied key on the request.
Idempotency-Key is currently an IETF Internet-Draft, not a published RFC. It is widely deployed anyway. Stripe, Adyen, and most payment processors use it.
Because there is no published RFC and no universal behavior across APIs, the parameter's description is where your guarantee actually lives, and it needs to answer three questions:
description: >
Unique key per payment attempt. Replaying a key with an identical
body returns the original response. A different body returns 422.
Keys are retained for 24 hours.
Retention window, conflict behavior, and what counts as "the same request" are the three things every integrator asks about, and the three an auditor will ask about too. A parameter declared as required: true with an empty description is worse than useless here: it implies a guarantee you never actually made.
What error format should a financial API use?
Use RFC 9457, Problem Details for HTTP APIs. It was published in July 2023 and obsoletes RFC 7807. If your spec still cites 7807, update the reference. The format is compatible, but 9457 adds an IANA registry for problem type URIs and clarifies how multiple problems should be treated.
Problem:
type: object
properties:
type: { type: string, format: uri, example: "https://api.example.com/problems/insufficient-funds" }
title: { type: string, example: "Insufficient funds" }
status: { type: integer, example: 422 }
detail: { type: string }
instance: { type: string, format: uri }
Served as application/problem+json. Define your problem type URIs once, reference them from every operation, and keep them stable, because clients will branch on them.
How should you document rate limits?
Declare the rate limit headers you actually emit on both your success responses and your 429.
The RateLimit and RateLimit-Policy header fields are still an Internet-Draft, not an RFC. Earlier draft versions defined RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset, and those names remain widely deployed. Whichever you emit, declare them in your 429 and success responses. Undocumented throttling is a support ticket generator.
How should financial APIs handle versioning and deprecation?
Pick one versioning strategy and signal the retirement timeline with the Deprecation and Sunset response headers.
Regulated APIs cannot break consumers on a schedule that suits the provider. Two HTTP headers make the timeline machine-readable.
Deprecation (RFC 9745, published 2025) marks when a resource became or becomes deprecated. Sunset (RFC 8594) marks when it stops working. The sunset time must not be earlier than the deprecation time.
responses:
'200':
description: OK
headers:
Deprecation:
schema: { type: string }
example: '@1735689600'
Sunset:
schema: { type: string }
example: 'Sat, 31 Jan 2026 23:59:59 GMT'
Mark the operation deprecated: true as well. Headers tell the running client; the flag tells the developer reading the docs.
For the version itself, most financial APIs use a path prefix (/v3/payments) because it survives caches, proxies, and log analysis, and because it is trivially visible in a support ticket. Header and media-type versioning are equally valid and keep URLs stable, at the cost of being invisible in a browser and easy to drop in a curl command. The stronger consideration is usually consistency with the ecosystem you are implementing against. The UK Open Banking Read/Write standard is the reference example here, now at v4.0.1, with a documented migration path from the 3.1.x line.
How do you split a large financial API spec into multiple files?
Keep one root document for info, servers, security, tags, and webhooks, and $ref out to per-resource path files and shared schema files.
A financial API spec is rarely small. Splitting it is not optional past a few thousand lines.
The pattern that works: one root document holding info, servers, security, tags, and webhooks, with paths referencing per-resource files and components/schemas referencing shared type files.
openapi: 3.1.2
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
info:
title: Payments API
version: 4.0.1
paths:
/payments:
$ref: './paths/payments.yaml'
components:
schemas:
Amount:
$ref: './schemas/money.yaml#/Amount'
webhooks:
paymentStatusChanged:
post:
requestBody:
content:
application/json:
schema:
$ref: './schemas/events.yaml#/PaymentStatusChanged'
responses:
'204': { description: Acknowledged }
A note on the pointers. ./schemas/money.yaml#/Amount assumes money.yaml is a plain schema fragment file with Amount at its root, which is the simplest way to share types across specs. If instead you keep a full OpenAPI component document, the pointer is ./schemas/money.yaml#/components/schemas/Amount. Both work. Pick one convention and make every file in the repository follow it, because mixed pointer styles are a reliable source of resolution bugs that only appear at bundle time.
Keep money.yaml, problems.yaml, and security.yaml as shared files across every API in the estate. Money precision and error format are exactly the things that drift between teams, and a single shared file is cheaper than a governance meeting.
Bundle the multi-file spec into a single document before publishing. Many client generators and documentation renderers handle external $ref badly, and you do not want a consumer's build to depend on your directory layout.
What should you check if you are documenting the API, not designing it?
Most of the decisions above belong to engineering. If you are the technical writer who has been handed the spec, your job is not to choose between a decimal string and minor units. It is to notice when the spec does not say what the API actually does, and to ask before it ships.
Six things are worth a look every time a financial spec lands on your desk.
Any amount typed as number. This is the single highest-value catch in the list. It is a rounding bug in every generated client, and it is a one-word fix while the spec is still in review.
A currency field with no enum. A plain type: string means the API claims to accept currencies it will reject at runtime. Ask which ones are actually supported.
Dates that should be timestamps, or the reverse. A settlement date and an event time are different things. If both are format: date-time, one of them is probably wrong.
An Idempotency-Key with a thin description. Ask three questions: how long are keys retained, what happens when the same key arrives with a different body, and what counts as "the same request." If engineering cannot answer quickly, the behavior may not be defined, which is worth finding out now rather than from a partner's duplicate payment.
Error responses documented as bare status codes. If a 422 has no schema and no example, nobody downstream knows what they are parsing.
format doing work it cannot do. format: decimal and format: iban validate nothing on their own. If a field's constraint matters, it needs a pattern or an enum next to it.
None of these require you to write YAML. They require you to read it, and to have somewhere to raise the question where an engineer will see it. Which is where the workflow usually breaks.
Why do financial API specs drift from the API they describe?
A financial API spec needs input from engineers, compliance, technical writers, and auditors. Engineering usually maintains the source in Git, while the other groups review descriptions, scopes, retention statements, and explanatory content through browser tools, email, or tracked documents.
Once a review moves into an exported copy, the team has two versions to reconcile. The reviewed document can diverge from the committed spec, while the published documentation may become a third version.
What does documentation drift actually look like?
Suppose an idempotency description says keys are retained for 24 hours. Engineering later extends retention to seven days and updates the spec and implementation. At the same time, compliance approves a four-week-old Word export that still says 24 hours. If a writer applies that approved wording back to the spec, the API now retains keys for seven days while the spec and regulatory submission say 24 hours.
Nobody had to ignore the process for this to happen. The review took place in a format that could not receive later source changes, so the exported copy became a fork.
How do you stop documentation drift?
Developers and non-developers need to work on the same source rather than copies reconciled later. That requires:
Someone without a terminal can edit and comment on the spec directly.
What they produce is the real artifact, not an export, so it can go back to the repository without anyone retyping it.
There is a revision history, with timestamps and publish states, that shows what the spec said on a given date.
This is the workflow Docuwiz is built around. Writers and reviewers can edit, comment, and preview in a browser while Markdown and imported OpenAPI files remain connected to the engineering workflow. Source-control integration sends the work back to a GitHub or GitLab branch as a commit rather than an email attachment.
Compliance reviews the current artifact, engineering receives changes through Git, and revision history records what the specification said at each stage. The team no longer has to reconcile an approved copy with a newer source file.
Conclusion
Financial-grade OpenAPI is mostly a set of small, boring decisions made consistently: amounts as constrained strings or minor units and never as floats, currencies as enums, dates separated from timestamps, FAPI requirements written down where a developer will read them, idempotency semantics stated rather than implied, RFC 9457 for errors, Deprecation and Sunset for lifecycle, and shared component files so none of it drifts.
OpenAPI 3.1 gives you the vocabulary. The remaining work is deciding once, writing it down, and making sure everyone who has to approve the spec can actually reach it.
FAQs
Why use OpenAPI 3.1 instead of 3.0 for financial APIs?
3.1 is fully aligned with JSON Schema Draft 2020-12, which gives you conditional validation (if/then/else), proper null handling via type: [string, "null"], top-level webhooks, and the mutualTLS security scheme type. 3.0's custom schema dialect could not express most financial validation rules.
How should I represent money amounts in OpenAPI 3.1?
Either a decimal string with a pattern that fixes scale and range, or an integer in the currency's minor units. Never type: number, because most JSON parsers deserialize it to a float and lose precision.
Is OpenAPI 3.1 compatible with FAPI 2.0?
Compatible, but not fully expressive. You can describe the OAuth 2.0 flows and mutual TLS. PAR, DPoP, and token binding have no native OpenAPI representation and belong in the scheme description and your developer documentation.
Is Idempotency-Key an official standard?
Not yet. It is an IETF Internet-Draft (draft-ietf-httpapi-idempotency-key-header), not a published RFC, despite being widely deployed by payment processors. Because there is no normative spec to point at, your parameter description is the contract.
What is the difference between RFC 7807 and RFC 9457?
RFC 9457 obsoletes RFC 7807. The object shape is compatible, so migration is cheap, but 9457 adds an IANA registry for problem type URIs and better handling of multiple errors in one response. New specs should cite 9457.
Should I publish a bundled OpenAPI file or a multi-file spec?
Author multi-file, publish bundled. Many client generators and documentation renderers handle external $ref poorly, and a consumer's build should not depend on your directory layout.
How do I document payment webhooks in OpenAPI 3.1?
Use the top-level webhooks object, which is a sibling of paths. Each entry is a Path Item describing the request your server sends and the response it expects back.
Does OpenAPI 3.2 change any of this?
No. OpenAPI 3.2.0 was published in September 2025 as a feature release, and existing 3.1 documents continue to work. None of the patterns in this article are invalidated by it.





