0% found this document useful (0 votes)
3 views24 pages

PACT Specification

PACT is a protocol designed for action-based coordinated transport that simplifies API design by using a consistent format for requests and responses. It emphasizes clear versioning, structured URL patterns, and naming conventions that describe full operations rather than just resources. PACT supports various transport methods including REST, webhooks, WebSockets, and server-sent events, allowing for parallel versioning and a structured deprecation process.

Uploaded by

engineerboy.ran
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views24 pages

PACT Specification

PACT is a protocol designed for action-based coordinated transport that simplifies API design by using a consistent format for requests and responses. It emphasizes clear versioning, structured URL patterns, and naming conventions that describe full operations rather than just resources. PACT supports various transport methods including REST, webhooks, WebSockets, and server-sent events, allowing for parallel versioning and a structured deprecation process.

Uploaded by

engineerboy.ran
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

PACT — Protocol for Action-based Coordinated Transport v1.

PACT
Protocol for Action-based Coordinated Transport

POST /api/:version/:scheme/:resource
Version + scheme + operation — everything the server needs in one URL.

Simple to learn. Simple to implement. Ready for production.


A beginner-friendly guide — no prior API design experience needed.

PACT · 1 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E What is PACT?
R

0
1

The simplest explanation


Imagine you are passing notes to a very organised assistant. Every note follows the same format: which version of the rules we are
using, who is allowed to send this note, what the task is called, and the information needed to do it. The assistant always replies in
the same format — either it worked and here is the result, or it did not work and here is exactly why.
That is PACT. One consistent format for asking. One consistent format for answering. Versioned so both sides always know which
rules apply. No surprises.

The two most important ideas

1 — The resource name describes the full operation. cancel-order-and-notify tells you everything. DELETE /orders/123 tells

you almost nothing.

2 — Version is a path segment your team controls freely. v1, v2, 2024-01, stable — any string. It sits right after the category

prefix, before the scheme, so routing resolves in one step.

REST vs PACT

With REST With PACT

DELETE /orders/123 POST /api/v1/private/cancel-order-and-refund


POST /users then POST /emails (two round trips) POST /api/v1/private/register-user-and-send-welcome
Version in URL? Header? Both? Team argues. /:version/ — one place, any format, team decides
HTTP 404 — or 200 with empty body? Depends who wrote it. Always HTTP 200 { success: false, error: { ... } }
Deprecated endpoint? Hope clients read the changelog. [Link] + Sunset header + 90-day minimum window

PACT · 2 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E The URL — Seven Prefixes, One Pattern
R

0
2

Structure before anything else


Every URL in a PACT server starts with a category prefix. Then a version. Then whatever that transport needs. Two things are
always true: you know what kind of communication this is before any routing, and you know exactly which contract version
applies.

Prefix Full pattern Protocol Notes

/api HTTP POST Core application operations


/api/:version/:scheme/:resour
ce
/webhook /webhook/:provider/:event HTTP POST Provider controls — no version
/webhook HTTP POST Provider owns their version
/webhook/:provider/:version/:
event
/webhook HTTP POST You own the version
/webhook/:version/:provider/:
event
/ws /ws/:version/:resource WebSocket Real-time bidirectional
/stream /stream/:version/:resource HTTP GET + SSE Server pushes — text/event-
stream
/events /events/:version/:resource HTTP POST Internal event bus publish
/rpc /rpc/:version/:resource HTTP POST Service-to-service direct call
/health /health /health/ready HTTP GET Infrastructure — no version,
/health/metrics no envelope

Version is any string your team agrees on: v1, v2, 2024-01, stable, beta. PACT does not enforce a format — it enforces the

position. Version always comes immediately after the category prefix.

/api — application operations


This is where all your product logic lives. The version lets you run v1 and v2 side by side with zero coupling — different contracts,
different schemas, different behaviour, same server.

POST /api / :version / :scheme / :resource

/api examples

PACT · 3 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0
POST /api/v1/public/fetch-product-catalogue
POST /api/v1/private/place-order-and-reserve-stock
POST /api/v2/private/place-order-and-reserve-stock ← new contract, runs in parallel
POST /api/v1/private/cancel-order-and-refund-and-notify
POST /api/stable/internal/reindex-search-and-report

/webhook — inbound from third parties


Webhooks from Stripe, GitHub, SendGrid arrive here. Version ownership depends on who controls the schema. If Stripe owns
their payload shape, they own the version. If you are wrapping and normalising their events, you own the version.

Provider owns version You own version


Stripe decides when their schema changes. Your route mirrors You wrap and normalise inbound events. You control when the
their versioning. schema changes.

Provider versioned Self versioned


POST /webhook/stripe/payment-succeeded POST /webhook/v1/stripe/payment-succeeded
POST /webhook/stripe/v2/payment-succeeded POST /webhook/v2/stripe/payment-succeeded

Webhooks never use the PACT request envelope — the body is whatever the provider sends. Your runner normalises it

internally. HMAC signature verification happens in [Link] before your runner sees the payload.

/ws — real-time bidirectional


WebSockets keep a persistent connection open. Both sides can send messages at any time. Version in the URL means a v2 client
and a v1 client can connect simultaneously — different message schemas, same server.

WS /ws / :version / :resource

/ws examples
WS /ws/v1/order-tracking-updates
WS /ws/v1/live-chat-session
WS /ws/v2/order-tracking-updates ← new message schema, runs in parallel

/stream — server-sent events (SSE)


SSE is a one-way channel — the server streams a continuous sequence of named events to the client. The client cannot send back.
The connection stays open and the server pushes whenever something happens.

SSE uses HTTP GET and responds with Content-Type: text/event-stream. This is the only transport in PACT where the

response is not a JSON envelope — the stream is a sequence of named events, not a single response.

GET /stream / :version / :resource

PACT · 4 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

Aspect How PACT handles it

Response type Content-Type: text/event-stream — not JSON


Normal event event: <name>\ndata: {JSON payload}\n\n
Error event event: error\ndata: {category, code, message, retryable}\n\n
Pre-stream failure HTTP 200 PACT envelope before stream opens — auth, rate limit
Client reconnect Client sends Last-Event-ID header — server resumes from that
point
Heartbeat event: ping\ndata: {}\n\n — sent every 30s to keep connection
alive
Stream end event: done\ndata: {}\n\n — server signals the stream is
complete

SSE event format


# Normal event
event: order-status-changed
data: {"orderId":"ord_123","status":"shipped","ts":"2024-01-15T10:30:00Z"}

# Error event — client does not retry unless retryable: true


event: error
data: {"category":"FORBIDDEN","code":"TENANT_MISMATCH","retryable":false}

# Heartbeat — keeps connection alive through proxies


event: ping
data: {}

# Stream complete
event: done
data: {}

/stream examples
GET /stream/v1/user-activity-feed
GET /stream/v1/order-export-progress
GET /stream/v2/user-activity-feed ← new event schema

/events — internal event bus publish


When something happens in your system and other services need to react asynchronously, you publish an event. The resource
name describes what happened. The version declares which schema consumers should expect.
When you ship /events/v2/order-placed, v1 and v2 run in parallel. Every consumer decides which version to subscribe to. You
retire v1 only after all consumers have migrated — that migration window is your team's responsibility.

POST /events / :version / :resource

PACT · 5 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

/events examples
POST /events/v1/order-placed
POST /events/v1/user-verified-email
POST /events/v2/order-placed ← new schema, v1 still running
POST /events/v1/payment-failed-needs-retry

/rpc — service-to-service direct call


When one of your services needs a synchronous answer from another — not a side effect, not an event — it uses /rpc. Version in
the URL means two services can negotiate their own migration timeline without affecting anyone else.

POST /rpc / :version / :resource

/rpc examples
POST /rpc/v1/check-user-permissions
POST /rpc/v1/calculate-shipping-cost
POST /rpc/v2/calculate-shipping-cost ← caller and callee migrate together

/health — infrastructure only


The only GET routes that do not carry a version. Load balancers, Kubernetes probes, and Prometheus scrapers have no concept of
your API version — they just need to know if the server is alive. Health routes never use the PACT response envelope.

Route Answers Response

GET /health Is the process alive? { status: 'ok', uptime: 12039 }


GET /health/ready Are all dependencies connected? { ready: true, checks: { db: true, redis: true
}}
GET /health/metrics Prometheus scrape text/plain — Prometheus format

PACT · 6 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Versioning — Rules & Migration
R

0
3

Version format — team decides


PACT enforces the position of the version segment, not its format. Your team agrees on a convention and applies it consistently.

Format style Example Works best when

Integer v1 v2 v3 Simple APIs, infrequent breaking changes


Date-based 2024-01 2024-06 APIs that release on a schedule
Semver minor v1 v1.1 v2 When additive changes also need routing
Named stages stable beta legacy Consumer-facing APIs with long support
windows

Whatever format you pick — use it everywhere. Mixing v1 on /api and 2024-01 on /events in the same system is worse

than picking either format consistently.

Running versions in parallel


PACT has no opinion on how long you run two versions simultaneously. The framework routes by version prefix — v1 and v2 are
completely separate contracts. They can have different schemas, different middleware config, different runners. They are
independent.
Parallel versions — separate folders
# v1 and v2 coexist — different route folders, different contracts
POST /api/v1/private/place-order-and-confirm
→ /routes/v1/place-order-and-confirm/[Link]
→ /routes/v1/place-order-and-confirm/[Link]

POST /api/v2/private/place-order-and-confirm
→ /routes/v2/place-order-and-confirm/[Link]
→ /routes/v2/place-order-and-confirm/[Link]

The deprecation contract — three parts


When you decide to retire a version, PACT enforces a three-part deprecation contract. All three parts are required for compliance.

Part What it is Where it appears

PACT · 7 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

1 — Response field [Link]: '2026-06-01' In every response from the deprecated


route
2 — HTTP header Sunset: Sat, 01 Jun 2026 00:00:00 GMT Standard header — tools and proxies read
this
3 — Minimum window 90 days between announcement and Compliance rule — enforced by your team
shutdown policy

Deprecation in practice
// [Link] — mark a version as deprecated
deprecated: '2026-06-01', // ISO date — triggers both meta field and Sunset header

// Response while deprecated (still works, still HTTP 200)


{
"success": true,
"data": { ... },
"meta": {
"duration": 24,
"cached": false,
"deprecation": "2026-06-01" // client: you have until this date
}
}

After the sunset date, the route returns UNAVAILABLE with code VERSION_RETIRED. It does not silently vanish. Clients get a

clear, machine-readable signal.

Events — the special case


Versioning /events/:version/:resource means v1 and v2 can run simultaneously — different schemas, both publishing, multiple
consumers. The team owns the migration window. PACT does not prevent you from retiring v1 too early — that is a team
discipline, not a framework rule.
• Document which consumers are on which version before retiring
• Set a migration deadline when you ship v2
• Apply the same 90-day minimum window as /api routes

PACT · 8 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Naming Operations
R

0
4

The resource is the full operation — not the noun


In REST you map actions onto HTTP verbs. In PACT the resource segment names the entire operation. Not the thing being acted on
— what actually happens, start to finish.

Rule Valid Invalid

Lowercase + hyphens only cancel-order CancelOrder cancel_order


Describe the full operation cancel-order-and-refund cancel-order
No HTTP verbs as prefix fetch-invoice-pdf GET-invoice DELETE-order
Spell it out — no abbreviations delete-draft-and-notify-team del-draft-ntfy

Simple operations

Simple
POST /api/v1/public/fetch-product-catalogue
POST /api/v1/public/search-articles-by-tag
POST /api/v1/private/update-billing-address
POST /api/v1/private/delete-draft-post

Compound operations
This is where PACT's naming pays off most. Operations that touch multiple systems get a name that is honest about all of it.
Compound
POST /api/v1/private/register-user-and-send-welcome
POST /api/v1/private/cancel-order-and-refund-and-notify
POST /api/v1/private/downgrade-plan-and-prorate-and-email
POST /api/v1/private/close-account-and-export-data
POST /api/v1/internal/expire-stale-sessions-and-audit-log

When you version a compound operation — POST /api/v2/private/cancel-order-and-refund-and-notify — the name stays

the same. The version says the contract changed, not the operation.

PACT · 9 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Tenants
R

0
5

Tenant lives in context — not the URL


Tenant never appears in the URL path. It lives in the JWT token or the request header. This keeps your versioned URLs clean and
your routing simple — the version segment already does enough work.

JWT token claim — wins always X-Tenant-ID header — fallback only


Baked into the token at login. Every request carries it Used when the token carries no tenant claim. Internal tools,
automatically. Server never needs to look anywhere else. service-to-service calls, testing.

JWT payload Header fallback


{ X-Tenant-ID: acme
"sub": "usr_123", Authorization: Bearer <service-token>
"tenant": "acme",
"role": "admin"
}

Scenario Result

JWT token has tenant claim Token value used — always wins
No tenant in token, header present Header value used as fallback
Both token and header carry tenant Token wins — header ignored
Neither token nor header Single-tenant mode — no isolation applied
Header value malformed VALIDATION error returned

PACT · 10 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Requests & Responses
R

0
6

Request headers

Header When Purpose

Content-Type: application/json Always Body is JSON


Authorization: Bearer <token> private + internal JWT or service token
X-Request-ID: req_abc123 Recommended Trace ID — echoed in every response
X-Tenant-ID: acme When token has no tenant Fallback tenant context
X-Idempotency-Key: <key> Mutating operations Prevents double-processing on retry

Request body
One field: payload. Your data goes inside. Nothing else — no version, no action, no metadata. The URL already carries all of that.
Body
{
"payload": {
"orderId": "ord_789"
}
}

Unknown top-level body fields are rejected. Body size limit is 1MB by default. payload must always be present and must be

an object — never null.

Response — always HTTP 200


PACT always returns HTTP 200. The success field tells you what happened. The HTTP status code tells you nothing — using it for
API semantics creates confusion.

Success Failure
HTTP 200 HTTP 200
{ {
"success": true, "success": false,
"requestId": "req_a3f9b2", "requestId": "req_a3f9b2",
"data": { ... }, "error": {
"meta": { "category": "NOT_FOUND",
"duration": 24, "code": "ORDER_NOT_FOUND",
"cached": false "retryable": false,

PACT · 11 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

} "source": "runner"
} },
"meta": { "duration": 8 }
}

Meta fields

Field Always present Appears when

duration Yes Always — server processing time in ms


cached Yes Always — true if served from cache
idempotent No Only when true — replay, runner did not
run again
deprecation No Only when set — ISO date this version
retires

PACT · 12 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Schemes — Who Can Call What
R

0
7

Three access levels


The scheme is the third segment in every /api URL — after prefix and version. It declares who is allowed to call this operation.
Enforced automatically before your code runs.

Scheme Auth Rate limit Timeout CORS

public None 100 req/min / IP 10s Any origin


private JWT — hard reject 300 req/min / identity 30s With credentials
internal Service token + IP None 60s None
allowlist

Internal operations require both a service token AND the caller IP to be on the allowlist. A valid token from an unknown IP

is rejected. This prevents internal routes from being reached from the internet even if a token leaks.

PACT · 13 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Errors
R

0
8

Eight categories — same shape everywhere

Error envelope
{
"success": false,
"requestId": "req_a3f9b2",
"error": {
"category": "VALIDATION",
"code": "MISSING_FIELD",
"message": "orderId is required",
"fields": { "orderId": "required" }, // VALIDATION only
"retryable": false,
"source": "pact"
},
"meta": { "duration": 3 }
}

Category Retry? Meaning

VALIDATION No Bad input — wrong types, missing fields,


invalid format
AUTH No Token missing, expired, or invalid
FORBIDDEN No Valid token — wrong role, wrong tenant,
wrong version
NOT_FOUND No The resource does not exist
CONFLICT No Cannot proceed — duplicate, already
cancelled, wrong state
RATE_LIMITED Yes Too many requests — wait and retry
INTERNAL Yes Server error — not your fault
UNAVAILABLE Yes Dependency down — database, cache,
external service

VERSION_RETIRED is a special code under UNAVAILABLE. It fires after the sunset date when a client calls a retired version.

retryable: false — client must upgrade.

PACT · 14 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E [Link] & [Link]
R

0
9

Every operation is a folder


Each operation lives in its own folder under /routes, organised by version. Every folder has the same files. You always know where
to look.
Version-organised routes
/routes
/v1
/place-order-and-confirm
[Link]
[Link]
[Link]
/v2
/place-order-and-confirm ← new contract, separate folder
[Link]
[Link]
[Link]

[Link] — declare the rules

[Link]
export const contract = {
scheme: 'private',
schema: [Link]({
items: [Link]([Link]({ productId: [Link](), quantity:
[Link]().min(1) })).min(1),
addressId: [Link](),
}),
idempotency: { required: true },
cache: { enabled: false },
rateLimit: { rpm: 30 },
deprecated: null, // set to ISO date string to trigger deprecation
}

[Link] — your logic only

The runner is completely blind to the protocol. No HTTP, no tokens, no rate limits, no version checks. By the time your code

runs, all of that is handled. You receive clean, validated data and return plain data.

PACT · 15 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

[Link]
export default async function run(ctx: PactContext) {
// [Link] — validated, schema-matched
// [Link] — decoded JWT (null if public)
// [Link] — resolved tenant (null if single-tenant)
// [Link] — 'v1', 'v2', etc — read-only, for logging only
// [Link] — trace ID

const order = await [Link]({ ... })


await [Link]('order-confirmation', { order })
return order // PACT wraps: { success: true, data: order }
}

PACT · 16 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E The 20-Step Middleware Chain
R

1
0

Fixed order, single responsibility per step


Every request passes through all 20 steps in order. Each step does one job. If any step fails, the request stops and an error
envelope is returned. Your runner only executes if steps 1–13 all pass.

# Step Fails with

01 Request Logger — record receipt, start Never fails


timer
02 Body Parser — parse JSON, enforce 1MB VALIDATION
limit
03 Protocol Check — validate URL format, VALIDATION
version present, body structure
04 Request ID — use X-Request-ID header or Never fails
generate req_[a-z0-9]{16}
05 Version Check — confirm version is UNAVAILABLE / VERSION_RETIRED
known, not retired
06 Scheme Resolver — load scheme VALIDATION
behaviour rules
07 Auth — verify JWT or service token per AUTH
scheme
08 Rate Limiter — check quota per scheme RATE_LIMITED
rules
09 Tenant Resolver — token first, header FORBIDDEN
fallback
10 Route Finder — locate versioned folder, NOT_FOUND
load [Link]
11 Payload Validator — run Zod schema from VALIDATION
[Link]
12 Idempotency Check — return stored Returns early
result if key seen before
13 Cache Check — return cached data if Returns early
available
14 Pre-hooks — custom checks from [Link] Any error
15 RUNNER — your business logic Any error you throw
16 Post-hooks — audit, side effects Logged, never fatal
17 Cache Store — save if [Link] = Logged, never fatal
true

PACT · 17 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

18 Idempotency Store — save for future Logged, never fatal


replay
19 Response Builder — wrap in success Never fails
envelope, add deprecation if set
20 Response Logger — log outcome, Never fails
duration, version

Step 5 (Version Check) is where retired versions return VERSION_RETIRED. Step 19 (Response Builder) is where

[Link] and the Sunset header are automatically added when [Link] is set.

PACT · 18 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

C
H
A
P
T
E Folder Structure
R

1
1

Transport-first. Version-organised. URL mirrors folder.


The top level of the project answers one question: what kind of communication is this? Every transport has its own folder under
/transports. Every operation under that transport is organised by version. The path to any file mirrors the URL that calls it.

A new developer receives a bug report on POST /api/v1/private/cancel-order-and-refund. They open



/transports/api/v1/cancel-order-and-refund/[Link]. No searching. No guessing. One path.

Complete project structure


/my-api

/transports ← all communication channels


/api ← mirrors POST /api/:version/:scheme/:resource
/v1
/place-order-and-confirm
[Link] ← schema, scheme, idempotency, cache, deprecated
[Link] ← business logic only
[Link]
/cancel-order-and-refund
[Link] [Link] [Link]
/register-user-and-send-welcome
[Link] [Link] [Link]
/v2 ← separate folder, separate contract, runs in parallel
/place-order-and-confirm
[Link] [Link] [Link]

/webhook ← mirrors POST /webhook/:provider/...


/stripe
/payment-succeeded
[Link] [Link] [Link]
/v2 ← when provider versions their events
/payment-succeeded
/github
/push
[Link] [Link] [Link]

/ws ← mirrors WS /ws/:version/:resource


/v1
/order-tracking-updates
[Link] [Link] [Link]

/stream ← mirrors GET /stream/:version/:resource (SSE)

PACT · 19 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0
/v1
/activity-feed
[Link] [Link] [Link]

/events ← mirrors POST /events/:version/:resource


/v1 ← v1 and v2 run in parallel during consumer migration
/order-placed
[Link] [Link] [Link]
/v2
/order-placed
[Link] [Link] [Link]

/rpc ← mirrors POST /rpc/:version/:resource


/v1
/check-user-permissions
[Link] [Link] [Link]
/calculate-shipping-cost
[Link] [Link] [Link]

/core ← framework engine — never edit for features


[Link] ← PactContext from any transport
[Link] ← auto-discovers /transports/**/[Link]
[Link] ← version registry, retired list, routing
[Link] ← public / private / internal
[Link] ← 20-step chain
[Link] ← L1 memory + L2 Redis + stampede
[Link]
[Link] ← [Link] + Sunset header
[Link] [Link] [Link] [Link]

/adapters ← outbound third-party — one folder each


/stripe ← [Link] [Link] [Link]
/sendgrid
/openai ← AI adapter — wraps the API call
/anthropic ← AI adapter — wraps the API call

/integrations ← service-to-service helpers


[Link] ← callService() helper for /rpc
[Link] ← typed calls to auth service

/queue ← message queue pub/sub + background jobs


[Link] ← publish events to bus
[Link] ← consume messages, build PactContext
/jobs ← jobs are queue consumers — they live here
/send-invoice-reminders
[Link] ← schedule, retries, timeout
[Link] ← business logic only
[Link]
/expire-stale-sessions
[Link] [Link] [Link]
/generate-monthly-reports
[Link] [Link] [Link]

/libs ← all shared internal code


/utils ← pure functions — no I/O, no side effects

PACT · 20 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0
[Link] [Link] [Link] [Link]
/types ← shared TypeScript types
[Link] [Link] [Link]
/constants ← app-wide constants
[Link] [Link] [Link]
/validators ← shared Zod schemas used across operations
[Link] [Link] [Link] [Link]
/errors ← custom error classes
NotFoundError ConflictError ValidationError ...
/helpers ← domain-specific helpers (not pure — can use libs)
[Link] [Link] [Link] [Link]
/middleware ← reusable middleware pieces
[Link] [Link] [Link] [Link]

/config ← single source of truth for all settings


[Link] ← version registry, retired versions list
[Link] ← validated env vars (Zod)
[Link] [Link] [Link]

[Link] ← entry point — boots core, starts all listeners

AI lives in two places: /adapters/openai and /adapters/anthropic wrap the outbound API calls. The /transports/api route

that exposes AI to your clients calls the adapter — same pattern as Stripe.

PACT · 21 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

R
E
F
E
R Quick Reference & Compliance Checklist
E
N
C
E

Complete URL surface

Method Pattern Notes


POST / Version required
api/:version/:scheme/:resource
POST /webhook/:provider/:event No version — provider controls
POST / Provider owns version
webhook/:provider/:version/:ev
ent
POST / You own version
webhook/:version/:provider/:ev
ent
WS /ws/:version/:resource Version required
GET /stream/:version/:resource SSE — text/event-stream, no envelope
POST /events/:version/:resource Version required — team manages
migration
POST /rpc/:version/:resource Version required
GET /health No version — liveness
GET /health/ready No version — readiness
GET /health/metrics No version — Prometheus
OPTIONS * 204 silent — pre-chain, no logging
Any other * 405 — no envelope

Deprecation contract

Requirement Detail

[Link] in response ISO date string — present on every response from deprecated
route
Sunset HTTP header Standard header — Sunset: <HTTP-date>
Minimum window 90 days between setting deprecated date and that date arriving
After sunset date Route returns UNAVAILABLE / VERSION_RETIRED — not silent

Compliance checklist

Versioning

PACT · 22 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

☐ /:version/ present in all routes except /health and /webhook (provider-controlled)


☐ Version format consistent across the whole system
☐ Parallel versions use separate /routes/:version/ folders
☐ Deprecated routes set [Link] to ISO date
☐ [Link] and Sunset header both present during deprecation window
☐ After sunset: VERSION_RETIRED — not silent 404

URL & routing


☐ POST only for /api, /webhook, /events, /rpc — anything else is 405
☐ OPTIONS = 204 silent, no logging, no middleware
☐ Resource names lowercase + hyphens, describe full operation
☐ /routes auto-discovered — no manual registration

Tenant
☐ JWT token claim wins over X-Tenant-ID header
☐ Neither present = single-tenant mode — not an error
☐ Cache keys include tenant and version

SSE (/stream)
☐ Response is text/event-stream — not JSON envelope
☐ Errors sent as: event: error\ndata: {category, code, retryable}
☐ Heartbeat sent every 30s: event: ping\ndata: {}
☐ Stream end: event: done\ndata: {}
☐ Last-Event-ID honoured for client reconnection

Response envelope
☐ Always HTTP 200 — no exceptions in PACT routes
☐ success true/false always present
☐ requestId always echoed
☐ meta: duration + cached only — never echoes request fields

Errors
☐ All 8 categories implemented
☐ VERSION_RETIRED under UNAVAILABLE after sunset date
☐ INTERNAL never exposes stack trace in production
☐ retryable and source always present

[Link]
☐ Zero HTTP or protocol logic
☐ [Link] available read-only — for logging only, not routing logic
☐ Returns plain data — framework wraps it

PACT · 23 / 24
PACT — Protocol for Action-based Coordinated Transport v1.0

Events migration
☐ All consumers documented per version before retiring
☐ 90-day minimum migration window applied to event versions
☐ v1 and v2 run in parallel until all consumers migrated

All boxes checked = PACT Compliant


One pattern. Every transport. Every version. Full control.

PACT · 24 / 24

You might also like