Public reference · REST + MCP

the api, in plain english

Drop new signups into a personalised demo, or let an agent author demos through MCP. Both surfaces share one implementation — everything here applies to the REST API and the hosted MCP server. Building an MCP server for the MyAISE assistant instead? See the MCP integration guide.

RESTapi.myaise.com/v1 MCPmcp.myaise.com Streamviewer.myaise.com
You'll need a Solo or AI SE workspace to make requests.

This page is the read-only mirror. The authenticated version at /dashboard/docs/api cross-references your live keys + webhooks.

See pricing

Drop new signups into a personalised demo, or let an AI agent author demos through MCP. Both surfaces share one implementation — everything here applies to the REST API and the hosted MCP server. Building an MCP server for the MyAISE assistant instead? See the MCP integration guide.

RESTapi.myaise.com/v1 MCPmcp.myaise.com Streamviewer.myaise.com
Section 01

Quick start #

Three commands and you have a working integration. Get a key, ping, list projects — that's it.

1 · Get an API key #

Visit /dashboard/api-keysCreate key. The secret is shown once — copy it into your secret store immediately.

Show-once secret. MyAISE never stores the plaintext. Lose it and you rotate.

2 · Smoke test #

bash · curl
curl https://api.myaise.com/v1/ping \\
  -H "Authorization: Bearer capa_live_…"

3 · List your projects #

bash · curl
curl https://api.myaise.com/v1/projects \\
  -H "Authorization: Bearer capa_live_…"
Section 02

Authentication #

Every request carries a workspace-scoped Bearer token. Keys are workspace-scoped — a team key can never see a personal workspace.

http · header
Authorization: Bearer capa_live_<prefix>_<random>

Scopes #

ScopeWhat it grants
workspace:readWorkspace metadata.
content:readRead projects, demos, rules, and assets.
content:writeCreate or change ordinary content.
sessions:readRead viewer sessions.
sessions:writeCreate and manage viewer sessions.
sessions:credentials:submitSubmit only client-encrypted viewer credential payloads.
webhooks:readRead webhook configuration and delivery status.
webhooks:manageOwner-only webhook management and redrive.
whitelabel_onlyIsolated partner scope for theme verification; it cannot be combined with workspace scopes.

Studio (free) #

Free workspaces get 402 Payment Required on every /v1 and /mcp request. Upgrade to Solo or AI SE to enable API access.
Section 03

Concepts #

Six resources, in nesting order. Everything else is a child or a side-effect.

ws_ · ws_user_ workspace Personal or team. One key = one workspace.
proj_ project A site + its rewriting rules. Owns demos.
demo_ demo A multi-step product tour. Embeds steps[].
step_ step One page of the tour. Embeds overlays + entry actions.
ov_ overlay A tooltip / modal / card layered on the page.
sess_ session A live, isolated browser running the demo for one prospect.

ID prefixes #

Every ID is prefixed so you can spot the resource by eye — no need to remember which is which.

  • ws_team workspace
  • ws_user_personal workspace
  • proj_project
  • demo_demo
  • step_step
  • ov_overlay
  • act_action
  • sess_viewer session
  • wh_webhook
  • del_delivery
Worked example

Embed MyAISE in a signup flow #

A new prospect signs up on your site. Your backend POSTs to MyAISE to mint a session URL, then redirects the prospect there instead of an empty account.

Use a sessions_only key for this integration.

Flow

Signup → live demo, in one round-trip

The Idempotency-Key header makes retries safe — the cloud replays the original response without spinning a second viewer machine.

  1. Prospect submits your signup form.
  2. Your backend POSTs /sessions with their identity + encrypted cookies.
  3. MyAISE returns a one-shot viewer_url.
  4. Redirect — they land in a fully personalised, live demo.
javascript · node 18+
// mint a session, redirect prospect
const res = await fetch(
  "https://api.myaise.com/v1/demos/demo_abc.../sessions",
  {
    method:  "POST",
    headers: {
      "Authorization":   `Bearer ${process.env.CAPARTIST_API_KEY}`,
      "Content-Type":    "application/json",
      "Idempotency-Key": prospect.id   // safe to retry
    },
    body: JSON.stringify({
      prospect: {
        name:    prospect.name,
        email:   prospect.email,
        company: prospect.company
      },
      encrypted_payload:  encryptCookies(cookies, capartistPubkey),
      expires_in_seconds: 3600
    })
  }
);

const { data } = await res.json();
res.redirect(data.viewer_url);
Worked example

A live demo before signup (website integration) #

A prospect on your marketing site fills in name, email and company. Your backend provisions a throwaway demo account inside your own app, encrypts a one-time login URL for it, creates a MyAISE session with a time limit, an AI-credit budget and a host allowlist, binds an MCP grant so the AI SE can act as that demo user, redirects the prospect to the viewer, and cleans up when the session.ended webhook arrives.

Before you start

  • An AI SE workspace with MyAISE (the assistant) enabled.
  • An API key with sessions:write + sessions:credentials:submit — a sessions_only key is exactly that set.
  • A webhook subscribed to session.ended (and optionally session.budget_exhausted).
  • The id of the demo to launch (demo_…).
  • Optionally, the workspace Website integration policy in the dashboard — default / maximum TTL, default / maximum credit budget, allowed-hosts mode. Values you send are clamped to it.
Flow

Marketing form → live demo → cleanup

Eight steps, four of them HTTP calls. Everything the prospect touches is disposable: a demo account you own, a session that expires, and a grant that dies with it.

  1. The prospect submits your form. Protect it — CAPTCHA, a honeypot field, per-IP and per-email rate limits, and a cap on concurrent live demos.
  2. Provision a demo account in your own app and mint a single-use, short-lived login URL for it.
  3. GET /viewer/public_key — the RSA key you wrap this session's AES key with.
  4. Encrypt { base_url, cookies, headers } into the sealed envelope.
  5. POST /demos/:id/sessions with the envelope, the time limit, the credit budget and the host allowlist.
  6. POST /customer_app/mcp_handoffs with viewer_session_id, so the AI SE acts as the demo user.
  7. Redirect the prospect (303) to the returned viewer_url.
  8. On session.ended, revoke the grant and purge the demo account.
http · the calls, in order
# your form handler
POST https://you.example/demo-requests

# provision the demo account, then:
GET  https://api.myaise.com/v1/viewer/public_key
POST https://api.myaise.com/v1/demos/demo_b3d2.../sessions
POST https://api.myaise.com/v1/customer_app/mcp_handoffs

# hand the prospect over
303  Location: <viewer_url>

# later, from MyAISE to you
POST https://you.example/webhooks/myaise   # session.ended

Fetch the public key #

Every workspace has an RSA public key (PEM) held by its viewer key provider. You wrap the per-session AES key with it; the matching private key never leaves the key provider, and the cloud only ever holds the sealed envelope.

bash · curl
curl https://api.myaise.com/v1/viewer/public_key \
  -H "Authorization: Bearer $CAP_KEY"

Fetch it per session and never persist it — the key is regenerated in development and rotated in production, and a stale key produces a session whose credentials can never be unsealed. The response spells out the whole envelope contract (padding, hashes, IV and tag sizes, the plaintext schema) so you never have to guess; the full shape is in the endpoint reference. If the key provider is unreachable you get 503 key_unavailable — retry. There is no plaintext fallback: plaintext credentials are rejected.

Encrypt the login #

The envelope is fixed:

  • A random 32-byte AES key and a random 12-byte IV, both fresh for every session.
  • AES-256-GCM over the UTF-8 JSON plaintext with empty additional data (AAD = ""), producing a 16-byte tag.
  • The AES key wrapped with RSA-OAEP — SHA-256 hash, MGF1-SHA-256, no label.
  • Every field base64 (standard alphabet, padded), sent as encrypted_key, iv, ciphertext, tag.

The plaintext is one small JSON object:

json · plaintext
{
  "base_url": "https://app.example.com/demo/enter/<one-time-token>",
  "cookies": [],
  "headers": {}
}

Prefer a one-time login URL over real cookies — it is the smallest thing you can hand over and it dies on first use. When you do need cookies, each entry is { name, value, domain, path, secure, httpOnly }; headers are sent on every request to the base_url host. The viewer machine consumes the credentials exactly once, and the cloud stores only the sealed envelope — never the plaintext, never the AES key.

Both examples are stdlib only — Ruby 3.x on OpenSSL 3, and Node 18+ on node:crypto. (On Ruby 3.4+ base64 is a bundled gem: add it to your Gemfile, or swap those four calls for [bytes].pack("m0").)

ruby · seal.rb
require "openssl"
require "json"
require "base64"

def seal_for_myaise(plaintext_hash, public_key_pem)
  cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt
  key = cipher.random_key          # 32 bytes
  iv  = cipher.random_iv           # 12 bytes
  cipher.auth_data = ""
  ciphertext = cipher.update(JSON.generate(plaintext_hash)) + cipher.final
  tag = cipher.auth_tag            # 16 bytes

  rsa = OpenSSL::PKey.read(public_key_pem)
  wrapped = rsa.encrypt(key,
    rsa_padding_mode: "oaep",
    rsa_oaep_md: "sha256",
    rsa_mgf1_md: "sha256")         # never public_encrypt: that is SHA-1 OAEP

  {
    encrypted_key: Base64.strict_encode64(wrapped),
    iv:            Base64.strict_encode64(iv),
    ciphertext:    Base64.strict_encode64(ciphertext),
    tag:           Base64.strict_encode64(tag)
  }
end
javascript · seal.mjs
import { randomBytes, createCipheriv, publicEncrypt, constants } from "node:crypto";

export function sealForMyaise(plaintext, publicKeyPem) {
  const key = randomBytes(32);
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  cipher.setAAD(Buffer.alloc(0));
  const ciphertext = Buffer.concat([cipher.update(JSON.stringify(plaintext), "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  const wrapped = publicEncrypt(
    { key: publicKeyPem, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
    key
  );
  return {
    encrypted_key: wrapped.toString("base64"),
    iv: iv.toString("base64"),
    ciphertext: ciphertext.toString("base64"),
    tag: tag.toString("base64"),
  };
}
Ruby's rsa.public_encrypt cannot build this envelope — it is PKCS#1 v1.5 by default, and OAEP-SHA-1 with the OAEP padding constant. The cloud rejects both. Use rsa.encrypt with rsa_padding_mode: "oaep", rsa_oaep_md: "sha256" and rsa_mgf1_md: "sha256", as above.

Create the session #

POST /demos/:demo_id/sessions, with an Idempotency-Key header so a retried POST replays the original response instead of spinning a second viewer machine.

json · request body
{
  "prospect": {
    "name": "Sarah Chen",
    "email": "sarah@acme.com",
    "company": "Acme",
    "metadata": { "lead_id": "ld_91" }
  },
  "encrypted_payload": { "encrypted_key": "…", "iv": "…", "ciphertext": "…", "tag": "…" },
  "expires_in_seconds": 1200,
  "credit_budget": 100,
  "allowed_hosts": ["app.example.com", "*.app.example.com"]
}

A 201 comes back with the one-time viewer URL and the limits as they were actually applied:

json · 201 Created
{
  "data": {
    "id": "sess_7a2e...",
    "token": "v1_AbCd...",
    "status": "pending",
    "viewer_url": "https://viewer.myaise.com/view/v1_AbCd...",
    "expires_at": "2026-09-06T14:30:00Z",
    "expires_in_seconds": 1200,
    "credit_budget": 100,
    "credits_spent": 0,
    "allowed_hosts": ["app.example.com", "*.app.example.com"],
    "demo_id": "demo_b3d2...",
    "project_id": "proj_e72b...",
    "workspace": { "type": "team", "id": "ws_3f1c..." }
  }
}

The three limits:

  • Time limit. expires_in_seconds is clamped to the workspace maximum, so read expires_at and expires_in_seconds back from the response instead of assuming. When the clock runs out the session ends with end_reason: "ttl_expired".
  • Credit budget. The AI SE's answers, walkthrough generation and agent turns each cost credits (see AI credits). The turn that reaches the budget is the last one, and the session ends with end_reason: "credit_budget_exhausted". Live-viewer session hours are metered separately and are not part of this budget.
  • Host allowlist. The streamed browser refuses top-level navigations to any other host and bounces back to the demo's base URL — links to your marketing site, your docs or an external site cannot carry the prospect away. Page subresources from other hosts still load.

Errors worth handling: 400 invalid_expires_in and 400 invalid_credit_budget for values that are not positive integers; 422 credit_budget_exceeds_policy when the budget is above the workspace maximum; 422 invalid_allowed_hosts when the list is not an array, holds more than 20 entries, or contains an entry that is not a hostname (the offending entry is named in the error message).

viewer_url is a bearer secret. Never log it, never email it, never put it in a query string you keep. Redirect once, straight from the response.

Bind the MCP grant #

If your app exposes an MCP server, the AI SE can answer "how many campaigns do I have?" by calling it as the demo user. Mint a short-lived grant scoped to the demo account you just provisioned — expiring no later than the session, with a tool-call cap — and push it server-side with viewer_session_id from the previous response:

json · POST /customer_app/mcp_handoffs
{
  "endpoint_url": "https://mcp.example.com/rpc",
  "access_token": "demo_user_grant_...",
  "expires_at": "2026-09-06T14:30:00Z",
  "app_ref": "example-app",
  "account_id": "demo_acct_91",
  "scopes": ["read_only"],
  "viewer_session_id": "sess_7a2e...",
  "tool_catalog": { "tools": [ /* your MCP tools/list result for this grant */ ] }
}

tool_catalog is what the AI SE can call: pass the tools/list result your server returns for this grant (MCP shape — name, description, inputSchema, annotations). MyAISE never calls tools/list itself, so a handoff without a catalog gives the AI SE zero tools.

The grant is bound to that prospect's viewer session, so it dies with the session. Re-pushing with the same viewer_session_id rotates the grant. Because the binding is authorised by the same scope that created the session, sessions:write is enough here — a sessions_only key can push a session-bound handoff, while an unbound one still needs content:write. Building the MCP server itself is covered in the MCP integration guide.

Handle session.ended #

Every terminal session fires the specific event plus the meta session.ended:

json · session.ended
{
  "id": "evt_9b1f...",
  "type": "session.ended",
  "created_at": "2026-09-06T14:21:03Z",
  "workspace_id": "ws_3f1c...",
  "api_version": "2026-06-01",
  "data": {
    "session_id": "sess_7a2e...",
    "status": "expired",
    "completion_type": "budget_exhausted",
    "end_reason": "credit_budget_exhausted",
    "ended_at": "2026-09-06T14:21:02Z",
    "seconds": 742,
    "credits_spent": 100,
    "credit_budget": 100,
    "allowed_hosts": ["app.example.com", "*.app.example.com"],
    "expires_at": "2026-09-06T14:30:00Z",
    "prospect": {
      "name": "Sarah Chen",
      "email": "sarah@acme.com",
      "company": "Acme",
      "metadata": { "lead_id": "ld_91" }
    },
    "demo_id": "demo_b3d2...",
    "project_id": "proj_e72b..."
  }
}

end_reason is one of:

ReasonWhat happened
ttl_expiredThe session hit its time limit.
credit_budget_exhaustedThe AI credit budget ran out. A session.budget_exhausted event fires just before session.ended.
runtime_incompatibleThe viewer fleet could not honour a requested feature — rare.
revokedYou deleted the session.
demo_completedThe prospect reached the end of the demo.

On receipt:

  1. Verify the signature (see Signature verification) before trusting anything in the body.
  2. Be idempotent on the event id — retries and replays are expected.
  3. Revoke the MCP grant.
  4. Disable or soft-delete the demo account immediately; hard-purge it after a grace period.

Run a sweeper too, for sessions whose webhook you never received: the session show endpoint reports status, end_reason and ended_at, so a periodic reconcile over your own open demo accounts closes the gap.

What the prospect sees #

A branded viewer page — your logo, colours and splash from the white-label theme — with a countdown pill showing the time left that turns amber under five minutes, and the AI SE chat alongside the live app. When the session ends they get an end screen keyed on end_reason: "Your demo time is up" for the time limit, an "AI budget has been used up" message for the credit budget, "Thanks for watching" when the demo completed, and your demo's exit URL if it defines one. Nothing about credits, hosts or policy is exposed beyond the countdown and that end screen.

Teams with a custom end page in their white-label theme keep it. Add <ca-slot name="end_headline">, <ca-slot name="end_subtitle"> or <ca-slot name="end_reason"> to that page and the viewer fills in why the session ended; slots the page does not use cost nothing.

Security checklist #

  • Login URLs are one-time and short-lived (five minutes or less), stored on your side as digests, never in plaintext.
  • Demo accounts are least-privilege: no billing, no team management, no API keys, no purchasing.
  • Ship only the cookies the demo needs, and only for the demo account.
  • viewer_url is a bearer secret — redirect once, never log or email it.
  • Allow only the hosts the demo needs; every extra host is somewhere a prospect can wander.
  • Rate-limit and CAPTCHA the form, and cap concurrent sessions per IP, per email and per workspace.
  • Keep prospect.metadata to what you would put in a CRM — no PII beyond that.
  • Verify webhook signatures, tolerate replays, and expect the same event id more than once.
  • Purge demo data on a schedule, not only on the webhook.
  • Never log the API key, the encrypted payload, or the login token.
Worked example

AI-design a demo via MCP #

Point Claude Desktop (or any MCP client) at mcp.myaise.com with the same Bearer key. The model authors demos the same way your backend would.

1 · Wire up the client

json · claude-desktop.json
{
  "mcpServers": {
    "myaise": {
      "transport": "http",
      "url":       "https://mcp.myaise.com",
      "headers": {
        "Authorization": "Bearer capa_live_..."
      }
    }
  }
}

2 · Ask the model

"I'm building a product tour for our pricing page. Use the design_demo prompt with my pricing URL, then create the demo."

3 · What it does, step-by-step

  1. tools/list — sees the full tool surface.
  2. The design_demo prompt — gets a structured prompt template covering URL flow + overlay copy guidelines.
  3. projects.create, then demos.create with nested steps + overlays.
  4. For anchored overlays, attaches target_context (+ an optional baseline screenshot) so the demo can self-heal — see “Drift protection” below.
  5. sessions.create to actually launch the demo.
Every mutating MCP tool routes through the same context function the REST controller would — so the AI can't produce an invalid demo any more easily than your backend can. On initialize, the server returns an instructions block describing this whole flow, so MCP clients prime the model automatically.

4 · Drift protection (self-healing) #

Customer sites change. When an overlay's target element moves or its selector breaks, MyAISE re-locates it using the overlay's target_context — a snapshot of the element's anchors. To make an agent-authored demo durable:

  1. Set auto_healing_enabled: true on the demo (demos.create / demos.update).
  2. Give every anchored overlay (one with a target_selector) a target_context on overlays.create — capture it from the live page you authored against:
json · target_context
"target_context": {
  "selectorCandidates": [
    { "kind": "id", "selector": "#new-report", "rank": 0 },
    { "kind": "role-name", "selector": "button[name=\"New report\"]", "rank": 2 }
  ],
  "role": "button",
  "accessibleName": "New report",
  "textContent": "New report",
  "nearbyText": ["Reports", "Create"],
  "rect": { "x": 120, "y": 340, "width": 96, "height": 32 }
}

Optionally pass a base64 screenshot alongside it; MyAISE stores it as a baseline demo asset and references it from target_context.screenshotRef for visual diffing. Re-baseline after a site change with overlays.update (a screenshot-only update keeps the existing anchors). Floating overlays (modal/banner with no target_selector) don't need this.

No credentials needed at authoring time. The baseline is credential-free — you supply it from the page you already loaded. Drift detection runs later inside the prospect's authenticated viewer session (the credentials from sessions.create), so it never needs the agent's cookies.
Section 06

Embed library #

Drop a <script> tag in your own product and run MyAISE-authored demos on your customer's REAL accounts. Overlays + transitions render on your DOM — no proxy hop, no PII upload, no screen recording. Team-tier only.

Embed mode never dispatches synthetic events on your customer's DOM. Demos that depend on pre-demo / driven actions render as informational overlays — your prospect advances with the library's Next button.

Install #

One bundle — capartist.min.js — does everything: overlay rendering, CSS step transitions, and the full Three.js transition set. Each demo's custom themes / animations / transitions are fetched at launch as a per-demo asset pack (one request, SRI-verified), so the bundle stays the same size no matter how customized your demos are. Strict-CSP-safe — no unsafe-eval, no new Function. Load from our CDN (versioned) or self-host with SRI:

html · CDN
<script src="https://myaise.com/embed/v1/capartist.min.js"></script>
<script>
  MyAISE.init({ apiKey: "capa_pub_xxx" });

  document.querySelector("#show-me-tour").addEventListener("click", () => {
    MyAISE.launch("your-demo-uuid");
  });
</script>

Or self-host (recommended for strict CSP):

html · self-host + SRI
<script
  src="/static/capartist.min.js"
  integrity="sha384-zwqsKNp7ntF5Pefjpm78C0inV5rxsuKZYeLfnYy+E10xU2MixL+YFEFADYD5FXm8"
  crossorigin="anonymous"
></script>

npm:

shell · npm
npm i @capartist/embed

From a bundler:

javascript · npm
// One bundle — overlays, CSS + Three.js transitions, per-demo asset packs.
import { init, launch } from "@capartist/embed";

Publishable keys #

Embed mode uses publishable capa_pub_* keys — different from the secret capa_live_* keys you use server-side. Generate one in Studio under Embed:

  • Domain allowlist: customer.com, *.customer.com. A leaked key can't be used from any other origin.
  • Demo allowlist: pin a key to one or more demo UUIDs.
  • Team-scoped: publishable keys belong to teams (AI SE plan required).

Publishable keys are designed to live in your HTML — they're usage scoping, not protection. Treat the demo spec as public content (your customer's visitors can see it anyway).

For tighter setups, mint short-lived launch JWTs from your backend:

javascript · jwt mode
MyAISE.init({
  jwt: await fetch("/api/capartist-token").then(r => r.text()),
  jwtRefresh: async () => fetch("/api/capartist-token").then(r => r.text()),
});

JWTs are HS256-signed with a per-key secret OR Ed25519-signed against a public key you register with us. Required claims: iss, aud=capartist.embed, typ=embed_access, sub=<demo_id>, kid (header), origin, team_id, jti, exp at most 5 minutes out. Optional: end_user_ref, context, and the credit claims.

Per-user credits #

When the assistant is embedded in your product, every answer draws on your workspace balance. Per-user allowances let you decide how much of it each of your users may spend — sell credits monthly, grant a fixed pool, or block a user — without giving anyone unbridled access.

Allowances need JWT mode: the user is identified by the end_user_ref claim (or sub). Three optional claims set the allowance for that user:

ClaimMeaning
credit_allowanceWhole number ≥ 0. Credits the user may spend per period. 0 blocks the user; omit it to use the workspace default (no cap unless you set one).
credit_period"monthly" (default — resets on the first of each UTC month) or "total" (never resets; raise the allowance to top up).
credit_period_keyYour own billing-cycle key, e.g. "2026-09-15". When it changes, the user's spend resets to zero. Implies a custom period.

The claim wins over an allowance set through the API, which wins over the workspace default; a JWT without credit claims never overwrites an API-set allowance. Every claim is clamped to the maximum allowance in the dashboard (Assist → Website integration → Embedded assistant), and invalid claims refuse the exchange with 400 invalid_credit_claims rather than silently granting unlimited credits.

javascript · jwt claims with an allowance
// Your backend, when rendering the page for a signed-in user:
const claims = {
  iss: "app.example.com", aud: "capartist.embed", kid: PUBLISHABLE_KEY_ID,
  origin: "https://app.example.com", team_id: TEAM_ID,
  sub: user.id,                      // the end_user_ref
  jti: crypto.randomUUID(), exp: now + 300,
  context: { key: account.id, label: account.name },
  credit_allowance: user.plan.aiCredits,   // e.g. 300 per month
  credit_period: "monthly",                // or "total"
  // credit_period_key: user.billingCycleStart  // your own cycle instead of calendar months
};

What a turn costs. An embedded answer costs 30 credits (a live-MCP agent turn), a confirmed customer-app action's verification the same, and a generated walkthrough 8. The turn that reaches the allowance is the last one — spend can exceed the allowance by less than one turn, and remaining is reported as 0. A user whose allowance cannot cover a walkthrough still gets the answer. Your workspace balance is charged as usual after the user's reservation; if the workspace itself is out of credits the reservation is returned.

What the widget shows. The exchange, the chat list and every turn response carry a credits object (null when the user has no cap): allowance, spent, remaining, period, period_key, resets_at and your out_of_credits message + link. The widget renders a "N credits left" pill in the header (amber under 10 %) and, once the user is out, locks the composer and shows your message with a "Get more credits" button pointing at your top-up page — both set in the dashboard. A turn refused for this reason returns 402 with code: "user_allowance_exhausted" and the same credits object; the workspace-level out_of_credits 402 is unchanged and unrelated.

json · 402 user_allowance_exhausted
{
  "error": {
    "type": "user_allowance_exhausted",
    "code": "user_allowance_exhausted",
    "message": "You've used this month's AI credits.",
    "url": "https://app.example.com/billing/credits",
    "credits": {
      "allowance": 300, "spent": 300, "remaining": 0,
      "period": "monthly", "period_key": "2026-09",
      "resets_at": "2026-10-01T00:00:00Z",
      "out_of_credits": { "message": "You've used this month's AI credits.", "url": "https://app.example.com/billing/credits" }
    }
  }
}

Managing allowances from your backend. GET /v1/embed/end_users/:end_user_ref/credits reads a user's standing; PUT sets credit_allowance, credit_period / credit_period_key, and reset_spent: true zeroes the counter (a refund or a new plan). Both use your secret key. When a user runs out you receive the embed.end_user_credits_exhausted webhook once per period with end_user_ref, the standing and resets_at — the hook for an upsell email or an in-app nudge. See the endpoint reference.

Step transitions #

Per-step transitions are stored as an object on the demo's step:

json · step.transition
{
  "id": "step-2",
  "url_pattern": "/dashboard/billing*",
  "transition": {
    "wipe": "cube",
    "bg_color": "#0c0a20"
  },
  "overlays": [ ... ]
}

The catalog has three kinds, all rendered by the one bundle:

  • Simple — pure CSS. Five ids: none, blur, zoom_css, slide, circle_css.
  • 2D — Three.js fragment shaders on a flat quad. dissolve, glitch, liquid, and dozens more.
  • 3D — true depth + multi-mesh scenes. cube, flip, fold, cosmic and others.

The runtime bundles a handful of scenes (dissolve, cube, flip, glitch); every other Three.js/html-doc transition arrives in the demo's asset pack and is registered at launch — still no eval. Model-backed transitions such as koi ship their html_doc plus a CORS model_url in the pack.

Overlay animations #

Each overlay can pick from 13 enter animations (matching the desktop studio's OVERLAY_ANIMATIONS list). The renderer applies a per-overlay-type default; set animation on the overlay to override:

json · overlay.animation
{
  "id": "hero-modal",
  "type": "modal",
  "title": "Welcome to Acme",
  "animation": "elastic",
  "body": "Three minutes to your first dashboard."
}

The full list of wire ids:

  • fade — quick opacity fade. Default for spotlights, outlines, hotspots, number badges, banners.
  • scale — opacity + scale-in from 0.5 with a tiny overshoot.
  • slide-up / slide-down / slide-left / slide-right — translate from off-axis.
  • rise — translateY + perspective + a blur dropoff. Default for tooltips, callouts, slideouts.
  • fly-in-3d — Z-axis fly-in with rotation + blur. Default for driven_action overlays.
  • portal — perspective scale + rotate-X. Default for modals.
  • elastic — scale-in with a rubber-band overshoot.
  • glitch — clip-path stutter + RGB color shift.
  • zoom-spin — scale from 0 + 180° rotation.
  • parallax — minimal translateY for content-heavy slideouts.

Unknown ids fall back to the per-type default (no error). Class names are .ca-enter-<id> exactly — easy to inspect in devtools.

Demo-level config #

Every demo carries a target_runtime field that the studio's picker filter consults and that gates which assets are embed-eligible (all embed targets load the same capartist.min.js):

  • viewer (default) — the desktop / cloud viewer pool. All transitions available.
  • embed_lite — customer-site embed; simple CSS transitions + overlays.
  • embed_pro — customer-site embed; adds Three.js scenes (bundled + pack-delivered) on top.

Set it on create or patch:

http · POST /v1/projects/:id/demos
POST /v1/projects/proj_<uuid>/demos
Content-Type: application/json

{
  "name": "Acme onboarding",
  "target_runtime": "embed_pro",
  "theme_id": "ocean",
  "steps": [ ... ]
}

MCP authoring tools (demos.create, demos.update) accept the same field. The cloud schema enum-validates the value, so a typo bubbles up as a changeset error instead of silently coercing to viewer.

Drift checks #

Demos break when your UI changes. To keep them healed without ever touching real user data, configure a test account per project in Studio. We run a headless Playwright check against that account every N minutes (max 30) and AI-heal selector drift when confidence ≥ your threshold. Two credential modes:

  • Encrypted at rest — cookies / headers arrive over TLS and are immediately protected with a KMS-backed AES-256-GCM data-key envelope. Cleartext is recovered only for the claimed drift check.
  • Customer-backend JWT callback — we POST your webhook at check time; you return a 5-minute signed token. We never persist credentials.

A callback response JWT must carry aud: "capartist.embed.callback_response". Saving the callback binds it to one active HS256 publishable key and returns callback_api_key_id; pass a replacement id explicitly when you rotate that callback key. Ed25519-only keys cannot authenticate MyAISE's request and are therefore not eligible for credential callbacks.

The drift envelope ships inline with every demo fetch — the library exposes it via onDriftDetected:

javascript · drift
MyAISE.launch("demo-uuid", {
  onDriftDetected: (info) => {
    // info.status: ok | unverified | stale | broken | healed
    // info.findings_count, info.checked_at, info.next_check_at
  },
});

Timeline capture & PII #

The library captures overlay clicks only — clicks on the Next/Back/dismiss buttons we render, never anything on your customer's page. There is no document-level listener and we don't read text content, attribute values, or input values from any element. URL paths aren't captured either.

Capture is opt-in:

javascript · init opts
MyAISE.init({
  apiKey: "capa_pub_xxx",
  captureTimeline: true,             // default false
  navigationMode: "auto",          // "spa" | "mpa" | "auto"
  variables: { firstName: "Alice" },
  cspNonce: "<your-csp-nonce>",  // for strict CSP
});

Event shape (overlay-scoped):

json · event
{
  "event_type": "overlay_click",
  "ts": 1717108800123,
  "step_index": 2,
  "overlay_id": "ov_abc...",
  "button_id": "1",
  "action": "next_step"
}
Cross-site demos are not supported in embed mode. The library is single-origin only — a step whose url_pattern resolves to a different origin fires onError({ code: "CROSS_ORIGIN_STEP" }). Multi-domain flows stay in viewer mode.
Section 07

Webhooks #

Subscribe to lifecycle events at /dashboard/webhooks or via POST /v1/workspaces/:ws/webhooks. Signing secret is shown once.

Signature verification #

Each delivery carries a header you must verify before trusting the payload:

http · header
X-MyAISE-Signature: t=<unix-seconds>,v1=<hmac-sha256>

Where hmac = HMAC-SHA256(signing_secret, "<t>.<raw_body>"). Every delivery also carries X-MyAISE-Event-Id (the payload's id, use it to de-duplicate retries) and X-MyAISE-Event-Type. Header names are case-insensitive. Verification (Node):

javascript · verify.mjs
import crypto from "node:crypto";

function verifyCapartistSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map(p => p.split("=", 2))
  );

  const t  = parseInt(parts.t, 10);
  const v1 = parts.v1;

  // ±5min clock-skew guard
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(v1,       "hex")
  );
}
±5 minute clock skew tolerance is on you (the example does it). Reject anything outside that window.

Retry & auto-disable #

Failed deliveries back off exponentially. After 8 consecutive non-2xx responses, the webhook auto-disables and we email the workspace owner.

try 1
1 s
try 2
5 s
try 3
30 s
try 4
5 m
try 5
30 m
try 6
2 h
try 7
8 h
try 8 → off
24 h

Redrive a specific failed delivery via POST /v1/webhook_deliveries/:id/redrive. Full event taxonomy at GET /v1/event_types.

Section 08

Ingest API #

The ingest API lets you push support conversations into MyAISE from anywhere — helpdesk exports, chat platforms, internal tools, whatever you run. We deliberately don't ship per-platform integrations: you point a small script at this API (an AI agent can usually write one in a sitting) and own the mapping from your source. Everything you push lands in the team's quarantined mining pipeline — raw conversations never reach your end users; only genericized, human-approved solutions do.

Keys & auth #

Requests authenticate with source-bound capa_ing_* keys minted on /dashboard/api-keys. Send the secret as Authorization: Bearer. Each key is bound to one team and one source slug you choose at mint time. The slug must be 2-32 chars, use lowercase letters, digits, dash, or underscore, and start with a letter or digit; common ones are telegram | slack | whatsapp | zendesk | other, or anything that names your source. The channel kind always derives from the key's ingest_source, never from the request — a key minted for internal-crm can only feed internal-crm channels. The secret is shown once at mint; revoke it and anything still using it stops immediately.

Authorization: Bearer capa_ing_<prefix>_<secret> team + source bound

Pushing items #

Base URL: https://api.myaise.com/v1/ingest. Register each external chat/channel once, maintain its roster, then replay message batches whenever needed.

EndpointWhat it does
GET/channels List the key's channels for its source. Each row includes {id, kind, external_key, display_name, enabled, cursor, participants, last_synced_at}.
POST/channels Register a channel. external_key is required and should be the collector's stable id for the chat/channel. Duplicate registration is idempotent: it returns the existing channel with 200 instead of erroring.
PUT/channels/:id/participants Replace the roster with a map of sender_key => "staff" | "customer" (up to 2000 entries). Server-derived roles win: roster staff becomes agent, roster customer stays customer, out: true is agent, and everything else is kept as unknown for classification. Roster edits retroactively re-stamp not-yet-mined history.
POST/channels/:id/items Idempotent message batch, up to 500 items. Each item needs external_id and body; optional fields are sender, sender_key, parent_external_id, sent_at, out, and attachments. Top-level cursor is persisted only after a successful batch.
Request body
itemsrequired array Up to 500 messages. Re-sending the same external_id upserts, so replaying a batch is safe.
items[].attachments array Up to 5 images per item as {mime, data_b64, width?, height?}. JPEG, PNG, or WebP only; decoded bytes must be ≤5MB. Pixels go to object storage and only refs land in item metadata.
cursor object Persisted on the channel only after the batch succeeds. Response shape: {ok: true, upserted: N, cursor: {...}}.
Example request
curl
curl -X POST https://api.myaise.com/v1/ingest/channels/ch_123/items \
  -H "Authorization: Bearer capa_ing_..." \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "external_id": "msg-1001",
        "sender": "Ava",
        "sender_key": "user:42",
        "body": "The billing page is blank",
        "sent_at": "2026-06-12T09:18:00Z"
      },
      {
        "external_id": "msg-1002",
        "sender": "Support",
        "sender_key": "agent:7",
        "out": true,
        "body": "Can you send a screenshot?",
        "attachments": [
          {
            "mime": "image/png",
            "data_b64": "iVBORw0KGgoAAAANSUhEUg...",
            "width": 1280,
            "height": 720
          }
        ]
      }
    ],
    "cursor": { "chat_id": "42", "offset": 1002 }
  }'
Errors worth handling
StatusMeaning
409ingest_pausedThe team paused ingest from the Assist dashboard. Rejected before side effects and the cursor is untouched, so you can replay the same window after resuming. Treat repeated 409s as a stop signal, not a retry.
502Attachment storage blip. Replay the same batch; upserts dedupe.
422Validation failure. The human-readable reason is in error.message.

The successful batch also emits an ingest_batch activity event into the Assist live feed.

Build your own collector #

Zendesk is the one hosted sync we ship. Chat platforms like Telegram don't allow a hosted service to read your conversations, so a collector for them runs on your side — a small always-on program that reads the chats you have access to and replays them into this API. An AI coding agent can build one in a sitting; the prompt below describes the collector we run ourselves (a Telegram tray app), and adapts to Slack, WhatsApp exports, or an internal tool by swapping the source.

prompt · paste into your AI coding agent
Build me an always-on desktop collector (a menu-bar/tray app) that harvests
our Telegram support chats into MyAISE's ingest API.

Requirements:
- I sign into Telegram Web once inside a persistent session partition; the
  session survives restarts and never leaves my machine. MyAISE's servers
  must never contact Telegram directly.
- A small manager window lets me pick which chats to harvest.
- Every 30 minutes, and on a "Harvest now" click, extract new messages from
  the Telegram Web DOM and send them as idempotent batches to
  POST https://api.myaise.com/v1/ingest/channels/:id/items, after
  registering each chat once via POST /v1/ingest/channels with a stable
  external_key. Each message carries a stable external_id so replays upsert
  instead of duplicating.
- Authenticate with my source-bound ingest key (capa_ing_..., minted at
  /dashboard/api-keys) sent as Authorization: Bearer. Store the key encrypted
  with the OS keychain; keep other config in a local config file.
- Track every sender seen; let me mark each one Staff or Customer in the
  manager, and sync the roster via PUT /v1/ingest/channels/:id/participants
  as a map of sender_key to "staff" or "customer". Send sender_key on each
  item and out: true for our own outbound messages.
- Persist a per-chat cursor in the batch payload so harvests resume where
  they left off.
- Read the API contract at https://myaise.com/docs/api#ingest-api first.
  Treat 409 ingest_paused as a stop signal (not a retry); replay the same
  batch on 502.
Section 09

AI credits #

Every hosted AI feature — MyAISE answers, walkthrough generation, rule generation, the design kit, self-healing, knowledge mining from your support channels — draws from one per-workspace credit balance. Monthly grants reset each billing period; purchased top-ups never expire. Check the balance before batch work:

http · balance
GET /v1/credits/balance

{
  "data": {
    "balance": { "granted": 640, "purchased": 500, "total": 1140 },
    "grant": { "amount": 1000, "period": "2026-08", "last_grant_period": "2026-08", "next_grant_at": "2026-09-01" },
    "low_balance": false,
    "topup_url": "https://myaise.com/dashboard/billing"
  }
}

Live-session hours #

Live viewer sessions draw on a separate monthly allowance of runtime hours: 10 hours on Solo, 20 hours per seat, pooled, on AI SE. A session is metered from the moment its runtime goes live until it ends. Time past the allowance is charged to the same credit balance at 50 credits per hour, prorated per started minute, when the session ends. Every session object carries the workspace's current-month usage as workspace.session_hours (used, included, period); once the allowance is spent and the balance cannot cover an hour, session creation returns 402 out_of_credits.

Out of credits #

When the balance is exhausted, AI endpoints return 402 Payment Required with code: "out_of_credits", the current balance, the next grant date, and a top-up URL. Demos keep playing — serving a demo never spends credits — and non-AI endpoints are unaffected. Handle it by pausing AI calls until the next grant or topping up from the billing dashboard.

json · 402 out_of_credits
{
  "code":  "out_of_credits",
  "error": "Out of AI credits",
  "balance": { "granted": 0, "purchased": 0, "total": 0 },
  "grant": { "amount": 1000, "period": "2026-08", "next_grant_at": "2026-09-01" },
  "topup_url": "https://myaise.com/dashboard/billing"
}

The embedded assistant has a second, per-user 402 — user_allowance_exhausted — when one of your users has spent the allowance you gave them while your workspace still has credits. See per-user credits.

Section 10

Endpoint reference #

Grouped by resource. Click any row to expand auth, request body, an example request, and a sample response. All requests carry Authorization: Bearer <api_key>. Use the narrow content:*, sessions:*, workspace:read, and owner-only webhooks:* scopes shown above; catch-all and plaintext-credential scopes do not exist.

Workspaces 2 endpoints
API keys 1 endpoint
Projects & rules 6 endpoints
Demos · steps · overlays · actions 9 endpoints
Viewer sessions 6 endpoints · the headline integration
MyAISE Q&A 3 endpoints · agent Q&A
Embed · per-user credits 2 endpoints · sell credits to your users
Customer-app MCP 1 endpoint · server-side handoff
Whitelabel verification 2 endpoints
Blocked paths 1 endpoint family
Webhooks 6 endpoints
MCP — JSON-RPC 2.0 1 endpoint · 8 methods · customer-app tools
POST
/mcp
JSON-RPC 2.0 →
Methods
initializepingtools/listtools/callresources/listresources/readprompts/listprompts/get
Customer-app tools

These tools require a MyAISE conversation with an attached customer-app handoff. Read-only keys see catalog/context/read/schema; action prepare/confirm/cancel require full scope and use the permission overview plus confirmation-token flow.

customer_app.catalog.getcustomer_app.context.summarycustomer_app.tool.readcustomer_app.schema.readcustomer_app.action.preparecustomer_app.action.confirmcustomer_app.action.cancel
Section 11

Errors #

Error responses use a Stripe-shape envelope so existing client libraries can be reused.

json · error envelope
{
  "error": {
    "type":       "validation_error",
    "message":    "One or more fields are invalid.",
    "details": [
      { "path": "demos.0.steps", "message": "must be a list" }
    ],
    "request_id": "F7xa..."
  }
}

Status codes #

StatusWhen you'll see it
400Malformed request.
401Missing / invalid / revoked API key.
402Studio (free) — upgrade to Solo or AI SE.
403Workspace mismatch or insufficient scope.
404Resource not in this workspace (we don't distinguish from "doesn't exist anywhere").
409Concurrent-session limit hit, or a locked resource.
422Validation failure — see details[].
429Rate limit. See Retry-After + X-RateLimit-* headers.

Found a typo? Drop us a note at docs@myaise.com · API status: status.myaise.com