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

# Canvas examples

> Working Node.js and PHP endpoints that handle the initial render, search-or-create flow, X-Secret verification, and confirmation state

Two minimal but realistic implementations show the full Canvas lifecycle. Both verify the `X-Secret` header, handle the initial render, react to button clicks, and return confirmation state.

## Minimal endpoint

<Tabs>
  <Tab title="Node.js (Express)">
    ```javascript theme={"system"}
    import express from 'express';

    const app = express();
    app.use(express.json());

    const SECRET = process.env.SLEEKPLAN_CANVAS_SECRET;

    app.post('/sleekplan/canvas', (req, res) => {
      if (req.get('X-Secret') !== SECRET) {
        return res.status(401).json({ error: 'Unauthorized' });
      }

      const { submit, ['button-action-create']: createClicked } = req.body;

      if (!submit) {
        return res.json([
          { type: 'text', text: 'Search or create item', style: 'paragraph' },
          { type: 'input', id: 'input-action-search', label: 'Search...', submit: true },
          { type: 'button', id: 'button-action-create', label: 'Create new item', style: 'blue', action: { type: 'submit' } }
        ]);
      }

      if (createClicked) {
        // ...create item in your system, then confirm:
        return res.json([
          { type: 'link', title: 'Linked with:', text: 'Item #123', href: 'https://example.com/items/123' },
          { type: 'button', id: 'button-action-unlink', label: 'Unlink item', style: 'blue', action: { type: 'submit' } }
        ]);
      }

      return res.json([{ type: 'text', text: 'No action', style: 'paragraph' }]);
    });

    app.listen(3000, () => console.log('Canvas listening on :3000'));
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={"system"}
    <?php
    $secret = getenv('SLEEKPLAN_CANVAS_SECRET');
    $input = json_decode(file_get_contents('php://input'), true);

    if (($_SERVER['HTTP_X_SECRET'] ?? '') !== $secret) {
      http_response_code(401);
      echo json_encode(['error' => 'Unauthorized']);
      exit;
    }

    $submit = $input['submit'] ?? false;
    $createClicked = $input['button-action-create'] ?? false;

    if (!$submit) {
      echo json_encode([
        ['type' => 'text', 'text' => 'Search or create item', 'style' => 'paragraph'],
        ['type' => 'input', 'id' => 'input-action-search', 'label' => 'Search...', 'submit' => true],
        ['type' => 'button', 'id' => 'button-action-create', 'label' => 'Create new item', 'style' => 'blue', 'action' => ['type' => 'submit']]
      ]);
      exit;
    }

    if ($createClicked) {
      echo json_encode([
        ['type' => 'link', 'title' => 'Linked with:', 'text' => 'Item #123', 'href' => 'https://example.com/items/123'],
        ['type' => 'button', 'id' => 'button-action-unlink', 'label' => 'Unlink item', 'style' => 'blue', 'action' => ['type' => 'submit']]
      ]);
      exit;
    }

    echo json_encode([
      ['type' => 'text', 'text' => 'No action', 'style' => 'paragraph']
    ]);
    ```
  </Tab>
</Tabs>

## Tips and gotchas

<AccordionGroup>
  <Accordion title="Always reject missing or wrong X-Secret early.">
    Treat the header check as the first thing your handler does. Any request that fails it should receive a `401` and nothing else.
  </Accordion>

  <Accordion title="Return arrays, not single objects.">
    Sleekplan accepts a single object, but arrays make intent obvious and are easier to extend when you need to render multiple components.
  </Accordion>

  <Accordion title="Use stable component IDs.">
    The `id` is the field name in subsequent payloads — renaming it mid-flow drops state and breaks your handler's ability to detect which button was clicked.
  </Accordion>

  <Accordion title="Show section errors with {&#x22;error&#x22;: &#x22;...&#x22;} as the first array element.">
    See [Inline errors](/canvas/components#inline-errors) for the exact shape Sleekplan expects when you need to surface a validation message.
  </Accordion>

  <Accordion title="Build links back to Sleekplan with feedback_id.">
    Pattern: `https://app.sleekplan.com/feedback/{feedback_id}`. The `feedback_id` is included in every Canvas payload so you can store it alongside your linked item.
  </Accordion>
</AccordionGroup>

<Note>
  Need extra help? Share your endpoint URL and a sample payload with [support@sleekplan.com](mailto:support@sleekplan.com).
</Note>
