API Contract Template

What it is

An API contract is a formal specification of an API's interface: the endpoints it exposes, the request and response schemas for each, the error codes it returns, the authentication mechanisms it requires, and the service level commitments it provides. It is the authoritative agreement between an API provider and its consumers about exactly how the API behaves, independent of any implementation details.

API contracts can take several forms. The most common is an OpenAPI (formerly Swagger) specification — a YAML or JSON document that machine-readably describes a REST API. GraphQL APIs use schema definition language (SDL). gRPC services use Protocol Buffer definitions. Asynchronous APIs can be described with AsyncAPI. All of these are formal contract formats that enable automated tooling: code generation, mock servers, contract testing, and documentation publishing. This template covers the human-readable API contract document that accompanies such formal specifications and provides context, intent, and guidance that the machine-readable spec cannot express.

The API contract serves as the single source of truth for the API's behavior. Consumers use it to build integrations, trust it for correctness, and hold the provider accountable when behavior deviates. Providers use it to define their commitments explicitly, enabling them to evolve the API safely within the constraints of backward compatibility.

Why it exists

Without a formal API contract, integrations are built against the current implementation rather than a specification. This creates fragile integrations that break when the implementation changes, even when those changes were not intended to affect consumers. Contract-first API development — where the contract is written before the implementation — inverts this relationship and produces more stable, evolvable APIs because breaking changes are visible at the specification level before any code is written.

API contracts enable parallel development. When a contract is published before the implementation is complete, consuming teams can begin building integrations against a mock server generated from the spec. This decoupling can unlock weeks of parallel development time in large projects where multiple teams depend on a shared API. Contract testing tools like Pact and Spring Cloud Contract enforce the contract programmatically, ensuring that implementations stay true to their specifications as they evolve.

For external APIs, the contract is a business commitment. Consumers make investment decisions — hiring engineers, designing architecture, building products — based on their understanding of the API's capabilities and stability. A well-maintained API contract with explicit versioning and deprecation policies builds consumer trust and reduces the cost of API evolution for both providers and consumers.

When to use

  • Before building any new API that will be consumed by external teams, partner organizations, or the public.
  • When adopting a contract-first development workflow, where the contract drives both the implementation and the consumer integrations simultaneously.
  • When establishing a service mesh or internal API catalog, where published contracts enable discoverability and self-service integration.
  • When an existing API has informal or undocumented behavior that multiple consumers depend on and that needs to be stabilized before further evolution.
  • As part of the design doc for any significant API change, to document the new endpoints, breaking changes, and migration path.

When NOT to use

  • For internal, single-consumer APIs where the provider and consumer are the same team and informal coordination is sufficient — the documentation overhead may not be warranted.
  • For experimental or prototype APIs under active design, where the interface is changing too rapidly to justify formal specification; write the contract once the interface has stabilized.
  • As a substitute for a full Design Doc when the API is complex enough to require architectural decisions, sequence diagrams, and implementation planning beyond interface specification.
  • When the API contract will not be maintained — a stale API contract is worse than no contract, as consumers will trust it and build against incorrect behavior.

Template

# API Contract: [API Name]

**API Name:** [Human-readable name — e.g., "Orders API"]
**Version:** [Semantic version — e.g., v2.1.0]
**Status:** Draft | Stable | Deprecated | Sunset
**Owner team:** [Team name]
**Contact:** [team-email@company.com or Slack channel]
**Base URL (production):** https://api.example.com/v2
**Base URL (staging):** https://api-staging.example.com/v2
**OpenAPI spec:** [Link to openapi.yaml in source control]
**Postman collection:** [Link if available]
**Last updated:** [YYYY-MM-DD]

---

## Overview
[2–3 sentences describing the API's purpose, what resources it manages,
and who the intended consumers are.]

**Supported protocols:** REST / HTTP 1.1 and HTTP 2
**Request/Response format:** JSON (application/json)
**Character encoding:** UTF-8

---

## Authentication

**Method:** Bearer token (OAuth 2.0 Client Credentials flow)

```
Authorization: Bearer <access_token>
```

**Token endpoint:** POST https://auth.example.com/oauth/token

**Required scopes:**
| Scope | Description |
|-------|-------------|
| `orders:read` | Read orders and order status |
| `orders:write` | Create and update orders |
| `orders:admin` | Cancel orders and access admin endpoints |

**Token lifetime:** 3600 seconds (1 hour). Clients must re-authenticate before expiry.

**Getting credentials:**
Contact [team-email@company.com] or submit a request via [link to access portal].

---

## Rate Limits

| Plan | Requests/minute | Burst | Daily limit |
|------|-----------------|-------|-------------|
| Standard | 100 req/min | 200 req | 50,000 req/day |
| Premium | 1,000 req/min | 2,000 req | 500,000 req/day |
| Internal | 10,000 req/min | 20,000 req | Unlimited |

**Rate limit headers returned on every response:**
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1700000060  (Unix timestamp when the window resets)
```

**When rate limited:** HTTP 429 Too Many Requests with Retry-After header.

---

## Pagination

Cursor-based pagination is used on all list endpoints.

**Request parameters:**
- `limit` (integer, 1–100, default 20): number of items per page
- `cursor` (string, opaque): cursor from previous response's `next_cursor` field

**Response envelope:**
```json
{
  "data": [...],
  "meta": {
    "total_count": 1452,
    "limit": 20,
    "next_cursor": "eyJpZCI6IjEyMzQ1In0=",
    "has_more": true
  }
}
```

---

## Endpoints

### POST /orders
Create a new order.

**Required scope:** `orders:write`

**Request body:**
```json
{
  "customer_id": "cus_01HXYZ123",         // string, required — customer identifier
  "items": [                               // array, required, min 1 item
    {
      "product_id": "prod_01HABC456",      // string, required
      "quantity": 2,                       // integer, required, min 1
      "unit_price_cents": 4999             // integer, required — price in cents
    }
  ],
  "shipping_address": {                    // object, required
    "line1": "123 Main St",
    "line2": "Apt 4B",                     // string, optional
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94105",
    "country": "US"                        // ISO 3166-1 alpha-2
  },
  "idempotency_key": "order-2024-11-xyz"  // string, optional but recommended
}
```

**Response: 201 Created**
```json
{
  "data": {
    "id": "ord_01HXYZ789",
    "status": "pending",
    "customer_id": "cus_01HXYZ123",
    "total_cents": 9998,
    "items": [
      {
        "id": "item_01HABC789",
        "product_id": "prod_01HABC456",
        "quantity": 2,
        "unit_price_cents": 4999,
        "total_cents": 9998
      }
    ],
    "created_at": "2024-11-15T14:32:00Z",
    "updated_at": "2024-11-15T14:32:00Z"
  }
}
```

---

### GET /orders/{id}
Retrieve a single order by its ID.

**Required scope:** `orders:read`

**Path parameters:**
- `id` (string, required): Order ID (format: `ord_[A-Z0-9]+`)

**Response: 200 OK**
```json
{
  "data": {
    "id": "ord_01HXYZ789",
    "status": "shipped",
    "customer_id": "cus_01HXYZ123",
    "total_cents": 9998,
    "items": [...],
    "shipping_address": {...},
    "tracking_number": "1Z999AA10123456784",
    "created_at": "2024-11-15T14:32:00Z",
    "updated_at": "2024-11-15T16:45:00Z"
  }
}
```

---

### GET /orders
List orders with optional filtering.

**Required scope:** `orders:read`

**Query parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| customer_id | string | No | Filter by customer |
| status | string | No | Filter by status (pending, paid, shipped, cancelled) |
| created_after | ISO 8601 datetime | No | Filter orders created after this time |
| created_before | ISO 8601 datetime | No | Filter orders created before this time |
| limit | integer | No | Page size (1–100, default 20) |
| cursor | string | No | Pagination cursor |

---

### PATCH /orders/{id}
Update the status of an order. Only specific status transitions are allowed.

**Required scope:** `orders:write`

**Allowed status transitions:**
```
pending  → paid
pending  → cancelled
paid     → shipped
shipped  → delivered
```

**Request body:**
```json
{
  "status": "paid",
  "reason": "Payment confirmed via Stripe charge ch_xyz"  // optional, recommended
}
```

**Response: 200 OK** — returns updated order object (same schema as GET /orders/{id}).

---

## Error Codes

All errors follow a consistent envelope:
```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable description of what went wrong",
    "details": [                                   // optional, for validation errors
      {
        "field": "items[0].quantity",
        "issue": "must be greater than 0"
      }
    ],
    "request_id": "req_01HABC789"                  // always present — use for support
  }
}
```

| HTTP Status | Error Code | Description |
|-------------|------------|-------------|
| 400 | VALIDATION_ERROR | Request body failed schema validation |
| 400 | INVALID_TRANSITION | Requested status transition is not allowed |
| 401 | UNAUTHORIZED | Missing or invalid authentication token |
| 403 | FORBIDDEN | Token does not have required scope |
| 404 | NOT_FOUND | Resource with given ID does not exist |
| 409 | CONFLICT | Idempotency key already used for a different request |
| 422 | UNPROCESSABLE_ENTITY | Request is syntactically valid but semantically incorrect |
| 429 | RATE_LIMIT_EXCEEDED | Request rate limit exceeded |
| 500 | INTERNAL_ERROR | Unexpected server error — retry with exponential backoff |
| 503 | SERVICE_UNAVAILABLE | Service temporarily unavailable — retry after Retry-After header |

---

## SLA

| Metric | Target |
|--------|--------|
| Availability | 99.9% (rolling 30 days) |
| p50 latency | < 50ms |
| p99 latency | < 300ms |
| p999 latency | < 1,000ms |

SLA exclusions and the full SLO document: [Link to SLO document]

---

## Changelog

| Version | Date | Type | Description |
|---------|------|------|-------------|
| v2.1.0 | 2024-11-01 | Minor | Added `tracking_number` field to order response |
| v2.0.0 | 2024-08-15 | Major | Breaking: replaced `price` (float) with `unit_price_cents` (integer) |
| v1.5.0 | 2024-05-10 | Minor | Added cursor pagination; deprecated offset pagination |
| v1.0.0 | 2024-01-20 | Major | Initial stable release |

**Versioning policy:** This API follows Semantic Versioning.
- **Major version** (v1 → v2): breaking changes; v1 supported for 12 months after v2 GA
- **Minor version** (v1.0 → v1.1): additive, backward-compatible changes
- **Patch version** (v1.0.0 → v1.0.1): bug fixes with no schema changes

**Deprecation policy:** Deprecated endpoints receive a `Deprecation` response header with
the sunset date. Sunset notice period is minimum 6 months for external consumers.

Pros and cons

Benefits

  • Enables consumer teams to begin building integrations before the provider implementation is complete, unlocking parallel development.
  • Provides a machine-readable specification that generates client SDKs, mock servers, and interactive documentation automatically.
  • Creates a clear accountability boundary: when behavior deviates from the contract, it is unambiguously a provider bug, not a consumer misunderstanding.
  • Supports safe API evolution by making breaking changes explicit and requiring intentional version management.

Pitfalls

  • Contract drift: the implementation evolves away from the specification without updates, making the contract an unreliable reference.
  • Over-specification of internal behavior that is subject to change, creating unnecessary maintenance overhead as the implementation evolves.
  • Treating the contract as a substitute for proper consumer feedback — the best APIs are designed collaboratively with the teams that will consume them.
  • Not testing the contract against the implementation automatically, allowing the spec and implementation to diverge silently over time.

Writing guidance

Write the contract before writing the implementation. This is the core discipline of contract-first API development, and it consistently produces better APIs. When you write the contract without the implementation to constrain you, you are forced to think about the consumer experience: what is the most natural way to represent this resource? What errors should be distinguishable? What fields will consumers always need versus occasionally need? These questions are much harder to answer honestly once an implementation exists and changing it has a cost.

Request and response examples are the most-read part of any API contract. Engineers building integrations scan directly to the examples before reading the field descriptions. Make every example complete, realistic, and representative of actual usage. Avoid placeholder values like "string" or 0 — use realistic data that conveys semantics. Include examples for both the happy path and the most common error responses.

The changelog is what transforms an API contract from a point-in-time snapshot into a living document. Every change to the API — additive or breaking — should produce a changelog entry with the version number, date, change type, and a clear description. Consumers who have pinned their integration to a specific version use the changelog to understand what they gain and what they risk by upgrading. A complete changelog is also an invaluable debugging tool when consumer integrations break after an API update.

Common mistakes

  • No idempotency key support for mutation endpoints: POST endpoints without idempotency keys force consumers to implement their own duplicate-prevention logic and make retry logic unreliable.
  • Using floats for monetary values: Floating-point representation introduces rounding errors that are unacceptable for financial data. Use integer cents or a string decimal type.
  • Undocumented error codes: Consumers cannot write correct error handling for errors they do not know exist. Every error code the API can return must be documented.
  • Missing deprecation policy: Consumers cannot plan migrations without knowing how much notice they will receive before breaking changes. Document and honor a minimum deprecation notice period.

Review checklist

  • Does every endpoint include a realistic, complete request and response example for the happy path?
  • Are all error codes the API can return documented with descriptions and HTTP status codes?
  • Is the authentication mechanism documented with an example token usage and instructions for obtaining credentials?
  • Is a versioning and deprecation policy documented, including the minimum notice period for breaking changes?
  • Is the machine-readable OpenAPI spec (or equivalent) linked and kept in sync with this document?

Example usage

  • Public API launch: A payments company writes an API contract for their new invoicing API before writing a line of implementation code, using it to gather consumer feedback from three pilot customers before finalizing the schema — resulting in two schema changes that would have been breaking changes if made after GA.
  • Internal service integration: A platform team publishes API contracts for all internal services to a developer portal, enabling application teams to discover and integrate with services without requiring direct communication with the platform team.
  • Contract testing setup: A team uses the API contract as the source of truth for Pact contract tests, automatically verifying that the implementation matches the specification on every CI build, preventing contract drift from ever reaching production.
  • Design Document (Tech Spec) — design docs for new APIs often reference or embed an API contract draft as the interface specification section.
  • SLO Document — the SLA section of the API contract references the SLO document for the service's reliability targets.
  • API Design — principles and patterns for designing high-quality REST APIs, GraphQL schemas, and gRPC services.

Further reading