Integration Contract Record (ICR) Template
A Markdown template for the agreement behind one integration between systems: purpose and parties, the pattern and its interaction flows, the interface specifications it relies on, data, security, service levels, error handling, change and support, and testable acceptance criteria. Written so a reviewer, an operator or an AI coding agent can act on it. Version 0.2 of the format, a draft.
A draft format, version 0.2. It is in the spirit of MADR: a short Markdown document with a fixed structure that can be reviewed in a pull request. It does not replace OpenAPI, AsyncAPI or the Open Data Contract Standard. It records the agreement around the interface those describe, and links to them. No standards body has reviewed or endorsed it.
The template and its schema, and the whole Appointment Booking sample (a design, seven decisions, requirements and three contracts), are in the public architecture-records repository. The template and its schema are licensed under CC BY 4.0, like the other templates there: you may copy and adapt them, including commercially, if you give credit to IT Architecture Patterns and say what you changed.
Copy and download act on the blank template shown below.
The example is the contract between the booking widget and the Booking API in Appointment Booking (sample), a fictional system documented with the templates on this site. Its other two contracts, one for calendar synchronisation and one for notifications, are in the same project. See the contract on GitHub (opens in a new tab)
See all the records of the Appointment Booking worked example together
icr: "0.2"
id: "{ICR-XXXX}"
title: "{short name of the integration}"
status: "{draft | proposed | agreed | active | deprecated | retired}"
version: "{0.1.0}"
date: "{YYYY-MM-DD}"
pattern:
id: "{pattern id, e.g. int-api-external}"
url: "{link to the pattern}"
interaction: "{sync-request-response | async-event | async-message | stream | batch-file}"
provider:
system: "{system offering the interface}"
owner: "{accountable team}"
contact: "{email or channel}"
consumers:
- system: "{consuming system}"
owner: "{accountable team}"
contact: "{email or channel}"
interfaces:
- type: "{openapi | asyncapi | odcs | cloudevents | wsdl | json-schema | other}"
ref: "{path or URL to the authoritative specification}"
version: "{interface version}"
data:
classification: "{your organisation's classification}"
personal-information: {true | false}
decisions: []
{integration title}
Purpose
{The capability this integration serves, who depends on it, and what happens to them when it stops.}
Parties and responsibilities
| Party | System | Responsible for |
|---|---|---|
| Provider | {provider system} | {what this party builds, operates, monitors and changes} |
| Consumer | {consumer system} | {what this party builds, operates, monitors and changes} |
Interaction
- Pattern: {pattern name}, {any deliberate deviation from it, and why}
- Direction and trigger: {who starts an exchange, and what starts it: a user action, an event, a schedule}
- Volume: typical {n per unit}, peak {n per unit}
- Ordering: {whether order matters, and what guarantees it}
Interaction flows
{The main exchange, and the failure the parties must handle, in one sentence.}
sequenceDiagram
accTitle: {The main exchange}
accDescr: {Two or three sentences: who calls whom, what comes back, and what happens when a part fails.}
participant C as {Consumer name}
participant P as {Provider name}
C->>P: {request message}
alt normal path
P-->>C: {response message}
else a part fails
P-->>C: {what the consumer is told}
end- {Step, naming the party and the message}
- {Next step, including how the failure is handled}
Interface specification
- {specification type} {specification reference} {interface version}: {what it covers}; authoritative copy: {where the authoritative copy lives}
- Design standard followed: {for example a national or organisational API design standard, or "none"}
Examples
The main exchange:
{The request and its response, the event with its envelope, or the file excerpt}
The main error, as the consumer receives it:
{The error}
Environments and onboarding
Optional — remove this section if you don't need it.
- Environments: {test and production: where each is, and how they differ}
- Test data or sandbox: {what exists, and what it does not simulate}
- Access: {how a new consumer asks, who approves, how long it takes}
- First call: {the smallest exchange that shows a new consumer is connected}
Data
- Entities: {the business entities that cross the interface}
- Classification: {data classification}; driven by: {which fields}
- Personal information: {none, or which fields, and the basis for sharing them}
- Residency: {where the data may be stored and processed}
- Retention: provider {provider retention period}; consumer {consumer retention period}
- Data quality: {the rules the data must meet (completeness, validity, uniqueness), and who is told when they are broken}
Security
- Authentication: {how each party proves who it is, for example OAuth 2.0 client credentials or mutual TLS}
- Credential issue and rotation: {who issues credentials, and how often they change}
- Authorisation: {what each consumer may and may not do}
- Transport: {protocol and minimum TLS version}
- Network path: {public internet, private link or dedicated connection, and any allow-listing}
- Pattern controls inherited: {the pattern}; additional controls: {what this integration adds}
Service levels
- Availability: {availability target}, measured at {measurement point}
- Latency: {percentile and limit}, measured at {measurement point}
- Throughput: {throughput limit}, beyond it: {behaviour beyond the limit}
- Freshness: {how soon a change at the source is visible to consumers}
- How a consumer learns its limit and its use of it: {response headers or documentation}
- Recovery: time {how long the integration may be down}; point {how much it may lose}
- Support hours for these objectives: {support hours}
Error handling
- Errors and their meaning: {each error the provider returns or emits, and what it means to the consumer}
- Timeouts: {on each side}
- Retries: {who retries, how many times, with what backoff}
- Idempotency: {how a duplicate is recognised and made harmless}
- Failed messages or files: {where they go (dead-letter, quarantine) and who is alerted}
- Recovery after an outage: {how a consumer catches up: replay, resend window}
Change and versioning
- Interface versioning: {the scheme, for example a major version in the path}
- Breaking change means: {what counts as one}
- Notice before a breaking change or deprecation: {notice period}, via {notice channel}
- What each consumer relies on: {fields, behaviours and timings, or a link to its contract tests}
- Changing this ICR: {who approves}
Observability
- Correlation identifier: {the identifier, and how it propagates}
- Logs: {what each side logs, without logging classified payloads unnecessarily}
- Metrics and alerts: {metric, threshold, recipient}
Support and escalation
| Party | Contact | Hours | Escalation |
|---|---|---|---|
| {party name} | {email or channel} | {support hours} | {who is next} |
Severity levels and response targets: {each level, what it means and the response target}
Acceptance criteria
- {A testable statement} — proved by: {the test, synthetic check or alert}
- {Another testable statement} — proved by: {the test, synthetic check or alert}
Related records
Optional — remove this section if you don't need it.
- Pattern: {the pattern page}
- Decisions: {the decision records this contract rests on}
Open issues
Optional — remove this section if you don't need it.
- {The question}: {owner of the question}, {date raised}
icr: "0.2"
id: ICR-AB-0001
title: Booking API
status: agreed
version: 1.2.0
date: 2026-10-09
pattern:
id: int-api-external
url: https://www.itarchitecturepatterns.net/patterns/int-api-external
interaction: sync-request-response
provider:
system: Booking API
owner: Booking platform team
contact: booking-platform@example.com
consumers:
- system: Booking widget
owner: Booking front-end team
contact: booking-frontend@example.com
interfaces:
- type: openapi
ref: booking-api/openapi.yaml
version: 1.2.0
data:
classification: Personal information
personal-information: true
decisions:
- ADR-AB-0001
- ADR-AB-0002
- ADR-AB-0003
- ADR-AB-0005
- ADR-AB-0006
- ADR-AB-0007
Booking API
Purpose
The booking widget, running inside a business's own website, uses this API to show a business's services and free times and to hold, confirm, reschedule and cancel bookings. It is the only way customers book; if it stops, no business on the platform can take a booking online.
Parties and responsibilities
| Party | System | Responsible for |
|---|---|---|
| Provider | Booking API | Availability, holds and bookings; enforcing business isolation and rate limits; this contract's service levels |
| Consumer | Booking widget | Calling only the operations below; showing every time with its zone; offering alternatives when a slot is taken |
Interaction
- Pattern: Integration API Management (External). The widget calls across the public internet from a customer's browser, through the API gateway. Why the interface takes this form is recorded in ADR-AB-0002.
- Direction and trigger: widget to API, on each customer action.
- Volume: typical 20 requests per second across all businesses; peak 200 per second (Monday mornings).
- Ordering: not applicable; each request stands alone, and concurrency on a slot is resolved by the provider (see the booking requirements).
Interaction flows
Finding a time, holding it and confirming it, with the ways a request can be turned away.
sequenceDiagram
accTitle: Holding and confirming a time through the Booking API
accDescr: The widget searches for free times through the gateway, which checks the widget key, the registered origin and the rate limit, and refuses with 403 or 429. The customer picks a time and the widget holds it for five minutes through the gateway, which passes it to the services. If the time has gone the answer is 409 slot_taken with the next free times. Otherwise the hold is created, and the widget may extend it (proposed). The widget then confirms with the customer's details and an idempotency key. An expired hold is 410, a time that has gone or a hold already confirmed is 409 with the next free times, and otherwise the booking is created and returned with its manage token.
participant W as Booking widget
participant G as API gateway
participant S as Booking and availability services
W->>G: GET availability (widget key, embedding origin)
G->>G: check the key, the registered origin and the rate limit
alt refused
G-->>W: 403 origin_not_registered, or 429 rate_limited with Retry-After
else allowed
G->>S: search
S-->>G: free times, as ISO 8601 instants
G-->>W: free times
end
W->>G: POST holds (Idempotency-Key)
G->>S: hold the time
alt the time has gone
S-->>G: 409 slot_taken with the next free times
G-->>W: 409 slot_taken with the next free times
else the time is free
S-->>G: 201 hold, expires in 5 minutes
G-->>W: 201 hold
opt the customer needs longer (proposed)
W->>G: POST holds/{id}/extend, up to 10 times
end
W->>G: POST holds/{id}/confirm (Idempotency-Key, customer details)
G->>S: confirm
alt the hold has expired
S-->>G: 410 hold_expired
G-->>W: 410 hold_expired
else the time has gone, or the hold is already confirmed
S-->>G: 409 slot_taken or calendar_conflict, with the next free times
G-->>W: 409 with the next free times
else the time is still free
S-->>G: 201 booking and manage token
G-->>W: 201 booking and manage token
end
end- The widget searches with the business's widget key and the page it is embedded in; the gateway checks the key, the page and the rate limit, and refuses with
403or429. Every request below passes the same checks and can be refused the same way. - The customer picks a time; the widget holds it. The hold lasts 5 minutes and can be extended up to 10 times (proposed).
- The widget confirms with the customer's details and a new
Idempotency-Key. The services answer410if the hold has expired,409with the next free times if the time has gone or the hold is already confirmed, and otherwise create the booking.
Interface specification
- openapi
booking-api/openapi.yamlv1.2.0 — services, availability search, holds, bookings; authoritative copy: the booking-api repository, published to the API developer portal. - Design standard followed: resource-oriented REST, JSON, ISO 8601 instants with offsets (see the availability requirements), RFC 9457 problem details for errors.
Operations marked proposed were found necessary while building the demo and are not yet agreed by the front-end and platform leads:
| Operation | Purpose |
|---|---|
GET /businesses/{businessId}/services | Services a business offers, with durations |
GET /businesses/{businessId}/availability?service=&from=&to= | Free slots, per staff member, as UTC instants |
POST /businesses/{businessId}/holds | Hold a slot for five minutes |
POST /holds/{holdId}/extend | Add five minutes to a hold, up to ten times (proposed) |
DELETE /holds/{holdId} | Release a hold the customer no longer wants (proposed) |
POST /holds/{holdId}/confirm | Confirm a hold as a booking, with the customer's details |
PATCH /bookings/{bookingId} | Reschedule, using the customer's manage-booking token |
DELETE /bookings/{bookingId} | Cancel, using the customer's manage-booking token |
Examples
Captured from the running demo (the fictional Example Clinic), shortened only where it says so, and with the response headers other than traceparent and Content-Type left out. /v1 is the path prefix, and every time is an ISO 8601 instant.
A hold:
POST /v1/businesses/example-clinic/holds
X-Widget-Key: pk_demo_example_clinic
X-Embedding-Origin: https://clinic.example.org
traceparent: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01
Idempotency-Key: 5b0d3f6e-7c1a-4d2e-9a58-3e1f0c7b2a64
Content-Type: application/json
{"serviceId": "followup", "staffId": "alex", "start": "2026-09-28T02:30:00.000Z"}
HTTP/1.1 201 Created
traceparent: 00-0af7651916cd43dd8448eb211c80319c-414fd98f903ab139-01
Content-Type: application/json
{"id": "hold_d5e1397fa031", "serviceId": "followup", "staffId": "alex", "start": "2026-09-28T02:30:00.000Z", "end": "2026-09-28T03:00:00.000Z", "expiresAt": "2026-09-28T00:05:00.000Z", "serverTime": "2026-09-28T00:00:00.000Z"}
Its confirmation (the name, email and phone number are fictional; the phone number is in the range reserved for fiction):
POST /v1/holds/hold_d5e1397fa031/confirm
X-Widget-Key: pk_demo_example_clinic
X-Embedding-Origin: https://clinic.example.org
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-1c2d3e4f5a6b7c8d-01
Idempotency-Key: 8e2c4a91-3b7d-4f60-a1c5-9d0e6b3f7a12
Content-Type: application/json
{"customer": {"name": "Jordan Example", "email": "jordan@example.org", "phone": "0491 570 006"}}
HTTP/1.1 201 Created
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-7e7d44ac36472bfa-01
Content-Type: application/json
{"id": "bk_4467814ce615", "holdId": "hold_d5e1397fa031", "status": "confirmed", "serviceId": "followup", "staffId": "alex", "start": "2026-09-28T02:30:00.000Z", "end": "2026-09-28T03:00:00.000Z", "customer": {"name": "Jordan Example", "email": "jordan@example.org", "phone": "0491 570 006"}, "token": "351106ca84511135c6bb220f81f406cf", "notifications": {"status": "queued", "messages": [{"type": "BookingConfirmed", "channel": "email", "status": "pending"}, {"type": "BookingConfirmed", "channel": "sms", "status": "pending"}]}, "serverTime": "2026-09-28T00:00:00.000Z"}
The main error, a time that has gone (a second request for the same time):
HTTP/1.1 409 Conflict
traceparent: 00-5ce0b7a1d4f94e2fa8d36b19c07e5d42-a3981b34b8138ead-01
Content-Type: application/problem+json
{"type": "https://www.itarchitecturepatterns.net/samples/appointment-booking/problems/slot_taken", "title": "Conflict", "status": 409, "code": "slot_taken", "detail": "That time is no longer free.", "instance": "POST /v1/businesses/{id}/holds", "alternatives": [{"staffId": "alex", "start": "2026-09-28T03:00:00.000Z", "end": "2026-09-28T03:30:00.000Z"}, {"staffId": "sam", "start": "2026-09-28T03:00:00.000Z", "end": "2026-09-28T03:30:00.000Z"}, {"staffId": "alex", "start": "2026-09-28T03:30:00.000Z", "end": "2026-09-28T04:00:00.000Z"}]}
The type URL identifies the kind of problem and is not yet served as a page (RFC 9457 allows that). There are no usage headers on the demo's responses; see Service levels.
Data
- Entities: service, slot, hold, booking, customer contact.
- Classification: Personal information; driven by: customer name, email address and phone number.
- Personal information: customer name, email and mobile number, collected to confirm and remind the customer of their booking and shared only with the business they booked with.
- Residency: stored in Australia.
- Retention: provider keeps bookings and contact details for 24 months after the appointment, then deletes them; the consumer keeps nothing (the widget holds no data beyond the page).
Security
- Authentication: each request carries the business's public widget key in
X-Widget-Key, which identifies the business and is restricted to the domains the business registers, and the page it is embedded in inX-Embedding-Origin(learned by the widget from the browser, which vouches for it); the gateway answers403if that page is not one of the registered domains; manage-booking operations also require the customer's per-booking token, sent in their confirmation message. - Credential issue and rotation: widget keys are issued in the admin console and rotated by the business at any time; booking tokens are single-purpose and expire when the appointment has passed.
- Authorisation: the gateway resolves the key to one business, and the service scopes every query to it through row-level security (ADR-AB-0005).
- Transport: TLS 1.2 or later; HSTS on the API domain.
- Network path: public internet to the API gateway; nothing behind the gateway is exposed.
- Pattern controls inherited: Integration API Management (External); additional controls: registered-origin check on the widget key, bot protection on hold and confirm.
Service levels
- Availability: 99.9% per month, measured at the API gateway.
- Latency: availability search p95 ≤ 400 ms; confirm p95 ≤ 800 ms (it includes the live calendar check), measured at the gateway.
- Throughput: 50 requests per second per business; beyond it,
429 Too Many RequestswithRetry-After. - Freshness: availability reflects staff calendar changes within 60 seconds, and a confirmation is checked live whenever the calendar provider can be reached (ADR-AB-0003). When it cannot, the booking is confirmed without the check, the skipped check is logged, and the calendar is reconciled when the provider returns; the possible double booking is an accepted risk, recorded in ADR-AB-0003.
- How a consumer learns its limit: from this contract and the developer portal. Beyond it the response is
429 rate_limitedwithRetry-Afterand a problem document; neither the response nor the problem names the limit, and there are no usage headers. - Recovery for the integration: time 1 hour, point 5 minutes (ADR-AB-0006 and the SAD, section 8.2).
- Support hours for these objectives: 24×7.
Error handling
- Errors and their meaning, each a problem document with a stable
code:400invalid_request,idempotency_key_required;401unknown_key(unknown or revoked key);403origin_not_registered,token_expired,forbidden;404unknown_business,unknown_service,unknown_hold,unknown_booking,unknown_route,not_found;409slot_taken(the time has gone, or the hold is already confirmed) andcalendar_conflict(the staff member's own calendar has an event there), both listing the next free times;too_many_extensions(proposed, with extending a hold) andalready_confirmed, which list none;410hold_expired;413payload_too_large,415unsupported_media_type;422idempotency_key_reused,not_bookable,invalid_request;429rate_limited;500internal_error,503temporarily unavailable.
- Timeouts: the widget waits 10 seconds, then shows a retry prompt.
- Retries: the widget retries only safe requests (
GET) and requests carrying an idempotency key, at most twice, with jittered backoff; it never retries a409. - Idempotency:
POSTrequests carry anIdempotency-Keyheader; the provider returns the original result for a repeated key within 24 hours, and answers422 idempotency_key_reusedif the key is reused for a different request. Confirming a hold that is already confirmed, with a new key, is a409listing alternatives, not a second booking. - Failed messages or files: not applicable; the interaction is synchronous.
- Recovery after an outage: holds that expired during the outage are released; the widget re-runs the customer's last search.
- Holds: a hold lasts five minutes, and the customer can extend it up to ten times (proposed; WCAG 2.2.1 requires a way to extend a time limit) or release it by going back (proposed), so they do not block their own first choice.
Change and versioning
- Interface versioning: the major version is in the path (
/v1/); additive changes ship without notice. - Breaking change means: removing or renaming a field or operation, changing a type or an error code's meaning, or tightening validation.
- Notice before a breaking change or deprecation: 90 days, via the API developer portal and the widget release notes.
- What each consumer relies on: the widget relies on
id,expiresAtandserverTimeon a hold,tokenon a booking, thecodevalues andalternativesof a problem document, and thetraceparentit receives. The demo's API tests checkexpiresAt,token, thecodevalues,alternativesandtraceparent; the rest are checked in review until the widget has contract tests. - Changing this ICR: the Booking platform and Booking front-end team leads, together.
Change log
- 1.2.0 (2026-10-09, agreed by the Booking platform and Booking front-end team leads; drafted 2026-10-04): agreed except the items marked proposed, which are extending and releasing a hold (with the
too_many_extensionscode and acceptance criterion 8); the alert on a skipped live check is planned, not built. The agreement accepts criteria 1 and 9 as not yet proved: the load test and the OpenAPI file are still to be built (owner: Lead back-end engineer; due 2026-10-31). Changes in this version: the errorcodevalues listed; the embedding origin header named; what the widget relies on; recovery targets; the examples and the criterion that checks them; the decisions list now includes ADR-AB-0007 (cited in Related records). - 1.1.0 (2026-10-03, agreed): extend and release a hold (proposed); acceptance criteria 7 and 8 (the skipped live check, hold extension); the
409and422cases; the live check that cannot be made. - 1.0.0 (2026-09-26): the first agreed version.
Observability
- Correlation identifier:
traceparent(W3C Trace Context), created by the widget and returned in every response, with the trace id kept and a new span id. - Logs: gateway access logs and service logs carry the correlation identifier and business id, never customer contact details.
- Metrics and alerts: 5xx rate above 1% for 5 minutes, and p95 latency above target for 10 minutes, page the Booking platform on-call. A skipped live check (ICR-AB-0002) is logged as a warning; an alert on it is planned (ADR-AB-0003, not yet built).
- Log entry shape:
seq,at(milliseconds since the epoch),component,event,level,ref(the record the event implements),traceId, and adetailobject holding ids, counts and statuses, never a name, email address, phone number or message body.
Support and escalation
| Party | Contact | Hours | Escalation |
|---|---|---|---|
| Booking platform team | booking-platform@example.com | 24×7 on-call | Head of engineering |
| Booking front-end team | booking-frontend@example.com | Business hours, Sydney | Booking platform on-call |
Severity levels and response targets: Sev 1 (no bookings possible) 15 minutes; Sev 2 (one business or one operation failing) 1 hour; Sev 3 (degraded) next business day.
Acceptance criteria
- A search for one service over seven days returns in under 400 ms at p95 under the typical load. Proved by: the search-latency test in
api.test.mjs, which runs on an empty server, not under load; the load test is still to be built. - Fifty simultaneous confirmations of one hold's slot produce exactly one booking and forty-nine
409responses listing alternatives. Proved by: the fifty-way race test. - A request from a domain not registered for the widget key receives
403. Proved by: the registered-origin test. - A repeated
POSTwith the same idempotency key creates nothing new and returns the original result. Proved by: the idempotency test. - Every time in every response is an ISO 8601 instant with an offset. Proved by: the time-format test over every response shape.
- No log line contains a customer's name, email address or phone number. Proved by: the log scan in
api.test.mjs. The logger also refuses such a field outright, but no test yet exercises that refusal. - With the calendar provider unavailable, a confirmation succeeds, the response is the same as when the check passes, and the skipped check appears in the log. Proved by: the provider-outage test.
- Proposed, with extending and releasing a hold: extending a hold adds five minutes, an eleventh extension is refused, and releasing a hold frees its time at once. Proved by: the hold-extension test.
- Every request and response shown in Examples is valid against
booking-api/openapi.yaml. Proved by: not yet automated. It is checked in review untilbooking-api/openapi.yamlexists and a validation step runs in CI.
Related records
- Pattern: Integration API Management (External)
- Decisions: ADR-AB-0001 (embed mechanism), ADR-AB-0002 (API exposure), ADR-AB-0003 (availability source of truth), ADR-AB-0005 (tenancy), ADR-AB-0006 (hosting and recovery), ADR-AB-0007 (data store)
- Requirements: booking, availability