A guide on how to add a Canvas to Sleekplan. Canvas lets your integration render a small, interactive UI inside the Sleekplan admin (e.g., on a feedback post sidebar) and react to user input.

This document covers:

  • Where Canvas appears and when it’s called
  • The request payload you’ll receive
  • The response schema to render UI
  • How user interactions get posted back
  • Security and verification
  • Examples and best practices

Current availability: one Canvas location is supported — view "admin_post" (the feedback detail view) - more to come.

# How Canvas works (high level)

  • In Sleekplan, a product can register one or more Canvas providers for a given view (e.g., admin_post).
  • When the view loads, Sleekplan calls your Canvas endpoint via HTTPS POST with JSON input to fetch components to render.
  • When the admin interacts with your UI (e.g., clicks a button, selects an item, types and submits), Sleekplan posts the submitted values back to your endpoint and expects an updated list of components in response.

Sleekplan invokes your endpoint with JSON and includes an X-Secret header for verification.

# Where Canvas shows up

  • View: admin_post — the right sidebar within a specific feedback post in Sleekplan Admin.
  • Query: you’ll receive feedback_id in the payload so you can act on the current post.

Sleekplan renders your response using a simple, documented component schema (see below for supported component types).

# Requests your endpoint will receive

Your endpoint must accept POST application/json and return application/json.

Two situations trigger calls to your endpoint:

  1. Initial render (view load)
  • Payload contains your product_id, the type (view) and the current query such as feedback_id.
  • Example payload:
{
  "product_id": 12345,
  "type": "admin_post",
  "feedback_id": 6789
}
  1. Submit/interaction
  • After the admin interacts, Sleekplan posts the submitted fields for your Canvas section along with any query params. A boolean submit: true is included.
  • Example payload:
{
  "product_id": 12345,
  "feedback_id": 6789,
  "submit": true,
  "button-action-create": true,
  "create-item-title": "Improve SSO",
  "create-item-description": "Support IdP-initiated SAML",
  "create-item-project": "proj_abc",
  "create-item-type": "Task"
}

Notes:

  • Field names come from the id you assign to components (e.g., inputs, dropdowns, buttons, lists). For a button, Sleekplan posts { [button.id]: true } when clicked.
  • For a list selection, Sleekplan posts { [list.id]: <selectedItem.id> }.

# Response schema (components you can return)

Return either a single object or an array of component objects. Sleekplan will normalize a single object to an array behind the scenes.

Supported component types and properties (as rendered by Sleekplan):

  • text
    • props: text (string), style ("header" | "paragraph")
  • divider
    • props: none
  • button
    • props: id (string), label (string), style (string, e.g., "blue"), action ({ type: "submit" | "link", href?: string })
  • input
    • props: id, label (used as placeholder), title? (label above), submit? (boolean; if true, Enter/arrow submits)
  • textarea
    • props: id, label, title?, submit?
  • dropdown
    • props: id, title, options (array of { id, text })
  • link
    • props: title?, text, href
  • list
    • props: id, title, items (array of items where each item is either { type: "option", id, text } or { type: "link", href, text })

Conditional visibility (optional): add a condition object to any component to show it only when another field matches a value.

{
  "type": "dropdown",
  "id": "create-item-type",
  "title": "Item Type",
  "condition": { "value": "create-item-project", "key": "proj_abc" },
  "options": [ { "id": "Task", "text": "Task" } ]
}

Error display: to show an inline error for a section, you can return the first element as { "error": "Human readable message" }.

# End-to-end flow example

Initial render (show search/create options):

[
  { "type": "text", "text": "Search or create DevOps item", "style": "paragraph" },
  { "type": "input", "id": "input-action-search", "label": "Search...", "submit": true, "placeholder": "leave blank to skip" },
  { "type": "button", "id": "button-action-create", "label": "Create new item", "style": "blue", "action": { "type": "submit" } }
]

User types a query and submits; you respond with a list:

[
  {
    "type": "list",
    "id": "link-item-list",
    "title": "Items",
    "items": [
      { "type": "option", "id": { "item_id": 42, "project_id": "p1" }, "text": "Item #42 - Task" },
      { "type": "option", "id": { "item_id": 43, "project_id": "p2" }, "text": "Item #43 - Bug" }
    ]
  },
  { "type": "button", "id": "button-action-create", "label": "Create new item", "style": "blue", "action": { "type": "submit" } }
]

User selects an option; you link it and return a confirmation with an unlink button and a link:

[
  { "type": "link", "title": "Linked with:", "text": "DevOps #42", "href": "https://devops.example.com/items/42" },
  { "type": "button", "id": "button-action-unlink", "label": "Unlink item", "style": "blue", "action": { "type": "submit" } }
]

This mirrors how native integrations implement Canvas: the endpoint returns arrays of the component types above in response to stateful interactions.

# Security and verification

  • Header: every request to your endpoint includes X-Secret: <token>. The token is set when you install the Custom integration in Sleekplan. Reject requests that don’t match.
  • IP allow-list (optional): outbound calls originate from Sleekplan through a fixed proxy. You may allow-list 168.119.102.99 if you restrict by IP.
  • Content types: requests use Content-Type: application/json and expect Accept: application/json.

# Installing and configuring in Sleekplan

  1. In Sleekplan Admin, open Settings → Integrations → Custom, and click Install/Connect.
  2. Fill out the fields below, then Save.

Fields and requirements:

  • Canvas URL (required if no Webhook URL)
    • Your HTTPS endpoint that accepts and returns JSON per this guide.
    • We call it on view load and on user actions with Content-Type: application/json.
    • Must be publicly reachable over HTTPS. We include X-Secret for verification.
  • Canvas section
    • Currently only admin_post is supported (Canvas shows on the feedback detail view’s sidebar).
  • Webhook URL (optional)
    • Your HTTPS endpoint to receive event webhooks you opt into.
    • Receives JSON POSTs and includes the same X-Secret header.
  • Webhook events (optional)
    • Comma-separated list of events you’d like to receive (e.g., item.create, item.update).
  • Secret token (required)
    • Any non-empty string you choose. We’ll send it as X-Secret: <token> on both Canvas and Webhook calls.

Validation and behavior:

  • At least one of Canvas URL or Webhook URL must be valid to complete setup.
  • URLs should be full HTTPS URLs (scheme + host). If a scheme is omitted, we’ll treat it as HTTPS.
  • If your network restricts inbound calls by IP, you may allow‑list 168.119.102.99.

Example configuration:

  • Canvas URL: https://integrations.example.com/sleekplan/canvas
  • Canvas section: admin_post
  • Webhook URL: https://integrations.example.com/sleekplan/webhook
  • Webhook events: item.create, item.update
  • Secret token: prod-abc-xyz

After saving:

  • Open any feedback post in Sleekplan Admin. The Canvas will request components from your Canvas URL and render them.
  • Interactions (button clicks, list selections, submitted inputs) will post back to your Canvas URL with the submitted fields.

Under the hood, Sleekplan registers your Canvas and calls your URL for the configured view. Your endpoint’s JSON response is rendered directly in the admin UI.

# Minimal endpoint examples

Node.js/Express

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 { type, feedback_id, 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 here, then show confirmation
    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' } }
    ]);
  }

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

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

PHP (plain)

<?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;
if (! $submit) {
  echo json_encode([
    [ 'type' => 'text', 'text' => 'Hello Canvas', 'style' => 'header' ]
  ]);
  exit;
}

echo json_encode([
  [ 'type' => 'text', 'text' => 'Submitted', 'style' => 'paragraph' ]
]);

# Tips and gotchas

  • Always return JSON arrays of components; a single object is accepted, but arrays are clearer.
  • Use stable id values for inputs/buttons; those keys appear in subsequent payloads.
  • To show a validation error, return [ { "error": "Something went wrong" } ] as the first element.
  • Keep responses small and fast. The UI updates immediately with your returned components.
  • If you need to link back to the Sleekplan post, you’ll receive feedback_id and can build a URL like https://app.sleekplan.com/feedback/{feedback_id}.

If you need anything else for your Canvas integration, contact support and share your endpoint URL and a sample payload so we can help verify it end‑to‑end.