# Building API integrations with Cursor's agent

URL: https://developertoolkit.ai/en/cursor-ide/build/api-integration/
Description: Build Stripe, SendGrid and Twilio integrations with Cursor's agent: tests first, retries, idempotency, signed webhooks, and gates that prove it works.

Cursor's agent builds a production-grade third-party API integration when it works against failing acceptance tests, current SDK documentation and a sandbox account. The agent writes the shared client (retries, circuit breaker, idempotency keys), each provider adapter and the signed webhook handlers, while mocked tests and the Stripe CLI prove the failure paths behave.

The previous developer left a half-finished Stripe integration. The webhook handler swallows errors, the SendGrid calls have no retries, and the Twilio code crashes on the first `429`. Now payment, receipt email and SMS confirmation have to work as one flow, and a double charge or a lost webhook is a support ticket with a customer's money attached.

This page is for **developers** wiring third-party APIs into a TypeScript backend with Cursor's agent. The examples use Next.js App Router routes and the `stripe` (22.6.2), `@sendgrid/mail` (8.1.6), `@sendgrid/eventwebhook` (8.0.0) and `twilio` (6.1.2) Node SDKs, checked on npm on 26 September 2026. Cursor features named here (Plan Mode, Rules, MCP, plugins, Bugbot) were verified against Cursor's docs on 28 August 2026, on the Cursor 3.x line; the current release on 12 October 2026 is Cursor 3.24 (released 9 October 2026, per Cursor's release notes). If you are new to the chat loop itself, read [agent chat workflows in Cursor](/en/cursor-ide/build/agent-chat/) first. Using another agent? See [third-party API integrations with Claude Code](/en/claude-code/build/integrations/) or [integrations with Codex](/en/codex/automate/slack-linear-integration/).

## What this API integration workflow gives you

- A seven-step order of work that takes an integration from audit to a pull request with evidence.
- Six copy-paste prompts: integration audit, acceptance tests, base client plus Stripe adapter, notification orchestration, signed webhooks, and a pull-request evidence report.
- A project `mcp.json` that gives the agent current SDK docs (Context7) and a Stripe test-mode account (Stripe MCP).
- A verification routine that proves retries, idempotency and signature checks work without reading every generated line.
- Six production failure modes, each with the recovery step.

## How does Cursor's agent build an integration you can trust?

The order matters more than the prompts. An agent that writes the client first and the tests second produces tests that describe whatever it built. An agent that writes against stale training data calls API versions and SDK methods that no longer exist.

1. **Audit what exists.** Map every provider, secret and failure path before anything changes.
2. **Connect current docs and a sandbox.** Context7 for SDK docs, Stripe MCP in test mode for fixtures.
3. **Write failing acceptance tests.** One test per behaviour in the acceptance table below, with every HTTP call mocked.
4. **Build the base client and one provider.** Stripe first, because it has the strictest idempotency and webhook rules.
5. **Add the remaining providers and the orchestration.** SendGrid and Twilio copy the Stripe adapter's shape.
6. **Secure the webhooks.** Signature verification on the raw body, replay window, deduplication by event ID.
7. **Run the gates and collect evidence.** Tests, type check, lint, a real signed webhook from the Stripe CLI, then review.

## Audit the existing integrations before changing anything

Start in **Plan Mode**, which produces a plan before writing any code, so the agent reads and reports instead of "fixing" the first thing it sees. Attach the directories that hold the integration code.

**Copy-paste prompt: integration audit**

```text
@src/integrations @src/services @src/app/api

Audit the third-party API integrations in this codebase. Do not edit any file.

For each provider (Stripe, SendGrid, Twilio, and any other you find), report:
1. Which operations are called, and from which files.
2. Where the API key comes from. Flag any key that is hard-coded or logged.
3. Retry behaviour: none, per-call, or shared. Which errors are retried?
4. Whether mutating calls send an idempotency key.
5. Whether each webhook route verifies the signature on the raw request body.
6. What happens when the call fails halfway through the payment -> email -> SMS flow.
7. Which of these paths have tests today.

End with a table: provider | status (production-ready / needs work / dangerous) | the one change that removes the biggest risk.
```

Save the table in the pull request description. It is the "before" half of your evidence, and it tells a reviewer which risks this change claims to remove.

## Give the agent current SDK docs and a sandbox

Payment and messaging SDKs change their method names, API versions and webhook helpers between majors. Two MCP servers remove most of the resulting guesswork:

- **Context7** (`@upstash/context7-mcp` 4.1.1) fetches version-specific library docs on request. Add "use context7" to a prompt and the agent looks up the SDK before writing against it.
- **Stripe MCP** (`@stripe/mcp` 0.3.3, remote `https://mcp.stripe.com` with OAuth) lets the agent create test products, prices and customers and inspect events.

For Stripe, the install the Stripe README lists for Cursor is `/add-plugin stripe`; use it first. Alternatively, add the remote server by URL in the project's `.cursor/mcp.json` next to Context7, and Cursor prompts for OAuth on first use (entry shape from Stripe's README, checked 2026-09-26). Neither entry holds a credential, so the file is safe to commit: Context7 works without a key (a free key raises limits), and the Stripe server signs you in with OAuth.

```json
{
  "mcpServers": {
    "context7": { "url": "https://mcp.context7.com/mcp" },
    "stripe": { "url": "https://mcp.stripe.com" }
  }
}
```

**Stripe MCP can move money**

Connect it to a Stripe **test mode** account only. If you run the local server instead, give it a restricted API key: the key's permissions decide which tools the server exposes. Keep the server out of any agent session that also has production credentials. [MCP security](/en/ecosystem/mcp/mcp-security/) covers the wider prompt-injection risk of tool-using servers.

Every connected server adds its tool definitions to the agent's context. Disable the ones this task does not need; [reducing MCP token cost](/en/ecosystem/mcp/reducing-mcp-token-cost/) shows how to measure the overhead. Full setup steps are in [MCP setup for Cursor](/en/cursor-ide/quick-start/mcp-setup/) and the [Context7 guide](/en/ecosystem/mcp/context7/).

## Write the acceptance tests before the client exists

The acceptance table is the specification. You own it; the agent turns each row into a test.

| Behaviour | Test that proves it |
| --- | --- |
| A `429` with `Retry-After: 2` waits at least 2 seconds, then retries | Mock returns `429` once, then `200`; fake timers assert the delay |
| A retried Stripe mutation reuses the same idempotency key | Mock records the `Idempotency-Key` header on both attempts |
| Five failed HTTP attempts in 60 seconds open the circuit (retries count); calls then fail fast | Mock returns `503` five times; the next call makes no HTTP request |
| An email or SMS failure never fails the checkout | SendGrid mock returns `500`; checkout returns success and a queued notification row exists |
| A webhook with a bad signature is rejected | Tampered body returns `400`, and no handler runs |
| A duplicate webhook delivery is processed once | Same event ID posted twice; side effect happens once |

**Copy-paste prompt: failing acceptance tests**

```text
@src/integrations @src/services

Write failing tests for the integration layer we are about to build. Do not write any implementation.

Use vitest and msw (Mock Service Worker) to intercept every request to api.stripe.com, api.sendgrid.com and api.twilio.com. Fail the test run on any unhandled request, so no test can reach a real API.

Cover exactly these behaviours, one test each:
1. A 429 with Retry-After: 2 waits at least 2 seconds, then retries (use fake timers).
2. A retried Stripe mutation sends the same Idempotency-Key header on every attempt.
3. Five failed HTTP attempts within 60 seconds open the circuit (each retry counts as an attempt); the next call throws CircuitOpenError without an HTTP request.
4. Payment succeeds, SendGrid returns 500: checkout still resolves as paid, and a notification row with status "queued" exists.
5. A Stripe webhook with a tampered body returns 400 and no event handler runs. Build valid signatures with stripe.webhooks.generateTestHeaderString.
6. The same Stripe event ID posted twice triggers the side effect once.

Run the tests and show me the output. Every test must fail for the right reason (missing module or missing behaviour), not because of a syntax error.
```

Read the six tests, not the whole diff. Commit them on their own so you can later prove the agent did not weaken them. [Testing patterns in Cursor](/en/cursor-ide/test/testing-patterns/) covers mocking strategy in more depth.

## Build the base client and the Stripe adapter

With the tests committed, ask for the shared client and one provider in the same turn. The base client is where retries, the circuit breaker and error mapping live, so every later provider inherits them.

**Copy-paste prompt: base client plus Stripe adapter**

```text
@src/integrations @src/integrations/__tests__

Make the committed tests pass. Do not modify any file under src/integrations/__tests__.

1. src/integrations/base-client.ts
   - Retries with exponential backoff and full jitter, max 4 attempts, only for network errors, 429 and 5xx.
   - Honour Retry-After when present.
   - Circuit breaker: open after 5 failures in 60 s, half-open after 30 s, allow a few probe requests, close only after 3 consecutive successes.
   - Per-provider timeout. Structured logs with API keys, card data and email bodies redacted.
2. src/integrations/errors.ts: IntegrationError (provider, code, retryable, cause) plus RateLimitError, AuthError, ValidationError, TimeoutError, CircuitOpenError.
3. src/integrations/stripe-client.ts
   - Uses the stripe npm package; read STRIPE_SECRET_KEY and the API version from environment variables.
   - createPaymentIntent, refundPayment, getPaymentStatus.
   - Every mutation passes an idempotencyKey derived from our order ID, and a retry reuses it.
   - Map Stripe errors (card declined, rate limit, connection error) to our error classes.
   use context7 to check the current stripe-node API before you write the calls.

After each file, run: npm test -- src/integrations && npx tsc --noEmit
Finish with the test output, the type-check output, and a list of any assumption you made.
```

Once Stripe passes, repeat the prompt for `sendgrid-client.ts` and `twilio-client.ts` with "follow the shape of `@src/integrations/stripe-client.ts`". Neither SDK's send call exposes an idempotency-key option (checked in `@sendgrid/mail` 8.1.6 and `twilio` 6.1.2), so deduplication for email and SMS has to come from your own notifications table, which the next step creates.

## Orchestrate payment, email and SMS without failing checkout

The hard part is not any single call; it is deciding what a partial failure means. The rule this prompt encodes: the payment is the source of truth, and notifications are best-effort with retries.

**Copy-paste prompt: notification orchestration**

```text
@src/integrations/stripe-client.ts @src/integrations/sendgrid-client.ts @src/integrations/twilio-client.ts @src/db/schema.ts

Create src/services/checkout-notifications.ts:

1. After a payment is confirmed, insert one notifications row per channel (payment_id, channel, status, attempt_count, last_error) with a UNIQUE constraint on (payment_id, channel). Write a migration for the table.
2. Send the receipt email, then the SMS. A failure marks the row "queued" with the error; it never throws to the checkout caller.
3. A retry worker picks up "queued" rows every 60 seconds with backoff, and moves a row to "dead" after 5 attempts.
4. Before sending, the worker re-reads the row and skips anything already "sent", so a crash between send and update cannot send twice from a normal retry.

Make test 4 in src/integrations/__tests__ pass and add tests for: the dead-letter transition, and two workers racing for the same row.
Run npm test and npx tsc --noEmit and show the output.
```

[Database design in Cursor](/en/cursor-ide/plan/database-design/) covers the schema side, and the [recovery patterns recipe](/en/cookbook/error-handling-recipes/recovery-patterns/) covers dead-letter handling in more detail.

## Secure the webhook endpoints

Webhook routes are public URLs. The three providers sign requests differently, and each helper needs the **raw** body: in a Next.js App Router route that means `await request.text()`, never `request.json()`, because re-serialised JSON no longer matches the signature.

| Provider | Verification helper (checked 2026-09-26) | Header |
| --- | --- | --- |
| Stripe | `stripe.webhooks.constructEvent` (Node) or `constructEventAsync` (Web Crypto runtimes); default tolerance 300 seconds | `Stripe-Signature` |
| SendGrid | `new EventWebhook().verifySignature(ew.convertPublicKeyToECDSA(key), rawBody, signature, timestamp)` from `@sendgrid/eventwebhook`; `key` is the verification key from Mail Settings | `X-Twilio-Email-Event-Webhook-Signature` and `-Timestamp` |
| Twilio | `twilio.validateRequest` for status callbacks | `X-Twilio-Signature` |

**Copy-paste prompt: signed webhook routes**

```text
@src/integrations @src/app/api

Create src/app/api/webhooks/stripe/route.ts, src/app/api/webhooks/sendgrid/route.ts and src/app/api/webhooks/twilio/route.ts.

1. Read the body with await request.text() and verify it before parsing:
   Stripe with stripe.webhooks.constructEventAsync and STRIPE_WEBHOOK_SECRET;
   SendGrid with new EventWebhook().verifySignature from @sendgrid/eventwebhook, passing
   ew.convertPublicKeyToECDSA(SENDGRID_WEBHOOK_PUBLIC_KEY), never the raw key string;
   Twilio with twilio.validateRequest, TWILIO_AUTH_TOKEN, the full public callback URL
   (not the internal request URL) and the form-encoded params parsed from the body.
2. Reject a missing or invalid signature with 400 and log the reason without the body.
3. Reject SendGrid events whose timestamp header is older than 5 minutes.
4. In one database transaction, insert the event ID and the raw event (status "pending") into processed_webhook_events with a UNIQUE constraint on the event ID. A worker processes pending rows. On a duplicate, return 200 and do nothing else. Return 200 only after the transaction commits.
5. The payment_intent.succeeded handler looks up the order by metadata.order_id and calls checkout-notifications.
6. Handle events by type; for state changes, re-fetch the object from the API instead of trusting arrival order or payload snapshots.

Make tests 5 and 6 pass. Run npm test and npx tsc --noEmit and show the output.
```

Point 4 corrects a common instruction, "return 200 immediately, then process async". If the process dies after the `200` and before the event is stored, the provider considers the delivery done and the event is gone. Store first, then acknowledge.

The dedupe row and the stored event must be written in the same transaction (a transactional outbox). If they were separate writes, a crash between them would leave the event ID recorded but the event never stored: the provider's retry would hit the UNIQUE constraint, get a `200`, and the event would be lost without a trace.

## How do you verify the integration without reading every line?

You check behaviour and evidence, not the generated code. Four gates carry the proof:

1. **The approved tests are unchanged.** Compare against the commit that added them, and list files git does not track yet, such as a new test file; any output from either command means the agent edited the specification.

   ```bash
   git diff --stat TESTS_COMMIT -- src/integrations/__tests__/
   git status --porcelain src/integrations/__tests__/
   ```

   Replace `TESTS_COMMIT` with the hash of the commit that added the acceptance tests.

2. **The suite, the type check and the linter are green.** Run the same commands CI runs, in a terminal, not in the agent's summary.
3. **A real signed webhook reaches your route.** The Stripe CLI (`npm install -g @stripe/cli` or `brew install stripe`) forwards test-mode events to your dev server and signs them with a session secret, which `stripe listen --print-secret` also prints. Put that secret in `STRIPE_WEBHOOK_SECRET` in `.env.local`, then run the Stripe CLI in two more terminals. `stripe listen` stays in the foreground until you stop it.

   Terminal 2:

   ```bash
   stripe listen --forward-to localhost:3000/api/webhooks/stripe
   ```

   Terminal 3, with the ID of an order you seeded in the test database:

   ```bash
   stripe trigger payment_intent.succeeded --add payment_intent:metadata.order_id=ORDER_ID
   ```

   Expect a `200` in the `listen` output, one row in `processed_webhook_events`, and one `notifications` row per channel (email, SMS). Do not use a second `stripe trigger` as a duplicate check: each run creates a new PaymentIntent with a new event ID, so a second pair of notifications is correct behaviour. Duplicate delivery of the same event ID is proven by acceptance test 6.
4. **A review pass on the money paths.** Bugbot or your AI reviewer covers the whole pull request; a human reviews only the payment mutation and the webhook route, which are the paths where a mistake costs money. See [Bugbot](/en/cursor-ide/ship/bugbot/) for setting it up.

Ask the agent to assemble the evidence so the reviewer does not have to.

**Copy-paste prompt: evidence report for the pull request**

```text
Prepare the pull request description for this integration work. Include:
1. The audit table from the start of this task, with a new column "after".
2. The output of npm test, npx tsc --noEmit and npm run lint, pasted verbatim.
3. The output of git diff --stat on src/integrations/__tests__ against the commit that added the tests, and of git status --porcelain on the same folder.
4. The list of files that move money or verify webhook signatures, so the human reviewer reads only those.
5. Every assumption you made about provider behaviour, with the doc you checked it against.
Do not summarise any command output. If a command fails, say so and stop.
```

The developer who owns the integration signs off on the acceptance table; a second person approves the payment and webhook files. Add a project rule that says "never modify files under `src/integrations/__tests__` unless asked" so future chats keep the contract; [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/) shows how. To enforce it rather than ask for it, a Cursor hook can reject edits to that folder; the git diff gate above still proves it.

## What breaks when third-party API integrations go live?

**The agent calls an SDK method that no longer exists.** Symptom: type errors on `stripe.*` calls, or runtime errors after an SDK upgrade. Recovery: revert to the last green commit, add "use context7" and the SDK version from `package.json` to the prompt, and rerun the step.

**Retries create duplicate charges.** A request times out after Stripe processed it, and the retry charges again. Recovery: refund the duplicate in the Stripe Dashboard, then make sure every mutation derives its idempotency key from a stable business ID (the order ID), never from a random value generated per attempt. Test 2 catches a regression.

**Rate limits appear only in production.** Mocked tests never hit a real limit. Recovery: add a per-provider token bucket in the base client set to about 80% of the published limit, queue the excess, and alert on the rate of `RateLimitError`.

**Webhook events arrive out of order or twice.** Providers do not guarantee order, and they retry deliveries they think failed. Recovery: key handlers on the event ID (already deduplicated by the webhook routes in step 6) and on the object's current state fetched from the API, not on the order events arrived in.

**The circuit breaker flaps.** A struggling provider fails the single half-open probe, the circuit reopens, and checkout degrades for minutes. Recovery: let a small share of traffic through while half-open and close the circuit only after several consecutive successes; test the transition with fake timers.

**Mocks drift from the real API.** Tests stay green while the provider changes a field. Recovery: run the same suite against the providers' test environments on a nightly schedule, outside the pull-request gate, and fail loudly on any shape mismatch.

## When should an MCP server replace integration code?

MCP servers are tools for the agent, not a runtime for your application. Use them while you build, and keep code for anything your product does on behalf of a customer.

| Need | Use | Why |
| --- | --- | --- |
| Create test products, customers or events while developing | Stripe MCP (test mode) | No throwaway scripts; the agent sets up fixtures in the same chat |
| Check the current SDK signature before writing a call | Context7 | Docs matched to the library version |
| Open issues or read CI logs about the integration | [GitHub MCP server](/en/ecosystem/mcp/github-mcp/) | Operational work, no product code involved |
| Charge a customer, send a receipt, verify a webhook | Your integration code | You need retries, idempotency, logging and tests you control |

## Where to go next with your integration layer

- [Testing patterns in Cursor](/en/cursor-ide/test/testing-patterns/) — Mocking strategy, fixtures and coverage for the test suite that guards this integration.
- [Security in Cursor](/en/cursor-ide/operate/security/) — Review the integration for key exposure, webhook forgery and injection before it ships.
- [API development patterns](/en/cookbook/backend-recipes/api-patterns/) — Recipes for the API your integration serves: validation, versioning and error formats.
- [Microservices in Cursor](/en/cursor-ide/build/microservices/) — Move the notification worker into its own service when the monolith outgrows it.
