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

# Funnel Lifecycle: Save, Publish & Go-Live

> Duplicate a funnel, mount it on staging, preview it, and promote it to a production domain — entirely via the API

# Funnel Lifecycle: Save, Publish & Go-Live

This guide covers the **full lifecycle** of a funnel managed through the API: duplicating an existing funnel, saving it (which mounts staging routes), previewing it, and publishing it to a production custom domain. It mirrors exactly what the CRM editor does when you click **Save** and **Publish**.

<Info>
  If you're creating your first funnel from scratch, start with the [Funnel Orchestrator](/developer-tools/funnels/funnel-orchestrator) guide. This page focuses on the staging → production lifecycle.
</Info>

## The two environments of a funnel

Every funnel has **two configurations**:

| Environment           | Field              | Served on                                   | Updated by                                   |
| --------------------- | ------------------ | ------------------------------------------- | -------------------------------------------- |
| **Staging** (draft)   | `config`           | `{funnel-slug}--{storeId}.cdn.tagada.io`    | `PUT /funnels/{funnelId}` (Save)             |
| **Production** (live) | `productionConfig` | Your custom domain (e.g. `pay.example.com`) | `POST /funnels/{funnelId}/promote` (Publish) |

The staging CDN alias is derived from the funnel's `config.id` (its slug) and the store id. Example: a funnel with slug `my-funnel` on `store_abc123` is served at `https://my-funnel--store_abc123.cdn.tagada.io`.

## The golden workflow: Save, then Publish

The CRM editor always performs these two steps in order — and so should you:

<Steps>
  <Step title="Save — PUT /funnels/{funnelId}">
    Persists the staging `config`, **mounts the staging routes** on the CDN alias, computes URL → step mappings (including path remaps), and triggers page prebuilds.
  </Step>

  <Step title="Publish — POST /funnels/{funnelId}/promote">
    Copies the staging routes to your custom domain, clones plugin instances for production isolation, and writes `productionConfig`.
  </Step>
</Steps>

<Warning>
  **Never promote a funnel that has not been saved at least once.** Promote inherits its route matchers and path remaps from the mounted staging routes. If you create a funnel and immediately promote it, the staging alias will 404 and production routes lose their path remapping (e.g. a step with `path: /checkout` remapped to `pagePath: /checkout-secure` will only answer on `/checkout-secure`).
</Warning>

## PUT replaces the entire config

`PUT /funnels/{funnelId}` is a **full replace**, not a patch. Sending a partial config (for example `{"config": {"name": "New name"}}`) will **erase your funnel's steps**.

Always follow the read-modify-write pattern:

```bash theme={null}
# 1. Read the current config
curl https://app.tagadapay.com/api/public/v1/funnels/funnelv2_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" > funnel.json

# 2. Modify the config object (jq, script, or by hand)

# 3. Write the COMPLETE config back
curl -X PUT https://app.tagadapay.com/api/public/v1/funnels/funnelv2_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "storeId": "store_abc123",
    "config": { /* the FULL config object, with ALL nodes and edges */ }
  }'
```

The response tells you what routing operations were performed:

```json theme={null}
{
  "funnel": { "id": "funnelv2_abc123", "name": "My Funnel" },
  "operations": [
    { "type": "mount", "stepId": "step_checkout", "status": "completed" },
    { "type": "mount", "stepId": "step_thankyou", "status": "completed" }
  ],
  "summary": { "total": 2, "mounted": 2, "unmounted": 0, "swapped": 0, "failed": 0 }
}
```

## Duplicate an existing funnel

There is no dedicated duplicate endpoint — duplication is a **read + create + save** sequence:

<Steps>
  <Step title="Read the source funnel">
    `GET /funnels/{sourceFunnelId}` and take its `config`.
  </Step>

  <Step title="Sanitize the config">
    Before creating the copy:

    * Change `config.id` (the slug) — it determines the staging alias and must be unique per store
    * Change `config.name`
    * Remove every `routeId` from node configs (they belong to the source funnel's routes)
    * Reset every `mountPointId` / `url` on node configs (they point to the source funnel's domains)
  </Step>

  <Step title="Create the copy">
    `POST /funnels` with the sanitized config.
  </Step>

  <Step title="Save it">
    `PUT /funnels/{newFunnelId}` with the same config — this mounts the staging routes and prebuilds the pages.
  </Step>
</Steps>

Typical use case: cloning a live funnel to point it at a different [payment flow](/api-reference/payment-flows) (e.g. migrating to a new processor) — after step 3, update `nodes[].config.stepConfig.payment.paymentFlowId` on the checkout node before saving.

## Preview the staging funnel

Create a preview session to test the staging environment without real payments:

```bash theme={null}
curl -X POST https://app.tagadapay.com/api/public/v1/funnels/funnelv2_abc123/preview-session \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "storeId": "store_abc123", "funnelId": "funnelv2_abc123", "stepId": "step_checkout" }'
```

Then open the staging alias with the returned session:

```
https://my-funnel--store_abc123.cdn.tagada.io/checkout?funnelSessionId=fs_...&token=eyJ...&funnelEnv=staging
```

## Publish to a production domain

```bash theme={null}
curl -X POST https://app.tagadapay.com/api/public/v1/funnels/funnelv2_abc123/promote \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "storeId": "store_abc123",
    "customDomain": "pay.example.com",
    "acknowledgeConflict": true
  }'
```

| Field                 | Description                                                                            |
| --------------------- | -------------------------------------------------------------------------------------- |
| `customDomain`        | The production domain the funnel goes live on. Must already be attached to your store. |
| `acknowledgeConflict` | Set `true` to confirm replacing routes another funnel currently owns on that domain.   |

Response:

```json theme={null}
{
  "success": true,
  "message": "Funnel promoted to pay.example.com",
  "funnelId": "funnelv2_abc123",
  "productionUrl": "https://pay.example.com",
  "stagingUrl": "https://my-funnel--store_abc123.cdn.tagada.io",
  "routesCopied": 2,
  "instancesCloned": 2
}
```

Promote is **idempotent**: re-running it re-copies the current staging routes to production. To ship changes to a live funnel, Save (PUT) then Publish (promote) again.

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 — Missing or invalid API key">
    Every funnel endpoint requires your API key as a Bearer token:

    ```
    Authorization: Bearer YOUR_API_KEY
    ```

    Watch out for an **empty variable** in shell scripts (`Authorization: Bearer ` with nothing after it) — it produces the same 401 as a missing header. Verify with `echo ${#KEY}` before calling.
  </Accordion>

  <Accordion title="Legacy error: &#x22;Clerk: auth() was called but Clerk can't detect usage of clerkMiddleware()&#x22;">
    Older API versions returned this confusing 500 when the `Authorization` header was **missing, empty, or not a recognized key format**. It never indicated a server problem — only that the API key didn't reach the server. Current versions return a clear 401 explaining how to pass the key. Fix: send `Authorization: Bearer YOUR_API_KEY` with a non-empty, valid key.
  </Accordion>

  <Accordion title="Staging alias returns 404">
    The funnel was created (and possibly promoted) but never **saved**, so no staging routes were mounted. Run `PUT /funnels/{funnelId}` with the full config once, then retry.
  </Accordion>

  <Accordion title="Production works on the internal path but 404s on the public path">
    A step can declare a public `path` (e.g. `/checkout`) remapped to an internal `pagePath` (e.g. `/checkout-secure`). The remap is materialized when staging routes are mounted (Save). If you promoted before ever saving, re-run Save then Publish to restore the remap.
  </Accordion>
</AccordionGroup>

## Doing this from an AI agent (MCP)

The [CRM MCP server](/developer-tools/crm-mcp/introduction) currently exposes funnels **read-only** (`funnels.list`, `funnels.retrieve`) — useful for diagnosing configuration. For mutations (create, save, promote), point your agent at the REST endpoints on this page with the same API key; they are designed to be safely scriptable.

## Endpoint reference

| Endpoint                                                                                 | Method | Lifecycle role                                             |
| ---------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------- |
| [`/funnels/{id}`](/api-reference/funnels/get-funnel-by-id)                               | GET    | Read config (start of every read-modify-write)             |
| [`/funnels`](/api-reference/funnels/create-a-new-funnel)                                 | POST   | Create (or duplicate from a copied config)                 |
| [`/funnels/{funnelId}`](/api-reference/funnels/update-funnel-with-routing)               | PUT    | **Save** — persist staging config + mount staging routes   |
| [`/funnels/{funnelId}/promote`](/api-reference/funnels/promote-funnel-to-production)     | POST   | **Publish** — copy staging routes to the production domain |
| [`/funnels/{funnelId}/preview-session`](/api-reference/funnels/create-a-preview-session) | POST   | Test staging without real payments                         |
