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

# API reference

> The Pocketsflow HTTP API — base URL, authentication, conventions, and links to the complete endpoint catalog.

The Pocketsflow HTTP API lets you manage products, checkout, orders, payments,
subscribers, customers, discounts, upsells, refunds, webhooks, newsletters,
your Link in Bio page, images, and the partner program — all from your own
systems.

<Card title="Open the interactive API reference" icon="server" href="https://api.pocketsflow.com/docs" horizontal>
  Browse every endpoint, see request/response schemas, and try calls live at
  **api.pocketsflow\.com/docs**.
</Card>

<Card title="Browse every public endpoint" icon="list" href="/https/docs.pocketsflow.com/api-reference/endpoints" horizontal>
  See all public paths, methods, parameters, request bodies, and response codes
  generated from the OpenAPI contract.
</Card>

The raw OpenAPI specification is served at
[`https://api.pocketsflow.com/docs.json`](https://api.pocketsflow.com/docs.json) —
import it into Postman/Insomnia or generate a typed client from it.

## Base URL

Every request is made over HTTPS to:

```
https://api.pocketsflow.com
```

There is no version prefix in the path — endpoints are addressed directly (for
example `GET /orders`). See [Versioning](#versioning) for how the API changes.

## Authentication

Every endpoint requires authentication via the `Authorization` header using the
**Bearer** scheme, except the few marked **public** below (they are used by the
hosted checkout). Two credential types are accepted:

| Credential | Format | Typical use |
| - | - | - |
| **API key** | `pk_live_…` / `pk_test_…` | Server-to-server integrations, scripts, agents. |
| **Auth0 JWT** | `Bearer eyJ…` | First-party dashboard / session-based calls. |

Create an API key in the dashboard under **Developers → API keys**, then send it
on every request:

```bash theme={"system"}
curl https://api.pocketsflow.com/users/me \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
```

| Prefix | Mode | Data |
| - | - | - |
| `pk_live_…` | Live | Real products, customers, payments, and payouts. |
| `pk_test_…` | Test | Sandbox data for development — never charges a real card. |

All data is automatically scoped to the account that owns the key — you never
pass a user id. **The key decides the mode:** a `pk_test_…` key always reads and
writes test-mode data and a `pk_live_…` key always live data, whatever the
test-mode toggle in the dashboard says: lists and searches return that mode's
data and anything you create lands in it. `GET /users/me` reports the key's
mode as `testMode`. Reads and updates by id act on your own items in either
mode, so keep test and live ids apart in your integration.

Keys are stored hashed, can be given an expiry, and can be archived/revoked at
any time; a revoked or expired key returns `401`. Accounts in a country the API
does not serve receive `403` with `"code": "COUNTRY_RESTRICTED"`.

<Warning>
  Treat secret API keys like passwords. Never expose them in client-side code,
  public repositories, or URLs. Prefer a dedicated, revocable key per
  integration, and rotate a key immediately if it leaks.
</Warning>

### Authentication errors

Missing or malformed credentials return `401` with a JSON body:

```json theme={"system"}
{
  "error": "Invalid API key",
  "message": "The provided API key is invalid, inactive, or has been archived"
}
```

Common cases: no `Authorization` header, a header that doesn't start with
`Bearer `, an empty token, and an inactive/archived/expired key.

## Making requests

* Send JSON bodies with `Content-Type: application/json`. `POST /images` takes
  `multipart/form-data`, and the product and Link in Bio write endpoints also
  accept `multipart/form-data` when you upload files.
* Responses are JSON. IDs are Mongo ObjectId strings (24 hex characters) unless
  noted (payment-processor ids are `plan_…`, `pay_…`, `mem_…`).
* Timestamps are ISO 8601 UTC strings (`2026-01-24T05:35:38.103Z`).
* Money in API requests and responses is a decimal amount in **major currency
  units** (e.g. `12.1` = \$12.10) — product prices, orders, payments,
  subscribers, discounts, upsells. Only fields whose name ends in `Cents` are
  integer cents. Webhook payloads have their own rules — see
  [Webhook events](/api-webhooks/events).

## Pagination

List endpoints paginate in one of the following ways.

| Style | Endpoints | Query params | Response envelope |
| - | - | - | - |
| Page-based | `/orders`, `/payments`, `/delivered-webhooks`, `/subscriptions/subscribers` | `page` (default `1`), `pageSize` (default `20`; maximum `100`) | `{ <items>, pagination }` |
| Page + limit | `/partners/referrals`, `/partners/referrals/sales` | `page` (default `1`), `limit` (default `20`, maximum `100`) | `{ data, pagination: { currentPage, totalPages, totalItems, hasNextPage, hasPrevPage, limit } }` |
| None | every other list (`/products`, `/customers`, `/reviews`, `/discounts`, `/newsletters/posts`, …) | — | full array in one response |

Keep requesting `page + 1` while `hasMore` is `true`. The page-based
`pagination` object looks like:

```json theme={"system"}
{
  "totalCount": 137,
  "totalPages": 7,
  "currentPage": 1,
  "pageSize": 20,
  "hasMore": true
}
```

## Errors

Errors use standard HTTP status codes and a JSON body. Most endpoints return a
`message`:

```json theme={"system"}
{ "message": "Product not found" }
```

Authentication failures return both `error` and `message`, and some errors add
a machine-readable `code` (for example `COUNTRY_RESTRICTED`, `ACCOUNT_BLOCKED`,
`ACCOUNT_DELETED`, `SEND_ALLOWANCE_EXCEEDED`, `EMAIL_SENDING_PAUSED`,
`DAILY_SEND_LIMIT`):

```json theme={"system"}
{ "error": "Country not supported", "message": "The API is not available for accounts in your country. Contact support.", "code": "COUNTRY_RESTRICTED" }
```

Read `message` first, fall back to `error`, and branch on `code` when it is
present. A few older endpoints answer validation errors with a plain-text body.

| Status | Meaning |
| - | - |
| `200` / `201` | Success. |
| `400` | Invalid request — malformed JSON, an invalid id, or a failed validation. |
| `401` | Missing, malformed, expired, or revoked credentials. |
| `402` | A plan allowance would be exceeded (newsletter sends). |
| `403` | Authenticated but not permitted (restricted account, unverified email sender, plan limit, or `FEATURE_LOCKED`). |
| `404` | Not found, not owned by you, or from the other test/live mode. |
| `409` | Conflict (e.g. already a partner). |
| `410` | The account was deleted. |
| `429` | Rate limit exceeded — wait `Retry-After` seconds. |
| `500` / `502` | Server-side or upstream error — safe to retry reads with backoff. |

### Plan features (`FEATURE_LOCKED`)

Some features need a paid plan: custom domains, premium templates and ad
pixels (Starter), upsells, abandoned checkout recovery and affiliates
(Growth). A request that would turn one on when the account's plan does not
include it answers `403`:

```json theme={"system"}
{
  "error": "FEATURE_LOCKED",
  "code": "FEATURE_LOCKED",
  "upgradeRequired": true,
  "message": "This feature is part of a higher plan. Please upgrade your plan to use it.",
  "details": { "feature": "upsells", "currentPlan": "free", "requiredPlan": "tier2", "requiredPlanName": "Growth", "requiredPlanPrice": 49, "upgradeRequired": true }
}
```

Nothing is saved and retrying fails the same way: send the creator to
[app.pocketsflow.com/profile/billing](https://app.pocketsflow.com/profile/billing).
Call `GET /users/entitlements` before you write to avoid it. See
[Plans and limits](/concepts/plans-and-limits).

## Rate limits

Requests authenticated with a valid API key get **600 requests per minute per
account**. Unauthenticated traffic (public endpoints, or a request whose key is
rejected) shares **200 requests per minute per IP address**, so always send
your key. Responses carry `RateLimit-Limit`, `RateLimit-Remaining`,
`RateLimit-Reset` (seconds until the window resets) and `RateLimit-Policy`
headers. Exceeding a limit returns `429` with a `Retry-After` header (seconds)
and a JSON body:

```json theme={"system"}
{ "message": "Too many requests for this account, please try again later.", "retryAfter": 60 }
```

Back off and retry after that delay. Beyond that, use reasonable concurrency,
back off on `5xx`, and cache where you can. Some endpoints have their own caps —
for example [`POST /newsletters/send`](/api-reference/send-email) accepts at
most **100 recipients** per request.

## Versioning

The API is unversioned — there is no version prefix in the path. Changes are
additive: new endpoints, optional request fields, response fields, and webhook
event types can appear at any time, so ignore fields you don't recognise and
never depend on field order. Breaking changes are announced in the
[changelog](https://www.pocketsflow.com/changelog) before they ship.

## Endpoint catalog

Everything below is available with an API key (a few endpoints are public and
say so). Full request/response schemas live in the
[interactive reference](https://api.pocketsflow.com/docs) and the
[endpoint reference](/api-reference/endpoints); the most important response
shapes are shown here.

### Products

| Method | Path | Description |
| - | - | - |
| `GET` | `/products` | List all products. |
| `POST` | `/products` | Create a product. Requires `name`; `price` defaults to `0` (free). |
| `GET` | `/products/{id}` | Get a product by id. |
| `POST` | `/products/update/{id}` | Update a product — send only the fields to change. |
| `POST` | `/products/copy/{id}` | Copy a catalog item — a one-time product or a subscription offer (whichever owns the id). |
| `DELETE` | `/products/{id}` | Delete a product. |
| `PATCH` | `/products/{id}/abandoned-checkout` | Turn abandoned checkout recovery on or off (`{ "enabled": true }`). Turning it on needs Growth. |

`POST /products` accepts (among others) `name`, `price`, `compareAtPrice`,
`description`, `subtitle`, `published` (new products are always created published; send `false` on update to unpublish), `slug`,
`thumbnail`, `images[]`, `hasProductPage` (`"yes"` or `"no"`), `payWant`,
`minPrice`, `maxPrice`, `showSales`, `showReviews`, `refundPolicy`,
`hasFirstName`, `hasLastName`, and `productType`.

* Prices are decimal amounts in your account currency (`currency` on
  `GET /users/me`), not cents. `compareAtPrice` is the crossed-out
  "was" price — display only, it never changes what the buyer pays.
* `refundPolicy` is the **id** of one of your refund policies (from
  `POST /refunds`), not the policy text.
* Pay-what-you-want products need a product page: a request that sets
  `payWant: true` must also send `hasProductPage: "yes"`, otherwise it fails
  with `400`.
* Subscription (recurring) offers are a separate resource: fetch them with
  `GET /subscriptions/{id}` and delete them with `DELETE /subscriptions/{id}`.
  Updating a subscription offer is not available with an API key yet.

`POST /products/copy/{id}` duplicates one of your catalog items. Send
`{ "testMode": false }` to copy a test-mode offer into live mode ("Copy to
live"); omit the body to copy within the same mode. The copy gets a
deduplicated `Name (Copy)` name and starts **unpublished** when copied into
live mode, so nothing can be charged before you review it. A copied one-time
product fires the `product.created` webhook against the copy's own mode.

### Checkout

| Method | Path | Description |
| - | - | - |
| `POST` | `/checkout/sessions` | Create a hosted checkout session and redirect the buyer to pay. |
| `GET` | `/checkout/abandoned/summary` | Live checkouts lost over the last 30 days: `{ count, amountUsd, windowDays }`. Every plan. |

```bash theme={"system"}
curl https://api.pocketsflow.com/checkout/sessions \
  -H "Authorization: Bearer pk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "665f1c2a9b3e4a0012ab34cd",
    "successUrl": "https://acme.com/thanks",
    "cancelUrl": "https://acme.com/pricing",
    "customerEmail": "buyer@example.com",
    "discountCode": "LAUNCH20"
  }'
```

```json theme={"system"}
{
  "id": "cs_3f2a9c0d1e2b4a5f6789abcdef0123456789abcdef012345",
  "url": "https://acme.pocketsflow.com/checkout?productId=665f1c2a9b3e4a0012ab34cd&checkout_session_id=cs_3f2a9c0d1e2b4a5f6789abcdef0123456789abcdef012345&customerEmail=buyer%40example.com&discount=LAUNCH20&successUrl=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%2Facme.com%2Fthanks&cancelUrl=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%2Facme.com%2Fpricing"
}
```

Required: `productId` (a product **or** subscription offer id). Optional:
`successUrl` and `cancelUrl` (absolute `http(s)` URLs), `customerEmail`,
`lockEmail`, `clientReferenceId` (max 256 characters), `discountCode`, and
`metadata` (an object, echoed back on the resulting webhooks). Invalid values
return `400`; a `productId` that is not yours returns `404`. Your account needs
a store `subdomain` first — the checkout link lives on it.

### Orders

| Method | Path | Description |
| - | - | - |
| `GET` | `/orders` | List one-time orders, newest first. Filters: `startDate` + `endDate` (together; an unparseable date is `400`), `productId`, `page`, `pageSize` (max `100`). |
| `GET` | `/orders/{id}` | Get an order by id. |
| `GET` | `/orders/subscriptions/{id}` | Get a subscriber with its subscription offer, customer, live `status`, and full `payments` history. `{id}` is the SubscriptionCustomer `_id`. |

```json theme={"system"}
{
  "orders": [
    {
      "_id": "6a43ce05fae6c4f0cb5a2193",
      "buyerEmail": "buyer@example.com",
      "gross": 12.1,
      "net": 8.46,
      "taxes": 2.1,
      "createdAt": "2026-01-24T05:35:38.103Z",
      "product": { "_id": "665f1c2a9b3e4a0012ab34cd", "name": "Starter Kit" },
      "customer": { "_id": "665f1c2a9b3e4a0012abaa11", "buyerEmail": "buyer@example.com" }
    }
  ],
  "pagination": { "totalCount": 42, "totalPages": 3, "currentPage": 1, "pageSize": 20, "hasMore": true }
}
```

### Payments

The unified payment ledger — **one-time product purchases and subscription
charges (initial + every renewal)** in a single resource. Every payment is a
`Sale`.

| Method | Path | Description |
| - | - | - |
| `GET` | `/payments` | List payments. Filters: `type` (`one_time` \| `subscription` \| `all`), `startDate`, `endDate`, `productId`, `subscriptionId`, `page`, `pageSize` (max `100`). |
| `GET` | `/payments/{id}` | Get a payment by id. Pass `?live=true` to also fetch the authoritative live payment from the payment processor as `paymentsPartnerLive`. |

A single **Payment** object:

```json theme={"system"}
{
  "_id": "665f1c2a9b3e4a0012ab34cd",
  "isSubscription": true,
  "billingReason": "renewal",
  "amount": 12.1,
  "grossAmount": 12.1,
  "amountBeforeTax": 10,
  "taxAmount": 2.1,
  "currency": "usd",
  "buyerEmail": "buyer@example.com",
  "firstName": "Jane",
  "lastName": "Buyer",
  "isRefunded": false,
  "isDisputed": false,
  "productId": "",
  "subscriptionId": "665f1c2a9b3e4a0012ab7788",
  "partnerPaymentId": "pay_rehv02B3gdIZT0",
  "partnerPlanId": "plan_vlD50mPbsqJLp",
  "membershipId": "mem_UCTD32gDLGTgb",
  "createdAt": "2026-01-24T05:35:38.103Z"
}
```

<ResponseField name="isSubscription" type="boolean">
  `true` for subscription payments (initial + renewals); `false` for one-time
  product purchases.
</ResponseField>

<ResponseField name="billingReason" type="string">
  Present only on subscription payments: `initial` for the first charge that
  activates the membership, `renewal` for every recurring charge after it.
</ResponseField>

<ResponseField name="productId" type="string">
  Set on one-time purchases; empty on subscription payments (use
  `subscriptionId` instead).
</ResponseField>

<ResponseField name="partnerPaymentId / partnerPlanId / membershipId" type="string">
  The payments partner's receipt, plan, and membership ids backing the sale.
  `membershipId` is present on subscription payments only and matches the
  subscriber's `membershipId`.
</ResponseField>

<Note>
  Some responses also carry older duplicate keys for these ids and for the
  `paymentsPartner`, `paymentsPartnerLive` and `membership` blocks below, with
  the same values. They are deprecated and will only be removed after notice —
  read the names documented here.
</Note>

`GET /payments` returns `{ payments: [Payment], pagination }`. `GET
/payments/{id}?live=true` returns a **PaymentDetail**:

```json theme={"system"}
{
  "payment": {
    "_id": "665f1c2a9b3e4a0012ab34cd",
    "isSubscription": true,
    "billingReason": "renewal",
    "amount": 12.1,
    "amountBeforeTax": 10,
    "taxAmount": 2.1,
    "currency": "usd",
    "net": 8.46,
    "fee": 0.94,
    "metadata": { "id": "pay_rehv02B3gdIZT0", "status": "paid", "...": "raw stored processor payload" }
  },
  "product": null,
  "subscription": { "_id": "665f1c2a9b3e4a0012ab7788", "name": "Pro Membership", "price": 29, "frequency": "monthly" },
  "customer": { "_id": "665f1c2a9b3e4a0012abaa11", "buyerEmail": "buyer@example.com" },
  "paymentsPartner": { "id": "pay_rehv02B3gdIZT0", "total": 12.1, "subtotal": 10 },
  "paymentsPartnerLive": { "id": "pay_rehv02B3gdIZT0", "status": "paid", "amount_after_fees": 8.46 }
}
```

`net` = `amountBeforeTax − affiliateCut − processingFee`; `fee` is the processor fee.
`paymentsPartnerLive` is `null` if the live lookup failed — the stored `paymentsPartner` block still
applies, so `?live=true` never fails the request.

<Note>
  The total transaction cost on a sale is itemized — currently an estimated
  **4.7% + \$0.30** (about **\$5.00** on \$100), covering the Pocketsflow platform,
  payment infrastructure, tax handling when required, and dispute prevention.
  The **order detail is authoritative**: it uses the settled payment and returns
  the platform fee, processing fee, total transaction cost, effective fee rate,
  and net amount. See [Pricing & fees](/payments/fees).
</Note>

### Subscriptions and subscribers

Two distinct concepts: a **subscription offer** is the recurring product you
sell; a **subscriber** is a buyer's membership in one of those offers.

| Method | Path | Description |
| - | - | - |
| `POST` | `/subscriptions` | Create a subscription **offer**. Requires `name`, `price`, and `frequency` (`weekly`, `monthly`, or `yearly`). |
| `GET` | `/subscriptions` | List your subscription **offers**. |
| `GET` | `/subscriptions/{id}` | Get a subscription **offer** by id. |
| `DELETE` | `/subscriptions/{id}` | Delete a subscription **offer** (existing subscribers are not cancelled). |
| `GET` | `/subscriptions/subscribers` | List **subscribers**. Filters: `status` (one of the statuses below), `buyerEmail` (alias `email`), `subscriptionId` — a single value each, otherwise `400`; `page`, `pageSize`. |
| `GET` | `/subscriptions/subscribers/{id}` | Get a subscriber + `subscription`, `customer`, live `status`, and full `payments` history. `?live=true` adds the live membership from the payment processor as `membership`. |
| `POST` | `/subscriptions/{id}/cancel` | Cancel at the end of the current billing period (access continues until then). `{id}` is the subscriber `_id` or membership id (`mem_…`). |
| `POST` | `/subscriptions/{id}/pause` | Pause collection; the buyer keeps access. `{id}` is the subscriber `_id` or `mem_…`. |
| `POST` | `/subscriptions/{id}/resume` | Restart collection on a paused subscriber, or undo a pending cancel-at-period-end. `{id}` is the subscriber `_id` or `mem_…`. |

<Note>
  Each subscriber carries a `portalUrl` — the hosted self-service portal where
  the buyer manages, cancels, or resumes their subscription. The same link is
  delivered as `portalUrl` in the `customer.subscription.created` / `.updated`
  webhooks. Send it to that buyer **exactly as returned**: its `?token=…` is
  what allows cancel/resume — a portal link built by hand from the two ids
  opens read-only.
</Note>

<Note>
  Subscription offer creation and `/subscriptions/{id}` accept both an API key
  and an Auth0 JWT. In SDK v1.2.0 and later, Node.js and TypeScript users can
  call `pocketsflow.subscriptionOffers.create()`. See the [SDK
  guide](/api-reference/sdk) for installation and usage; use the REST request
  above if your installed SDK predates v1.2.0.
</Note>

A **Subscriber** object carries a processor-sourced live `status`
(`incomplete`, `incomplete_expired`, `trialing`, `active`, `past_due`,
`canceled`, `unpaid`, `succeeded`, `refunded`, `paused`) and the joined offer:

```json theme={"system"}
{
  "_id": "665f1c2a9b3e4a0012abccdd",
  "buyerEmail": "buyer@example.com",
  "status": "active",
  "active": true,
  "partnerPlanId": "plan_vlD50mPbsqJLp",
  "membershipId": "mem_UCTD32gDLGTgb",
  "cancelAtPeriodEnd": false,
  "portalUrl": "https://app.pocketsflow.com/portal/665f1c2a9b3e4a0012ab7788/665f1c2a9b3e4a0012abccdd?token=9f2c…",
  "subscriptionId": "665f1c2a9b3e4a0012ab7788",
  "firstName": "Jane",
  "lastName": "Buyer",
  "country": "NL",
  "createdAt": "2026-01-24T05:35:38.103Z",
  "subscription": { "_id": "665f1c2a9b3e4a0012ab7788", "name": "Pro Membership", "price": 29, "frequency": "monthly" }
}
```

`GET /subscriptions/subscribers/{id}?live=true`:

```json theme={"system"}
{
  "subscriber": { "_id": "665f1c2a9b3e4a0012abccdd", "status": "active", "buyerEmail": "buyer@example.com" },
  "subscription": { "_id": "665f1c2a9b3e4a0012ab7788", "name": "Pro Membership", "price": 29, "frequency": "monthly" },
  "customer": { "_id": "665f1c2a9b3e4a0012abaa11", "buyerEmail": "buyer@example.com" },
  "status": "active",
  "payments": [
    { "_id": "665f...01", "billingReason": "initial", "amount": 29, "createdAt": "2026-05-24T10:00:00.000Z" },
    { "_id": "665f...02", "billingReason": "renewal", "amount": 29, "createdAt": "2026-06-24T10:00:00.000Z" }
  ],
  "membership": { "id": "mem_UCTD32gDLGTgb", "status": "active", "valid": true }
}
```

### Discounts

| Method | Path | Description |
| - | - | - |
| `GET` | `/discounts` | List discounts. |
| `POST` | `/discounts` | Create a discount. Requires `name`, `code`, `value`, `mainProductIds`. |
| `GET` | `/discounts/{id}` | Get a discount by id. |
| `POST` | `/discounts/{id}` | Update a discount — only the fields you send change. |
| `DELETE` | `/discounts/{id}` | Delete a discount. |
| `GET` | `/discounts/apply/{discountCode}/{productId}` | Public (no API key) — validate a code against a product; returns the discount or `404`. |
| `GET` | `/discounts/check-product/{productId}` | Public (no API key) — returns `true`/`false` whether any discount targets the product. |

`valueType` is `percentage` (default, `value` at most 100) or `fixed` (a
decimal amount off in your account currency); `value` must be greater than 0.
Optional `active` and `expiration` (an ISO 8601 date after which checkout
rejects the code; `null` removes it). `mainProductIds` must all be your own
products or subscription offers in the key's mode, otherwise `400`.

### Upsells

| Method | Path | Description |
| - | - | - |
| `GET` | `/upsells` | List upsells. |
| `POST` | `/upsells` | Create an upsell. Requires `mainProductIds`, `upsellProductId`, `upsellPrice`. Needs Growth. |
| `GET` | `/upsells/{id}` | Get an upsell by id. |
| `POST` | `/upsells/{id}` | Update an upsell (full update — send every field you want to keep). |
| `DELETE` | `/upsells/{id}` | Delete an upsell. |

Optional: `name`, `offer`, `compareAtPrice`, `upsellDescription`,
`primaryButtonText`, `secondaryButtonText`, `active`, and the colour/font
overrides. `upsellProductId` must be one of your products and
`mainProductIds` your own products in the key's mode. On update,
`upsellProductId`, `upsellPrice`, and `mainProductIds` change only when sent;
the other fields are written as sent. `GET /upsells` returns
`{ upsells, salesWithUpsells }`.

### Refunds

| Method | Path | Description |
| - | - | - |
| `POST` | `/refunds` | Create a refund policy: `name`, `policy` (text shown to buyers), `period`, `periodType` (`days`/`weeks`). |
| `GET` | `/refunds` | List refund policies, each with `appliedTo` (number of products using it). |
| `GET` | `/refunds/{id}` | Public (no API key) — get a refund policy by id. |
| `POST` | `/refunds/{id}` | Update a refund policy (only the fields you send; `404` if it isn't yours). |
| `DELETE` | `/refunds/{id}` | Delete a refund policy. |

These endpoints manage refund **policies** — the refund terms shown to buyers at
checkout, which products reference by id (`refundPolicy`). They do not refund
payments: refunding a payment is done from the dashboard and has no API-key
endpoint. Refunds that happen arrive as the `order.refunded` webhook and as
`isRefunded: true` on the payment.

### Reviews

| Method | Path | Description |
| - | - | - |
| `GET` | `/reviews` | List all your product reviews, newest first (no filters). |

### Webhooks

| Method | Path | Description |
| - | - | - |
| `GET` | `/webhooks` | List webhook endpoints (secrets omitted). |
| `POST` | `/webhooks` | Create a webhook. Requires `url` (HTTPS) and a non-empty `events[]` of known event types; `description` max 500 characters. Returns the signing `secret`. |
| `GET` | `/webhooks/{id}` | Get a webhook by id (includes its signing `secret`). |
| `PATCH` | `/webhooks/{id}` | Update `url`, `events`, `description`, or `active` (response omits the secret). |
| `DELETE` | `/webhooks/{id}` | Delete a webhook (`404` if it isn't yours). |
| `POST` | `/webhooks/{id}/rotate-secret` | Replace the signing secret. Returns `{ id, secret }`. |
| `POST` | `/webhooks/{id}/test` | Send a sample event (`test: true`) to this endpoint only and return the outcome (`delivered`, `status`, `durationMs`, `eventId`, `deliveryId`). |
| `GET` | `/delivered-webhooks` | The delivery log, newest first. Filters: `webhookId`, `event`, `page`, `pageSize` (max `100`). Entries are kept 30 days. |
| `GET` | `/delivered-webhooks/{id}` | One delivery, including the exact body sent (`webhookData`). |
| `POST` | `/delivered-webhooks/{id}/redeliver` | Send a logged delivery to its endpoint again. |

Deliveries are signed (`X-Pocketsflow-Signature-V2: t=<unix>,v1=<hex HMAC-SHA256>`
plus the legacy `X-Pocketsflow-Signature`), carry the event id in
`X-Pocketsflow-Event-Id` and the body's `id`, and are retried up to 4 attempts
on `5xx`, `429`, timeouts, and connections that never reached your endpoint. See [Webhook events](/api-webhooks/events)
for the full event list, payloads, and headers, and
[Authentication & security](/api-webhooks/authentication-and-security) for
signature verification.

### Newsletters

| Method | Path | Description |
| - | - | - |
| `GET` | `/newsletters/posts` | List posts, newest first. Optional filter: `status`. |
| `POST` | `/newsletters/posts` | Create a post. Requires `name`. |
| `GET` | `/newsletters/posts/{id}` | Get a post by id. |
| `POST` | `/newsletters/posts/{id}` | Update a post — only the fields you send (sent posts can no longer be edited). |
| `DELETE` | `/newsletters/posts/{id}` | Delete a post (unschedules it first). |
| `POST` | `/newsletters/posts/{id}/send` | Queue the post for immediate delivery to its audience. |
| `POST` | `/newsletters/send` | Send a one-off email to up to 100 recipients. See [Send email](/api-reference/send-email). |
| `GET` | `/newsletters/audiences` | List product audiences (buyers and contacts per product). |
| `POST` | `/newsletters/audiences/sync` | Add a product's buyers (or all buyers) to your contacts. |
| `GET` | `/newsletters/subscribers` | List newsletter subscribers (filter: `productId`). |
| `GET` | `/newsletters/subscribers/{id}` | Get a subscriber by id. |
| `DELETE` | `/newsletters/subscribers/{id}` | Remove a subscriber. |

A **post** has a `name` (internal, required), `subject`, `preview`
(inbox preview text), `contentHtml` (the email body; `contentJson` is the
optional editor document), and an audience: `sendToAll`, `subscriberIds`,
`productIds` (everyone who bought those products), and/or `tags`. The sender
(`fromName`, `replyToEmail`) defaults to your newsletter settings, which must be
configured first. `status` is `draft` (default), `scheduled`, `sending`,
`sent`, `failed`, or `automation`; you can set `draft`, `scheduled` (with
`scheduledAt` and optional `scheduledTimezone`), or `automation`. Any status
other than `draft`/`automation` needs `subject`, `preview`, `fromName`, and
`contentHtml`.

```bash theme={"system"}
curl https://api.pocketsflow.com/newsletters/posts \
  -H "Authorization: Bearer pk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "June product update",
    "subject": "New in June",
    "preview": "Three new templates",
    "contentHtml": "<p>Hi there — here is what is new.</p>",
    "sendToAll": true
  }'
```

Then send it with `POST /newsletters/posts/{id}/send`, which answers
`{ "message": "Post queued for sending", "post": { … } }`. Sending requires a
verified sender email (`400` otherwise); exceeding your monthly send allowance
returns `402` (`code: "SEND_ALLOWANCE_EXCEEDED"`), and a restricted account, a
paused sender (`EMAIL_SENDING_PAUSED`) or an over-limit subscriber count
returns `403`. New accounts can send up to 1,000 emails a day (`429`,
`code: "DAILY_SEND_LIMIT"`); see
[Deliverability protection](/api-reference/send-email#deliverability-protection).

### Link in Bio (creator pages)

| Method | Path | Description |
| - | - | - |
| `GET` | `/creator-pages` | Get your Link in Bio page (`404` if you have none). |
| `POST` | `/creator-pages` | **Create** your page. Requires `username`. Returns `400` if you already have one. |
| `PUT` | `/creator-pages` | **Update** your page — only the fields you send change. |
| `DELETE` | `/creator-pages` | Delete your page. |
| `PATCH` | `/creator-pages/username` | Change only the username (`{ "username": "acme" }`). |
| `GET` | `/creator-pages/check-username/{username}` | Check whether a username is available. |

Page fields: `creatorName`, `creatorBio`, `username` (3–30 letters, numbers,
`-` or `_`; it is also your store subdomain), `socialLinks` and `regularLinks`
(arrays of `{ name, url, customName? }`, at most 20 each — the whole array is
replaced on update), `profilePicture`, `coverImage`, `images`, `template`,
`themeV2`, `gaMeasurementId`, `selectedProductIds` / `selectedSubscriptionIds`
(which of your products the page shows), `subscriberForm`, `bookingEnabled`,
`showFirstName`, and `showLastName`. There are no per-link endpoints: to add,
edit, or remove a link, send the full `regularLinks` array with
`PUT /creator-pages`.

```bash theme={"system"}
curl -X PUT https://api.pocketsflow.com/creator-pages \
  -H "Authorization: Bearer pk_live_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "creatorBio": "Templates for busy founders.",
    "regularLinks": [
      { "name": "Free guide", "url": "https://acme.com/guide" },
      { "name": "Course", "url": "https://acme.com/course" }
    ]
  }'
```

### Images

| Method | Path | Description |
| - | - | - |
| `POST` | `/images` | Upload an image (`multipart/form-data`, field `image`). Returns `{ imageUrl }`. A missing file or unexpected field name returns `400`; anything but PNG, JPEG, GIF, WebP, or AVIF (detected from the file's bytes) returns `415`. |

### Customers

| Method | Path | Description |
| - | - | - |
| `GET` | `/customers` | List customers (full list, deduplicated by email, with `numberOfOrders`). Filter by `productId`; sort with `sortBy` (`email` \| `country` \| `numberOfOrders` \| `createdAt`) and `sortOrder` (`asc` \| `desc`). |
| `GET` | `/customers/{id}` | Get a customer by id. |

### Account

| Method | Path | Description |
| - | - | - |
| `GET` | `/users/me` | Get the authenticated account. |
| `POST` | `/users/update` | Set or remove ad tracking pixels (`facebookPixel`, `googlePixel`, `tiktokPixel`, …). Adding one needs Starter. |

```json theme={"system"}
{
  "_id": "665f1c2a9b3e4a0012ab0001",
  "userId": "auth0|68fd3a499605389e7305cdc1",
  "email": "creator@example.com",
  "firstName": "Jane",
  "lastName": "Creator",
  "creatorName": "Acme Studio",
  "subdomain": "acme",
  "currency": "USD",
  "country": "US",
  "testMode": false,
  "canSell": true,
  "hasSales": true,
  "createdFirstProduct": true,
  "statementDescriptor": "ACME STUDIO",
  "createdAt": "2026-01-24T05:35:38.103Z"
}
```

Use `subdomain` to build product/checkout URLs
(`https://{subdomain}.pocketsflow.com/...`). `testMode` reflects the key you
called with (`true` for a `pk_test_…` key). `canSell` is `false` while selling
is paused on the account. Other fields may appear in the response; they are
dashboard-internal and can change without notice. A suspended account answers
`403` (`code: "ACCOUNT_BLOCKED"`) and a deleted one `410`
(`code: "ACCOUNT_DELETED"`).

### Plans

| Method | Path | Description |
| - | - | - |
| `GET` | `/users/entitlements` | Your active plan, billing interval, platform fee rate, seats, and `features.<key>.allowed` for every feature key. |
| `GET` | `/users/billing-tiers` | **Public.** Every plan with monthly and yearly price, fee rate, caps, seats and feature keys. |

```json theme={"system"}
{
  "plan": "volume",
  "planName": "Volume",
  "billingInterval": "year",
  "feeRate": 0.01,
  "seats": null,
  "enforced": true,
  "features": {
    "upsells": { "allowed": true, "requiredPlan": "tier2", "requiredPlanName": "Growth", "reason": "plan", "grandfatheredUntil": null }
  }
}
```

Plans are bought in the dashboard only. See
[Plans and limits](/concepts/plans-and-limits).

### Partners

| Method | Path | Description |
| - | - | - |
| `POST` | `/partners` | Become a partner. Returns your `referralCode`. |
| `GET` | `/partners` | Get your partner profile. |
| `POST` | `/partners/register-referral` | Record a referral signup (`referralCode`). Only accounts under 30 days old, never a self-referral. |
| `GET` | `/partners/referrals` | List referred users with live sales count and USD revenue (`page`, `limit` ≤ 100). |
| `GET` | `/partners/referrals/sales` | List referred users' live sales — seller, amounts, `revenueUsd`, `commission`; no buyer data (`page`, `limit` ≤ 100). |
| `GET` | `/partners/stats` | Live-computed signups, sales, revenue, and commission. |

### Weekly report

| Method | Path | Description |
| - | - | - |
| `GET` | `/users/weekly-report/preview` | Last week's growth report: revenue, sales, visits, conversion, best seller, top source and one next step, plus the rendered email. See [Weekly growth report](/operations/weekly-report). |
| `GET` | `/users/weekly-report/settings` | Whether the Monday email is on, its timezone and the next send. |
| `PUT` | `/users/weekly-report/settings` | Turn the Monday email on or off (`{ "enabled": false }`). |

## Connect an AI agent

Prefer to drive Pocketsflow from an AI assistant? The
[MCP server](/api-reference/mcp-server) exposes this entire API as Model
Context Protocol tools — add `https://api.pocketsflow.com/mcp` to Claude,
ChatGPT, Cursor, or your own agent, sign in, and click **Allow access**, and it
can manage your account in natural language. Connecting over MCP needs no API
key; the REST API and SDK keep using the API keys described above.

## Building an integration?

If you're connecting Pocketsflow to an external store or platform (for example
WooCommerce), start with the [Integrations](/integrations/overview) section — it
walks through the end-to-end pattern with code examples.

## Related topics

* [MCP server](/api-reference/mcp-server)
* [Plans and limits](/concepts/plans-and-limits)
* [Send email](/api-reference/send-email)
* [Webhooks & API overview](/api-webhooks/overview)
* [Webhook events](/api-webhooks/events)
* [Authentication & security](/api-webhooks/authentication-and-security)
* [Integrations overview](/integrations/overview)


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