Service 28

Systems integration built on written contracts, not hopeful connections

APIs, webhooks and queues joining tools never designed to talk, with a contract per seam: one identity key, a stated delivery guarantee, an ordering rule, and a queue a named person reads.

On this page
  1. What systems integration actually is
  2. Who it is for, and who it is not for
  3. What we actually build
  4. How it works technically
  5. The seam contract, written before any code
  6. The build process stage by stage
  7. What you get at handover
  8. Where systems integrations go wrong
  9. What it costs to run once live
  10. How to tell whether you need this
  11. How to start

What systems integration actually is

The short answer

Systems integration is the work of making separate pieces of software exchange facts reliably: the APIs, webhooks, queues, transforms and retry logic sitting between tools bought at different times by different departments. The visible part is a connection. The part that decides whether it survives production is a contract per seam, answering five questions in writing: which key identifies the record on both sides, what delivery guarantee the receiver actually gets, whether order matters and per what, what happens to a message that has failed five times, and how the producer announces a schema change. Integration projects fail on those five answers, almost never on the HTTP call.

Connecting two systems is a morning's work. Keeping them connected through a token rotation, a rate limit change, an enum that gained a sixth value and a bulk import somebody ran on a Friday is the actual job.

There are two shapes of integration failure and the second is expensive. The loud shape throws errors and gets fixed the same day. The silent shape is a connection that stopped: no exceptions, a green dashboard, two systems drifting apart for three weeks until somebody in finance notices a wrong number.

  • One keyEach seam names a single natural key both systems agree identifies the record. Two keys means two versions of the truth and a duplicate rate that grows weekly.
  • At least onceNetworks offer at-least-once delivery. Anything sold as exactly once is at-least-once plus deduplication somewhere. If nobody wrote the deduplication, you do not have it.
  • Order per entityGlobal ordering is expensive and rarely needed. Ordering per entity key comes free from partitioning on that key, and stops a stale update overwriting a fresh one.
  • Failure has an ownerEvery dead letter queue names a team and a review time. A queue nobody reads is a folder where data goes to be forgotten.

Who it is for, and who it is not for

This is for companies where the same fact lives in four or more systems and somebody keeps them in step by hand. If you have two systems and a supported native connector, install the connector. Custom work earns its cost when the pairings, the volume, or the price of being wrong outgrow what a point-to-point tool can express.

Your situationVerdictWhy
Five systems each holding part of the customer record, kept in step by handGood fitFive systems means ten possible pairings. A hub with declared contracts beats ten scripts inside a year.
A vendor API with no native connector to the tools you runGood fitThe ordinary case. Mapping, retries and a queue rather than anything exotic.
Two tools, one supported connector, nobody complainingNot a fitInstall the connector. Custom work buys a thing to maintain and no capability you lacked.
Nightly bulk movement into a warehouse for reportingDifferent serviceBatch work with different guarantees. See data pipeline automation.
The systems disagree today and nobody knows which is rightFix firstIntegration propagates state. Propagating state you do not trust spreads the problem to every system on the seam.
You are replacing one of the systems within six monthsWaitBuild against the system you are keeping. Seams written against software on its way out leave with it.
The fifth row turns a six week build into a five month one, and it is visible before anybody writes code.

Three things that must exist before the build

  1. Credentials owned by the company, not by a person. A personal OAuth token works perfectly until that person changes role, and then the seam dies with no error anybody recognises.
  2. A documented rate limit for every system on the seam, and somebody who can raise it. Integrations that pass testing and fail in production are usually meeting a ceiling nobody checked.
  3. A named owner per system schema. If any employee can add a required field on a Tuesday afternoon, the seam breaks on a Tuesday afternoon and nobody connects the two events.

Who should not buy this

  • Teams whose real problem is two departments disagreeing about who owns the customer. A seam surfaces that argument in production rather than settling it.
  • Anyone hoping the integration will clean data on the way through. Normalising a format is fine. Inventing a missing value gives you bad data that is now auditable back to you.
  • Teams who want a dashboard. That is a reporting job, and building it as a live seam makes it slower and more fragile.

What we actually build

Six components, and almost none of them contain a model. Integration is where AI projects go to die, so this layer gets built the boring way and the models go on top once facts move reliably.

The seam inventory and the contracts

One file per producer and consumer pair, in version control, naming the key, the guarantee, the ordering rule, the retry policy, the rate budget, the dead letter owner and the notice period for schema changes. It is readable by an operations lead, and it is the artefact people argue about before anybody writes a transform.

The ingress layer

Webhook receivers that verify the signature, persist the raw body byte for byte, return 200 quickly and do no work in the request. Providers time out and retry if you process inline, which is how a slow downstream write becomes a duplicate storm from a well behaved sender. Where webhooks do not exist, polling with a stored cursor does the same job less elegantly, and the trade-off is in webhooks vs polling.

The queue and the workers

A durable queue with a visibility timeout longer than the slowest downstream call, partitioned on the entity key so events for one record are handled in order by one consumer. Concurrency is capped per destination, because the limit that matters is the receiving system's rate limit and not your CPU.

The transform layer

Mapping code, versioned, with recorded payloads as fixtures, rather than a mapping buried in a vendor's web interface where nobody can diff it. Transforms stay pure functions, so the whole mapping is testable offline against a folder of captured messages.

The idempotency store

A key built from the seam id and the producer's event id, held long enough to outlive your longest replay window, checked before every write. This is what turns at-least-once delivery into an effectively once outcome, and skipping it is the usual reason a customer gets the same email twice after an outage.

The dead letter queue, the replay tool and the dashboards

Messages that exhaust their retries land in a queue carrying the full envelope, the error, the attempt history and the trace id, and a command line tool replays them by seam and time range once the cause is fixed. Alongside it sits one dashboard per seam: events in, events out, lag, retry rate, dead letter depth, and an alert that fires when hourly volume falls below its floor.

A connector platformAn integration layer you own
Time to first seamHoursDays to weeks
Custom fields and odd objectsWhatever the vendor mappedWhatever your schema contains
IdempotencySometimes, rarely documentedExplicit key, stated window, tested
Ordering guaranteeUsually unstatedPer entity key, by construction
Replay after an outageManual re-run, often duplicatingBounded replay, deduplicated on arrival
Cost shapePer operation, grows with volumeBuild cost, then flat infrastructure
Use the connector platform until it hurts, then write down where it hurt

Run the cheap tool first and note every compromise it forces: a field it cannot map, an order it cannot preserve, a duplicate it created, a failure you found only by opening the run history. After a month you have a specification written from evidence, and plenty of teams find the cheap tool was adequate for four of their six seams.

How it works technically

The data flow is the same on every seam: verify, persist, acknowledge, enqueue, transform, write with an idempotency key, confirm, commit the cursor. The interesting decisions are all about what happens when one of those steps fails.

Acknowledge before you work

The receiver validates the signature, writes the raw body to durable storage exactly as it arrived, and returns 200. Providers enforce short timeouts and retry on anything slower, so a receiver doing the downstream write inline gets sent the same event again while the first attempt is still running. Most duplicate storms are caused by the receiving code, not the sender.

Ordering without serialising everything

Global ordering requires a single consumer and destroys throughput. Business logic needs something narrower: two updates to the same contact must not race, while updates to different contacts are independent. Partition on the natural key and each key is handled in sequence by exactly one worker. Where the queue cannot partition, take a short lease on the key before writing.

Idempotency, plus the stale write rule

Two protections are needed and teams build only the first. Deduplication stops the same event being applied twice. The stale write rule stops an older event being applied after a newer one: compare the event's occurred_at against the value stored on the record and drop the write if it is older. Without it, a retry landing ten minutes late overwrites a correction somebody just made, and nothing logs an error because the write was valid.

The envelope every producer writes to

One envelope shape across all seams pays for itself during the first incident. It gives every message an id to deduplicate on, a version to pin, a partition key, an actor field that kills echo loops, and a trace id that follows the fact across systems.

Canonical event envelope, JSON Schema 2020-12json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Integration event envelope, version 1",
  "type": "object",
  "additionalProperties": false,
  "required": ["event_id","event_type","schema_version","occurred_at","source","entity","payload"],
  "properties": {
    "event_id": { "type": "string", "format": "uuid",
      "description": "Generated once by the producer, identical across every retry of the same fact. This is what the consumer deduplicates on." },
    "event_type": { "type": "string", "pattern": "^[a-z_]+\.[a-z_]+\.(created|updated|deleted)$",
      "examples": ["billing.invoice.created", "crm.contact.updated"] },
    "schema_version": { "type": "integer", "minimum": 1 },
    "occurred_at": { "type": "string", "format": "date-time",
      "description": "When the fact happened in the source, UTC with an offset. Not when it was sent: retries make sent_at useless for ordering." },
    "source": { "type": "string", "enum": ["billing","crm","product_db","support"] },
    "actor": { "type": "string",
      "description": "What caused the change. Stamp this integration's service name on its own writes, then drop inbound events carrying it. Echo loops stop here." },
    "entity": {
      "type": "object",
      "required": ["type","natural_key"],
      "properties": {
        "type": { "type": "string", "examples": ["contact","invoice","order"] },
        "natural_key": { "type": "string",
          "description": "Also the partition key, so events for one entity stay in order without serialising the stream." }
      }
    },
    "payload": { "type": "object",
      "description": "Full current state of the fields in scope, never a diff. A diff applied out of order corrupts silently. Full state does not." },
    "trace_id": { "type": "string" }
  }
}

Delivery guarantees, and why exactly once is a sales phrase

Pick the guarantee per seam and write it in the contract, because the choice decides what the consumer has to build. There are three, and the third does not exist at the network level no matter what a product page says.

GuaranteeWhat it meansWhat the consumer must buildUse it when
At most onceFire and forget. A failure loses the message.Nothing. Accept the loss.Telemetry and non-critical notifications, anything you would never reconcile.
At least onceThe message arrives, sometimes twice, especially after an outage.Deduplication on an idempotency key, plus a stale write rule.Almost every business seam. The sane default.
Effectively onceAt-least-once delivery with deduplication proven at the consumer.The row above, plus a stated key window and a test that replays a duplicate.Money movement, outbound customer messages, anything a repeat makes visible to a person.
Effectively once is a property of your consumer, not of the transport. Nobody can sell it to you.
Ask every vendor one question about their webhooks

Ask whether they retry on non-200 responses, how many times, over what window, and whether the retried request carries the same event id. If they retry without a stable id you cannot deduplicate cleanly and will have to derive a key from payload content, which breaks the moment they add a field.

Terms used on this page
At-least-once delivery
A guarantee that a message will arrive one or more times, which is what queues and webhook senders actually provide. The receiver is responsible for recognising repeats. Anything marketed as exactly once is at-least-once delivery plus deduplication performed somewhere specific.
Idempotency key
A value derived from the event itself, usually a hash of the seam identifier and the producer's event id, stored on first processing and checked before every write. If the key is already present, the write is skipped and the message is acknowledged.
Partition key
The field a queue uses to decide which consumer handles a message. Using the entity's natural key preserves order for that entity while letting unrelated entities be processed in parallel.
Poison message
A message that fails every time it is processed, usually from malformed data rather than a transient fault. Without a retry ceiling and a dead letter queue, it blocks its partition and stalls every entity behind it.

The seam contract, written before any code

Every failed integration we have been asked to rescue was missing at least three of these six clauses. They are decisions rather than documentation, and each one changes what gets built.

Framework

The ChatGPTalker Seam Contract

Six clauses per producer and consumer pair. A clause that cannot be answered is the next conversation, not a detail to settle during implementation.

01
Identity: one key, agreed by both sides

Name the single natural key, with the exceptions listed: shared mailboxes, subsidiaries, personal addresses on business accounts. Matching on a system-local id means the first migration on either side creates a second copy of every record.

02
Delivery: state the guarantee and who deduplicates

At most once, at least once, or effectively once. Name the component holding the idempotency key and how long it holds it. A guarantee without a named owner surfaces later as duplicate charges.

03
Ordering: required or not, and per what

If order matters, declare the partition key and the stale write rule. If it does not, say so, so nobody builds a serialisation nobody needed. Most seams need per entity order and nothing more.

04
Failure custody: who owns a message that failed

Name the queue, the alert threshold, the team and the time of day they look. This clause separates an integration from a rumour, and it is the one most often left blank.

05
Rate budget: live traffic and backfill in separate lanes

The calls per minute the seam may use in normal running, and the smaller allowance a backfill gets. Backfills sharing the live quota take the live path down while somebody is watching.

06
Change notice: how a schema change is absorbed

A notice period, a version bump on breaking changes, both versions served during the notice, and consumer owned contract tests in the producer's pipeline. External producers cannot be held to this, which is why you version defensively.

One seam contract, filled in. Copy per producer and consumer pairyaml
# seam: billing -> crm (contact, invoice). One file per pair, in version control.
seam_id: billing_to_crm_v1
owner_team: revenue_ops          # the team paged when this breaks, named not implied

identity:
  natural_key: email_lower       # both sides agree on this and nothing else
  fuzzy_match: false

delivery:
  guarantee: at_least_once       # the only one a network actually offers
  consumer_must: deduplicate
  ack_policy: after_commit       # never ack before the write is durable

ordering:
  partition_by: natural_key      # per-entity order, global order not required
  stale_write_rule: drop_if_occurred_at_older_than_stored

idempotency:
  key: "sha256(seam_id + event_id)"
  ttl_hours: 168                 # must outlive your longest replay window
  on_duplicate: ack_and_skip

retries:
  attempts: 5
  backoff: exponential
  jitter: full                   # without it, every consumer retries in the same second
  retry_on: [429, 500, 502, 503, 504, timeout]
  never_retry_on: [400, 401, 403, 404, 422]

rate_budget:
  live_calls_per_minute: 240
  backfill_calls_per_minute: 60  # separate lane, so a backfill cannot starve live traffic

dead_letter:
  queue: billing_to_crm_dlq
  alert_after: 10
  reviewed_by: revenue_ops
  review_cadence: "every weekday 09:00"
  replay: "bin/replay --seam billing_to_crm_v1 --since"

schema_change:
  notice_days: 30
  contract_tests: "consumer payloads replayed in producer CI before merge"

health:
  expected_events_per_hour_min: 5   # alert on silence, not only on errors
  max_lag_seconds: 300
  reconcile: "nightly count and field sample, ticket on drift"
The contract is the estimate

When a client asks why one seam takes three days and another takes three weeks, the answer is visible in these files. A seam with one key, no ordering requirement and a tolerant consumer is a small job. A seam with money on it, a strict order and a producer who changes schemas without warning is a different project wearing the same word.

The build process stage by stage

Six stages. The first two produce no running code, and skipping them is why the third takes twice as long as anybody planned.

  1. Seam inventory3 to 5 days

    List every place data moves between systems today, including the exports somebody does by hand and the script on a laptop. Each entry gets a producer, a consumer, a volume estimate, an owner and a note on what breaks if it stops for a day. Most inventories come back with two or three seams nobody knew were load bearing.

  2. Contracts and payload capture3 to 5 days

    Write the contract per seam and capture real payloads from every producer, including malformed ones. Reading a hundred real messages teaches you more than the vendor's documentation does, and it is where you find the field documented as an integer that is sometimes an empty string.

  3. Skeleton: ingress, queue, dead letter, observability1 to 2 weeks

    The plumbing before any business logic: receivers, the queue and its visibility timeout, the retry policy, the dead letter queue and its alert, trace propagation, dashboards. It carries no value on its own and it is what makes every later seam take days instead of weeks.

  4. First seam end to end1 to 2 weeks

    One seam in production with real traffic and a rollback. Break it in staging first: kill the consumer mid-write, send the same event five times, send one with a missing required field, send one an hour late. The behaviour under each of those is the design.

  5. Remaining seams, then the backfill1 to 4 weeks

    Later seams reuse the skeleton and are mostly mapping and tests. The backfill runs last, on its own rate lane, resumable from a cursor, with the destination's automations paused first, because a bulk write fires workflows built for human activity.

  6. Handover and shared on-call1 week

    Runbooks, an incident walkthrough, and two weeks where your team is paged first while ours is on the call. The failure that teaches most is the one that happens while both teams are watching.

The backfill is a separate project with a separate risk profile

Live sync moves tens of events a minute. A backfill moves years of history as fast as the destination allows. It meets different limits, exposes data quality nobody has looked at since it was entered, and can trigger every automation watching those fields. Give it its own rate lane, cursor and kill switch.

What you get at handover

The deliverable is a system your team can operate without us, so the artefacts matter as much as the running code. All of it sits in your repositories and your accounts from the first day.

Handover contents
0 of 8 done

The last item is the one clients remember. Every system has edges its builder knows about and its operator does not, and writing them down is the difference between an inheritance and an ambush. More on that in who owns the automation after launch.

Where systems integrations go wrong

Five failure modes account for most of the integration incidents we are called in to diagnose. None are exotic, and all are cheaper to prevent than to find.

The echo loop

System A writes to B. B fires a webhook. The handler writes back to A, which fires a webhook. This runs until a rate limit stops it or somebody notices a record updated four thousand times in an hour. The cure costs one field: tag every write with the actor that made it, and drop inbound events whose actor is your own integration.

The retry storm after an outage

A provider returns 503 for twenty minutes and every consumer retries. Without jitter they retry in the same second, so when the provider recovers it meets a thundering herd and falls over again. Exponential backoff with full jitter, plus a circuit breaker that stops calling a destination that is clearly down, turns a self-inflicted second outage into a queue that drains quietly.

The silent stop

A credential expires, a subscription lapses, a consumer dies without restarting. No errors are raised because nothing is running to raise them. Alert on the absence of expected events with a floor per seam per hour, and reconcile counts nightly. Zero events processed looks identical to a quiet Sunday unless something is watching for it.

Schema drift with no notice

An enum gains a sixth value. A nullable field becomes required. A string that always parsed as a date arrives empty. Reject and log unknown values rather than coercing them, keep the raw payload so you can replay once the mapping is fixed, and run consumer owned contract tests inside the producer's pipeline for anything internal.

The rate limit discovered in production

Limits are usually per account rather than per integration, so a new seam shares an allowance with the marketing tool, the reporting export and whatever somebody built in 2023. Budget the seam explicitly, measure real consumption in week one, and give backfills a smaller separate lane. The arithmetic is in rate limits, retries and backoff.

Duplicates are worse than losses, and teams optimise for the wrong one

A lost message shows up in a reconciliation and is replayable from the raw store. A duplicate has already been applied: the invoice is posted twice, the customer got two emails, the stock count is wrong and nobody knows which write was the extra one. Design so failures fall on the side of losing and retrying, then test deduplication hardest.

What it costs to run once live

An integration layer costs less to run than most teams expect and more to maintain than they budget. The infrastructure is small. The maintenance is a real recurring line, driven by other people's release schedules rather than anything you control.

Cost lineWhat drives itHow to size it honestly
Queue, workers and key storeEvents per day, and the slowest downstream callUsually the smallest line. Managed queues charge per million messages and workers idle most of the day. Price it from your provider's current rates.
ObservabilityLog volume and trace sampling rateThis line surprises people. Full payload logging on a busy seam can cost more than the compute. Sample traces and log envelopes rather than bodies.
Vendor API tierCalls per minute at peak, not averagePeaks decide the tier. If a fifth of the day's volume lands in one hour, size for that hour.
MaintenanceSeams times the release cadence of the systems on themBudget engineering hours per seam per month. Vendors deprecate endpoints and change enums on their timetable, not yours.
Dead letter reviewVolume times failure rate times minutes per itemA human line, small when the design is good and unbounded when it is not. Run the estimator below.

The number worth running before you build is retry amplification, because it sets both your API consumption and the human time the seam quietly demands each week. A three percent transient failure rate sounds harmless until it is multiplied by five attempts across a busy seam.

Retry amplification and dead letter workload

Enter your own volumes. Transient failure rate is the share of calls that fail then later succeed. Permanent failure rate is the smaller share that exhausts every retry and lands in the queue a person reads.

0API calls per day including retries
0Calls per minute if a fifth lands in one hour
0Dead letters per day
0Human hours per week clearing them

Two lessons fall out of that arithmetic. Retries multiply consumption against a limit somebody set without knowing your seam existed, so the peak figure is the one to compare against your tier. And a permanent failure rate under one percent still generates a daily queue somebody has to own.

The cheapest optimisation is not moving the field

Before adding a field to a seam, ask which system reads it and what decision changes because of it. A useful share of the fields on any wish list have no reader anywhere. Removing them cuts calls, conflicts, maintenance and personal data exposure in one edit.

How to tell whether you need this

Integration work is easy to justify emotionally and hard to justify with evidence, which is how companies end up with an expensive layer moving fields nobody reads. Four or more of these and the case is real. Two or fewer and the connector platform you already pay for is probably enough for another year.

Signals that a custom integration layer is worth it
0 of 8 done
Run the seam inventory even if you buy nothing

List every place data moves between systems, who owns each end, and what breaks if it stops for a day. It takes about a day with the right people in a room. Most teams find at least one seam that is business critical and maintained by nobody.

How to start

The first conversation is technical and takes about an hour. Bring somebody who knows the systems rather than somebody who knows the org chart. We come out of it with a seam inventory, a first contract, and an honest view of whether this is a build or a configuration job.

  1. Send the list of systems, roughly how many records move between them per day, and what breaks first when a connection stops.
  2. We spend an hour on the two seams that hurt most, and write one contract live so you can see what the artefact is.
  3. You get that contract, a sizing with the assumptions written out, and a straight answer on whether a connector platform already covers this. That answer costs nothing and is sometimes yes.
  4. If it is a build, we start with the inventory and the skeleton. The first seam reaches production within a few weeks, and it is the one costing you most today rather than the one easiest to demonstrate.

If the real problem is a whole process held together by people copying between tabs, the shape you want may be workflow automation rather than a seam layer. We will say so.

Cite this

ChatGPTalker. Systems Integration: Seam Contracts, Queues and Replay. chatgptalker.com/services/systems-integration/

Questions we get asked

How long does a systems integration project take?
Three to ten weeks for most engagements. The inventory and contract stage takes about a week and produces no running code. The shared skeleton of ingress, queue, retries, dead letter handling and observability takes one to two weeks and is what makes every later seam cheap. The first production seam adds one to two weeks, and each seam after it is usually days.
Should we use a connector platform like Zapier, Make or n8n instead?
Often yes, and we will say so. If your seams are low volume, tolerant of duplicates and covered by supported connectors, a platform is faster and cheaper than anything custom. The argument changes when volume makes per-operation pricing painful, when you need provable deduplication or per-entity ordering, or when you need to replay a day of failed messages.
What is the difference between systems integration and a data pipeline?
Integration moves individual facts between operational systems in near real time, and correctness per record matters because a person or a process acts on each one. A data pipeline moves large volumes on a schedule into a warehouse, where completeness over a window matters more than the latency of any single row. Building one as the other is a common and expensive mistake.
Can you integrate with a system that has no API?
Sometimes, and the options degrade in a predictable order. A supported API comes first. A documented file drop or scheduled export to storage is a respectable second and more reliable than people expect. A database read replica is third where the vendor permits it. Browser automation is last, because it breaks whenever a layout changes and usually violates the terms of service.
Who owns the integration after handover?
Your team, with everything needed to do it. Code, infrastructure definitions, contracts, dashboards, runbooks and credentials sit in your accounts from the first day. We run a fortnight of shared on-call where your engineers are paged first, and we deliberately break a seam in a recorded walkthrough so the first real incident is not the first time anybody has watched a replay.
How do you stop a historical backfill from taking down a production system?
Four controls, all boring. The backfill gets its own rate lane with a smaller allowance than live traffic, so it cannot starve the path customers depend on. It runs from a resumable cursor, so stopping and restarting does not repeat work. Every automation watching the destination fields is paused first. And it runs against a copy before production.

Tell us what is eating the hours.

Send the process, the volume and the tools it touches. You get a scoped plan with a build shape and a timeline, not a brochure.

Start a project