Copy for AI assistant

Paste this entire document into Claude, ChatGPT, Cursor, or any AI for complete API context.

Outerfaced API — AI Context Document

Copy-paste this entire document into any AI assistant (Claude, ChatGPT, Cursor, etc.) for complete working knowledge of the Outerfaced API.


Section 1: What Outerfaced Is

Outerfaced lets you give your automation workflows a shareable, real-time web interface — without writing any frontend code. You create a channel (a live mini-dashboard with its own URL), then push cards to it via a single POST request from any HTTP client. A card is composed of blocks — the atomic UI units: text, badge, key-value rows, images, buttons, inputs, selects, and more. When a visitor interacts with a block (clicks a button, submits an input, changes a toggle), Outerfaced fires a webhook to your configured URL — that's an interaction, and it's how the loop closes: your automation sees the human's response and continues. The core mental model is: channel = shareable live interface, card = content unit you push via API, blocks = the building blocks of a card, interactions = what fires when a user does something.


Section 2: Base URL and Authentication

Base URL: https://outerfaced.com/api/v1

Authentication: Every v1 API call requires a Bearer token in the Authorization header. API keys have the prefix chnd_sk_. Keys are workspace-scoped — you do not pass a workspace ID in requests; the key identifies the workspace.

curl https://outerfaced.com/api/v1/channels \
  -H "Authorization: Bearer chnd_sk_your_key_here"

Generate an API key from the dashboard, or via:

curl -X POST https://outerfaced.com/api/user/api-key \
  -H "Cookie: <your-session-cookie>"
# Returns { "key": "chnd_sk_...", "prefix": "chnd_sk_..." }
# Plaintext shown ONCE — store it immediately.

Public routes (no auth required):

  • POST /api/public/interactions — fires when a visitor clicks/submits something on a channel. This is called by the Outerfaced frontend, not by you. No Bearer token needed. Channel visibility (private/protected/public) is enforced here instead.

Section 3: Every API Route

Channels


List channels

# GET /api/v1/channels
# Returns all channels in the workspace
curl "https://outerfaced.com/api/v1/channels" \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
[
  {
    "id": "chnl_4d72e1a3b9c0",
    "name": "SMS Lead Review",
    "description": "Inbound SMS leads requiring human qualification.",
    "mode": "interactive",
    "icon": "📱",
    "workspace_id": "ws_a1b2c3d4",
    "created_at": "2024-11-15T09:00:00.000Z",
    "card_count": 14
  }
]

Create a channel

# POST /api/v1/channels
curl -X POST https://outerfaced.com/api/v1/channels \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SMS Lead Review",
    "description": "Inbound SMS leads requiring human qualification.",
    "mode": "interactive",
    "icon": "📱",
    "webhook_url": "https://your-server.com/webhooks/sms-leads",
    "workspace_id": "ws_a1b2c3d4",
    "attributes": {
      "region": "us-west",
      "pipeline": "inbound-sms"
    }
  }'

# Response (201)
{
  "id": "chnl_4d72e1a3b9c0",
  "name": "SMS Lead Review",
  "url": "https://outerfaced.com/c/chnl_4d72e1a3b9c0",
  "workspace_id": "ws_a1b2c3d4"
}

Fields: name (required, max 80 chars), workspace_id (required), description (optional, max 300), mode ("view" | "interactive", default "view"), icon (emoji), webhook_url, attributes (flat key-value object). Limit: 50 channels/day.


Get a channel

# GET /api/v1/channels/:channelId
curl "https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0" \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
{
  "id": "chnl_4d72e1a3b9c0",
  "name": "SMS Lead Review",
  "description": "Inbound SMS leads requiring human qualification.",
  "mode": "interactive",
  "icon": "📱",
  "webhook_url": "https://your-server.com/webhooks/sms-leads",
  "workspace_id": "ws_a1b2c3d4",
  "visibility": "public",
  "created_at": "2024-11-15T09:00:00.000Z",
  "actions": [
    { "id": "actn_1e3b5d7f", "label": "Refresh All", "style": "secondary", "position": 0 }
  ]
}

Update a channel

# PATCH /api/v1/channels/:channelId
curl -X PATCH https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0 \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SMS Lead Review — Active",
    "webhook_url": "https://your-server.com/webhooks/sms-leads-v2",
    "mode": "interactive",
    "icon": "🔥",
    "description": "Only showing hot leads."
  }'

# Response: full updated channel object

Delete a channel

# DELETE /api/v1/channels/:channelId
# Cascades: deletes all cards, actions, attributes, interaction logs
curl -X DELETE https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0 \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
{ "deleted": true }

Cards


List cards

# GET /api/v1/channels/:channelId/cards
# Returns cards ordered by created_at DESC
curl "https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards" \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
[
  {
    "id": "card_7b2f4a1d3e8c",
    "channel_id": "chnl_4d72e1a3b9c0",
    "status": "pending",
    "blocks": [...],
    "created_at": "2024-11-15T14:32:00.000Z"
  }
]

Push a card

# POST /api/v1/channels/:channelId/cards
curl -X POST https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "pending",
    "blocks": [
      {
        "type": "text",
        "id": "blk_01",
        "content": "### New inbound SMS lead\nReceived from +1 (555) 867-5309. Review and qualify below.",
        "size": "md"
      },
      {
        "type": "badge",
        "id": "blk_02",
        "label": "Awaiting Review",
        "variant": "pending"
      },
      {
        "type": "kv",
        "id": "blk_03",
        "rows": [
          { "key": "Phone", "value": "+1 (555) 867-5309" },
          { "key": "Message", "value": "Hi, I saw your ad about solar panels. Interested." },
          { "key": "Received", "value": "2024-11-15 14:31 UTC" },
          { "key": "Source", "value": "Google Ads — Solar Campaign" }
        ]
      },
      {
        "type": "button",
        "id": "blk_04",
        "buttons": [
          { "id": "btn_qualify", "label": "Qualify", "style": "primary", "disabled": false },
          { "id": "btn_disqualify", "label": "Disqualify", "style": "danger", "disabled": false },
          { "id": "btn_callback", "label": "Schedule Callback", "style": "secondary", "disabled": false }
        ]
      }
    ]
  }'

# Response (201)
{
  "id": "card_7b2f4a1d3e8c",
  "channel_id": "chnl_4d72e1a3b9c0",
  "created_at": "2024-11-15T14:32:00.000Z"
}

Fields: blocks (required, array), status ("default" | "success" | "warning" | "error" | "pending"). Block IDs auto-assigned as UUIDs if omitted — but set them explicitly if you need to patch blocks later. Limit: 200 cards/day. Card changes broadcast via Supabase Realtime to all viewers of the channel instantly.


Update a card

# PATCH /api/v1/channels/:channelId/cards/:cardId
# Patch the card's status and/or specific blocks by their id
curl -X PATCH https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards/card_7b2f4a1d3e8c \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "success",
    "blocks": [
      {
        "id": "blk_02",
        "patch": {
          "label": "Qualified",
          "variant": "success"
        }
      },
      {
        "id": "blk_04",
        "patch": {
          "buttons": [
            { "id": "btn_qualify", "disabled": true },
            { "id": "btn_disqualify", "disabled": true },
            { "id": "btn_callback", "disabled": true }
          ]
        }
      },
      {
        "id": "blk_03",
        "patch": {
          "append_rows": [
            { "key": "Decision", "value": "Qualified by Sarah" },
            { "key": "Next Step", "value": "AE call booked for Nov 16 at 10am" }
          ]
        }
      }
    ]
  }'

# Response
{
  "id": "card_7b2f4a1d3e8c",
  "channel_id": "chnl_4d72e1a3b9c0",
  "status": "success",
  "blocks": [...],
  "updated_at": "2024-11-15T14:35:00.000Z"
}

Patch semantics:

  • blocks[].patch is a shallow merge onto the block's current fields.
  • KV blocks: use append_rows (array) to add rows without replacing existing ones. Using rows directly replaces all rows.
  • Button blocks: pass buttons as an array of { id, ...fields } to patch individual buttons by id. Only buttons whose id matches get updated.
  • All other blocks: patch merges into top-level fields.

Delete a card

# DELETE /api/v1/channels/:channelId/cards/:cardId
curl -X DELETE https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards/card_7b2f4a1d3e8c \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response: 204 No Content

Attributes

Channel attributes are key-value metadata attached to a channel. They display in the channel UI and can be used as filters in workspace sidebars. Keys are auto-lowercased.


Get all attributes

# GET /api/v1/channels/:channelId/attributes
curl "https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/attributes" \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
{
  "region": "us-west",
  "pipeline": "inbound-sms",
  "assigned_to": "[email protected]",
  "created_at": "2024-11-15T09:00:00.000Z",
  "updated_at": "2024-11-15T14:35:00.000Z"
}

Upsert attributes

# PUT /api/v1/channels/:channelId/attributes
# Only provided keys are updated — omitted keys are untouched.
# Empty string clears an attribute's value. Reserved keys: created_at, updated_at (forbidden).
curl -X PUT https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/attributes \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "assigned_to": "[email protected]",
    "status": "active",
    "tier": "enterprise"
  }'

# Response: full updated attributes object

Delete an attribute

# DELETE /api/v1/channels/:channelId/attributes/:key
curl -X DELETE "https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/attributes/tier" \
  -H "Authorization: Bearer chnd_sk_your_key_here"

# Response
{ "deleted": true }

Interactions

Interactions are fired by the Outerfaced frontend when a visitor interacts with a channel. You do not call this endpoint — Outerfaced calls it internally and then forwards the event to your webhook_url. The endpoint is public (no Bearer token).

# POST /api/public/interactions
# Called by Outerfaced frontend, not by your automation.
# Visibility is enforced: private channels return 403, protected channels require cookie.
curl -X POST https://outerfaced.com/api/public/interactions \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "chnl_4d72e1a3b9c0",
    "type": "button_click",
    "pub_card_token": "<encrypted-token-from-frontend>",
    "pub_action_token": null,
    "payload": {
      "button_id": "btn_qualify",
      "label": "Qualify"
    }
  }'

# Response
{
  "id": "intr_9f3a1bc2d4e5",
  "channel_id": "chnl_4d72e1a3b9c0",
  "type": "button_click",
  "payload": { "button_id": "btn_qualify", "label": "Qualify" },
  "created_at": "2024-11-15T14:32:07.412Z"
}

Workspaces

These routes use Supabase session auth (dashboard login), not API key Bearer tokens. Workspace IDs are inferred from the API key on v1 routes — you do not need workspace endpoints in your automation scripts.

# GET /api/workspaces — list all workspaces for logged-in user
# POST /api/workspaces — create workspace (body: { "name": "...", "icon": "🏢" })
# GET /api/workspaces/:id — get a workspace
# PATCH /api/workspaces/:id — update (name, icon, visibility, layout, custom_slug, etc.)
# DELETE /api/workspaces/:id — delete (cannot delete last workspace; cascades everything)

API Keys

Session-authenticated (dashboard login). Plaintext returned once on creation only.

# POST /api/user/api-key — generate a new key
curl -X POST https://outerfaced.com/api/user/api-key \
  -H "Cookie: <supabase-session-cookie>"
# Response (201): { "key": "chnd_sk_...", "prefix": "chnd_sk_abc123..." }

# DELETE /api/user/api-key — revoke current key
curl -X DELETE https://outerfaced.com/api/user/api-key \
  -H "Cookie: <supabase-session-cookie>"
# Response: { "revoked": true }

Section 4: Every Block Type with Full Schema

Every block requires a type field. The id field is optional at creation (auto-assigned UUID) but required when patching. Set explicit IDs on any block you plan to update later.


text

{
  "type": "text",           // required
  "id": "blk_01",          // optional at creation, required for patching
  "content": "### Lead enriched\nSalesforce match found. Score: **94/100**.",  // required — supports markdown
  "size": "md",            // optional — "sm" | "md" | "lg" | "xl" — default: "md"
  "bold": false            // optional — applies bold weight to entire block — default: false
}

badge

{
  "type": "badge",          // required
  "id": "blk_02",          // optional
  "label": "Qualified",    // required
  "variant": "success"     // required — "default" | "success" | "warning" | "error" | "pending"
}

kv

{
  "type": "kv",             // required
  "id": "blk_03",          // optional
  "rows": [                // required
    { "key": "Name", "value": "Jordan Ellis" },
    { "key": "Company", "value": "Meridian Labs" },
    { "key": "Title", "value": "VP of Engineering" },
    { "key": "ARR Potential", "value": "$85,000" },
    { "key": "Source", "value": "Inbound SMS" }
  ]
}

When patching: append_rows adds to existing rows; rows replaces all rows.


image

{
  "type": "image",          // required
  "id": "blk_04",          // optional
  "url": "https://cdn.example.com/leads/jordan-ellis-profile.png",  // required — must be publicly accessible
  "alt": "Jordan Ellis LinkedIn profile screenshot"                  // optional — accessibility alt text
}

divider

{
  "type": "divider",        // required
  "id": "blk_05"           // optional
}

No other fields. Renders a horizontal rule. Use to separate sections of a card.


button

{
  "type": "button",         // required
  "id": "blk_06",          // optional
  "buttons": [             // required
    {
      "id": "btn_qualify",           // required — used to identify which button was clicked in webhook
      "label": "Qualify",            // required
      "style": "primary",            // required — "primary" | "secondary" | "danger"
      "disabled": false              // optional — default: false
    },
    {
      "id": "btn_disqualify",
      "label": "Disqualify",
      "style": "danger",
      "disabled": false
    },
    {
      "id": "btn_callback",
      "label": "Schedule Callback",
      "style": "secondary",
      "disabled": false
    }
  ]
}

input

{
  "type": "input",          // required
  "id": "blk_07",          // optional
  "label": "Notes for handoff",                          // optional
  "placeholder": "Add context for the account exec…",   // optional
  "input_type": "text",    // optional — "text" | "number" | "email" | "url" — default: "text"
  "default_value": "",     // optional — pre-filled value
  "disabled": false,       // optional — default: false
  "required": false,       // optional — default: false
  "clear_on_submit": true, // optional — clears field after submit — default: true
  "send_on_change": false  // optional — fires webhook on every keystroke — default: false
}

select

{
  "type": "select",         // required
  "id": "blk_08",          // optional
  "label": "Disposition",  // optional
  "options": [             // required
    { "value": "hot", "label": "Hot — Call Today" },
    { "value": "warm", "label": "Warm — Follow Up This Week" },
    { "value": "cold", "label": "Cold — Add to Nurture" }
  ],
  "selected_value": "hot", // optional — must match a value in options
  "placeholder": "Choose disposition…",  // optional
  "disabled": false,       // optional — default: false
  "clear_on_submit": false, // optional — default: true (set false to persist selection)
  "send_on_change": true   // optional — fires webhook when selection changes — default: false
}

multiselect

{
  "type": "multiselect",    // required
  "id": "blk_09",          // optional
  "label": "Tags",         // optional
  "options": [             // required
    { "value": "solar", "label": "Solar Interest" },
    { "value": "homeowner", "label": "Homeowner" },
    { "value": "financing", "label": "Needs Financing" },
    { "value": "high-intent", "label": "High Intent" }
  ],
  "selected_values": ["solar", "homeowner"],  // optional — array of pre-selected values
  "max_selections": 3,     // optional — caps how many items can be selected
  "disabled": false,       // optional — default: false
  "clear_on_submit": false, // optional — default: true
  "send_on_change": false  // optional — default: false
}

toggle

{
  "type": "toggle",         // required
  "id": "blk_10",          // optional
  "label": "Send SMS confirmation",  // required
  "enabled": false,        // required — initial on/off state
  "disabled": false,       // optional — default: false
  "description": "Automatically sends a confirmation text to the lead when toggled on.",  // optional — helper text
  "clear_on_submit": false, // optional — default: true
  "send_on_change": true   // optional — fires webhook when toggled — default: false
}

Section 5: Webhook Payload Shapes

All webhooks are POST requests to your channel's webhook_url with Content-Type: application/json. Timeout: 10 seconds. No retries on failure.


Button block click

Fired when a visitor clicks a button inside a card's button block.

{
  "interaction_id": "intr_9f3a1bc2d4e5",
  "channel_id": "chnl_4d72e1a3b9c0",
  "card_id": "card_7b2f4a1d3e8c",
  "action_id": null,
  "type": "button_click",
  "payload": {
    "button_id": "btn_qualify",
    "label": "Qualify"
  },
  "timestamp": "2024-11-15T14:32:07.412Z"
}
  • card_id — the card containing the clicked button block.
  • action_id — always null for button block clicks.
  • payload.button_id — matches the id you set on the button object inside the block. Use this to branch your automation logic.
  • payload.label — the button's display label at click time.

Channel action button click

Fired when a visitor clicks a persistent action button in the channel header (not attached to any card).

{
  "interaction_id": "intr_2c8d5e1a7f04",
  "channel_id": "chnl_4d72e1a3b9c0",
  "card_id": null,
  "action_id": "actn_1e3b5d7f9a2c",
  "type": "header_action",
  "payload": {
    "label": "Refresh All"
  },
  "timestamp": "2024-11-15T14:35:22.881Z"
}
  • card_id — always null. Header actions are channel-level, not card-level.
  • action_id — ID of the channel action that was clicked.
  • payload.label — the action button's label.

Reply submission

Fired when a visitor submits an input block (with send_on_change: false, the default). This is a form submission — the user typed something and clicked submit.

{
  "interaction_id": "intr_3d9e2fb1c507",
  "channel_id": "chnl_4d72e1a3b9c0",
  "card_id": "card_7b2f4a1d3e8c",
  "action_id": null,
  "type": "reply",
  "payload": {
    "block_id": "blk_07",
    "block_type": "input",
    "value": "Customer confirmed they own their home. Good fit for solar lease program."
  },
  "timestamp": "2024-11-15T14:38:54.001Z"
}

Input/select/multiselect/toggle change (send_on_change)

Fired when a visitor changes a block that has send_on_change: true. Fires on every change, not on explicit submit.

{
  "interaction_id": "intr_5a1c3e7d9b2f",
  "channel_id": "chnl_4d72e1a3b9c0",
  "card_id": "card_7b2f4a1d3e8c",
  "action_id": null,
  "type": "input_change",
  "payload": {
    "block_id": "blk_08",
    "block_type": "select",
    "value": "hot"
  },
  "timestamp": "2024-11-15T14:40:11.233Z"
}
  • payload.block_type"input" | "select" | "multiselect" | "toggle"
  • payload.value — string for input/select, string array for multiselect, boolean for toggle

For multiselect change:

{
  "payload": {
    "block_id": "blk_09",
    "block_type": "multiselect",
    "value": ["solar", "homeowner", "high-intent"]
  }
}

For toggle change:

{
  "payload": {
    "block_id": "blk_10",
    "block_type": "toggle",
    "value": true
  }
}

Section 6: Complete End-to-End Example

Scenario: SMS lead qualification. An inbound SMS arrives, your n8n/Make workflow runs enrichment, pushes a review card to a channel, and waits for a human to qualify or disqualify the lead before continuing.


Step 1: Create the channel (once, at setup)

curl -X POST https://outerfaced.com/api/v1/channels \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SMS Lead Review",
    "description": "Inbound SMS leads. Qualify or disqualify before handoff to AE.",
    "mode": "interactive",
    "icon": "📱",
    "webhook_url": "https://your-server.com/webhooks/sms-qualification"
  }'

# Save the returned channel id: "chnl_4d72e1a3b9c0"
# Share the URL: https://outerfaced.com/c/chnl_4d72e1a3b9c0

Step 2: Push a card when a new SMS lead arrives

Your automation runs enrichment, then pushes a card:

curl -X POST https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "pending",
    "blocks": [
      {
        "type": "text",
        "id": "blk_01",
        "content": "### New SMS Lead — Jordan Ellis\nEnrichment complete. Score: **91/100**. Review below.",
        "size": "md"
      },
      {
        "type": "badge",
        "id": "blk_02",
        "label": "Awaiting Review",
        "variant": "pending"
      },
      {
        "type": "kv",
        "id": "blk_03",
        "rows": [
          { "key": "Phone", "value": "+1 (555) 867-5309" },
          { "key": "SMS Message", "value": "Saw your ad about solar panels. Own my home in Phoenix. Interested." },
          { "key": "Name", "value": "Jordan Ellis (enriched)" },
          { "key": "Company", "value": "Self-employed" },
          { "key": "Home Value", "value": "$420,000 (Zillow)" },
          { "key": "Roof Age", "value": "~6 years (permit data)" }
        ]
      },
      {
        "type": "divider",
        "id": "blk_div"
      },
      {
        "type": "button",
        "id": "blk_04",
        "buttons": [
          { "id": "btn_qualify", "label": "Qualify — Send to AE", "style": "primary", "disabled": false },
          { "id": "btn_disqualify", "label": "Disqualify", "style": "danger", "disabled": false },
          { "id": "btn_more_info", "label": "Need More Info", "style": "secondary", "disabled": false }
        ]
      }
    ]
  }'

# Save: "id": "card_7b2f4a1d3e8c"

The reviewer opens https://outerfaced.com/c/chnl_4d72e1a3b9c0, sees the card appear in real time, and clicks Qualify — Send to AE.


Step 3: Your webhook receives the interaction

Outerfaced POSTs to https://your-server.com/webhooks/sms-qualification:

{
  "interaction_id": "intr_9f3a1bc2d4e5",
  "channel_id": "chnl_4d72e1a3b9c0",
  "card_id": "card_7b2f4a1d3e8c",
  "action_id": null,
  "type": "button_click",
  "payload": {
    "button_id": "btn_qualify",
    "label": "Qualify — Send to AE"
  },
  "timestamp": "2024-11-15T14:32:07.412Z"
}

Your handler branches on payload.button_id:

  • "btn_qualify" → create CRM contact, assign to AE, send confirmation SMS
  • "btn_disqualify" → mark lead dead, send opt-out SMS
  • "btn_more_info" → add to drip sequence, flag for follow-up

Step 4: Update the card to reflect the decision

After your automation handles the interaction, patch the card to show what happened:

# Update badge to "success" + disable all buttons + append decision row to KV
curl -X PATCH https://outerfaced.com/api/v1/channels/chnl_4d72e1a3b9c0/cards/card_7b2f4a1d3e8c \
  -H "Authorization: Bearer chnd_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "success",
    "blocks": [
      {
        "id": "blk_02",
        "patch": {
          "label": "Qualified",
          "variant": "success"
        }
      },
      {
        "id": "blk_03",
        "patch": {
          "append_rows": [
            { "key": "Decision", "value": "Qualified by Sarah K." },
            { "key": "AE Assigned", "value": "[email protected]" },
            { "key": "CRM Contact", "value": "salesforce.com/c/003xx0000..." },
            { "key": "SMS Sent", "value": "Confirmation sent at 14:32 UTC" }
          ]
        }
      },
      {
        "id": "blk_04",
        "patch": {
          "buttons": [
            { "id": "btn_qualify", "disabled": true },
            { "id": "btn_disqualify", "disabled": true },
            { "id": "btn_more_info", "disabled": true }
          ]
        }
      }
    ]
  }'

The reviewer sees the card update live — badge turns green, buttons go grey, KV table shows the outcome. No reload needed.


Section 7: Common Patterns


Human-in-the-loop approval

# 1. Push card with Approve/Reject buttons
curl -X POST .../cards -d '{ "blocks": [
  { "type": "text", "id": "t1", "content": "**Wire transfer $12,400** to Acme Corp. Approve?" },
  { "type": "kv", "id": "k1", "rows": [{ "key": "Amount", "value": "$12,400" }, { "key": "Recipient", "value": "Acme Corp — Bank of America" }] },
  { "type": "button", "id": "b1", "buttons": [{ "id": "approve", "label": "Approve", "style": "primary" }, { "id": "reject", "label": "Reject", "style": "danger" }] }
]}'

# 2. Webhook fires with button_id "approve" or "reject"
# 3. PATCH card: update badge to result, disable buttons

Live status update

# Push a card with a "Running" badge, then PATCH as stages complete
curl -X PATCH .../cards/card_abc -d '{
  "status": "warning",
  "blocks": [
    { "id": "status_badge", "patch": { "label": "Step 2 / 5 — Enriching contacts", "variant": "warning" } },
    { "id": "progress_kv", "patch": { "append_rows": [{ "key": "14:31 UTC", "value": "Salesforce sync complete" }] } }
  ]
}'
# Repeat for each step; final PATCH sets status success, badge "Complete"

Client reporting card

# Push a weekly summary card with KV + image + badge — no interactive blocks needed
curl -X POST .../cards -d '{
  "status": "success",
  "blocks": [
    { "type": "text", "id": "t1", "content": "## Weekly SEO Report — Week of Nov 11", "size": "lg" },
    { "type": "badge", "id": "b1", "label": "All Targets Met", "variant": "success" },
    { "type": "kv", "id": "k1", "rows": [
      { "key": "Organic Sessions", "value": "12,440 (+18%)" },
      { "key": "New Backlinks", "value": "34" },
      { "key": "Avg Position", "value": "6.2 (was 8.1)" }
    ]},
    { "type": "image", "id": "i1", "url": "https://cdn.example.com/charts/seo-week-nov11.png", "alt": "Traffic chart" }
  ]
}'

Multi-step form across cards

# Card 1: collect initial info, send_on_change false, button to submit
# Webhook fires with reply payload → validate → push Card 2 with next question
# Card 2: deeper qualification with select + input
# Final webhook fires → push confirmation card, send result to CRM

# Card 1
curl -X POST .../cards -d '{ "blocks": [
  { "type": "text", "id": "t1", "content": "**Step 1 of 3** — What is your monthly energy bill?" },
  { "type": "input", "id": "inp1", "label": "Monthly bill (USD)", "input_type": "number", "placeholder": "e.g. 220", "clear_on_submit": false },
  { "type": "button", "id": "b1", "buttons": [{ "id": "next", "label": "Next →", "style": "primary" }] }
]}'

# After webhook fires for "next" click, push Card 2 with the next question...

Quick Reference

Resource Daily Limit
Channels created 50 / day
Cards created 200 / day
Replies 100 / day
Workspaces 3 per user (total)

Limits reset at midnight UTC.

Channel URL pattern: https://outerfaced.com/c/<channelId>

Error shape: { "error": "ErrorType", "message": "human-readable detail" }

Common status codes: 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Rate Limited, 500 Internal Server Error