> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ancha.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These docs describe the Ancha CLI, npm package @ancha-labs/cli version 0.1.4, and the hosted workspace at app.ancha.io. Coding agents should start at https://docs.ancha.io/coding-agents. Run the CLI as `ancha` after `npm install -g @ancha-labs/cli`, or as `npx -y @ancha-labs/cli@0.1.4` without installing. Pass --json for machine-readable output, or set ANCHA_JSON=1. Every command ends with a `next:` line naming the next command. Exit codes: 0 pass, 1 a check failed or a comparison regressed, 2 usage, config or authentication error, 3 infrastructure error, 4 unimplemented endpoint with --strict.

# PayPal

> A stateful replica of PayPal's REST APIs: checkout orders, authorizations, captures and refunds, catalog products, billing plans and subscriptions, and transaction reports.

The PayPal replica implements 35 endpoints of PayPal's REST APIs for one business account: checkout orders and payments, catalog products, billing plans and subscriptions, and transaction and balance reports. Its key is `paypal`. It was tested with `@paypal/paypal-server-sdk`.

## Connect

| Variable | Value |
| - | - |
| `PAYPAL_API_URL` | Base URL, standing in for `https://api-m.sandbox.paypal.com`. |
| `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET` | Fake REST app credentials. Exchange them for an access token at `POST /v1/oauth2/token` with the `client_credentials` grant, as the SDK does. |

```js Node theme={null}
import { Client, Environment, SubscriptionsController } from '@paypal/paypal-server-sdk';
const paypal = new Client({
  clientCredentialsAuthCredentials: { oAuthClientId: process.env.PAYPAL_CLIENT_ID!, oAuthClientSecret: process.env.PAYPAL_CLIENT_SECRET! },
  environment: Environment.Sandbox,
  unstable_httpClientOptions: { adapter: async (config: any) => {
    const headers: Record<string, string> = Object.fromEntries(Object.entries(config.headers.toJSON()).map(([k, v]) => [k, String(v)]));
    if (config.auth) headers.authorization = 'Basic ' + Buffer.from(config.auth.username + ':' + config.auth.password).toString('base64');
    const res = await fetch(String(config.url).replace(/^https:\/\/[^/]+/, process.env.PAYPAL_API_URL!), { method: config.method.toUpperCase(), headers, body: config.data });
    return { data: await res.text(), status: res.status, statusText: res.statusText, headers: Object.fromEntries(res.headers), config, request: {} };
  } },
});
const { result } = await new SubscriptionsController(paypal).listSubscriptions({ statuses: 'ACTIVE' });
```

This is what `ancha connect paypal --lang ts` prints. The server SDK knows only PayPal's own hosts, so the adapter sends each request to the replica and sets the Basic header for the token request itself. Plain `fetch` against `PAYPAL_API_URL` works too.

## Endpoints

| Method | Path | Support |
| - | - | - |
| `POST` | `/v1/oauth2/token` | supported: the `client_credentials` grant |
| `POST` | `/v2/checkout/orders` | partial: PayPal wallet orders the payer must approve; card and other payment sources, payee and shipping preferences are gaps |
| `GET` | `/v2/checkout/orders/:id` | supported |
| `PATCH` | `/v2/checkout/orders/:id` | partial: amount, description, `custom_id`, `invoice_id` and `soft_descriptor` of a purchase unit |
| `POST` | `/v2/checkout/orders/:id/capture` | supported: only orders the payer approved |
| `POST` | `/v2/checkout/orders/:id/authorize` | supported: only approved orders; the authorization is honored for 3 days and valid for 29 |
| `POST` | `/v2/checkout/orders/:id/track` | supported |
| `PATCH` | `/v2/checkout/orders/:id/trackers/:tracker` | partial: status, tracking number, carrier and `notify_payer` |
| `POST` | `/v2/checkout/orders/:id/confirm-payment-source` | partial: always a gap, since confirming needs a payment source the replica does not model |
| `GET` | `/v2/payments/authorizations/:id` | supported |
| `POST` | `/v2/payments/authorizations/:id/capture` | supported: up to the authorized amount; `final_capture` defaults to false |
| `POST` | `/v2/payments/authorizations/:id/void` | supported |
| `POST` | `/v2/payments/authorizations/:id/reauthorize` | partial: always a gap |
| `GET` | `/v2/payments/captures/:id` | supported: checkout captures and subscription payments |
| `POST` | `/v2/payments/captures/:id/refund` | supported: full or partial, within 180 days; returns the percentage part of the fee |
| `GET` | `/v2/payments/refunds/:id` | supported |
| `GET` | `/v1/catalogs/products`, `/v1/catalogs/products/:id` | supported |
| `GET` | `/v1/billing/plans`, `/v1/billing/plans/:id` | supported |
| `POST` | `/v1/billing/plans` | partial: one `REGULAR` fixed-price cycle; trials, tiers, setup fees and taxes are gaps |
| `POST` | `/v1/billing/plans/:id/activate`, `/v1/billing/plans/:id/deactivate` | supported |
| `GET` | `/v1/billing/subscriptions` | partial: `plan_ids` and `statuses` filters |
| `GET` | `/v1/billing/subscriptions/:id` | supported |
| `POST` | `/v1/billing/subscriptions` | partial: waits for the payer's approval, which the replica cannot give |
| `PATCH` | `/v1/billing/subscriptions/:id` | partial: `custom_id` and the outstanding balance |
| `POST` | `/v1/billing/subscriptions/:id/cancel`, `suspend`, `activate` | supported |
| `POST` | `/v1/billing/subscriptions/:id/capture` | supported: the outstanding balance; the payment succeeds |
| `POST` | `/v1/billing/subscriptions/:id/revise` | partial: always a gap, since a revision needs the payer's approval |
| `GET` | `/v1/billing/subscriptions/:id/transactions` | supported |
| `GET` | `/v1/reporting/transactions` | partial: date range, ID, event code, status, amount range and currency filters; store and terminal filters are gaps |
| `GET` | `/v1/reporting/balances` | supported |

Anything else returns HTTP 501 with PayPal's error shape and `x-ancha-gap: 1`.

## Starter data

The `paypal-starter` dataset is the business account **Acme Inc.** (`billing@acme.example`, USD) with an opening balance of 18,240.00 USD.

| Record | Detail |
| - | - |
| Product and plans | "Acme Analytics", with the plans "Team (monthly)" at 49.00 USD a seat, "Team (annual)" at 490.00 USD a seat, and a retired "Starter (monthly)". |
| Subscriptions | Lakeview Retail (5 seats, active), Redwood Labs (3 seats, suspended after two failed payments), Harbor Foods (annual, active) and Summit Freight (cancelled, moved to annual invoicing). |
| Orders | Kestrel Credit Union's admin training (completed, one session refunded), Bluefield Logistics' onboarding workshop (authorized and completed), Pinecrest Health's implementation package (completed), Ridgeway Robotics' pilot extension (approved an hour ago and ready to capture) and Helix Manufacturing's replacement gateway (created, waiting for the payer). |

## Checks

PayPal has no state checks. Use trace checks, for example `{ called: "POST /v2/checkout/orders/*/capture" }` or `{ not_called: "POST /v2/payments/captures/*/refund" }`.

## Limits

* Exchange the client ID and secret at `PAYPAL_API_URL/v1/oauth2/token`. The server SDK knows only PayPal's hosts: call `PAYPAL_API_URL` over HTTP, or give the SDK the adapter above.
* Payers approve orders and subscriptions in their browser, which the replica cannot do: new ones wait for approval, and seeded approved orders can be captured. Invoicing, disputes, payouts and webhooks are gaps.


## Related topics

- [Introduction](https://docs.ancha.io/index.md)
- [Tools](https://docs.ancha.io/tools/overview.md)
- [Environment variables](https://docs.ancha.io/reference/environment-variables.md)
- [Worlds](https://docs.ancha.io/concepts/worlds.md)
- [Changelog](https://docs.ancha.io/changelog.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.