> ## 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.

# External Steps

> Put a page you host on the funnel — declare it, then report what happens on it from your server, from the browser, or with curl

# External Steps

An **external step** is a step of the funnel that lives on a site you host: your own landing page, a WooCommerce product page, a quiz on your marketing site. TagadaPay mounts no plugin for it, creates no CDN route, and never rewrites its URL. It keeps its own address and appears on the [canvas](/developer-tools/funnels/canvas) like any other block, so the flow reads end to end instead of stopping at your domain boundary.

Two things to do:

1. **Declare it** — the block exists on the canvas, with its URL and, if you want, its named exits.
2. **Report it** — every time a visitor reaches the page, converts on it, or does something you care about, tell TagadaPay so analytics and routing see it.

<Note>
  **Reporting is enough to make the block appear.** A step the funnel has never seen, reported with an `https://` URL, is turned into an `add step.external` command on its own — you do not have to declare it first. Three things decide whether you actually see it:

  * the report must carry a `url`, and it must be `https://`. Without one the event is still tracked, and the block is skipped with `reported-step-needs-url` — no address is ever invented;
  * the store's canvas must be in `live` mode. In `shadow` the command is journalled and the funnel is not written, so nothing is drawn yet;
  * the block arrives **unconnected**. Nothing knows which exit it should hang from, so wire it with a `connect` command or from the canvas screen.

  Reporting the same `stepId` again never adds a second block: the command id is derived from the store and the step, and replaying it returns the original reply.
</Note>

<Warning>
  **A report that carries a `funnelSessionId` has no URL to mount.** Page context is only kept for a standalone event: as soon as the report ties itself to a funnel run, `url` is dropped before the event leaves, so the block is skipped with `reported-step-needs-url` however carefully you filled the field. Send the first report of a page without `funnelSessionId` — or declare the step once at deploy time — and then report inside the session as much as you like.
</Warning>

***

## Declare the step

On the canvas, an external step is an `add` command of kind `step.external`:

```json theme={null}
{
  "type": "add",
  "kind": "step.external",
  "after": { "node": "step_checkout", "exit": "step_checkout:paid" },
  "props": {
    "name": "Quiz",
    "url": "https://shop.example.com/quiz",
    "exits": ["passed", "failed"]
  }
}
```

`url` must be an absolute `https://` address. `exits` names up to ten outcomes of your page.

<Warning>
  Named exits are drawn on the canvas and never lost, but the runtime cannot branch on them yet. Their wires are written as default wires, so the path actually taken is the highest-priority one leaving the step. Use a `rule` block if you need the funnel to fork on a condition today.
</Warning>

The step type also exists in the funnel config directly — see [Funnel Pages](/developer-tools/node-sdk/custom-checkout) for the whole list of types and what each one unlocks.

***

## Report what happens

One endpoint does all of it.

```text theme={null}
POST https://api.tagada.io/api/public/v1/funnel-tracking/step
```

Authenticate with a CRM API key as a Bearer token. The key decides the account; the account must own the `storeId` you send.

| Field                                                      | Required | What it is                                                   |
| ---------------------------------------------------------- | -------- | ------------------------------------------------------------ |
| `eventType`                                                | yes      | `view`, `enter`, `convert` or `custom`                       |
| `storeId`                                                  | yes      | The store the step belongs to                                |
| `stepId`                                                   | yes      | Your id for the step — the same one on every call            |
| `stepName`, `stepType`                                     | no       | What to label the block with (`external` for these steps)    |
| `funnelId`, `funnelSessionId`, `fromStepId`                | no       | Ties the event to a funnel run rather than a standalone page |
| `url`, `referrer`, `pageTitle`, `userAgent`, `deviceType`  | no       | Page context                                                 |
| `orderId`, `orderAmount`, `orderCurrency`, `transactionId` | no       | Fill these on a `convert`                                    |
| `customEventName`, `customEventProperties`                 | no       | With `eventType: 'custom'` — anything else worth recording   |
| `source`, `medium`, `campaign`, `content`, `term`          | no       | Attribution                                                  |
| `metadata`                                                 | no       | Free-form object kept alongside the event                    |

The reply is small and has no envelope:

```json theme={null}
{ "success": true, "eventType": "view", "stepId": "step_quiz" }
```

### With the Node SDK

`@tagadapay/node-sdk` 3.18.0 and later ship two helpers on `funnels`.

```ts theme={null}
import { Tagada } from '@tagadapay/node-sdk';

const tagada = new Tagada({ apiKey: process.env.TAGADA_API_KEY ?? '' });

// Once, at deploy time: describe your steps to the canvas.
await tagada.funnels.declareSteps('store_abc123', [
  {
    id: 'quiz',
    name: 'Skin quiz',
    url: 'https://shop.example.com/quiz',
    exits: ['passed', 'abandoned'],
  },
]);

// On every visit.
await tagada.funnels.reportStep({
  storeId: 'store_abc123',
  stepId: 'quiz',
  event: 'view',
  url: 'https://shop.example.com/quiz',
});

// When the visitor leaves through a named exit.
await tagada.funnels.reportStep({
  storeId: 'store_abc123',
  stepId: 'quiz',
  event: 'custom',
  exit: 'passed',
});
```

`declareSteps` sends one declaration per step and does not stop on the first failure — read `steps[]` in the result to see which were accepted.

<Warning>
  **Running `declareSteps` again is safe, but it does not yet update a block that already exists.** A declaration for a step the canvas has already drawn is recognised as such and nothing is written — so no duplicate block, and no rename either. Change a name, a URL or the exits of a drawn step with a `set` command on the [canvas](/developer-tools/funnels/canvas#set), or on the canvas screen.
</Warning>

<Note>
  The tracking endpoint has no `exit` field. `reportStep` sends `event` as `eventType`, and an `exit` as `eventType: 'custom'` with the exit name in `customEventName`. Passing `exit` therefore always sends a custom event, whatever `event` says — which is exactly what the custom-event curl below does by hand.
</Note>

### curl

```bash theme={null}
curl -X POST https://api.tagada.io/api/public/v1/funnel-tracking/step \
  -H "Authorization: Bearer $TAGADA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "view",
    "storeId": "store_abc123",
    "stepId": "step_quiz",
    "stepName": "Quiz",
    "stepType": "external",
    "url": "https://shop.example.com/quiz"
  }'
```

```json theme={null}
{ "success": true, "eventType": "view", "stepId": "step_quiz" }
```

A conversion on your own page carries the order:

```bash theme={null}
curl -X POST https://api.tagada.io/api/public/v1/funnel-tracking/step \
  -H "Authorization: Bearer $TAGADA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "convert",
    "storeId": "store_abc123",
    "stepId": "step_quiz",
    "isConversionStep": true,
    "orderId": "order_9f21",
    "orderAmount": 4900,
    "orderCurrency": "EUR"
  }'
```

A named exit — and anything else worth recording — is a custom event:

```bash theme={null}
curl -X POST https://api.tagada.io/api/public/v1/funnel-tracking/step \
  -H "Authorization: Bearer $TAGADA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "custom",
    "storeId": "store_abc123",
    "stepId": "step_quiz",
    "customEventName": "passed",
    "customEventProperties": { "exit": "passed" }
  }'
```

### From a Next.js route handler

Report from your server, never from the browser with an API key in it. This handler takes the browser's word for the page and nothing else, and keeps the key server-side.

```ts app/api/tagada/step/route.ts theme={null}
import { NextResponse } from 'next/server';

const TAGADA_API = 'https://api.tagada.io/api/public/v1';
const STORE_ID = process.env.TAGADA_STORE_ID ?? '';
const API_KEY = process.env.TAGADA_API_KEY ?? '';

export async function POST(request: Request) {
  const body = await request.json();

  // Only ever forward fields you are willing to let a browser choose.
  const response = await fetch(`${TAGADA_API}/funnel-tracking/step`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      eventType: body.eventType === 'convert' ? 'convert' : 'view',
      storeId: STORE_ID,
      stepId: 'step_quiz',
      stepName: 'Quiz',
      stepType: 'external',
      url: body.url,
      referrer: body.referrer,
      funnelSessionId: body.funnelSessionId,
    }),
  });

  if (!response.ok) {
    // Tracking must never break the page it is tracking.
    console.error('tagada step tracking failed', response.status, await response.text());
    return NextResponse.json({ ok: false }, { status: 202 });
  }

  return NextResponse.json(await response.json());
}
```

Call it from the page with a plain `fetch('/api/tagada/step', …)`.

### From the browser

If you would rather not run a server at all, the CDN tracker does the same job with no key: it sends anonymous page events tied to a store and a step.

```html theme={null}
<script src="https://cdn.jsdelivr.net/npm/@tagadapay/plugin-sdk/dist/external-tracker.min.js"></script>
<script>
  TagadaTracker.init({
    storeId: 'store_abc123',
    accountId: 'acc_abc123',
    stepId: 'step_quiz',
  });
</script>
```

Its options, the npm build and the session rules are on the [External Page Tracker](/developer-tools/web-integration/external-page-tracker) page. Use the tracker for page views and the server route for anything involving an order.

***

## When it does not work

| What you get                                            | What it means                                                                                                                      |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `401` — *Missing or invalid API key*                    | No `Authorization` header, an empty Bearer token, or a revoked key                                                                 |
| `403` — *You do not have access to this store*          | The key's account does not own that `storeId`                                                                                      |
| `400` with a zod issue list                             | A required field is missing, or `eventType` is not one of the four values                                                          |
| `500` — *customEventName is required for custom events* | `eventType: 'custom'` with no `customEventName`. The SDK refuses this one before it leaves your process; the raw endpoint does not |

<Tip>
  Send the same `stepId` on every call for a given page. It is the key everything else is grouped by: change it and you start a second block on the canvas with none of the history of the first.
</Tip>

## Next

<CardGroup cols={2}>
  <Card title="Funnel Canvas" icon="sitemap" href="/developer-tools/funnels/canvas">
    The five commands, the exits, and the error catalogue.
  </Card>

  <Card title="External Page Tracker" icon="radar" href="/developer-tools/web-integration/external-page-tracker">
    The browser tracker in full.
  </Card>
</CardGroup>
