My co-founder and I manage our work in ClickUp and a calendar app we built ourselves. We’re proud of our glorified calendar, but it’s new and has no integrations. So we copy tasks between it and ClickUp by hand several times a day.
Eventually we got tired of copying tasks and sat down to connect them.
The workflow sounded simple:
sequenceDiagram
participant ClickUp
participant Mine as My calendar
participant Cofounder as Co-founder's calendar
ClickUp-->>Mine: Sync in-progress tasks
Mine->>ClickUp: Complete task
Note over ClickUp: Task moves to in review
ClickUp-->>Cofounder: Sync in-review tasks
Cofounder->>ClickUp: Complete task
Note over ClickUp: Task moves to done
Notice that “complete” means something different depending on who clicks it. For me, it means the work needs review. For him, it means the work is finished. Same button, two meanings, both correct.
Our first attempt was to build a ClickUp integration directly into the calendar.
The API calls were straightforward. The decisions around them were not:
- ClickUp stores tasks in spaces, folders, and lists. We wanted tasks from only a few lists, so the calendar would need a ClickUp-shaped selection screen.
- Every list can define its own statuses. Someone would have to map them onto the calendar’s simple
openanddone. - Completion depended on who acted. When I completed a task, ClickUp should move it to
in review; when my co-founder did, it should move todone.
No single decision was fatal. The problem was where they led. The calendar would acquire a ClickUp-shaped settings screen and ClickUp-specific types and conditionals throughout its code. And that was only ClickUp. Every new source would bring another configuration model, another set of edge cases, and another product’s concepts into the calendar.
ClickUp knew how its API worked. Our calendar knew what it needed. Only we knew how the two should relate. We happened to be both the developers and the users, so we could hardcode our workflow into the calendar. But then every user would inherit it, and theirs would be different.
We could build our ClickUp integration. We could not ship it as everyone’s ClickUp integration.
The conventional way to move integration logic out of an application is to give the application a public API. Users or developers can then connect it to other tools, often through workflow automation. That model works well when the integration is a reaction: when something happens, post a message, add a row, or send a notification.
Our calendar needed more than reactions. It maintained a local cache of tasks. On first run, it needed a complete snapshot. After being offline, it needed every change and deletion since its last cursor. After an interrupted sync, it needed to resume without duplicating work. And when someone clicked complete, the calendar needed to learn whether ClickUp accepted the command, rejected it, or had changed since the task was last synchronized. Without those answers, the calendar could not know whether its own state was correct.
A workflow could imitate each step with triggers and actions, but it did not naturally give the calendar one durable interface to call whenever its state needed repair. If we made the workflow store cursors, answer reads, accept commands, return conflicts, and support recovery, we would have built a server inside the workflow tool. That server, not the trigger chain, was what the calendar actually needed.
Dedicated sync services came closer. They kept records aligned, and many let users configure mappings and rules. Our workflow might even fit into one. But our calendar was not in their catalog. Their logic extended only as far as the rule forms they had designed. The first requirement beyond that vocabulary had nowhere to go, and any custom implementation would live inside their product.
We could escape those limits by building a custom integration service. It could speak both APIs and implement our workflow exactly. But then we would own its hosting, authentication, secrets, pagination, cursors, retries, rate limits, deduplication, recovery, and monitoring. The mapping was small. The infrastructure around it was not.
Every option put the part unique to our workflow in the wrong place. Put it inside the calendar, and the calendar inherited ClickUp’s complexity. Put it inside a workflow or sync product, and our implementation inherited that product’s vocabulary and limits. Write it as a custom service, and we inherited an integration platform’s worth of infrastructure. We needed a place for a small, workflow-specific implementation behind a server the calendar could reliably call.
Then we looked at the problem from the calendar’s side.
Our calendar already knew what a task looked like to the calendar: a title, a due date, and a state such as open or done. It knew how those tasks should be synchronized and which commands it needed to send. It did not care about ClickUp spaces, folders, lists, or custom status types. It did not even care whether the tasks came from ClickUp.
That led us to a different question:
What if every user could provide our calendar with a server whose API it already understood perfectly?
One client, written once, could work for every user. Behind one user’s server might be ClickUp. Behind another’s might be Notion, a local application, an internal database, or several systems acting together. Our calendar would talk to the same API every time.
That immediately raised a practical problem: we could not ask every user to design and implement a perfectly compatible server from scratch. But they did not need to design the API. The calendar could define the exact API it needed and specify it in a document. The user—or, more generally, the resource owner—would decide whether and how to implement it.
That document is a contract.
Our calendar asks for:
GET /tasks/sync
-> tasks, deletions, next_cursor
POST /tasks/{id}/complete
{ basis_version }
Task {
id
title
due_date
state: open | done
version
}
Notice what the contract does not say: ClickUp. The calendar wants tasks. Where they live is not its business.
A contract specifies exactly the server API a client needs. The resource owner decides whether and how to provide it.
The contract fixed the interface, but it did not produce a server. ClickUp was not going to expose our calendar’s API, and putting ClickUp code back into the calendar would return us to the original problem. Something on the user’s side had to expose the contract API and translate its calls into ClickUp API calls. We named that role a mediator.
A user could build and run one from scratch, but then they would inherit the custom-service burden we had just identified. The more useful form was a reusable mediator: it might run locally, be shared by a team, or be operated by a provider, while each workflow required only a small implementation.
The integration logic inside our mediator had two parts. A ClickUp adapter would handle ClickUp’s API. The binding would specify only what was unique to us: which lists to use, how their statuses map, and what completion means for each person. It could remain small because it served one workflow rather than trying to anticipate everyone else’s. The calendar would see only its contract API.
This was the separation we had been missing. Source mechanics could be reused, while the knowledge unique to one relationship could remain in a small binding. The ClickUp adapter could serve many contracts; only the binding was specific to our calendar–ClickUp workflow.
When the calendar sent complete, the mediator would return an answer: the resulting task, a rejection, or a conflict if the source had changed. The same command could move the task to in review when I sent it and to done when my co-founder sent it, because the binding knew what completion meant for each of us.
flowchart LR
subgraph Client["Client side"]
Calendar["Calendar"]
Contract["Contract"]
Interface["Required API"]
Calendar -. "authors" .-> Contract
Contract -->|"defines"| Interface
end
subgraph Mediator["Mediator implementation"]
API["Conforming server API"]
Binding["Binding"]
Adapter["ClickUp adapter"]
API --> Binding --> Adapter
end
ClickUp["ClickUp"]
Interface -. "implemented by" .-> API
Calendar -->|"calls"| API
Adapter -->|"reads and writes"| ClickUp
The inversion was not about synchronization. It was about who defines the interface. A client can define the server shape it needs, and the resource owner can control its implementation. The same approach can support collections, queries, commands, streams, or ordinary CRUD. Producer-defined APIs remain the right choice when one producer owns the service and its canonical interface.
From a definition to a protocol
But if every client invented its own contract format and every mediator interpreted contracts differently, we would have recreated bespoke integrations one level higher.
We needed a common protocol for submitting a contract, checking whether a mediator supported its API standard and requirements, and tracking the resulting contract instance from pending to either ready or rejected. The client also needed to be able to tear that instance down when it no longer needed the connection. A ready instance would mean more than a deployed server: it had to satisfy the contract and the guarantees defined by its selected standard.
The protocol that emerged is Treaty.
Treaty makes a contract portable across mediators that support its API standard and requirements. It standardizes how clients submit contracts and how mediators report readiness, not how bindings are written. Different mediators may implement the same contract differently, provided each resulting server conforms to what the client defined.
We will publish Treaty as an open protocol. We are also building Intra, a software integration platform, with Treaty as its first supported protocol. Intra’s v1 starts with the pattern we needed here: synchronizing simple collections into an application’s cache and routing explicit commands back to the source. Our calendar is its first client.
Client-defined APIs could have existed earlier, but for shallow integrations an in-app catalog is hard to beat. Click a logo, approve access, and continue. Deeper, user-specific integrations traditionally required both custom logic and a custom service around it. A mediator lets many contracts share that operational machinery, while AI is making the remaining binding code much cheaper to write and maintain. The architecture becomes economical precisely where catalogs stop being expressive enough.
In short: the client defines the server API it needs. A contract specifies that API. The resource owner decides whether and how to provide it. A mediator implements it and exposes a conforming server. Treaty standardizes contract submission, support negotiation, and readiness; Intra provides the machinery to build and operate the implementation.
Integration catalogs will remain useful defaults. They just no longer have to be the boundary of what an application can support.
APIs have traditionally begun with what servers offer. They can also begin with what clients need.
P.S. What about the alternatives?
Integration catalogs remain the easiest answer for common, shallow workflows. Their limit is authorship: an application’s developers cannot anticipate every user’s rules. A mediator can still offer a catalog of popular bindings; contracts keep that catalog a convenience rather than a ceiling.
Public APIs are the usual way to let others extend an application. They remain essential: a mediator normally reaches external tools through the APIs they already expose. But a producer-shaped API tells integrators what a server offers. It does not give a client the stable server interface a particular client needs.
Workflow automation tools are a popular way to bind public APIs together. They are excellent at reactions: when this happens, do that. With enough state and custom code, they can reproduce much of a server, but then the server surface, not the trigger chain, is doing the important work. A workflow platform could act as a Treaty mediator if it exposed the contract API and satisfied the selected API standard. Local-first apps expose another mismatch: many workflow platforms assume a reachable hosted endpoint or outgoing webhook, so a relay, tunnel, hosted component, or local agent may be required before automation begins.
Sync services come closest to Intra’s v1 pattern. They keep records aligned and often provide useful mapping rules. But they remain bounded by the sources and configuration vocabulary their vendor supports. A sync service could act as a mediator when its model is sufficient. Treaty makes the contract portable, not the binding code, so another compatible mediator can implement the same contract without changing the client.
Custom integration services offer complete freedom, but their owners must build and operate the surrounding infrastructure. A custom service can itself act as a Treaty mediator; doing so gives the client a portable contract API and gives the service an explicit conformance target.
MCP lets servers expose tools, resources, and prompts for clients to discover. Treaty starts from the other direction: the client defines the server interface it needs, and a mediator implements it. The two can complement each other; an MCP server could sit behind a Treaty binding.
API contracts usually describe an interface a producer already offers. A Treaty contract specifies a client-defined interface and becomes operative only when a mediator exposes a conforming server to the client on the resource owner’s behalf.
Consumer-driven contract testing records consumer expectations and checks whether a producer-shaped API still satisfies them. Under Treaty, the client’s shape is not a test against the API. It is the API.
GraphQL lets a client select from a schema published by a producer. That is useful, but the possibility space remains producer-defined. Treaty lets the client define the interface, while the resource owner chooses whether and how to provide it.
API standards and formats. Treaty does not prescribe a single API format. The Intra OpenAPI Profile, based on OpenAPI 3.1, is the first standard Intra supports; other mediators may support others.