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

# Plugin Manifest

> Understanding plugin.manifest.json and its role in funnel orchestration

# Plugin Manifest Guide

The `plugin.manifest.json` is the **contract** between your plugin and the TagadaPay platform. It defines what your plugin does, what data it needs, and what data it provides to other steps in a funnel.

## Why the Manifest Matters

The manifest is **critical** for the Funnel Orchestrator to work correctly:

1. **Parameter Propagation**: Tells the orchestrator what data your plugin needs (requirements) and what data it provides (resources)
2. **Type Safety**: Ensures data flows correctly between funnel steps
3. **URL Building**: Defines pages and their paths for routing
4. **Feature Detection**: Allows the platform to discover what your plugin can do

<Warning>
  Without a proper manifest, the orchestrator cannot resolve parameters or route users correctly through your
  funnel.
</Warning>

## Basic Structure

```json theme={null}
{
  "name": "My Awesome Plugin",
  "pluginId": "my-plugin",
  "description": "A brief description of what your plugin does",
  "version": "1.0.0",
  "author": "Your Name",
  "mode": "direct-mode",
  "category": "checkout",
  "tags": ["checkout", "payment"],
  "configuration": {
    "schema": "./config/schema.json",
    "uiSchema": "./config/ui-schema.json",
    "presets": []
  },
  "pages": [],
  "router": {
    "basePath": "/",
    "matcher": ".*",
    "excluder": null
  },
  "requirements": {
    "sdk": "^2.2.0",
    "sdkVersion": "v2"
  }
}
```

### Key Fields

* **name**: Display name shown in the CRM
* **pluginId**: Unique identifier for your plugin
* **mode**: `"direct-mode"` for modern V2 plugins
* **category**: Plugin category (checkout, upsell, thankyou, custom)
* **configuration**: References to configuration schemas and presets
* **router**: Routing configuration for the plugin
* **requirements**: SDK version requirements

## Configuration System

The configuration system allows you to define customizable settings for your plugin that can be edited in the CRM.

### Configuration Structure

```json theme={null}
{
  "configuration": {
    "schema": "./config/schema.json",
    "uiSchema": "./config/ui-schema.json",
    "presets": [
      {
        "id": "default",
        "name": "Default",
        "description": "Default checkout configuration",
        "config": "./config/default.config.json"
      },
      {
        "id": "custom-theme",
        "name": "Custom Theme",
        "description": "Custom branded theme",
        "config": "./config/custom-theme.config.json"
      }
    ]
  }
}
```

### Configuration Files

#### `schema.json` - JSON Schema

Defines the structure and validation rules for your plugin configuration:

```json theme={null}
{
  "type": "object",
  "properties": {
    "branding": {
      "type": "object",
      "properties": {
        "primaryColor": {
          "type": "string",
          "format": "color"
        }
      }
    }
  }
}
```

#### `ui-schema.json` - UI Hints

Provides hints for rendering the configuration editor in the CRM:

```json theme={null}
{
  "branding": {
    "ui:order": ["primaryColor", "logo"],
    "primaryColor": {
      "ui:widget": "color"
    }
  }
}
```

#### Preset Configs

Preset configuration files contain complete plugin configurations that users can select in the CRM:

```json theme={null}
{
  "branding": {
    "primaryColor": "#3B82F6",
    "logo": "https://example.com/logo.png"
  },
  "layout": {
    "template": "modern"
  }
}
```

<Info>The CRM automatically loads and validates these configurations when deploying your plugin.</Info>

## Static Resources System

Static resources allow you to configure plugin-specific data (like offer IDs, product IDs, etc.) in the CRM's funnel editor that can be accessed at runtime.

### What Are Static Resources?

Static resources are configuration values that:

* Are defined in the funnel step configuration in the CRM
* Can be different per deployment/funnel
* Are accessible via `context.static` in your plugin code
* Don't require URL parameters

### Defining Static Resources in Manifest

Use `type: "static"` in your requirements:

```json theme={null}
{
  "path": "/offer",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "offer",
          "from": [
            {
              "name": "id",
              "type": "static"
            }
          ]
        }
      ]
    }
  ]
}
```

This tells the orchestrator: "This page needs an offer ID from static configuration."

### Accessing Static Resources in Code

```typescript theme={null}
import { useFunnel } from '@tagadapay/plugin-sdk/v2';

function OfferPage() {
  const { context } = useFunnel();

  // Access static resources
  const offerId = context?.static?.offer?.id;

  return <div>Offer ID: {offerId}</div>;
}
```

### Local Development: `resources.static.json`

For local development, create `/config/resources.static.json` to mock static resources:

```json theme={null}
{
  "offer": {
    "id": "offer_ad2e64728dd8"
  },
  "product": {
    "id": "product_abc123"
  }
}
```

<Warning>
  **Important**: This file is ONLY loaded in local development (localhost, \*.localhost, ngrok). Values from
  the backend (CRM) always take precedence. Do NOT deploy this file to production.
</Warning>

### Example Use Cases

#### Single Offer Upsell Page

```json theme={null}
{
  "path": "/upsell-special",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "order",
          "from": [{ "name": "id", "type": "context" }]
        },
        {
          "resource": "offer",
          "from": [{ "name": "id", "type": "static" }]
        }
      ]
    }
  ]
}
```

Configure in CRM: Set `offer.id = "offer_abc123"` for this step. The page will always show this specific offer.

#### Multi-Product Bundle Page

```json theme={null}
{
  "path": "/bundle",
  "features": [
    {
      "type": "custom",
      "requirements": [
        {
          "resource": "products",
          "from": [{ "name": "ids", "type": "static" }]
        }
      ]
    }
  ]
}
```

Configure in CRM: Set `products.ids = ["prod_1", "prod_2", "prod_3"]`. Your plugin receives all product IDs.

## Pages Definition

Pages define the routes your plugin exposes. The orchestrator uses this to build URLs and route users.

### Page Structure

```json theme={null}
{
  "path": "/checkout",
  "features": [
    {
      "type": "checkout",
      "requirements": []
    }
  ],
  "remappable": true
}
```

### Key Fields

* **path**: URL path for this page (can include path parameters like `:orderId`)
* **features**: Array of features this page provides
* **remappable**: Whether this page can be remapped to a different URL in the funnel editor

### Resource Requirement Types

The modern manifest uses a flexible `requirements` array with a `from` field that specifies where data comes from:

#### 1. Query Parameters (`type: "query"`)

Data passed in the URL query string:

```json theme={null}
{
  "path": "/checkout",
  "features": [
    {
      "type": "checkout",
      "requirements": [
        {
          "resource": "checkoutSession",
          "from": [
            {
              "name": "checkoutToken",
              "type": "query"
            }
          ]
        }
      ]
    }
  ],
  "remappable": true
}
```

**Result**: `/checkout?checkoutToken=tok_abc123`

#### 2. Path Parameters (`type: "path"`)

Data embedded in the URL path:

```json theme={null}
{
  "path": "/thankyou/:orderId",
  "features": [
    {
      "type": "thankyou",
      "requirements": [
        {
          "resource": "order",
          "from": [
            {
              "name": "id",
              "type": "path"
            }
          ]
        }
      ]
    }
  ],
  "remappable": true
}
```

**Result**: `/thankyou/ord_abc123`

#### 3. Context Resources (`type: "context"`)

Data from previous funnel steps (stored in session):

```json theme={null}
{
  "path": "/offer",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "order",
          "from": [
            {
              "name": "id",
              "type": "context"
            }
          ]
        }
      ]
    }
  ],
  "remappable": true
}
```

The orchestrator automatically resolves `order.id` from the session and includes it in the URL.

#### 4. Static Resources (`type: "static"`)

Data configured in the CRM funnel editor (not in URL):

```json theme={null}
{
  "path": "/offerfromstatic",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "order",
          "from": [
            {
              "name": "id",
              "type": "context"
            }
          ]
        },
        {
          "resource": "offer",
          "from": [
            {
              "name": "id",
              "type": "static"
            }
          ]
        }
      ]
    }
  ],
  "remappable": true
}
```

**Accessing in code**:

```typescript theme={null}
const { context } = useFunnel();
const offerId = context?.static?.offer?.id; // From CRM configuration
const orderId = context?.order?.id; // From session
```

### Multi-Page Example (Complete Funnel)

```json theme={null}
{
  "pages": [
    {
      "path": "/checkout",
      "features": [
        {
          "type": "checkout",
          "requirements": [
            {
              "resource": "checkoutSession",
              "from": [
                {
                  "name": "checkoutToken",
                  "type": "query"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/offer",
      "features": [
        {
          "type": "offer",
          "requirements": [
            {
              "resource": "order",
              "from": [
                {
                  "name": "id",
                  "type": "context"
                }
              ]
            },
            {
              "resource": "offer",
              "from": [
                {
                  "name": "id",
                  "type": "static"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/thankyou/:orderId",
      "features": [
        {
          "type": "thankyou",
          "requirements": [
            {
              "resource": "order",
              "from": [
                {
                  "name": "id",
                  "type": "path"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    }
  ]
}
```

## Requirements: What Your Plugin Needs

Requirements tell the orchestrator what data your plugin needs to function. The orchestrator will:

1. Check if this data exists based on the `type` (query, path, context, static)
2. Resolve it from the appropriate source
3. Make it available to your plugin via the SDK

### Requirement Structure

Each requirement has:

* **resource**: The name of the resource (e.g., `"order"`, `"offer"`, `"checkoutSession"`)
* **from**: Array of sources that can provide this data

```json theme={null}
{
  "requirements": [
    {
      "resource": "order",
      "from": [
        {
          "name": "id",
          "type": "context"
        }
      ]
    }
  ]
}
```

### Data Flow by Type

#### Query Parameters (`type: "query"`)

```json theme={null}
{
  "resource": "checkoutSession",
  "from": [
    {
      "name": "checkoutToken",
      "type": "query"
    }
  ]
}
```

**URL**: `/checkout?checkoutToken=tok_abc123`\
**Access**: Automatically handled by SDK, creates checkout session

#### Path Parameters (`type: "path"`)

```json theme={null}
{
  "resource": "order",
  "from": [
    {
      "name": "id",
      "type": "path"
    }
  ]
}
```

**URL**: `/thankyou/:orderId` → `/thankyou/ord_abc123`\
**Access**: Via `context.order.id` in your plugin

#### Context Resources (`type: "context"`)

```json theme={null}
{
  "resource": "order",
  "from": [
    {
      "name": "id",
      "type": "context"
    }
  ]
}
```

**Source**: Previous funnel steps (stored in session)\
**Access**: Via `context.order.id` in your plugin

#### Static Resources (`type: "static"`)

```json theme={null}
{
  "resource": "offer",
  "from": [
    {
      "name": "id",
      "type": "static"
    }
  ]
}
```

**Source**: CRM funnel editor configuration\
**Access**: Via `context.static.offer.id` in your plugin

<Info>
  The orchestrator automatically resolves all requirements before loading your page, ensuring all required
  data is available.
</Info>

### Multiple Requirements

A page can have multiple requirements from different sources:

```json theme={null}
{
  "path": "/offerfromstatic",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "order",
          "from": [
            {
              "name": "id",
              "type": "context"
            }
          ]
        },
        {
          "resource": "offer",
          "from": [
            {
              "name": "id",
              "type": "static"
            }
          ]
        }
      ]
    }
  ]
}
```

This page needs:

* An `order.id` from the session (previous checkout step)
* An `offer.id` from static configuration (CRM editor)

## Resource Production: What Your Plugin Provides

Your plugin can produce resources that become available to subsequent funnel steps. While not explicitly declared in the modern manifest format, you emit resources via the SDK.

### Emitting Resources

When your plugin completes an action, emit resources via the funnel SDK:

```typescript theme={null}
import { useFunnel } from '@tagadapay/plugin-sdk/v2';

function CheckoutPage() {
  const funnel = useFunnel();

  const handleCheckoutComplete = async (orderData) => {
    // Navigate to next step with resources
    await funnel.next({
      resources: {
        order: {
          id: orderData.id,
          total: orderData.total,
          customer: {
            email: orderData.customer.email,
            id: orderData.customer.id
          }
        }
      }
    });
  };

  return <CheckoutForm onComplete={handleCheckoutComplete} />;
}
```

<Check>
  The orchestrator automatically stores these resources in the session and makes them available to subsequent
  steps.
</Check>

### Accessing Resources in Next Steps

Resources emitted by previous steps are available via `context`:

```typescript theme={null}
function ThankYouPage() {
  const { context } = useFunnel();

  // Access order from previous checkout step
  const orderId = context?.order?.id;
  const orderTotal = context?.order?.total;
  const customerEmail = context?.order?.customer?.email;

  return (
    <div>
      <h1>Thank you for your order!</h1>
      <p>Order ID: {orderId}</p>
      <p>Total: ${orderTotal}</p>
      <p>Confirmation sent to: {customerEmail}</p>
    </div>
  );
}
```

### Resource Naming Best Practices

Use semantic, descriptive names:

```typescript theme={null}
// ✅ Good: Clear intent
resources: {
  order: { id: 'ord_123' },
  mainOrder: { id: 'ord_123' }, // For persistence
  upsellOrder: { id: 'ord_456' }
}

// ❌ Bad: Unclear naming
resources: {
  order1: { id: 'ord_123' },
  order2: { id: 'ord_456' }
}
```

## Remappable Pages

The `remappable` property determines whether a page can be remapped to a different URL in the funnel editor.

```json theme={null}
{
  "path": "/checkout",
  "features": [
    {
      "type": "checkout",
      "requirements": []
    }
  ],
  "remappable": true
}
```

### When to Use Remappable

**`remappable: true`** - Most pages should be remappable:

* Checkout pages
* Upsell pages
* Thank you pages
* Offer pages

This allows users to customize URLs in the funnel editor (e.g., `/checkout` → `/buy-now`).

**`remappable: false`** - Use for special routes:

* Wildcard routes (`/club/*`)
* API endpoints
* Utility pages

```json theme={null}
{
  "path": "/club/*",
  "features": [
    {
      "type": "custom",
      "requirements": [
        {
          "resource": "club"
        }
      ]
    }
  ],
  "remappable": false
}
```

<Info>
  When a page is remapped, the SDK's path remapping system automatically handles the URL translation. See the
  [Path Remapping Guide](/developer-tools/sdk/path-remapping) for details.
</Info>

## How the Orchestrator Uses the Manifest

### 1. Parameter Resolution

When navigating to a step, the orchestrator:

```typescript theme={null}
// Step URL template: /thankyou/:orderId
// Step requirements: { orderId: "string" }

// Orchestrator checks session for "orderId"
// If not found, looks for "order.id" in resources
// If found, resolves: /thankyou/ord_123
```

### 2. URL Building

The orchestrator builds URLs using:

* **Mount Point ID**: The funnel alias (e.g., `my-funnel--store123.cdn.tagadapay.com`)
* **Page Path**: From manifest (e.g., `/thankyou/:orderId`)
* **Path Parameters**: Resolved based on requirement type (query, path, context)
* **Static Resources**: Loaded from CRM configuration (not in URL)

**Examples:**

```typescript theme={null}
// Query parameter requirement
// URL: /checkout?checkoutToken=tok_abc123

// Path parameter requirement
// URL: /thankyou/ord_123

// Context requirement
// Orchestrator resolves from session, adds to URL: /offer?orderId=ord_123

// Static requirement
// Loaded from CRM config, accessed via context.static (not in URL)
```

### 3. Resource Propagation

The orchestrator maintains a session with all resources:

```typescript theme={null}
// After checkout completes
context = {
  order: { id: 'ord_123', total: 99.99 },
  static: {
    /* CRM configuration */
  },
};

// After upsell completes
context = {
  order: { id: 'ord_123', total: 99.99 },
  upsellOrder: { id: 'ord_456', total: 29.99 },
  static: {
    /* CRM configuration */
  },
};

// All resources available to subsequent steps via useFunnel()!
```

## Resource Naming Strategy

### Use Descriptive Names

Choose names that clearly indicate what the resource represents:

```typescript theme={null}
// ✅ Good: Clear and semantic
resources: {
  order: { id: 'ord_123' },           // Current/hot order
  mainOrder: { id: 'ord_123' },       // Original checkout order
  upsellOrder: { id: 'ord_456' },     // Upsell order
  downsellOrder: { id: 'ord_789' }    // Downsell order
}

// ❌ Bad: Unclear naming
resources: {
  order: { id: 'ord_123' },
  order2: { id: 'ord_456' },
  order3: { id: 'ord_789' }
}
```

### Generic vs Specific Resources

**Generic Resources** (Hot Context):

* Use common names like `order`, `product`, `customer`
* Represent the "current" item in focus
* Can be overwritten by subsequent steps

```typescript theme={null}
// Checkout emits
resources: {
  order: {
    id: 'ord_123';
  }
}

// Upsell also emits (overwrites)
resources: {
  order: {
    id: 'ord_456';
  }
}
```

**Specific Resources** (Persistent):

* Use unique names like `mainOrder`, `upsellOrder`
* Never get overwritten
* Available for the entire funnel session

```typescript theme={null}
// Better: Both generic and specific
resources: {
  order: { id: 'ord_123' },      // Hot - for next step
  mainOrder: { id: 'ord_123' }   // Persistent - for later
}
```

## Advanced Patterns

### Multiple Path Parameters

Combine multiple path parameters with context requirements:

```json theme={null}
{
  "path": "/order/:orderId/upsell/:productId",
  "features": [
    {
      "type": "upsell",
      "requirements": [
        {
          "resource": "order",
          "from": [{ "name": "id", "type": "path" }]
        },
        {
          "resource": "product",
          "from": [{ "name": "id", "type": "path" }]
        }
      ]
    }
  ]
}
```

Result: `/order/ord_123/upsell/prod_456`

### Combining Query and Context

Mix query parameters (user input) with context (session data):

```json theme={null}
{
  "path": "/thankyou",
  "features": [
    {
      "type": "thankyou",
      "requirements": [
        {
          "resource": "order",
          "from": [{ "name": "id", "type": "context" }]
        },
        {
          "resource": "utm",
          "from": [{ "name": "source", "type": "query" }]
        }
      ]
    }
  ]
}
```

Result: `/thankyou?orderId=ord_123&utm_source=facebook`

## Complete Example: Multi-Step Funnel

### Native Checkout Plugin (Modern Format)

```json theme={null}
{
  "name": "Native Checkout",
  "pluginId": "native-checkout",
  "description": "Modern checkout with multiple page types",
  "version": "2.0.0",
  "author": "TagadaPay Team",
  "mode": "direct-mode",
  "category": "checkout",
  "tags": ["checkout", "native", "v2"],
  "configuration": {
    "schema": "./config/schema.json",
    "uiSchema": "./config/ui-schema.json",
    "presets": [
      {
        "id": "default",
        "name": "Default",
        "description": "Default checkout configuration",
        "config": "./config/default.config.json"
      }
    ]
  },
  "pages": [
    {
      "path": "/checkout",
      "features": [
        {
          "type": "checkout",
          "requirements": [
            {
              "resource": "checkoutSession",
              "from": [
                {
                  "name": "checkoutToken",
                  "type": "query"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/offer",
      "features": [
        {
          "type": "offer",
          "requirements": [
            {
              "resource": "order",
              "from": [
                {
                  "name": "id",
                  "type": "context"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/offerfromstatic",
      "features": [
        {
          "type": "offer",
          "requirements": [
            {
              "resource": "order",
              "from": [
                {
                  "name": "id",
                  "type": "context"
                }
              ]
            },
            {
              "resource": "offer",
              "from": [
                {
                  "name": "id",
                  "type": "static"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/thankyou/:orderId",
      "features": [
        {
          "type": "thankyou",
          "requirements": [
            {
              "resource": "order",
              "from": [
                {
                  "name": "id",
                  "type": "path"
                }
              ]
            }
          ]
        }
      ],
      "remappable": true
    },
    {
      "path": "/club/*",
      "features": [
        {
          "type": "custom",
          "requirements": [
            {
              "resource": "club"
            }
          ]
        }
      ],
      "remappable": false
    }
  ],
  "router": {
    "basePath": "/",
    "matcher": ".*",
    "excluder": null
  },
  "requirements": {
    "sdk": "^2.2.0",
    "sdkVersion": "v2"
  }
}
```

## How Data Flows Through This Funnel

### Step 1: Checkout

```typescript theme={null}
// User navigates to: /checkout?checkoutToken=tok_123

// Requirement: checkoutSession from query parameter
// SDK automatically creates checkout session

// On completion, plugin emits resources:
funnel.next({
  resources: {
    order: {
      id: 'ord_123',
      total: 99.99,
      customer: { email: 'user@example.com', id: 'cus_456' },
    },
  },
});
```

### Step 2: Dynamic Offer (Context)

```typescript theme={null}
// Orchestrator navigates to: /offer?orderId=ord_123

// Requirement: order.id from context (previous step)
// Orchestrator resolves: context.order.id = 'ord_123'

// In plugin:
const { context } = useFunnel();
const orderId = context?.order?.id; // 'ord_123'

// On completion:
funnel.next({
  resources: {
    upsellOrder: { id: 'ord_456', total: 29.99 },
  },
});
```

### Step 3: Static Offer (Static + Context)

```typescript theme={null}
// Orchestrator navigates to: /offerfromstatic?orderId=ord_123

// Requirements:
// 1. order.id from context (previous checkout)
// 2. offer.id from static (CRM configuration)

// In plugin:
const { context } = useFunnel();
const orderId = context?.order?.id; // 'ord_123' from session
const offerId = context?.static?.offer?.id; // 'offer_abc' from CRM

// Show specific offer configured in CRM for this order
```

### Step 4: Thank You

```typescript theme={null}
// Orchestrator navigates to: /thankyou/ord_123

// Requirement: order.id as path parameter
// Orchestrator resolves from context and embeds in URL

// In plugin:
const { context } = useFunnel();
const order = context?.order; // Full order data available
const upsellOrder = context?.upsellOrder; // Also available!

// All previous resources are accessible
```

<Check>
  The orchestrator handles ALL parameter resolution, URL building, and resource propagation automatically!
</Check>

### Key Takeaways

1. **Query parameters** (`type: "query"`) - User provides in URL
2. **Path parameters** (`type: "path"`) - Orchestrator embeds in URL from context
3. **Context resources** (`type: "context"`) - From previous steps, available in session
4. **Static resources** (`type: "static"`) - From CRM configuration, not in URL

All resources from all previous steps remain available in `context` for the entire funnel session.

## Validation & Best Practices

### ✅ Always Specify Resource Type

Be explicit about where data comes from:

```json theme={null}
{
  "requirements": [
    {
      "resource": "order",
      "from": [
        {
          "name": "id",
          "type": "context"
        }
      ]
    }
  ]
}
```

<Check>Clear type specification helps the orchestrator resolve data correctly.</Check>

### ❌ Don't Mix Resource Types Incorrectly

```json theme={null}
// ❌ Bad: Using query for session data
{
  "resource": "order",
  "from": [
    {
      "name": "id",
      "type": "query"  // Wrong! order.id should be from context
    }
  ]
}

// ✅ Good: Use context for session data
{
  "resource": "order",
  "from": [
    {
      "name": "id",
      "type": "context"
    }
  ]
}
```

### ✅ Use Static Resources for Configuration

Use static resources for values that should be configured in the CRM:

```json theme={null}
{
  "path": "/special-offer",
  "features": [
    {
      "type": "offer",
      "requirements": [
        {
          "resource": "offer",
          "from": [{ "name": "id", "type": "static" }]
        },
        {
          "resource": "order",
          "from": [{ "name": "id", "type": "context" }]
        }
      ]
    }
  ]
}
```

<Check>This allows different funnels to show different offers without code changes.</Check>

### ✅ Make Pages Remappable

Most pages should be remappable for flexibility:

```json theme={null}
{
  "path": "/checkout",
  "features": [{ "type": "checkout", "requirements": [] }],
  "remappable": true
}
```

Only set `remappable: false` for wildcard routes or special functionality.

### ✅ Use Semantic Resource Names

```typescript theme={null}
// ✅ Good: Clear intent
resources: {
  mainOrder: { id: 'ord_123' },
  upsellOrder: { id: 'ord_456' },
  customer: { email: 'user@example.com' }
}

// ❌ Bad: Unclear
resources: {
  order1: { id: 'ord_123' },
  order2: { id: 'ord_456' },
  data: { email: 'user@example.com' }
}
```

### ✅ Test with resources.static.json

For local development, create `/config/resources.static.json`:

```json theme={null}
{
  "offer": {
    "id": "offer_test123"
  },
  "product": {
    "id": "prod_test456"
  }
}
```

This lets you test static resource pages without configuring them in the CRM.

## Debugging

### Debug Context and Resources

Log the full context to see what's available:

```typescript theme={null}
import { useFunnel } from '@tagadapay/plugin-sdk/v2';

function MyPage() {
  const { context } = useFunnel();

  // Debug: See all available data
  console.log('[MyPage] Full context:', context);
  console.log('[MyPage] Order:', context?.order);
  console.log('[MyPage] Static resources:', context?.static);

  return <div>...</div>;
}
```

### Debug Static Resources

Check if your static resources are loading correctly:

```typescript theme={null}
const { context } = useFunnel();

console.log('[Debug] Static offer ID:', context?.static?.offer?.id);
console.log('[Debug] Is localhost?', window.location.hostname === 'localhost');
```

<Info>
  Remember: `resources.static.json` only works in development (localhost, \*.localhost, ngrok). In production,
  static resources come from the CRM configuration.
</Info>

### Check Requirement Types

Verify requirements are resolving correctly:

```typescript theme={null}
// Query parameter (checkoutToken)
console.log('[Debug] Query checkoutToken:', new URLSearchParams(window.location.search).get('checkoutToken'));

// Path parameter (orderId)
console.log('[Debug] Path orderId:', window.location.pathname.split('/').pop());

// Context resource (order from previous step)
console.log('[Debug] Context order:', context?.order);

// Static resource (from CRM config)
console.log('[Debug] Static offer:', context?.static?.offer);
```

### Backend Logs

Check backend logs for orchestrator decisions:

```
[Orchestrator] Resolving requirements for step: /thankyou/:orderId
[Orchestrator] Requirement: order.id from type:path
[Orchestrator] Found in context.order.id: ord_123
[Orchestrator] Built URL: /thankyou/ord_123

[Orchestrator] Resolving requirements for step: /offerfromstatic
[Orchestrator] Requirement: order.id from type:context -> ord_123
[Orchestrator] Requirement: offer.id from type:static -> offer_abc
[Orchestrator] Static resources loaded from CRM config
```

## Summary

The manifest is your plugin's **contract** with the platform:

### Core Concepts

1. **Configuration System** - Define customizable settings with schemas and presets
2. **Static Resources** - Configure plugin-specific data in the CRM (accessible via `context.static`)
3. **Define Pages** - Specify routes, features, and whether they're remappable
4. **Declare Requirements** - Specify data needs using `type` (query, path, context, static)
5. **Emit Resources** - Produce data via `funnel.next()` for subsequent steps
6. **Trust the Orchestrator** - It handles parameter resolution, URL building, and resource propagation

### Requirement Types Quick Reference

| Type      | Source            | In URL?                   | Access Via                  |
| --------- | ----------------- | ------------------------- | --------------------------- |
| `query`   | URL query params  | ✅ Yes (query param)       | Auto-handled by SDK         |
| `path`    | URL path segments | ✅ Yes (path param)        | `context.{resource}`        |
| `context` | Previous steps    | 🔄 Optional (via session) | `context.{resource}`        |
| `static`  | CRM configuration | ❌ No (server-side only)   | `context.static.{resource}` |

**Context Resources Explained:**

* Context resources are stored in the **funnel session** on the server
* They're available via `context.{resource}` without needing to be in the URL
* The orchestrator may optionally include them in the URL for specific use cases (like when a path requires `:orderId`)

### Development Files

* `/config/schema.json` - Configuration structure
* `/config/ui-schema.json` - CRM editor UI hints
* `/config/*.config.json` - Preset configurations
* `/config/resources.static.json` - **Local dev only** - Mock static resources

<Info>A well-defined manifest = Seamless funnel orchestration with static resources ✨</Info>

## Next Steps

<CardGroup cols={2}>
  <Card title="Funnel Resources" icon="share-nodes" href="/developer-tools/sdk/funnel-resources">
    Learn about the resource system in detail
  </Card>

  <Card title="Build Your First Funnel" icon="rocket" href="/developer-tools/sdk/tutorial">
    Follow our step-by-step tutorial
  </Card>

  <Card title="SDK API Reference" icon="code" href="/developer-tools/sdk/api">
    Explore all available SDK functions
  </Card>

  <Card title="Examples" icon="lightbulb" href="/developer-tools/sdk/examples">
    See real-world examples
  </Card>
</CardGroup>
