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.
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-keys → Create key. The secret is shown once — copy it into your secret store immediately.
2 · Smoke test #
curl https://api.myaise.com/v1/ping \\
-H "Authorization: Bearer capa_live_…"
3 · List your projects #
curl https://api.myaise.com/v1/projects \\
-H "Authorization: Bearer capa_live_…"
Authentication #
Every request carries a workspace-scoped Bearer token. Keys are workspace-scoped — a team key can never see a personal workspace.
Authorization: Bearer capa_live_<prefix>_<random>
Scopes #
| Scope | What it grants |
|---|---|
workspace:read | Workspace metadata. |
content:read | Read projects, demos, rules, and assets. |
content:write | Create or change ordinary content. |
sessions:read | Read viewer sessions. |
sessions:write | Create and manage viewer sessions. |
sessions:credentials:submit | Submit only client-encrypted viewer credential payloads. |
webhooks:read | Read webhook configuration and delivery status. |
webhooks:manage | Owner-only webhook management and redrive. |
whitelabel_only | Isolated partner scope for theme verification; it cannot be combined with workspace scopes. |
Studio (free) #
402 Payment Required on every /v1 and /mcp request. Upgrade to Solo or AI SE to enable API access.Concepts #
Six resources, in nesting order. Everything else is a child or a side-effect.
steps[].
ID prefixes #
Every ID is prefixed so you can spot the resource by eye — no need to remember which is which.
ws_team workspacews_user_personal workspaceproj_projectdemo_demostep_stepov_overlayact_actionsess_viewer sessionwh_webhookdel_delivery
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.
// 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);
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— asessions_onlykey is exactly that set. - A webhook subscribed to
session.ended(and optionallysession.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.
# 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.
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:
{
"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").)
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
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"),
};
}
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.
{
"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:
{
"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_secondsis clamped to the workspace maximum, so readexpires_atandexpires_in_secondsback from the response instead of assuming. When the clock runs out the session ends withend_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:
{
"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:
{
"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:
| Reason | What happened |
|---|---|
ttl_expired | The session hit its time limit. |
credit_budget_exhausted | The AI credit budget ran out. A session.budget_exhausted event fires just before session.ended. |
runtime_incompatible | The viewer fleet could not honour a requested feature — rare. |
revoked | You deleted the session. |
demo_completed | The prospect reached the end of the demo. |
On receipt:
- Verify the signature (see Signature verification) before trusting anything in the body.
- Be idempotent on the event
id— retries and replays are expected. - Revoke the MCP grant.
- 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_urlis 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.metadatato 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.
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
{
"mcpServers": {
"myaise": {
"transport": "http",
"url": "https://mcp.myaise.com",
"headers": {
"Authorization": "Bearer capa_live_..."
}
}
}
}
2 · Ask the model
design_demo prompt with my pricing URL, then create the demo."3 · What it does, step-by-step
tools/list— sees the full tool surface.- The
design_demoprompt — gets a structured prompt template covering URL flow + overlay copy guidelines. projects.create, thendemos.createwith nested steps + overlays.- For anchored overlays, attaches
target_context(+ an optional baselinescreenshot) so the demo can self-heal — see “Drift protection” below. sessions.createto actually launch the demo.
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:
- Set
auto_healing_enabled: trueon the demo (demos.create/demos.update). - Give every anchored overlay (one with a
target_selector) atarget_contextonoverlays.create— capture it from the live page you authored against:
"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.
sessions.create), so it never needs the agent's cookies.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.
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:
<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):
<script
src="/static/capartist.min.js"
integrity="sha384-zwqsKNp7ntF5Pefjpm78C0inV5rxsuKZYeLfnYy+E10xU2MixL+YFEFADYD5FXm8"
crossorigin="anonymous"
></script>
npm:
npm i @capartist/embed
From a bundler:
// 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:
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:
| Claim | Meaning |
|---|---|
credit_allowance | Whole 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_key | Your 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.
// 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.
{
"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:
{
"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,cosmicand 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:
{
"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:
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:
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:
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):
{
"event_type": "overlay_click",
"ts": 1717108800123,
"step_index": 2,
"overlay_id": "ov_abc...",
"button_id": "1",
"action": "next_step"
}
url_pattern resolves to a different origin fires onError({ code: "CROSS_ORIGIN_STEP" }). Multi-domain flows stay in viewer mode.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:
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):
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")
);
}
Retry & auto-disable #
Failed deliveries back off exponentially. After 8 consecutive non-2xx responses, the webhook auto-disables and we email the workspace owner.
Redrive a specific failed delivery via POST /v1/webhook_deliveries/:id/redrive. Full event taxonomy at GET /v1/event_types.
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.
| Endpoint | What 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. |
external_id upserts, so replaying a batch is safe.
{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.
{ok: true, upserted: N, cursor: {...}}.
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 }
}'
| Status | Meaning |
|---|---|
409ingest_paused | The 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. |
| 502 | Attachment storage blip. Replay the same batch; upserts dedupe. |
| 422 | Validation 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.
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.
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:
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.
{
"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.
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.
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/workspaces \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "ws_3f1c8e2a-...",
"type": "team",
"name": "Acme Inc",
"slug": "acme",
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-05-20T09:30:00Z"
}
]
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/workspaces/ws_3f1c8e2a-... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "ws_3f1c8e2a-...",
"type": "team",
"name": "Acme Inc",
"slug": "acme",
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-05-20T09:30:00Z"
}
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/workspaces/ws_3f1c.../api_keys \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "key_a1b2c3d4-...",
"name": "CI key",
"prefix": "cap_live_",
"last4": "9f3a",
"scopes": ["workspace:read", "content:read"],
"rate_limit_per_minute": 120,
"last_used_at": "2026-05-26T11:00:00Z",
"revoked_at": null,
"created_at": "2026-04-10T08:00:00Z"
}
]
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/projects \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "proj_e72b1a4c-...",
"name": "Acme Demo",
"description": "",
"base_url": "https://app.acme.com",
"version": 7,
"rule_count": 4,
"demo_count": 2,
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-05-20T09:30:00Z"
}
]
}
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/projects/proj_e72b... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "proj_e72b...", "name": "Acme Demo", "base_url": "https://app.acme.com",
"version": 7, "rule_count": 4, "demo_count": 2,
"rules": [{ "id": "8c1f...", "match": "/api/users", "type": "json", "index": 0 }],
"demos": [{ "id": "demo_b3d2...", "name": "Onboarding", "steps": [] }]
}
}
https://app.acme.com.
"".
[].
[].
curl -X POST https://api.myaise.com/v1/projects \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Demo","base_url":"https://app.acme.com"}'
{
"data": {
"id": "proj_e72b...", "name": "Acme Demo", "base_url": "https://app.acme.com",
"version": 1, "rule_count": 0, "demo_count": 0, "rules": [], "demos": []
}
}
curl -X PATCH https://api.myaise.com/v1/projects/proj_e72b... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"version":7,"name":"Acme Demo 2026"}'
{ "data": { "id": "proj_e72b...", "name": "Acme Demo 2026", "version": 8 } }
curl -X DELETE https://api.myaise.com/v1/projects/proj_e72b... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"version":8}'
204 No Content
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/projects/proj_e72b.../rules \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{ "id": "8c1f2a...", "match": "/api/users", "type": "json",
"rewrite": "$.data[*].plan", "enabled": true, "index": 0 }
]
}
json, html, block, etc.
curl -X POST https://api.myaise.com/v1/projects/proj_e72b.../rules \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"match":"/api/users","type":"json","rewrite":"$.plan","body":"enterprise"}'
{
"data": {
"rule": { "id": "8c1f2a...", "match": "/api/users", "type": "json" },
"rules": [{ "id": "8c1f2a...", "index": 0 }]
}
}
curl -X PATCH https://api.myaise.com/v1/projects/proj_e72b.../rules/8c1f2a... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'
{ "data": { "rule": { "id": "8c1f2a...", "enabled": false }, "rules": [ ... ] } }
curl -X DELETE https://api.myaise.com/v1/projects/proj_e72b.../rules/8c1f2a... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/projects/proj_e72b.../demos \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "demo_b3d2...", "name": "Onboarding", "theme_id": "glass", "steps": [] } ] }
"glass".
http:// or https:// URL where the demo sends the prospect when finished.
false.
curl -X POST https://api.myaise.com/v1/projects/proj_e72b.../demos \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Onboarding","theme_id":"glass"}'
{
"data": {
"id": "demo_b3d2...", "name": "Onboarding", "theme_id": "glass",
"exit_url": null, "auto_healing_enabled": false, "steps": [], "pre_demo_actions": []
}
}
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/demos/demo_b3d2... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "demo_b3d2...", "name": "Onboarding", "theme_id": "glass",
"color_scheme": { "primary": "#7c3aed", "accent": "#a855f7" },
"steps": [{ "id": "step_91fe...", "name": "Dashboard", "url_pattern": "/dashboard*",
"overlays": [{ "id": "ov_4c8a...", "type": "tooltip" }] }],
"pre_demo_actions": []
}
}
curl -X PATCH https://api.myaise.com/v1/demos/demo_b3d2... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"exit_url":"https://acme.com/thanks"}'
{ "data": { "id": "demo_b3d2...", "exit_url": "https://acme.com/thanks" } }
curl -X DELETE https://api.myaise.com/v1/demos/demo_b3d2... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: content:write
"<name> (copy)".
curl -X POST https://api.myaise.com/v1/demos/demo_b3d2.../clone \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Onboarding — EU"}'
{ "data": { "id": "demo_9a77...", "name": "Onboarding — EU", "steps": [ ... ] } }
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/demos/demo_b3d2.../steps \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "step_91fe...", "name": "Dashboard", "url_pattern": "/dashboard*", "overlays": [] } ] }
/dashboard*.
{}.
curl -X POST https://api.myaise.com/v1/demos/demo_b3d2.../steps \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Dashboard","url_pattern":"/dashboard*"}'
{ "data": { "id": "step_91fe...", "name": "Dashboard", "url_pattern": "/dashboard*", "overlays": [], "entry_actions": [] } }
curl -X PATCH https://api.myaise.com/v1/demos/demo_b3d2.../steps/step_91fe... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"url_pattern":"/reports*"}'
{ "data": { "id": "step_91fe...", "url_pattern": "/reports*" } }
curl -X DELETE https://api.myaise.com/v1/demos/demo_b3d2.../steps/step_91fe... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/steps/step_91fe.../overlays \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "ov_4c8a...", "type": "tooltip", "title": "Click here", "target_selector": "#new-report" } ] }
tooltip, modal, banner, spotlight, outline, etc.
top/bottom/left/right.
[{label, action}].
slide-up.
video_bubble, a safe absolute http:// or https:// embed URL.
selectorCandidates plus role / accessibleName / textContent / nearbyText / rect. Lets MyAISE re-locate this element after the customer's site changes. See “Drift protection” below.
curl -X POST https://api.myaise.com/v1/steps/step_91fe.../overlays \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"tooltip","title":"Click here","target_selector":"#new-report"}'
{ "data": { "id": "ov_4c8a...", "type": "tooltip", "title": "Click here", "target_selector": "#new-report" } }
curl -X PATCH https://api.myaise.com/v1/steps/step_91fe.../overlays/ov_4c8a... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"animation":"slide-up"}'
{ "data": { "id": "ov_4c8a...", "animation": "slide-up" } }
curl -X DELETE https://api.myaise.com/v1/steps/step_91fe.../overlays/ov_4c8a... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/demos/demo_b3d2.../pre_demo_actions \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "act_2f9b...", "type": "fill", "target": "#email", "value": "jane@acme.com" } ] }
click, fill, wait, navigate, etc.
curl -X POST https://api.myaise.com/v1/demos/demo_b3d2.../pre_demo_actions \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"fill","target":"#email","value":"jane@acme.com"}'
{ "data": { "id": "act_2f9b...", "type": "fill", "target": "#email", "value": "jane@acme.com" } }
curl -X PATCH https://api.myaise.com/v1/demos/demo_b3d2.../pre_demo_actions/act_2f9b... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"delay_ms":500}'
{ "data": { "id": "act_2f9b...", "delay_ms": 500 } }
curl -X DELETE https://api.myaise.com/v1/demos/demo_b3d2.../pre_demo_actions/act_2f9b... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/steps/step_91fe.../entry_actions \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "act_7c1a...", "type": "click", "target": ".accept-cookies" } ] }
click, fill, wait, etc.
curl -X POST https://api.myaise.com/v1/steps/step_91fe.../entry_actions \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"click","target":".accept-cookies"}'
{ "data": { "id": "act_7c1a...", "type": "click", "target": ".accept-cookies" } }
curl -X PATCH https://api.myaise.com/v1/steps/step_91fe.../entry_actions/act_7c1a... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"delay_ms":300}'
{ "data": { "id": "act_7c1a...", "delay_ms": 300 } }
curl -X DELETE https://api.myaise.com/v1/steps/step_91fe.../entry_actions/act_7c1a... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/demos/demo_b3d2.../assets \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "asset_5d1c...", "demo_id": "demo_b3d2...", "project_id": "proj_e72b...",
"content_hash": "sha256:abc123...", "kind": "baseline_screenshot",
"storage_url": "https://cdn.myaise.com/assets/abc123.png",
"width": 1280, "height": 800, "byte_size": 48213, "mime_type": "image/png",
"created_at": "2026-05-20T09:30:00Z"
}
]
}
curl https://api.myaise.com/v1/demos/demo_b3d2.../assets/abc123... \
-H "Authorization: Bearer $CAP_KEY"
{ "data": { "id": "asset_5d1c...", "content_hash": "sha256:abc123...", "storage_url": "https://cdn.myaise.com/assets/abc123.png" } }
Authorization: Bearer <api_key>
scope: sessions:read / sessions:write
curl https://api.myaise.com/v1/viewer/public_key \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0B...\n-----END PUBLIC KEY-----\n",
"key_type": "RSA",
"algorithm": {
"key_wrap": "RSA-OAEP", "hash": "SHA-256",
"mgf1_hash": "SHA-256", "content": "AES-256-GCM"
},
"envelope_fields": ["encrypted_key", "iv", "ciphertext", "tag"],
"iv_bytes": 12,
"tag_bytes": 16,
"encoding": "base64",
"plaintext_schema": { "base_url": "https://…", "cookies": [], "headers": {} }
}
}
Authorization: Bearer <api_key>
scope: sessions:write + sessions:credentials:submit
{ name, email, company, metadata } — shown in the dashboard.
{ encrypted_key, iv, ciphertext, tag }. Plaintext credentials are rejected.
expires_in), effective range 60..604800 — smaller values are raised to 60, larger ones clamped to the workspace maximum, so read expires_at / expires_in_seconds back from the response. Omitted → the workspace default. Not a positive integer → 400 invalid_expires_in.
credit_budget_exceeds_policy; not a positive integer → 400 invalid_credit_budget.
*.example.com matches subdomains; no IP literals). Omitted → the workspace's allowed-hosts policy (off, or the project base_url host + its subdomains + any extras). Invalid → 422 invalid_allowed_hosts.
curl -X POST https://api.myaise.com/v1/demos/demo_b3d2.../sessions \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"prospect": { "name": "Jane", "email": "jane@acme.com" },
"encrypted_payload": { "encrypted_key": "...", "iv": "...", "ciphertext": "...", "tag": "..." },
"expires_in_seconds": 1200,
"credit_budget": 100,
"allowed_hosts": ["app.example.com", "*.app.example.com"]
}'
{
"data": {
"id": "sess_7a2e...",
"token": "v1_AbCd...",
"status": "pending",
"viewer_url": "https://viewer.myaise.com/view/v1_AbCd...",
"expires_at": "2026-05-28T14:00: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..." }
}
}
Authorization: Bearer <api_key>
scope: sessions:write + sessions:credentials:submit
{ name, email, company, metadata } — shown in the dashboard.
{ encrypted_key, iv, ciphertext, tag }.
curl -X POST https://api.myaise.com/v1/projects/proj_e72b.../assistant_sessions \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"prospect":{"name":"Jane"},"encrypted_payload":{"encrypted_key":"...","iv":"...","ciphertext":"...","tag":"..."}}'
{
"data": {
"id": "sess_7a2e...", "token": "v1_AbCd...", "status": "pending",
"viewer_url": "https://viewer.myaise.com/view/v1_AbCd...",
"expires_at": "2026-05-28T14:00:00Z", "project_id": "proj_e72b...",
"workspace": { "type": "team", "id": "ws_3f1c..." }
}
}
Authorization: Bearer <api_key>
read · any key
curl "https://api.myaise.com/v1/workspaces/ws_3f1c.../sessions?status=active" \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "sess_7a2e...", "status": "active", "demo_id": "demo_b3d2...",
"token_prefix": "v1_AbCd", "share_link_available": false,
"workspace": { "type": "team", "id": "ws_3f1c..." },
"prospect": { "name": "Jane", "email": "jane@acme.com" },
"demo_status": "in_progress", "demo_step_index": 2,
"machine_id": "d8...", "warming_state": "ready",
"expires_at": "2026-05-28T14:00:00Z", "created_at": "2026-05-28T13:00:00Z"
}
]
}
Authorization: Bearer <api_key>
scope: sessions:read / sessions:write
curl https://api.myaise.com/v1/demo_sessions/sess_7a2e... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "sess_7a2e...", "status": "active",
"token_prefix": "v1_AbCd", "share_link_available": false,
"workspace": { "type": "team", "id": "ws_3f1c..." },
"project_id": "proj_e72b...", "demo_id": "demo_b3d2...",
"prospect": { "name": "Jane", "email": "jane@acme.com", "company": "Acme", "metadata": {} },
"current_url": "https://app.acme.com/dashboard",
"demo_status": "in_progress", "demo_step_index": 2,
"demo_started_at": "2026-05-28T13:01:00Z", "demo_completed_at": null,
"credit_budget": 100, "credits_spent": 34,
"allowed_hosts": ["app.example.com", "*.app.example.com"],
"end_reason": null, "ended_at": null,
"machine_id": "d8...", "warming_state": "ready",
"warming_completed": 4, "warming_total": 4,
"expires_at": "2026-05-28T14:00:00Z"
}
}
curl -X DELETE https://api.myaise.com/v1/demo_sessions/sess_7a2e... \
-H "Authorization: Bearer $CAP_KEY"
{ "ok": true }
Authorization: Bearer <api_key>
scope: full or read_only
curl -X POST https://api.myaise.com/v1/assist/messages \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Which object backs the account settings form?",
"origin": "customer-app-mcp-planner"
}'
{
"data": {
"conversation_id": "9f4d2c1b-...",
"conversation_created": true,
"message": {
"id": "3e533c0a-...",
"role": "assistant",
"content": "Open Campaigns and verify the routing target is active."
},
"answer_gap": false,
"suggested_walkthroughs": [],
"wants_walkthrough_generation": false,
"retrieval_refs": {
"solution_ids": ["8c1a..."],
"chunk_ids": [],
"groundtruth_ids": []
}
}
}
Authorization: Bearer <api_key>
scope: full or read_only
curl -X POST https://api.myaise.com/v1/assist/conversations \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"origin":"customer-app-mcp-planner"}'
{
"data": {
"conversation_id": "9f4d2c1b-...",
"status": "active",
"origin": "customer-app-mcp-planner"
}
}
Authorization: Bearer <api_key>
scope: full or read_only
curl -X POST https://api.myaise.com/v1/assist/conversations/9f4d2c1b-.../messages \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"What should I check first?"}'
{
"data": {
"conversation_id": "9f4d2c1b-...",
"conversation_created": false,
"message": {
"id": "3e533c0a-...",
"role": "assistant",
"content": "Open Campaigns and verify the routing target is active."
},
"answer_gap": false,
"suggested_walkthroughs": [],
"wants_walkthrough_generation": false,
"retrieval_refs": {
"solution_ids": ["8c1a..."],
"chunk_ids": [],
"groundtruth_ids": []
}
}
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/embed/end_users/user_8842/credits \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"end_user_ref": "user_8842",
"credit_allowance": 300,
"allowance_source": "claim",
"credit_period": "monthly",
"period_key": "2026-09",
"credits_spent": 90,
"credits_remaining": 210,
"resets_at": "2026-10-01T00:00:00Z",
"out_of_credits": false,
"last_seen_at": "2026-09-04T18:20:11Z",
"out_of_credits_message": "You've used this month's AI credits.",
"out_of_credits_url": "https://app.example.com/billing/credits"
}
}
Authorization: Bearer <api_key>
scope: write
curl -X PUT https://api.myaise.com/v1/embed/end_users/user_8842/credits \
-H "Authorization: Bearer $CAP_KEY" \
-H 'Content-Type: application/json' \
-d '{"credit_allowance": 500, "credit_period": "monthly", "reset_spent": true}'
{
"data": {
"end_user_ref": "user_8842",
"credit_allowance": 500,
"allowance_source": "api",
"credit_period": "monthly",
"period_key": "2026-09",
"credits_spent": 0,
"credits_remaining": 500,
"resets_at": "2026-10-01T00:00:00Z",
"out_of_credits": false,
"last_seen_at": "2026-09-04T18:20:11Z",
"out_of_credits_message": "You've used this month's AI credits.",
"out_of_credits_url": "https://app.example.com/billing/credits"
}
}
Authorization: Bearer <api_key>
scope: full, or read_only for read-only catalogs
sess_… id of a live viewer session created by this workspace. Binds the grant to that prospect's session so the AI SE acts as that demo user — see the website integration guide. 404 session_not_found if it is not this workspace's session; 409 session_not_live if it has ended or expired. With it, sessions:write is sufficient (a sessions_only key works); without it content:write is required.
curl -X POST https://api.myaise.com/v1/customer_app/mcp_handoffs \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://mcp.customer-app.example/rpc",
"access_token": "customer_app_mcp_token_...",
"expires_at": "2026-06-26T16:00:00Z",
"app_ref": "supportdesk",
"account_id": "acct_123",
"scopes": ["read_only"],
"tool_catalog": [{"name":"records.get","readOnlyHint":true}],
"capartist_conversation_id": "9f4d2c1b-...",
"viewer_session_id": "sess_7a2e..."
}'
{
"data": {
"handoff_ref": "customer_app_handoff_abc123",
"expires_at": "2026-06-26T16:00:00Z",
"viewer_session_id": "sess_7a2e...",
"connected": true,
"credential_handling": "server-side only",
"customer_app": {
"app_ref": "supportdesk",
"account_id": "acct_123",
"tool_count": 1,
"read_only": true
}
}
}
Authorization: Bearer <api_key>
scope: whitelabel_only
curl -X POST https://api.myaise.com/v1/whitelabel/theme_verifications \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"css": ":root { --brand: #4f46e5; }",
"preview_html": "<!doctype html><html><body>Preview</body></html>",
"external_ref": "theme_123"
}'
{
"data": {
"id": "6d8d4f2a-...",
"external_ref": "theme_123",
"status": "queued",
"findings": [],
"cleared_css": null
}
}
curl https://api.myaise.com/v1/whitelabel/theme_verifications/6d8d4f2a-... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "6d8d4f2a-...",
"external_ref": "theme_123",
"status": "approved",
"findings": [],
"cleared_css": ":root { --brand: #4f46e5; }"
}
}
Authorization: Bearer <api_key>
scope: whitelabel_only
curl -X POST https://api.myaise.com/v1/whitelabel/theme_verifications \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"css": ":root { --brand: #4f46e5; }",
"preview_html": "<!doctype html><html><body>Preview</body></html>",
"external_ref": "theme_123"
}'
{
"data": {
"id": "6d8d4f2a-...",
"external_ref": "theme_123",
"status": "queued",
"findings": [],
"cleared_css": null
}
}
curl https://api.myaise.com/v1/whitelabel/theme_verifications/6d8d4f2a-... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "6d8d4f2a-...",
"external_ref": "theme_123",
"status": "approved",
"findings": [],
"cleared_css": ":root { --brand: #4f46e5; }"
}
}
Authorization: Bearer <api_key>
scope: content:write
curl https://api.myaise.com/v1/workspaces/ws_3f1c.../blocked_paths \
-H "Authorization: Bearer $CAP_KEY"
{ "data": [ { "id": "bp_77aa...", "pattern": "/admin/*", "status_code": 403, "enabled": true, "locked": false } ] }
/admin/*.
curl -X POST https://api.myaise.com/v1/workspaces/ws_3f1c.../blocked_paths \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"pattern":"/admin/*","status_code":403}'
{ "data": { "id": "bp_77aa...", "pattern": "/admin/*", "status_code": 403, "enabled": true, "locked": false } }
curl -X PATCH https://api.myaise.com/v1/workspaces/ws_3f1c.../blocked_paths/bp_77aa... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'
{ "data": { "id": "bp_77aa...", "enabled": false } }
curl -X DELETE https://api.myaise.com/v1/workspaces/ws_3f1c.../blocked_paths/bp_77aa... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: webhooks:manage
curl https://api.myaise.com/v1/workspaces/ws_3f1c.../webhooks \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "wh_9c3d...", "url": "https://acme.com/hooks", "active": true,
"events": ["session.created", "session.demo_completed"],
"project_ids": ["proj_e72b..."], "demo_ids": [],
"signing_secret_prefix": "whsec_ab12cd", "consecutive_failures": 0,
"last_success_at": "2026-05-27T10:00:00Z", "last_failure_at": null
}
]
}
curl -X POST https://api.myaise.com/v1/workspaces/ws_3f1c.../webhooks \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://acme.com/hooks","events":["session.created"]}'
{
"data": {
"id": "wh_9c3d...", "url": "https://acme.com/hooks", "active": true,
"events": ["session.created"],
"signing_secret": "whsec_ab12cd...FULL_SECRET_SHOWN_ONCE",
"signing_secret_prefix": "whsec_ab12cd"
}
}
curl -X PATCH https://api.myaise.com/v1/workspaces/ws_3f1c.../webhooks/wh_9c3d... \
-H "Authorization: Bearer $CAP_KEY" \
-H "Content-Type: application/json" \
-d '{"active":false}'
{ "data": { "id": "wh_9c3d...", "active": false } }
curl -X DELETE https://api.myaise.com/v1/workspaces/ws_3f1c.../webhooks/wh_9c3d... \
-H "Authorization: Bearer $CAP_KEY"
204 No Content
Authorization: Bearer <api_key>
scope: webhooks:manage
curl -X POST https://api.myaise.com/v1/workspaces/ws_3f1c.../webhooks/wh_9c3d.../test \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "del_b81a...", "webhook_id": "wh_9c3d...", "event_type": "session.created",
"status": "delivered", "attempt": 1, "response_status": 200, "created_at": "..."
}
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/workspaces/ws_3f1c.../webhook_deliveries \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{
"id": "del_b81a...", "webhook_id": "wh_9c3d...", "event_type": "session.demo_completed",
"status": "delivered", "attempt": 1, "response_status": 200,
"delivered_at": "2026-05-27T10:00:01Z", "next_attempt_at": null
}
]
}
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/webhook_deliveries/del_b81a... \
-H "Authorization: Bearer $CAP_KEY"
{
"data": {
"id": "del_b81a...", "webhook_id": "wh_9c3d...", "event_type": "session.demo_completed",
"status": "delivered", "attempt": 1, "response_status": 200,
"payload": { "type": "session.demo_completed", "data": { "session_id": "sess_7a2e..." } },
"response_body": null, "error_message": null,
"signature": "sha256=9f86d0818..."
}
}
Authorization: Bearer <api_key>
scope: webhooks:manage
curl -X POST https://api.myaise.com/v1/webhook_deliveries/del_b81a.../redrive \
-H "Authorization: Bearer $CAP_KEY"
{ "data": { "id": "del_b81a...", "status": "pending", "attempt": 2, "next_attempt_at": "2026-05-28T13:05:00Z" } }
Authorization: Bearer <api_key>
read · any key
curl https://api.myaise.com/v1/event_types \
-H "Authorization: Bearer $CAP_KEY"
{
"data": [
{ "type": "session.created", "category": "session",
"description": "A new viewer session was created via the API." },
{ "type": "session.demo_completed", "category": "session",
"description": "The prospect reached the end of the demo." },
{ "type": "demo.drift_detected", "category": "demo_authoring",
"description": "The validator found drift in this demo." }
]
}
initializepingtools/listtools/callresources/listresources/readprompts/listprompts/get
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
Errors #
Error responses use a Stripe-shape envelope so existing client libraries can be reused.
{
"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 #
| Status | When you'll see it |
|---|---|
| 400 | Malformed request. |
| 401 | Missing / invalid / revoked API key. |
| 402 | Studio (free) — upgrade to Solo or AI SE. |
| 403 | Workspace mismatch or insufficient scope. |
| 404 | Resource not in this workspace (we don't distinguish from "doesn't exist anywhere"). |
| 409 | Concurrent-session limit hit, or a locked resource. |
| 422 | Validation failure — see details[]. |
| 429 | Rate limit. See Retry-After + X-RateLimit-* headers. |
Found a typo? Drop us a note at docs@myaise.com · API status: status.myaise.com