API Development and Integrations
We design and build the contracts your software talks over — public and internal APIs, webhooks, and connections to payment gateways, ERPs and CRMs. The work is mostly about what happens when the other side changes, retries, or goes down.
Who this is for
- Your product works, but every customer integration is a one-off branch in the codebase and you now need one documented API that partners can build against.
- You are wiring a payment gateway, a logistics provider or a KYC vendor into a live system and cannot afford double charges, lost callbacks or silent failures.
- Your ERP or CRM holds the data everyone wants, and the current answer is a nightly CSV export that nobody trusts by Wednesday.
- You changed a field last quarter, three consumers broke, and you now need a versioning and deprecation policy before the next change.
What usually goes wrong
The problems this work exists to solve
Any change to a response body breaks somebody, so nothing gets changed
We move the contract into a spec file — OpenAPI 3.1 for REST, .proto for gRPC, SDL for GraphQL — and put a breaking-change diff check in CI so a removed field or a narrowed type fails the build rather than a customer. Changes become additive by default: new fields, new optional parameters, new endpoints. When semantics genuinely change we cut a major version, run both in parallel, mark the old one with Deprecation and Sunset headers, and instrument per-consumer usage so you can see exactly which API key is still calling the field you want to delete. Deprecation without that telemetry is guesswork, and teams end up carrying dead fields for years.
A payment or partner call times out, gets retried, and the customer is charged twice
Every mutating endpoint takes an idempotency key, stored with the request fingerprint and the eventual response, so a retry of the same key returns the original result instead of performing the action again. On the outbound side we write the intent and the outbound message in one database transaction using an outbox table, then dispatch from that table — so we never end up in the state where the payment row committed but the provider call was lost, or vice versa. Where the provider supports it we reconcile against their API as the source of truth on a schedule, because a webhook you did not receive is invisible until you go and ask.
Integrations fail quietly, and when they fail loudly they take the partner down with them
We classify partner errors into retryable (429, 502, connection reset) and terminal (422 validation, 401) and only retry the first group, with exponential backoff plus jitter and a hard attempt ceiling. A circuit breaker trips after sustained failure so a struggling partner is not hammered by your queue workers, and each integration gets its own connection pool and concurrency limit so one slow vendor cannot starve the rest of the system. Anything that exhausts its retries lands in a dead-letter queue with the full request and response, and alerting is on DLQ depth and age rather than raw error rate — a rising DLQ is actionable, a spike in 4xx from one misbehaving client usually is not.
The documentation is a wiki page that stopped being true nine months ago
Docs and client SDKs are generated from the same spec the server validates against, so drift is impossible in the direction that matters. Request and response examples come out of the fixtures used in the test suite, which means an example that is wrong is a failing test. For partner-facing APIs we publish a sandbox with seeded data and a changelog tied to the spec diff, so an integrator can answer their own questions without a call with your engineers.
A single list endpoint is responsible for most of your database load
Unbounded collection endpoints and offset pagination are the usual cause: page 4,000 makes the database walk 200,000 rows to discard them. We move list endpoints to keyset (cursor) pagination ordered on an indexed, stable sort key, cap page size server-side, and return an opaque cursor so the client cannot construct an expensive query by hand. For GraphQL we add depth and complexity limits with a per-query cost budget, batch resolvers to kill N+1 fan-out, and use persisted queries so production only executes shapes you have already seen.
Authentication is one shared API key that has been in circulation for years
Different consumers need different mechanisms, and using one for all of them is what forces the key into a shared spreadsheet. First-party web and mobile clients get short-lived tokens issued through a backend-for-frontend rather than a long-lived key shipped into the app. Server-to-server consumers get OAuth2 client credentials with scopes narrow enough that a leak has a blast radius. Named partners on high-value flows get mutual TLS. Keys become issuable, scopable and revocable per consumer, with rotation that supports two live keys at once so a partner can roll over without downtime.
Scope
What you actually get
The contract, written before the code
An OpenAPI, protobuf or GraphQL schema checked into the repo, reviewed by the people who will consume it, with a written versioning and deprecation policy attached — what counts as breaking, how long an old version lives, how consumers are notified.
The API implementation
Handlers, request validation derived from the schema, a consistent error contract with stable machine-readable codes (not just HTTP status), correlation IDs propagated through every downstream call, and structured logs and traces keyed to those IDs.
Authentication and authorisation layer
OAuth2 flows, JWT issuance and verification, API key management with scopes and rotation, or mTLS for partner channels — plus object-level authorisation checks, so a valid token for tenant A cannot read tenant B's record by changing an ID.
Rate limiting and quotas
Per-key and per-tenant limits at the gateway or in the application, standard rate-limit headers, 429 responses carrying Retry-After, and separate budgets for expensive endpoints so one report query cannot consume a client's whole allowance.
Webhooks, in both directions
Outbound: signed payloads with a rotating secret, timestamped to prevent replay, retried on a decaying schedule, with a delivery log and manual replay. Inbound: signature verification, deduplication on the provider's event ID, and fast acknowledgement with processing moved off the request path.
Third-party integrations
Payment gateways, KYC and identity vendors, logistics and courier APIs, accounting, ERP and CRM systems. Each one goes behind an adapter so the vendor's data model never leaks into your domain, which is what makes replacing a vendor a contained piece of work instead of a rewrite.
Contract tests and consumer confidence
Consumer-driven contract tests in CI, schema fuzzing against the spec, recorded partner interactions replayed in test so the suite runs without hitting a sandbox, and load tests against the endpoints that carry the traffic.
Technology
Technology options
We are technology-agnostic. These are the choices we reach for, and how the decision actually gets made.
API style
REST is the default for anything a third party will integrate against — every integrator already knows it and can debug it with curl. GraphQL earns its place when many different clients need different slices of a rich object graph and you would otherwise ship a dozen one-off endpoints; the price is query cost control and caching you now have to build yourself. gRPC is for internal service-to-service calls where the schema is shared and the latency and payload savings are real; it is a poor choice for a public API because browser and partner tooling support is still awkward. Most systems end up with more than one, and that is fine as long as one of them is the documented front door.
Gateway and edge
A gateway is worth it when you have several services behind one hostname and want auth, rate limiting and routing in one place. It is overhead when you have one service — putting a managed gateway in front of a single container mostly buys you an extra hop, an extra bill and an extra place for a config change to break things. We start in the application and pull concerns out to the edge when there is a second consumer of them.
Runtime and schema tooling
The deciding factor is usually where your validation lives. FastAPI and NestJS generate the OpenAPI document from the same types that validate requests, so the spec cannot drift. Go is the choice when the API is a high-throughput edge in front of slower systems and predictable memory matters. If your team maintains it after us, their language wins over our preference.
Async, queues and eventing
Queue choice matters less than ordering and delivery semantics. SQS is enough for most webhook fan-out and retry work. Kafka is justified when you need retained, replayable, ordered event logs that several consumers read independently — as an integration backbone across systems, not as a fancier task queue. Whichever you pick, the consumer must be idempotent, because at-least-once delivery is what you actually get.
Testing and contracts
Consumer-driven contract tests are the right tool when your consumers are internal teams who can publish expectations. For external consumers you cannot get contracts from, spec-based fuzzing plus a breaking-change diff on the schema gives most of the protection with none of the coordination cost.
How it runs
The shape of the engagement
- 01
Inventory the consumers
Before any design work: who calls this, what do they actually use, and what are they allowed to depend on. For an existing API we read access logs to find which endpoints and fields are live, because the answer is almost never what the team believes. For a new API we need at least one named first consumer with a real use case — designing for hypothetical future partners produces endpoints nobody can use.
- 02
Agree the contract
We write the spec, the error catalogue and the pagination and filtering conventions, and review them with the consuming team before implementation. A mock server is generated from the spec on day one, so client work can start in parallel and disagreements about the shape surface while they are still cheap to fix.
- 03
Build one vertical slice against a real caller
One endpoint or one integration, complete: auth, validation, error paths, tests, logging, docs. This settles the conventions the rest of the surface will copy, and it exposes the awkward parts of the partner's API — the undocumented field, the sandbox that behaves differently from production, the rate limit nobody mentioned — while there is time to design around them.
- 04
Harden the failure paths
We deliberately break things: partner returns 500, partner returns 200 with an error in the body, callback arrives twice, callback arrives before the outbound call returns, token expires mid-flight, connection hangs until timeout. Each one gets a defined behaviour and a test. This step is where integration work is won or lost, and it is the step most often skipped.
- 05
Instrument before publishing
Per-consumer request rates, error rates by code, latency percentiles per endpoint, webhook delivery success and DLQ depth. Without per-consumer breakdown you cannot tell a broken deploy from one client's bad script, and you cannot safely deprecate anything.
- 06
Publish, then manage the lifecycle
Docs, sandbox, SDKs or at least a working Postman collection, and a changelog. After launch the work shifts to running the deprecation cycle: announce, track who is still on the old shape, chase, remove. We hand over that policy with the code so it keeps running after we leave.
Engineering
Architecture, security and performance
The decisions that are expensive to change later, and where we stand on them.
Architecture
- The integration boundary is an adapter, not a passthrough
- Every third-party system gets a translation layer that converts its model into yours at the edge. This is not ceremony — it is what stops a payment provider's status enum from ending up in forty conditionals across your codebase, and it is why swapping a gateway or adding a second one becomes a contained job. It also gives you one place to record every request and response to the vendor, which is what you will want the first time there is a dispute about what they sent you.
- Decide what belongs in the request path
- A synchronous API call should only do work whose latency the caller can accept and whose failure the caller can act on. Anything else — notifying downstream systems, syncing to a CRM, generating documents — moves behind an outbox and a queue, and the API returns an identifier the caller can poll or subscribe to. The common failure we are called in to fix is an endpoint that makes three vendor calls in series, so its availability is the product of four systems and its p99 is the sum of their worst days.
- Errors are part of the contract
- We define a stable error catalogue with machine-readable codes and a consistent body shape, so consumers branch on a code rather than string-matching a message. HTTP status alone is not enough: 400 tells a client something was wrong, not whether retrying helps. Every error says whether it is retryable, and validation errors point at the offending field. Once published, an error code is as much a breaking change to remove as a field.
- Versioning is additive until it cannot be
- New optional fields and new endpoints do not need a version bump; changing the meaning, type or cardinality of an existing field does. We prefer one major version in the URL path for REST — visible in logs and easy to route — over header negotiation, which is invisible in the places you debug. In GraphQL we deprecate fields rather than version the endpoint; in protobuf we never reuse a field number and reserve the ones we retire.
- One system of record per piece of data
- Two-way sync between an ERP and an application is where integration projects go to die, because both sides claim authority over the same record and conflicts have no correct resolution. We define ownership field by field, make the flow one-directional wherever possible, and where genuinely bidirectional sync is unavoidable we use explicit conflict rules and an audit trail of every change with its origin — rather than last-write-wins, which silently destroys data.
Security
- Object-level authorisation, checked on every read
- The most common serious API vulnerability is not a broken login — it is an authenticated user changing an ID in a URL and receiving someone else's record. Route-level permission checks do not catch this. We enforce ownership and tenancy at the data access layer so a query cannot be written that ignores it, and we test for it directly: an integration test that authenticates as tenant A and asserts a 404 on tenant B's resources.
- Credentials matched to the consumer, and rotatable
- Public API keys are fine for low-risk read access with scopes; they are not fine as the only control on a money-moving endpoint. Server-to-server consumers use OAuth2 client credentials with short-lived tokens; partner channels handling sensitive data use mutual TLS so possession of a string is not sufficient. Every credential mechanism supports two valid keys during rotation, because a rotation scheme that requires downtime never gets used.
- Webhooks are verified, not trusted
- Inbound callbacks carry a signature over the raw body with a shared secret, checked with a constant-time comparison before the body is parsed, plus a timestamp with a tolerance window so a captured payload cannot be replayed a week later. Events are deduplicated on the provider's event ID. For any callback that moves money or changes state, we treat it as a notification and confirm state against the provider's API rather than acting on the payload alone.
- Responses expose fields deliberately
- Serialising a database row straight to JSON is how internal flags, soft-delete markers and other users' identifiers leak into a public response. Every resource has an explicit output shape per audience, generated from the spec, so adding a column to a table does not silently publish it. The same applies to error responses — stack traces and driver messages never leave the process.
- Secrets and partner credentials stay out of the codebase
- Vendor keys live in a secret manager, differ per environment, and are injected at runtime. Sandbox and production credentials are never interchangeable in config, because the failure mode of getting that wrong is a real transaction against a real customer. Requests and responses to vendors are logged with card data, tokens and personal identifiers redacted at the logging layer rather than by remembering not to log them.
Performance
- Keyset pagination, and no unbounded lists
- Offset pagination degrades linearly with depth and produces duplicate or missing rows when the underlying data changes between pages. We paginate on an indexed, stable, unique sort key and return an opaque cursor, cap the page size on the server, and require a bounded time range or filter on endpoints over large tables. If a consumer genuinely needs everything, that is a bulk export job, not a loop over an API.
- Timeouts and budgets that are set, not defaulted
- Most client libraries default to no timeout or a very generous one, which turns a slow partner into exhausted connection pools and a queue of stuck requests. Each downstream call gets an explicit connect and read timeout that is shorter than the deadline of the request calling it, so the caller can return a useful error rather than being killed by its own gateway. Concurrency to each dependency is bounded separately, so one degraded vendor does not consume every worker.
- Caching that consumers can participate in
- Read endpoints emit ETag and Cache-Control, and support conditional requests, so a polling client can be answered with 304 instead of a full render. Writes support optimistic concurrency with If-Match, which removes a class of lost-update bugs that clients otherwise have no way to detect. Server-side caching goes behind an explicit key strategy with invalidation on write — a cache without a defined invalidation path is a bug with a delay on it.
- Rate limiting chosen for the traffic shape
- Fixed windows allow a client to send two full allowances across a window boundary; sliding-window counters or token buckets do not. Limits are applied per key and per tenant rather than globally, with a separate, smaller budget for expensive endpoints, and every response carries the remaining allowance so a well-behaved client can pace itself. A 429 always includes Retry-After — otherwise clients retry immediately and make the overload worse.
- Chattiness is a design problem, not a client problem
- If a screen needs eleven calls to render, no amount of per-call optimisation fixes it. The answer is either a composite endpoint or backend-for-frontend shaped to that view, or batching in the resolver layer for GraphQL. We measure the number of round trips per user-visible action alongside per-endpoint latency, because that is the number the user actually experiences.
Size of work
Where your project probably sits
A rough map so you can locate yourself before talking to us. Actual scope comes from a conversation, not a table.
| Level | Example | Engagement shape |
|---|---|---|
| Focused task | One integration done properly: a payment gateway with idempotent charge and a verified, deduplicated webhook receiver; a CRM lead-sync connector; or a hardening pass that adds rate limiting and key rotation to an API you already have. | One senior engineer, days to a couple of weeks, ending with the endpoint live in staging and an OpenAPI or Postman collection the calling team can work from. From about Rs 25,000. |
| Project | A documented v1 public API over an existing product — spec, auth and scopes, pagination and error contract, rate limits, docs and sandbox — or an integration set connecting a product to three or four vendors with retries, DLQs and reconciliation. | Several weeks to a few months, a small team with one engineer owning the contract, weekly demos against a working mock and then the real thing. Deprecation policy and runbooks handed over at the end. |
| Platform | An integration layer between an ERP, a CRM, a warehouse system and a customer-facing product, with an event backbone, replayable delivery, per-partner onboarding and a self-serve developer portal. | Multi-month engagement with an architect involved throughout, a roadmap reviewed each quarter, and a phased cutover from whatever batch process exists today. Usually runs alongside your own team rather than replacing it. |
Honest limits
When this is not what you need
Telling you this early is cheaper for both of us than discovering it in month two.
- You want a public API built for partners who do not exist yet. Without at least one real consumer you will design the wrong resources, and you will find that out after you have committed to supporting them. Build the API the first integrator needs, then generalise.
- The actual problem is the screens, not the contract. If the API works and the complaint is about the product experience, web or mobile development is the right page — we would just be adding a layer.
- The actual problem is running the platform: gateway operations, cluster capacity, deployment pipelines, cost. That is cloud and DevOps work. We will design the API to fit the infrastructure, but this engagement does not own it.
- You need a partner's private or undocumented API reverse-engineered, or a rate limit or terms-of-service restriction worked around. We integrate against documented interfaces and agreements, and we will say so early rather than after taking the brief.
Related work
Projects in this space
Payment platform
Payment flows built with idempotent requests, verified callbacks and PCI DSS scope kept small.
Read the case study: Payment platformHeadless commerce migration
A storefront rebuilt against commerce APIs, with the catalogue, cart and checkout contracts defined up front.
Read the case study: Headless commerce migrationPatient records system
Clinical record integration across systems that each held part of the patient's data.
Read the case study: Patient records system
Questions
Common questions
How do you choose between REST, GraphQL and gRPC?
By who is calling and how many different shapes of data they need. REST is the default for anything external, because every integrator can read it, debug it with curl and cache it with standard tooling. GraphQL is worth its extra machinery when many clients need different slices of a connected object graph and the alternative is a growing pile of one-off endpoints — but you take on query cost limiting and caching yourself. gRPC is for internal service-to-service calls where both ends share a schema and the payload and latency savings matter. Plenty of systems run two of the three, with one documented as the public front door.
Can you add an API to a system we already have without breaking it?
Usually yes, and it is a large part of what we do. We start by reading access logs and the existing client code to establish what is really depended on, then put the new surface alongside the old behaviour rather than replacing it. Where the internal data model is not fit to be exposed, the API gets its own representation layer so you are not committing to your current schema in public. The riskiest part is normally the authentication change, so that goes first, behind a flag, with the old path still working.
We use an ERP or CRM. Can you connect it?
We have worked with the common categories — accounting and ERP systems, CRMs, HR and payroll platforms — and the approach is the same regardless of vendor: read the API docs and rate limits, build an adapter that translates their model into yours, decide field by field which system owns which data, and run the sync one-directionally where at all possible. The parts that need care are their rate limits, their pagination behaviour on large exports, and what happens when a record is deleted on one side. If the system only offers file-based exchange, we build around that rather than pretending it is an API.
How do you make sure a payment integration cannot double-charge?
Three mechanisms together. Idempotency keys on every charge request, so a retry returns the original outcome instead of creating a second transaction. A transactional outbox, so the database record and the outbound call cannot diverge if the process dies between them. And reconciliation against the provider as the source of truth, because a webhook you never received is otherwise invisible. Callbacks are signature-verified and deduplicated on the provider's event ID, and anything that moves money is confirmed against the provider's API rather than trusted from the payload.
What happens when a third-party API changes or goes down?
Going down is the easier case: timeouts, bounded retries with backoff, a circuit breaker so we stop hammering them, a dead-letter queue for what could not be delivered, and a defined degraded behaviour for the user-facing flow. Changing is harder, because most vendors do not version well. The adapter layer contains the damage to one file, contract tests against their sandbox catch shape changes early, and we log every request and response to the vendor so a behaviour change can be evidenced rather than argued about.
Do you deliver documentation and client SDKs?
Yes, generated from the spec rather than written separately, so they cannot drift from the implementation. That means an OpenAPI or protobuf document in your repository, rendered reference docs, examples drawn from the test fixtures, and generated clients in the languages your consumers use. For most APIs a well-maintained spec plus a working sandbox is more valuable to integrators than a hand-written SDK, so we start there and add SDKs when there is a consumer asking for one.
How do you test integrations without hitting the vendor's system constantly?
Real interactions with the sandbox are recorded once and replayed in the test suite, so CI runs fast and offline and does not depend on a sandbox being up. On top of that we run the failure cases the sandbox will not produce on demand — timeouts, malformed responses, duplicate callbacks, out-of-order callbacks — against a stub. A smaller set of tests does run against the live sandbox on a schedule, which is what catches the vendor changing something without telling anyone.
Keep reading
Related work and reading
Related services
- Custom software development
When the API is one part of a larger system being built rather than a layer over something that already exists.
- Software architecture consulting
If the question is how to split the system into services before deciding what the interfaces between them should be.
- Software testing and QA
Contract tests, load tests against endpoints, and the automated suite that keeps a published API from regressing.
- Cloud and DevOps
Where the gateway, scaling, deployment pipeline and cost of running the API surface are handled.
- SaaS development
Multi-tenant products where the API is the product, and tenancy, billing and per-customer quotas are part of the contract.
Tell us what has to talk to what
Send the API docs of the system you need to integrate with, or a description of the consumers you need to serve. We will come back with the contract questions that matter — auth model, idempotency, failure behaviour, who owns which data — and a scope shaped around them. Small integrations start from Rs 25,000; larger API programmes get a written proposal after a scoping call.