# Postforge: the complete developer docs > Postforge is the open-source ecommerce email platform: it connects to your store (Shopify first), brings in customers, orders, products and on-site events, and runs campaigns and automations on that data, sent through your own Resend or Amazon SES account. Every feature is in a typed API with an MCP server, so AI agents can set it up and operate it; people approve every change. Every page of the developer docs (https://postforge.dev/developers) as Markdown: the guides, then the API reference, one operation per section. The index of everything else is https://postforge.dev/llms.txt; the full schemas are in the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. --- # API reference Every operation of the Postforge REST API, its parameters and its answers, generated from the OpenAPI spec. The reference is generated from the API's own contracts, so it always matches what the API does. Operations are grouped by resource in the sidebar; each page shows its parameters, request body, answers and code samples. - **Base URL:** `https://api.postforge.dev/v1` - **Spec:** [`openapi.json`](https://api.postforge.dev/v1/openapi.json) (OpenAPI 3.1): import it into your HTTP client or generate a typed client from it - **Authentication:** operations marked with the `apiKey` scheme accept a workspace API key with the scope they name; the rest need a person signed in to the dashboard ([Authentication](/developers/authentication)) - **Errors:** every operation answers [problem details](/developers/problems) on failure Source: https://postforge.dev/developers/api --- # Authentication Postforge workspace API keys, their scopes, and how to rotate and revoke them. Postforge's API accepts workspace API keys as bearer tokens: ```http Authorization: Bearer pf_… ``` ## Workspace keys - A key belongs to **one workspace**; every request with it works in that workspace only. - Keys are stored as a SHA-256 hash and shown once, when created or rotated. - Everyone in the workspace sees the list under **Developers → API keys**; owners and admins create, rotate and revoke them. The list shows when each key was last used. ## Scopes Each key has one scope, and each scope includes the ones before it: | Scope | Can | | --- | --- | | `read` | Read data: profiles, orders, products, coupons, definitions, … | | `propose` | Everything `read` can, plus create [change proposals](/developers/definitions) that a person approves | | `write` | Everything `propose` can, plus create, change and delete | Operations that manage the team, API keys or workspace ownership never accept keys: they need a person signed in to the dashboard. The [API reference](/developers/api) marks which operations accept keys; a key with too small a scope gets a `403` problem with code `scope`. ## Rotating and revoking - **Rotate** gives a key a new secret with the same name and scope; the old secret stops working at once. - **Revoke** stops a key for good; it stays listed so you can see what it was. Rotate a key whenever it may have leaked, and give every integration its own key so you can revoke one without breaking the others. ## AI clients AI clients will connect to the [MCP server](/developers/mcp) with OAuth 2.1, acting with the signed-in person's role in the workspace they choose. Until it is live, agents use a workspace key; a `propose` key keeps every change behind a person's approval. Source: https://postforge.dev/developers/authentication --- # Definitions and schemas Postforge's typed definitions (automations, segments, templates, campaigns), their JSON Schemas, versions and change proposals. Automations, segments, templates, campaigns and the brand kit are **definitions**: typed JSON documents with immutable versions. The same schema validates the API, drives the dashboard's editor and is published for agents. ## Schemas - [`GET /v1/schemas`](https://api.postforge.dev/v1/schemas) lists every kind with its resource, label and current `schemaVersion`. - `GET /v1/schemas/{kind}` answers the JSON Schema of one kind. Validate a document against its schema before you send it; the API refuses an invalid one with a [validation problem](/developers/problems/validation) listing each invalid path. ## Versions Every save creates a new, immutable version with its author. A definition has a draft and, once published, a published version; you can read any earlier version and restore it. ## Change proposals A `propose` key, the in-app assistant and AI clients never change a definition directly: they create a **change proposal** with the proposed document, a summary and a diff from the current draft. A person approves or rejects it in the dashboard. That keeps every change an AI drafts behind a human decision. Source: https://postforge.dev/developers/definitions --- # Idempotency How Postforge API writes use Idempotency-Key so a retry never applies a change twice. Networks fail: a request can time out after Postforge already did the work. Send an `Idempotency-Key` header with every write, and retry with the **same key** until you get an answer. ```http PUT /v1/profiles/0192f3a4-5b6c-7d8e-9f01-23456789abcd/consent/email Idempotency-Key: 7f9c2b8e-3d41-4f0a-9a6e-2c1d5e8b4a10 ``` ## How it works - A key is 1 to 255 characters; a UUID per logical change is the simplest choice. - The first request runs. Its answer, success or refusal, is kept for **24 hours**. - A retry with the same key and the same request gets that answer back, with `Idempotent-Replayed: true`, without running again. - While the first request is still running, a retry gets `409` with code `idempotency_key_in_use`: wait a moment and retry. A request that died stops blocking its key after 5 minutes. - The same key with a different request (another operation or body) gets `422` with code `idempotency_key_reused`. Use a new key for a new change. - If the request failed unexpectedly (a `5xx`), the key is freed, so a retry runs again. Keys are scoped to the workspace: every API key and person in it shares one set of keys, so use a fresh UUID per change and two integrations can never collide. ## Where it doesn't apply `GET` requests are safe to repeat and ignore the header. A few writes whose answer holds a secret shown once (creating or rotating an API key) refuse it with code `idempotency_key_not_supported`, because that answer can't be stored for a replay. Records that come from outside (orders and customers from a store, webhooks) are also written once by their own natural keys, so a platform resending them never creates duplicates. Source: https://postforge.dev/developers/idempotency --- # Postforge developers Build on Postforge, the open-source ecommerce email platform, with its REST API. Everything the Postforge dashboard does goes through the same REST API you can call: workspaces, stores, profiles, orders, products, coupons, feeds and the typed definitions behind automations, segments, templates and campaigns. People, API keys and AI clients get the same operations with the same permissions. ## At a glance | | | | --- | --- | | Base URL | `https://api.postforge.dev/v1` | | Format | JSON in and out, camelCase | | Authentication | `Authorization: Bearer pf_…` (a workspace API key) | | Errors | [RFC 9457 problem details](/developers/problems) | | Retries | [`Idempotency-Key`](/developers/idempotency) on every write | | Limits | [600 requests a minute per key](/developers/rate-limits) | | Spec | [`openapi.json`](https://api.postforge.dev/v1/openapi.json) (OpenAPI 3.1) | | Schemas | [`/v1/schemas`](https://api.postforge.dev/v1/schemas) (JSON Schema for definitions) | ## For AI agents Postforge is built for AI to set up and operate. Agents read [llms.txt](https://postforge.dev/llms.txt) (the index) or [llms-full.txt](https://postforge.dev/llms-full.txt) (every docs page in one file), add `.md` to any docs page for its Markdown, call the API with a workspace key, and write definitions against their [JSON Schemas](/developers/definitions); changes made with a `propose` key become proposals a person approves. An [MCP server](/developers/mcp) is coming before launch. Until version 1 the API can change without notice. From launch, `/v1` follows the [versioning policy](/developers/versioning). Source: https://postforge.dev/developers --- # MCP server Postforge's Model Context Protocol server for Claude, Codex, Cursor and other AI clients, coming before launch. The MCP server is not live yet. Until it is, AI agents use the REST API with a workspace API key. Postforge's MCP server will run at `https://api.postforge.dev/mcp` over Streamable HTTP. Claude, Claude Code, Codex, Cursor and any other MCP client will connect with OAuth 2.1: you sign in, pick a workspace, and the client acts with your role in it, revocable at any time. Its tools come from the same operations as the [REST API](/developers/api), with the same permissions, plus a few made for AI: reading a definition's schema, proposing a change, rendering a template, dry-running an automation and explaining a segment or an order's attribution. Changes to definitions from AI clients always become [proposals](/developers/definitions) a person approves. Source: https://postforge.dev/developers/mcp --- # business-rule 422 This isn’t allowed: what the business-rule problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/business-rule` | 422 | This isn’t allowed | The request is well formed, but a rule of Postforge refuses it: for example removing the last way to sign in, or reusing an idempotency key for a different request. ## Codes Common `code` values (new ones can appear): - `idempotency_key_reused`: this `Idempotency-Key` was used for a different request - `feed_unreadable`: the feed’s URL can’t be read as a feed ## What to do Read `detail`: it says which rule applies and what to do instead. Source: https://postforge.dev/developers/problems/business-rule --- # conflict 409 This conflicts with the current state: what the conflict problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/conflict` | 409 | This conflicts with the current state | The request is valid but clashes with the resource’s current state: a stale version, a duplicate, or another request with the same idempotency key still running. ## Codes Common `code` values (new ones can appear): - `idempotency_key_in_use`: the first request with this key is still running - `store_not_connected`: the store isn’t connected to Postforge ## What to do Read the resource again and decide; for `idempotency_key_in_use`, wait a moment and retry with the same key. Source: https://postforge.dev/developers/problems/conflict --- # forbidden 403 You can’t do this: what the forbidden problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/forbidden` | 403 | You can’t do this | The caller is known but isn’t allowed to run this operation here: the key’s scope is too small, the person’s role is too low, or the workspace requires two-factor sign-in. ## Codes Common `code` values (new ones can appear): - `scope`: the operation needs a key with a larger scope, or doesn’t accept keys at all - `role`: only owners, or owners and admins, can do this - `two_factor_required`: the workspace requires two-factor sign-in ## What to do Use a key with the right scope, or ask a workspace owner or admin. Retrying doesn’t help. Source: https://postforge.dev/developers/problems/forbidden --- # gone 410 No longer available: what the gone problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/gone` | 410 | No longer available | The resource existed but can’t be used any more: for example an invitation that was withdrawn or has expired. ## What to do Don’t retry. Ask for a new link or invitation. Source: https://postforge.dev/developers/problems/gone --- # Problems The errors the Postforge API answers, as RFC 9457 problem details, and what to do about each. Every error the API answers is a problem details object ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)) with `Content-Type: application/problem+json`: ```json { "type": "https://postforge.dev/developers/problems/forbidden", "title": "You can’t do this", "status": 403, "detail": "This needs a key with the write scope.", "code": "scope" } ``` | Field | Meaning | | --- | --- | | `type` | The kind of problem: a link to its page below | | `title` | The kind in words; the same for every problem of a kind | | `status` | The HTTP status | | `detail` | What went wrong and what to do, written for people: safe to show in your UI | | `code` | A finer reason within the kind, for your code to branch on | | `errors` | Validation only: one `{ path, message }` per invalid field | Branch on `type` and `code`, show `detail`. New codes can appear within a kind at any time, so treat an unknown `code` like its kind. | Kind | Status | Title | | --- | --- | --- | | [`validation`](/developers/problems/validation) | 400 | The request isn’t valid | | [`unauthenticated`](/developers/problems/unauthenticated) | 401 | Sign in to continue | | [`forbidden`](/developers/problems/forbidden) | 403 | You can’t do this | | [`not-found`](/developers/problems/not-found) | 404 | Not found | | [`conflict`](/developers/problems/conflict) | 409 | This conflicts with the current state | | [`gone`](/developers/problems/gone) | 410 | No longer available | | [`business-rule`](/developers/problems/business-rule) | 422 | This isn’t allowed | | [`rate-limited`](/developers/problems/rate-limited) | 429 | Too many requests | | [`internal`](/developers/problems/internal) | 500 | Something went wrong | Source: https://postforge.dev/developers/problems --- # internal 500 Something went wrong: what the internal problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/internal` | 500 | Something went wrong | Postforge failed unexpectedly. The failure is logged and looked at; it isn’t caused by your request. ## What to do Retry with backoff. Writes sent with an `Idempotency-Key` are safe to retry: a failed request frees its key. Source: https://postforge.dev/developers/problems/internal --- # not-found 404 Not found: what the not-found problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/not-found` | 404 | Not found | The resource doesn’t exist, or it belongs to another workspace. Postforge answers both the same way, so ids from other workspaces reveal nothing. ## What to do Check the id and that the key belongs to the workspace you expect. Source: https://postforge.dev/developers/problems/not-found --- # rate-limited 429 Too many requests: what the rate-limited problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/rate-limited` | 429 | Too many requests | The caller used up one of its [rate limit](/developers/rate-limits) quotas. ## Extra fields - `Retry-After`: Seconds to wait before trying again (a header). - `RateLimit`: The quota that ran out, with `r=0` (a header). ## Codes Common `code` values (new ones can appear): - `standard_rate_limit`: 600 requests a minute - `expensive_rate_limit`: 60 expensive operations a minute ## What to do Wait at least `Retry-After` seconds, with some random jitter, then retry. Spread bulk work out. Source: https://postforge.dev/developers/problems/rate-limited --- # unauthenticated 401 Sign in to continue: what the unauthenticated problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/unauthenticated` | 401 | Sign in to continue | The request has no valid credentials: no `Authorization` header, an unknown or revoked API key, or an expired session. ## Codes Common `code` values (new ones can appear): - `invalid_api_key`: the key doesn’t exist or was revoked or rotated - `invalid_session`: the Shopify app’s session token isn’t valid ## What to do Check that the key is sent as `Authorization: Bearer pf_…` and still listed under Developers → API keys. Don’t retry without new credentials. Source: https://postforge.dev/developers/problems/unauthenticated --- # validation 400 The request isn’t valid: what the validation problem from the Postforge API means and what to do. | Type | Status | Title | | --- | --- | --- | | `https://postforge.dev/developers/problems/validation` | 400 | The request isn’t valid | The request can’t be run as sent: a missing or invalid parameter, an unknown filter value, a body that doesn’t match the operation’s schema, or a missing header. ## Extra fields - `errors`: One entry per invalid field, with its `path` (`body.state`, `query.pageSize`) and a `message`. ## Codes Common `code` values (new ones can appear): - `no_workspace`: a dashboard session request without `X-Postforge-Workspace` - `idempotency_key_invalid`: an `Idempotency-Key` outside 1 to 255 characters - `idempotency_key_not_supported`: the operation can’t replay its answer, so it refuses the header ## What to do Fix the request using `errors` and send it again. Retrying unchanged gets the same answer. Source: https://postforge.dev/developers/problems/validation --- # Quickstart Create a workspace API key and make your first Postforge API request. ## 1. Create an API key In the Postforge dashboard, open your workspace and go to **Developers → API keys**. Owners and admins create keys; pick the scope your integration needs (`read` is enough to start). The key is shown once: copy it and store it as a secret. ## 2. Call the API Every request carries the key as a bearer token. The key belongs to one workspace, so you never name the workspace yourself. ```bash tab="cURL" curl https://api.postforge.dev/v1/orders?pageSize=10 \ -H "Authorization: Bearer $POSTFORGE_API_KEY" ``` ```ts tab="TypeScript" const response = await fetch('https://api.postforge.dev/v1/orders?pageSize=10', { headers: { Authorization: `Bearer ${process.env.POSTFORGE_API_KEY}` }, }) const orders = await response.json() ``` Lists answer one page at a time: ```json { "items": [ { "id": "0192f3a4-5b6c-7d8e-9f01-23456789abcd", "number": "#1001", "placedAt": "2026-10-14T13:05:22.000Z", "financialStatus": "paid", "total": { "amount": 4950, "currency": "EUR" }, "locale": "nl-NL" } ], "total": 128, "page": 1, "pageSize": 10 } ``` ## 3. Write safely Writes need a `write` key. Send an `Idempotency-Key` so a retry after a timeout can never apply the same change twice. This unsubscribes a profile's email address, with your own form as proof: ```bash tab="cURL" curl -X PUT https://api.postforge.dev/v1/profiles/$PROFILE_ID/consent/email \ -H "Authorization: Bearer $POSTFORGE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"state": "unsubscribed", "proof": {"reference": "acme.com preference form"}}' ``` ```ts tab="TypeScript" const response = await fetch( `https://api.postforge.dev/v1/profiles/${profileId}/consent/email`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.POSTFORGE_API_KEY}`, 'Content-Type': 'application/json', // The same key on a retry: the change happens once. 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ state: 'unsubscribed', proof: { reference: 'acme.com preference form' } }), }, ) ``` ## 4. Handle problems Errors are [problem details](/developers/problems): show `detail` to people, branch on `type` and `code` in code. ```ts type Problem = { type: string; title: string; status: number; detail?: string; code?: string } if (!response.ok) { const problem: Problem = await response.json() if (problem.code === 'scope') { // This key can’t do it: use a key with a larger scope. } throw new Error(problem.detail ?? problem.title) } ``` ```json { "type": "https://postforge.dev/developers/problems/forbidden", "title": "You can’t do this", "status": 403, "detail": "This needs a key with the write scope.", "code": "scope" } ``` ## Next - [Authentication](/developers/authentication): scopes and key rotation - [Requests](/developers/requests): pagination, filters, money and dates - [API reference](/developers/api): every operation Source: https://postforge.dev/developers/quickstart --- # Rate limits The Postforge API's rate limits per caller, the RateLimit-Policy and Retry-After headers, and how to back off. Rate limits keep one runaway client from slowing Postforge down for everyone. Each caller has its own quota, so one integration hitting its limit never affects another. | Quota | Limit | Counts | | --- | --- | --- | | `standard` | 600 requests a minute | Every request | | `expensive` | 60 requests a minute | Operations that start heavy work: imports, reading a feed, applying a bulk change | A caller is an API key, a person signed in to the dashboard, or the Shopify app's store. Limits never count by IP address. Operations anyone may call without signing in aren't counted. ## Headers Every counted answer names its quotas in `RateLimit-Policy` ([draft-ietf-httpapi-ratelimit-headers-11](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)): ```http RateLimit-Policy: "standard";q=600;w=60 ``` An expensive operation lists both: `"standard";q=600;w=60, "expensive";q=60;w=60`. ## When you hit a limit You get `429` with a [`rate-limited` problem](/developers/problems/rate-limited): ```http HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 60 RateLimit: "standard";r=0;t=60 {"type":"https://postforge.dev/developers/problems/rate-limited","title":"Too many requests","status":429,"code":"standard_rate_limit","detail":"Too many requests: wait 60 seconds before trying again."} ``` ```ts async function withRetry(request: () => Promise): Promise { for (;;) { const response = await request() if (response.status !== 429) return response const seconds = Number(response.headers.get('Retry-After') ?? 60) // Wait as long as asked, plus up to 5 seconds of jitter. await new Promise((resolve) => setTimeout(resolve, (seconds + Math.random() * 5) * 1000)) } } ``` Wait at least `Retry-After` seconds, add a little random jitter so many clients don't come back at the same moment, and spread bulk work out instead of sending it in bursts. Limits are counted per Cloudflare location and are approximate: treat them as a ceiling, not a budget to use up. Limits may change as Postforge grows; the `RateLimit-Policy` header always tells you the current ones. Source: https://postforge.dev/developers/rate-limits --- # Requests Postforge API conventions for URLs, JSON bodies, money, dates, ids, pagination, search, filters and sorting. ## URLs and resources The API lives at `https://api.postforge.dev/v1`. Resources are plural nouns (`/v1/orders`, `/v1/orders/{id}`); actions are verbs below a resource with `POST` (`/v1/product-priority-changes/{id}/apply`). An operation a resource doesn't support doesn't exist. ## Bodies - JSON in and out, property names in camelCase. - **Money** is an object in minor units with its ISO 4217 currency: `{ "amount": 4950, "currency": "EUR" }` is €49.50. - **Timestamps** are ISO 8601 in UTC: `2026-10-16T09:30:00.000Z`. - **Ids** are UUIDv7 strings. An id that doesn't exist, or belongs to another workspace, answers `404`. - **Locales** are full BCP 47 locales, a language and a country: `nl-NL`, `en-US`, never `en`. ## Lists Every list takes the same query parameters: | Parameter | Meaning | | --- | --- | | `page` | Page number, starting at 1 | | `pageSize` | 10, 25 (default), 50 or 100 | | `q` | Search text | | `sort` | A sort key, `-` in front for descending: `sort=-placedAt` | Filters are named per list and take comma-separated values: `payment=paid,refunded`. An unknown value is a [validation problem](/developers/problems/validation), never silently ignored. The answer is a page: ```json { "items": [], "total": 128, "page": 2, "pageSize": 25, "all": 412 } ``` `total` counts what matches the search and filters; top-level lists also give `all`, the count before them. A record's small sub-collections (a profile's merges) answer `{ "items": [] }`. Large exports and imports are jobs you start and then read, not pages. ## Writes Send `Content-Type: application/json` and an [`Idempotency-Key`](/developers/idempotency). Creates answer `201` with the record; changes answer `200`; deletes answer `204`. Source: https://postforge.dev/developers/requests --- # Versioning and deprecation How the Postforge API is versioned, what counts as a breaking change, and how much notice you get before one. The API's version is in its path: `/v1`. Within a version, Postforge only makes changes that don't break working integrations; anything that would break one is announced first. Postforge is in development. Until version 1 of the product, the API can change without notice. The promises below start at launch. ## What may change without notice - New operations, resources and webhooks - New optional request parameters and fields - New fields in answers, and new values in lists of codes (`code` in a problem, a status) - New problem `code`s within an existing `type` Write clients that ignore fields they don't know. ## Breaking changes Removing or renaming an operation, field or parameter, making an optional parameter required, or changing what a field means is a breaking change. After launch: | Change | Notice | | --- | --- | | Removing or breaking one operation within `/v1` | **6 months** | | A new major version (`/v2`) | `/v1` keeps working for **12 months** next to it | ## How deprecation is signalled A deprecated operation is marked `deprecated` in the [OpenAPI spec](https://api.postforge.dev/v1/openapi.json), and every answer from it carries: ```http Deprecation: @1798761600 Sunset: Thu, 01 Jul 2027 00:00:00 GMT Link: ; rel="deprecation", ; rel="sunset" ``` - `Deprecation` ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)): since when, as a Unix timestamp - `Sunset` ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)): when it stops working - `Link`: the notice explaining what to use instead Watch for these headers in your logs, and plan the move before the `Sunset` date. Source: https://postforge.dev/developers/versioning --- # Get the signed-in user The user and the workspaces they can open. ## GET /v1/me API keys: not accepted; a person signed in to the dashboard only. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/me/get --- # Forget trusted browsers Every browser asks for a two-factor code again at its next sign-in. ## POST /v1/me/two-factor/trusted-browsers/forget API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/me/forget-trusted-browsers --- # Turn off email codes Sign-ins then need the authenticator app or a backup code. Only while the app is on; otherwise turn two-factor off. ## POST /v1/me/two-factor/email/disable API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/me/disable-two-factor-email --- # List your sessions Where you’re signed in, most recently active first. ## GET /v1/sessions API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/sessions/list --- # Sign out a session That browser or device is signed out at once. ## DELETE /v1/sessions/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/sessions/delete --- # Sign out everywhere else Every session except the one making this request is signed out at once. ## POST /v1/sessions/sign-out-others API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/sessions/delete-others --- # Create a workspace The caller becomes its owner; its address (slug) is derived from the name. ## POST /v1/workspaces API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/workspaces/create --- # Update a workspace Owner only: its name, and whether members need two-factor on to open it (the owner needs it on first). The address (slug) never changes, so shared links keep working. ## PATCH /v1/workspaces/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/workspaces/update --- # Preview a workspace address The slug a new workspace with this name would get. ## GET /v1/workspaces/available-slug API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `name` | query | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/workspaces/available-slug --- # List members People with access to the workspace. ## GET /v1/members API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `role` | query | no | Comma-separated, any of: owner, admin, member | | `sort` | query | no | One of name, joinedAt; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/members/list --- # Transfer ownership Makes this member the owner; the current owner becomes an admin. ## POST /v1/members/{id}/transfer-ownership API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/members/transfer-ownership --- # Change a member’s role Admin or member; the owner’s role changes only through a transfer. ## PATCH /v1/members/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/members/update-role --- # Remove a member They lose access to the workspace. The owner can’t be removed. ## DELETE /v1/members/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/members/delete --- # Leave the workspace The caller stops being a member. The owner transfers ownership first. ## POST /v1/members/leave API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/members/leave --- # List invitations Open invitations to the workspace, including expired ones. ## GET /v1/invitations API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/list --- # Invite someone Emails a link to join the workspace; valid for 7 days. Each person sends at most 50 invitation emails a day (429 `invitation_send_limit`). ## POST /v1/invitations API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/create --- # Preview an invitation For the accept page, before signing in: the id in the emailed link is the capability. ## GET /v1/invitations/{id}/preview API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/preview --- # Resend an invitation Emails it again and makes it valid for another 7 days; not within an hour of the last send (429 `invitation_recently_sent`), and within the daily budget (429 `invitation_send_limit`). ## POST /v1/invitations/{id}/resend API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/resend --- # Cancel an invitation The link stops working. ## DELETE /v1/invitations/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/delete --- # Accept an invitation For the invited person, signed in with the invited (verified) email address. ## POST /v1/invitations/{id}/accept API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/accept --- # Decline an invitation For the invited person, signed in with the invited email address. ## POST /v1/invitations/{id}/decline API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/invitations/decline --- # List API keys The workspace’s keys, active first, newest first; never the keys themselves. ## GET /v1/api-keys API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/apikeys/list --- # Create an API key Answers the key once; only its hash is stored. ## POST /v1/api-keys API keys: not accepted; a person signed in to the dashboard only. Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/apikeys/create --- # Rotate an API key Replaces the key with a new one, same name and scope; the old key stops working at once. Answers the new key once. ## POST /v1/api-keys/{id}/rotate API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/apikeys/rotate --- # Revoke an API key The key stops working at once; it stays listed as revoked. ## POST /v1/api-keys/{id}/revoke API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/apikeys/revoke --- # List locales The default first, then by code. ## GET /v1/locales API keys: the `read` scope or more. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/locales/list --- # Add a locale The workspace’s first locale becomes its default. 409 `locale_exists` when it’s there already. ## POST /v1/locales API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/locales/create --- # Make a locale the default The workspace's main locale, used when no locale is given. ## POST /v1/locales/{id}/default API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/locales/set-default --- # Remove a locale Only locales people added, and never the default (422 `locale_default`); a store’s locales come back when it syncs (422 `locale_from_store`). ## DELETE /v1/locales/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/locales/delete --- # List stores The workspace’s connected stores, by name. ## GET /v1/stores API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/stores/list --- # Get a store The store with its import progress. ## GET /v1/stores/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/stores/get --- # List where a store sells The store’s locales (ARCHITECTURE.md §5.5): every country of its markets in each language its storefront offers there, the primary locale first, then by name. Search matches the code, language, country or currency. ## GET /v1/stores/{id}/locales API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/stores/list-locales --- # Disconnect a store Uninstalls Postforge from the store on its platform and stops syncing. The store and its history stay in the workspace, marked disconnected; it can be connected again. ## POST /v1/stores/{id}/disconnect API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/stores/disconnect --- # List a store’s imports Newest first. ## GET /v1/stores/{id}/imports API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/storeimports/list --- # Import a store’s history again Reads every customer and order, or every product, from the store again; live sync continues meanwhile. Records already in Postforge are updated, never duplicated, and only by data at least as new. Imports run one at a time per store: this one may wait in the queue. 409 `import_active` when this kind is queued or running already. ## POST /v1/stores/{id}/imports API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/storeimports/create --- # Ask to connect a store Records that this workspace wants the store and answers the platform’s install page. The store’s staff confirm it in the platform before it joins (ARCHITECTURE.md D12). ## POST /v1/connect-intents API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connectintents/create --- # Cancel a request to connect a store Withdraws the workspace’s request before the store’s staff confirm it: the store no longer shows it. ## DELETE /v1/connect-intents/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connectintents/delete --- # List connections Every store connected to the workspace, and those waiting for confirmation. ## GET /v1/connections API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `type` | query | no | Comma-separated, any of: shopify | | `state` | query | no | Comma-separated, any of: connected, importing, pending, needs_attention, disconnected | | `sort` | query | no | One of name, connectedAt; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connections/list --- # Preview a connect link The store’s name and platform, before signing in. ## GET /v1/connect-links/{token}/preview API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `token` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connectlinks/preview --- # Get a connect link The store and the workspaces the signed-in user can add it to. ## GET /v1/connect-links/{token} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `token` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connectlinks/get --- # Complete a connect link Adds the store to one of the user’s workspaces, or to a new one they own. ## POST /v1/connect-links/{token}/complete API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `token` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/connectlinks/complete --- # List profiles The workspace’s people, most recently seen first. ## GET /v1/profiles API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `store` | query | no | Comma-separated ids | | `customer` | query | no | Only people who did (true) or didn’t (false) place an order | | `sort` | query | no | One of lastSeenAt, firstSeenAt; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/list --- # Get a profile A merged profile answers with the profile it joined; its id differs from the one asked for. ## GET /v1/profiles/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/get --- # List a profile’s events The timeline, newest first. ## GET /v1/profiles/{id}/events API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/list-events --- # List a profile’s identities Every identifier the person is known by. ## GET /v1/profiles/{id}/identities API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `type` | query | no | Comma-separated, any of: email, phone, platform_customer_id, shopify_client_id, anonymous_id | | `sort` | query | no | One of lastSeenAt, firstSeenAt; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/list-identities --- # List the profiles merged into a profile ## GET /v1/profiles/{id}/merges API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/list-merges --- # List a profile’s customer records The person as each store knows them. ## GET /v1/profiles/{id}/customers API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/profiles/list-customers --- # List a profile’s consent changes Every change of the person’s marketing consent, newest first, with its source and proof. The latest change per channel is the person’s consent. ## GET /v1/profiles/{id}/consent API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `channel` | query | no | Comma-separated, any of: email, sms | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/consent/list --- # Subscribe or unsubscribe a profile’s email Records the change (source `manual` in the dashboard, `api` with an API key) on the person’s email address and writes it back to every store that knows them. Answers the latest email change; when the person already has this state nothing new is recorded. 409 `no_email` when the profile has no email address. Pass `proof` when the person gave or withdrew consent themselves (e.g. on your own form). ## PUT /v1/profiles/{id}/consent/email API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/consent/set-email --- # List orders Orders of the workspace’s stores, newest first. A Shopify app session sees its own store only. ## GET /v1/orders API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `store` | query | no | Comma-separated ids | | `payment` | query | no | Comma-separated, any of: pending, authorized, partially_paid, paid, partially_refunded, refunded, voided | | `fulfillment` | query | no | Comma-separated, any of: unfulfilled, partially_fulfilled, fulfilled, restocked | | `profile` | query | no | Only orders of this profile | | `product` | query | no | Only orders that include this product | | `placedFrom` | query | no | Placed from (inclusive) | | `placedTo` | query | no | Placed until (exclusive) | | `sort` | query | no | One of placedAt, total; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/orders/list --- # Get an order The order with its buyer, line items and totals. ## GET /v1/orders/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/orders/get --- # List products Products of the workspace’s stores, by title, in a locale. Search matches the title, vendor, handle or a variant’s SKU. Without a status filter, removed products are left out. A Shopify app session sees its own store only. ## GET /v1/products API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `locale` | query | no | Content locale; the workspace default without one | | `store` | query | no | Comma-separated ids | | `status` | query | no | Comma-separated, any of: active, unlisted, draft, archived, removed | | `stock` | query | no | Comma-separated, any of: in_stock, out_of_stock, not_tracked | | `sort` | query | no | One of title, price, inventory, changedAt; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/list --- # Get a product The product’s details in a locale; its variants, sources and sales have their own operations. ## GET /v1/products/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `locale` | query | no | Content locale; the workspace default without one | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/get --- # List a product’s variants In the store’s order. Search matches the variant’s title or SKU. ## GET /v1/products/{id}/variants API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `locale` | query | no | Content locale; the workspace default without one | | `stock` | query | no | Comma-separated, any of: in_stock, out_of_stock, not_tracked | | `sort` | query | no | One of position, title, price, inventory; prefix with - for descending | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/list-variants --- # Get a product variant ## GET /v1/products/{id}/variants/{variantId} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `variantId` | path | yes | | | `locale` | query | no | Content locale; the workspace default without one | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/get-variant --- # List a product’s sources What each source sent about the product and its variants in a locale, including sources that no longer have it, and which record won each product field. ## GET /v1/products/{id}/sources API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `locale` | query | no | Content locale; the workspace default without one | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/list-sources --- # Sync a product now Gets the product and its variants from the store’s platform again (changes normally arrive within seconds). A product the platform no longer has comes back `removed` (never deleted). 409 `store_unreachable` when the store must be reconnected; 422 `no_store_source` for a product only feeds have. ## POST /v1/products/{id}/sync API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/sync --- # Get a product’s sales Orders that include the product, units and revenue, placed in the period (all time without one). ## GET /v1/products/{id}/sales API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `from` | query | no | From (inclusive) | | `to` | query | no | Until (exclusive) | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/products/sales --- # Get a store’s product priority The store’s product sources in order (first wins), the fields with an order of their own, the custom fields its sources send, and the change that’s open or was applied last. ## GET /v1/stores/{id}/product-priority API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productpriority/get --- # List a store’s priority changes Newest first: the change that’s open (being previewed, waiting for review or being applied) and the ones applied or failed before. Discarded and replaced changes are gone. ## GET /v1/product-priority-changes API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `store` | query | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/list --- # Preview a priority change Starts working out what every product of the store would show with these rules; nothing changes until it’s applied. Replaces a change that’s still open. 422 `invalid_rules` when the order doesn’t name every source of the store exactly once; 409 `priority_applying` while a change is being applied. ## POST /v1/product-priority-changes API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/create --- # Get a priority change ## GET /v1/product-priority-changes/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/get --- # Discard a priority change Drops a change that isn’t applied; the priority stays as it is. ## DELETE /v1/product-priority-changes/{id} API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/discard --- # List a priority change’s products The store’s products by title, each with the fields it would show differently. Search matches the title, vendor, handle, a variant’s SKU or the product’s id in a source; `changed=true` lists only products that change. ## GET /v1/product-priority-changes/{id}/products API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `changed` | query | no | `true`: only products that change | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/list-products --- # Apply a priority change Makes the change the store’s priority and merges every product again with it. 409 `priority_change_not_ready` before its preview is done; 409 `priority_change_stale` when the store’s sources changed since. ## POST /v1/product-priority-changes/{id}/apply API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/productprioritychanges/apply --- # List feeds The workspace’s product feeds, by name. Search matches the name or URL. ## GET /v1/feeds API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `store` | query | no | Comma-separated ids | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/list --- # Add a feed Adds a feed as a product source of the store, after the store’s other sources, and imports it right away. 422 `feed_mapping_incomplete` when the mapping lacks what the settings need. ## POST /v1/feeds API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/create --- # Get a feed ## GET /v1/feeds/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/get --- # Change a feed Changes apply from the next import. 422 `feed_mapping_incomplete` as for create. ## PATCH /v1/feeds/{id} API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/update --- # Delete a feed Removes the feed as a source: its data leaves every product, which is merged again from its other sources. Products only the feed had stay, as removed. ## DELETE /v1/feeds/{id} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/delete --- # Preview a feed Reads the first rows of a feed without adding it: its format, its columns with what each likely is and sample values, and how many rows match a variant or product of the store by id. 422 `feed_unreadable` when the URL can’t be read as a feed. ## POST /v1/feeds/preview API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/preview --- # Check a feed before adding it Reads the first rows of a feed with the settings it would get and says how many would import, match nothing (enrich only) or be skipped, with the first skipped rows and why. Adds nothing. 422 `feed_unreadable` as for preview. ## POST /v1/feeds/check API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/check --- # List a feed’s imports Newest first. ## GET /v1/feeds/{id}/imports API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/list-imports --- # Import a feed now Imports the feed again now. 409 `import_active` when an import of it is queued or running. ## POST /v1/feeds/{id}/imports API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/create-import --- # List the rows an import skipped In file order, with why each was skipped. ## GET /v1/feeds/{id}/imports/{importId}/skipped-rows API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `importId` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/feeds/list-skipped-rows --- # List coupons Newest first. Search matches the name or prefix. ## GET /v1/coupons API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `store` | query | no | Comma-separated ids | | `status` | query | no | Comma-separated, any of: active, ended, archived | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/list --- # Create a coupon Creates the discount in the store’s platform and starts making codes: a personal coupon keeps some ready as emails go out; a bulk coupon makes `quantity` at once. 422 `invalid_quantity` when a bulk coupon has none (or a personal one has one). 409 `coupons_unsupported` for a store without a supported platform; 409 `store_permission_missing` when the store hasn’t allowed Postforge to manage discounts yet; 409 `store_unreachable` when it must be reconnected; 422 `coupon_refused` with the platform’s reason. ## POST /v1/coupons API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/create --- # Get a coupon ## GET /v1/coupons/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/get --- # Update a coupon Changes what its codes give, in the platform too. An imported coupon changes its name, validity and reissue only (what its codes give is set where they came from). A new validity in days applies to codes handed out from now on; a new end date to every code. A bulk coupon’s quantity can grow (422 `invalid_quantity` when it shrinks). The store, kind and prefix stay. ## PATCH /v1/coupons/{id} API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/update --- # Check a file of codes Reads a CSV of codes (a column named code, coupon or discount code, else the first column) and says how many would be imported, which the file has twice, which the store has already, and which rows can’t be a code and why. Imports nothing. ## POST /v1/coupons/import-check API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/check-import --- # Import a coupon’s codes Creates a coupon from a CSV of codes made elsewhere: Postforge hands them out and sees them used in orders; the discount stays where the codes came from. Codes the store has already are left out. A code works until a date or as long as the coupon is active (422 `invalid_validity` for days). 422 `no_codes` when no row is a code. ## POST /v1/coupons/imports API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/import --- # Archive a coupon Turns the discount off in the platform: no code of it works anymore and none is handed out. Its codes stay, as history. ## POST /v1/coupons/{id}/archive API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/archive --- # List a coupon’s codes Newest first. Search matches the code or the email it was handed out to. ## GET /v1/coupons/{id}/codes API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `q` | query | no | Search | | `status` | query | no | Comma-separated, any of: pending, available, assigned, used, expired, failed | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/list-codes --- # Hand out a code Hands a code to a profile (its email): the same code never goes to two people. With `after_use_or_expiry` a person whose code still works gets that code again. Its days start when it’s first handed out. 409 `coupon_archived` or `coupon_ended`; 409 `coupon_codes_unavailable` when none is ready yet (more are being made); 422 `profile_without_email`. ## POST /v1/coupons/{id}/codes/assign API keys: the `write` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/coupons/assign-code --- # Search the workspace Profiles (name, email or phone), orders (number, or the buyer’s exact email), products (title, vendor, handle, SKU or id), coupons (name or prefix), feeds (name or URL) and stores (name or domain): the first few of each kind and how many match. ## GET /v1/search API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `q` | query | yes | What to look for | | `limit` | query | no | Results per kind | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/search/search --- # List change proposals Newest first. ## GET /v1/change-proposals API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `page` | query | no | Starts at 1 | | `pageSize` | query | no | One of 10, 25, 50, 100 | | `status` | query | no | Comma-separated, any of: open, approved, rejected | | `kind` | query | no | A definition kind, e.g. template | | `definitionId` | query | no | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/changeproposals/list --- # Propose a change A new version of a definition (`definitionId` and the `baseVersionId` it builds on: the current draft) or a new definition (`name`). The document must fit the kind’s schema (`/v1/schemas/{kind}`) and pass the checks before publish. ## POST /v1/change-proposals API keys: the `propose` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/changeproposals/create --- # Get a change proposal With the proposed document and its diff from the base version (JSON Patch). ## GET /v1/change-proposals/{id} API keys: the `read` scope or more. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/changeproposals/get --- # Approve a change proposal Saves the proposed version and publishes it; a new definition is created as a draft. A stale proposal (409 `proposal_stale`) must be proposed again on the current draft. ## POST /v1/change-proposals/{id}/approve API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/changeproposals/approve --- # Reject a change proposal ## POST /v1/change-proposals/{id}/reject API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `id` | path | yes | | | `idempotency-key` | header | no | Run this write once: a retry with the same key gets the first answer (24 hours). | Takes a JSON body. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/changeproposals/reject --- # List definition schemas ## GET /v1/schemas API keys: not accepted; a person signed in to the dashboard only. Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/schemas/list --- # Get a definition schema The kind’s document as JSON Schema (draft 2020-12). ## GET /v1/schemas/{kind} API keys: not accepted; a person signed in to the dashboard only. | Parameter | In | Required | Description | | --- | --- | --- | --- | | `kind` | path | yes | | Full schemas: the OpenAPI spec at https://api.postforge.dev/v1/openapi.json. Source: https://postforge.dev/developers/api/schemas/get