> For the complete documentation index, see [llms.txt](https://docs.peakcommerce.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.peakcommerce.com/developers/admin-api/admin-content.md).

# Content

*Add an overview of the Admin - Content API here. This block is preserved across regenerations.*

## Endpoints

### `GET /api/v1/business-rules`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /business-rules` go here.*

### `POST /api/v1/business-rules`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

A business rule is **policy, not content**. It decides whether a plan change is permitted at all (`blocksChange`), when it takes effect, how it prorates, and which subscription state and actor surface it governs.

**`isActive` is required.** Unlike journeys, pages and product sets, a rule has no draft state — no version to publish, no status to stage. Set it `true` and the rule is enforced from the moment it is written; set it `false` and it is authored but dormant. Neither is a safe default to pick on your behalf, so the API makes you say which you mean. The response carries an `enforcement` line restating what just happened.

**`isPlatformDefault` cannot be set.** Platform-default rules were retired; `clearPlatformDefaultFlags()` demotes any such row on every boot. Writing the flag would look applied and then silently revert at the next restart.

`custom_delay` and `grace_period` require both `timingValue` and `timingUnit` — without them the rule has no defined timing when the policy engine evaluates it.

### `DELETE /api/v1/business-rules/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Deleting a rule **removes a policy**. When the rule was an active hard block (`blocksChange: true`), that is the removal of a guardrail, not just a tidy-up.

Because the row is gone afterwards, the audit entry records what the rule was doing — its direction, whether it was active, whether it was blocking, the subscription state it governed, and the journeys it was attached to. Check `GET /business-rules/{id}/journeys` first to see the blast radius.

### `GET /api/v1/business-rules/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /business-rules/{id}` go here.*

### `PATCH /api/v1/business-rules/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

The timing rule is validated against the **merged** result, not just the fields you send. Patching `changeTiming` to `custom_delay` or `grace_period` without also supplying a value and unit is refused, because the stored rule would otherwise be left with undefined timing.

Flipping `isActive` changes enforcement immediately in both directions — there is no publish step.

### `GET /api/v1/business-rules/{id}/journeys`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/business-rules/{id}/journeys" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /business-rules/{id}/journeys` go here.*

### `GET /api/v1/context/host-routes`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/context/host-routes" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

The entry points — which hostname reaches which journey, step or page.

Filter by `journeyId` or `pageId` to answer "where do customers actually reach this?" after publishing something.

`failureReason` is included, because a route that is not serving is exactly the kind of thing an agent should be able to report back. The internal Cloudflare custom-hostname id is omitted; it is infrastructure plumbing with no authoring use.

### `GET /api/v1/context/integrations`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/context/integrations" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Returns **only** an integration's identity and health: `id`, `type`, `name`, `isActive`, `lastCatalogSyncAt`, `lastCatalogSyncStatus`.

**Credentials are never returned** — not masked, not their key names. Neither is `config`.

This is an **allowlist**, and the distinction matters. The admin UI masks credentials by taking the whole row and overwriting one field; that approach exposes anything added to the table later, silently, the day it lands. Here nothing is returned unless it is named, so a new column stays invisible until someone deliberately exposes it.

What you get is what an authoring agent actually needs: enough to know *which* system a commerce action calls and whether it is usable.

### `GET /api/v1/context/integrations/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/context/integrations/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /context/integrations/{id}` go here.*

### `GET /api/v1/context/personas`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/context/personas" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Persona configurations describe the audiences a journey or page renders for, and their capabilities.

Read-only, like everything under `/context`. The `context` scope has exactly one verb and always will — an agent has to resolve what a journey references, and must never mutate it.

### `GET /api/v1/context/personas/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/context/personas/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /context/personas/{id}` go here.*

### `GET /api/v1/custom-components`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/custom-components" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /custom-components` go here.*

### `POST /api/v1/custom-components`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/custom-components" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /custom-components` go here.*

### `DELETE /api/v1/custom-components/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/custom-components/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /custom-components/{id}` go here.*

### `GET /api/v1/custom-components/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/custom-components/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /custom-components/{id}` go here.*

### `PATCH /api/v1/custom-components/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/custom-components/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /custom-components/{id}` go here.*

### `GET /api/v1/journeys`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Supports `?status=` (`draft`, `active`, `archived`) and `?type=` filters. An unrecognised status is a `400` rather than an empty list, so a typo cannot look like "no journeys".

### `POST /api/v1/journeys`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

A journey is always created as a **draft**. Passing `status: "active"` is rejected rather than silently downgraded, because creating a live journey would bypass every publish gate.

Create the journey, add its steps, then publish it.

### `DELETE /api/v1/journeys/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Deleting a draft or archived journey needs `journeys:delete`.

Deleting an **active** journey takes it off the air, so it additionally requires `journeys:publish`. Archive it first if your key only holds delete authority.

### `GET /api/v1/journeys/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}` go here.*

### `PATCH /api/v1/journeys/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Editing a journey's name, description or audience needs `journeys:write`.

**Changing `status` is different.** A journey's status is its live state, so a status *transition* additionally requires `journeys:publish` — in **either** direction. Deactivating a live journey is gated as tightly as activating one, because taking a journey off the air affects customers just as much as putting it up.

Re-sending the journey's *current* status is not a transition, so a client that submits the whole object on every save can keep editing a live journey with only `journeys:write`.

A transition to `active` runs the full publish validation and records a journey version — it behaves exactly like `POST /journeys/{id}/publish`, including its error responses.

### `POST /api/v1/journeys/{id}/publish`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/publish" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Publishes the journey through the platform's publish-time validation. Four gates run, and a failure returns the gate's own error code so you can fix the specific problem:

| Code                                   | Meaning                                                                                                                                  |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `BROKEN_BRANCHING_TARGETS` (422)       | A step branches to a target that no longer exists. The response lists `brokenTargets`.                                                   |
| `UNSATISFIABLE_REQUIRED_ACTIONS` (422) | A required commerce action has a hard-required field that nothing maps. The response lists `errors`.                                     |
| graph validation (400)                 | The draft introduces a **new** cycle or unreachable step versus what is already published. Pre-existing issues do not block a republish. |

Republishing a journey with no changes returns `200` with `unchanged: true` and does **not** mint a new version — check that flag rather than assuming a new version exists.

Non-blocking findings come back in `warnings` alongside a successful publish.

### `GET /api/v1/journeys/{id}/steps`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}/steps` go here.*

### `POST /api/v1/journeys/{id}/steps`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Adding, editing and removing steps is **draft-side** and needs only `journeys:write`.

That is deliberate, not a gap. For a published journey the runtime serves the version snapshot frozen at publish time, so a step edit is invisible to live sessions until you publish again. `journeys:delete` means deleting the *journey*, not a step.

`stepOrder` is optional — omit it and the step is appended.

`pageId` must reference a page in the same tenant.

### `DELETE /api/v1/journeys/{id}/steps/{stepId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /journeys/{id}/steps/{stepId}` go here.*

### `GET /api/v1/journeys/{id}/steps/{stepId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}/steps/{stepId}` go here.*

### `PATCH /api/v1/journeys/{id}/steps/{stepId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /journeys/{id}/steps/{stepId}` go here.*

### `GET /api/v1/journeys/{id}/steps/{stepId}/actions`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}/actions" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}/steps/{stepId}/actions` go here.*

### `POST /api/v1/journeys/{id}/steps/{stepId}/actions`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}/actions" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /journeys/{id}/steps/{stepId}/actions` go here.*

### `DELETE /api/v1/journeys/{id}/steps/{stepId}/actions/{actionId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}/actions/{actionId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /journeys/{id}/steps/{stepId}/actions/{actionId}` go here.*

### `PATCH /api/v1/journeys/{id}/steps/{stepId}/actions/{actionId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}/actions/{actionId}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /journeys/{id}/steps/{stepId}/actions/{actionId}` go here.*

### `POST /api/v1/journeys/{id}/steps/{stepId}/actions/reorder`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/{stepId}/actions/reorder" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

A step's action order **is** its execution order, so `actionIds` must name every action on the step exactly once. A partial list is refused — silently accepting one would change which action runs first.

### `POST /api/v1/journeys/{id}/steps/reorder`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/steps/reorder" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Supply `stepIds` naming **every** step of the journey exactly once. A partial or stale list is rejected rather than reordered into something you did not ask for.

The reorder is refused with `journey_graph_invalid` if the new order introduces a cycle or leaves a step unreachable. The check is **non-retroactive**: only problems the reorder itself introduces block it, so a journey that already had a graph issue can still be rearranged toward a fix. The response carries `graphIssues` with the offending step orders.

### `GET /api/v1/journeys/{id}/versions`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/versions" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}/versions` go here.*

### `GET /api/v1/journeys/{id}/versions/{versionId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{id}/versions/{versionId}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{id}/versions/{versionId}` go here.*

### `GET /api/v1/journeys/{journeyId}/business-rules`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{journeyId}/business-rules" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /journeys/{journeyId}/business-rules` go here.*

### `POST /api/v1/journeys/{journeyId}/business-rules`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{journeyId}/business-rules" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Attaching a rule to a journey changes that journey's policy. It is scoped as a `rules:*` operation because the **rule** is what is being applied; the journey is still verified to belong to your tenant.

Both ends are re-checked under the tenant — a rule id from another tenant is refused rather than silently attaching a foreign policy. Attaching a rule that is already on the journey returns `409 already_linked` rather than a raw database error.

### `DELETE /api/v1/journeys/{journeyId}/business-rules/{linkId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/journeys/{journeyId}/business-rules/{linkId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /journeys/{journeyId}/business-rules/{linkId}` go here.*

### `GET /api/v1/pages`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /pages` go here.*

### `POST /api/v1/pages`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /pages` go here.*

### `DELETE /api/v1/pages/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /pages/{id}` go here.*

### `GET /api/v1/pages/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /pages/{id}` go here.*

### `PATCH /api/v1/pages/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /pages/{id}` go here.*

### `GET /api/v1/pages/{id}/manifest`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}/manifest" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /pages/{id}/manifest` go here.*

### `POST /api/v1/pages/{id}/publish`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}/publish" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /pages/{id}/publish` go here.*

### `GET /api/v1/pages/{id}/versions`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}/versions" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /pages/{id}/versions` go here.*

### `GET /api/v1/pages/{id}/versions/{versionId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}/versions/{versionId}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /pages/{id}/versions/{versionId}` go here.*

### `POST /api/v1/pages/{id}/versions/{versionId}/restore`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/pages/{id}/versions/{versionId}/restore" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /pages/{id}/versions/{versionId}/restore` go here.*

### `GET /api/v1/product-sets`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /product-sets` go here.*

### `POST /api/v1/product-sets`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Product sets are always created as **drafts**. Passing `status: "active"` is rejected rather than downgraded, because a set born active would skip the activation timing gate entirely.

The normal shape is: create the set → add its items → PATCH `status` to `active` with a key holding `productsets:publish`.

### `DELETE /api/v1/product-sets/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /product-sets/{id}` go here.*

### `GET /api/v1/product-sets/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /product-sets/{id}` go here.*

### `PATCH /api/v1/product-sets/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

A product set is the strictest surface in the API, for a reason worth understanding.

Unlike a journey or a page, **a product set has no version snapshot.** A journey freezes its steps at publish time, so editing the draft cannot reach a running session. A product set is resolved straight from its live rows — so if the set is `active`, an edit is in front of customers immediately.

Accordingly:

| The set is… | `productsets:write` can edit it?     |
| ----------- | ------------------------------------ |
| `draft`     | yes                                  |
| `archived`  | yes                                  |
| `active`    | **no** — needs `productsets:publish` |

A **status transition needs `productsets:publish` in either direction**, since deactivating a live set withdraws it from customers just as surely as activating one exposes it.

Activating also runs the §1.5 **anchor rule**: at least one line must use `same_as_order` timing, so something bills or activates immediately. A set that fails it comes back `400 product_set_timing_invalid` with the specific errors in `configErrors`. Draft edits are allowed to defer this — activation is where it bites.

### `GET /api/v1/product-sets/{id}/bundles`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/bundles" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /product-sets/{id}/bundles` go here.*

### `POST /api/v1/product-sets/{id}/bundles`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/bundles" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

A **bundle** is a composition unit inside a set: one **face** entry (the card the customer actually sees) plus one or more **member** entries that are added silently to the order when the face is selected.

Two shapes are refused: a face that is also a member of its own bundle, and a bundle with no members at all.

Bundles are distinct from the classification-driven *groups* in the editor — groups are taxonomy that organize the card list, bundles compose sellable units. An entry keeps its classification while being a bundle member; the two axes are orthogonal.

### `DELETE /api/v1/product-sets/{id}/bundles/{bundleId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/bundles/{bundleId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /product-sets/{id}/bundles/{bundleId}` go here.*

### `PATCH /api/v1/product-sets/{id}/bundles/{bundleId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/bundles/{bundleId}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /product-sets/{id}/bundles/{bundleId}` go here.*

### `GET /api/v1/product-sets/{id}/dependencies`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/dependencies" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /product-sets/{id}/dependencies` go here.*

### `POST /api/v1/product-sets/{id}/dependencies`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/dependencies" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Both `sourceItemId` and `targetItemId` must be items of **this** set — a dependency pointing outside it would be unresolvable at runtime — and an item cannot depend on itself.

### `DELETE /api/v1/product-sets/{id}/dependencies/{depId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/dependencies/{depId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /product-sets/{id}/dependencies/{depId}` go here.*

### `GET /api/v1/product-sets/{id}/items`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/items" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Each item includes its `effectiveDateTiming` and a derived `orderGroup` (`"default"` or `"own"`) — see [`POST /product-sets/{id}/items`](#post-apiv1product-setsiditems) for what they do.

### `POST /api/v1/product-sets/{id}/items`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/items" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

`productId` must reference a product in the same tenant.

If the set is `active`, adding an item requires `productsets:publish` — the item is purchasable the moment it lands, since a product set resolves from its live rows with no draft buffer.

**How the line is booked.** Two optional fields control how this line lands in the order when a journey checks out:

| Field                 | Values                                                                                              | Default         | Effect                                                                                                                                                                                          |
| --------------------- | --------------------------------------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orderGroup`          | `"default"` · `"own"`                                                                               | `"default"`     | `"own"` books the line as its own order action (an Add Product) within the same order and subscription. Takes effect only when the journey's `create_order` action has `honorOrderGrouping` on. |
| `effectiveDateTiming` | `{"mode":"same_as_order"}` · `{"mode":"set_period","value":1–120,"unit":"days"\|"months"\|"years"}` | `same_as_order` | When the line starts, counted from the order date. Applied only when the `create_order` action has `honorActivationOffsets` on.                                                                 |

```json
{
  "productId": "5d0c…",
  "planId": "a41e…",
  "classification": "addon",
  "orderGroup": "own",
  "effectiveDateTiming": { "mode": "set_period", "value": 1, "unit": "months" }
}
```

* `orderGroup` is stored in the item's `metadata.orderGroup` and merged in on the server, so sending it never replaces other `metadata` keys such as `visibility`. Every item response carries the derived `orderGroup`.
* On an `active` set, at least one line must stay `same_as_order`. A write that breaks this is refused with `400 product_set_timing_invalid`, the same error activation returns. A draft set may pass through that state while you assemble it.
* When the set's lines resolve to more than two distinct start dates, the response includes `configWarnings`.

See [Booking a Line as Its Own Order Action](https://docs.peakcommerce.com/guides/feature-guides/billing-setup/guide-booking-a-line-as-its-own-order-action) for how the order is built from these fields.

### `DELETE /api/v1/product-sets/{id}/items/{itemId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/items/{itemId}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /product-sets/{id}/items/{itemId}` go here.*

### `PATCH /api/v1/product-sets/{id}/items/{itemId}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/items/{itemId}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

Send only the fields you are changing. `orderGroup` and `effectiveDateTiming` behave as described under [`POST /product-sets/{id}/items`](#post-apiv1product-setsiditems): `orderGroup` merges into the stored `metadata`, and a timing change that would leave an `active` set with no `same_as_order` line is refused with `400 product_set_timing_invalid`.

```json
{ "orderGroup": "own" }
```

Editing an item on an `active` set requires `productsets:publish`.

### `POST /api/v1/product-sets/{id}/items/reorder`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/product-sets/{id}/items/reorder" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /product-sets/{id}/items/reorder` go here.*

### `GET /api/v1/templates`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/templates" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /templates` go here.*

### `POST /api/v1/templates`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/templates" method="post" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `POST /templates` go here.*

### `DELETE /api/v1/templates/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/templates/{id}" method="delete" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `DELETE /templates/{id}` go here.*

### `GET /api/v1/templates/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/templates/{id}" method="get" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `GET /templates/{id}` go here.*

### `PATCH /api/v1/templates/{id}`

{% openapi src="<https://api.peakcommerce.app/api/v1/openapi.json>" path="/templates/{id}" method="patch" %}
<https://api.peakcommerce.app/api/v1/openapi.json>
{% endopenapi %}

*Notes, examples, or caveats for `PATCH /templates/{id}` go here.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.peakcommerce.com/developers/admin-api/admin-content.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
