Checking your session…

Quickstart — Create, Configure, Go Live

The fastest way to understand the platform is to build an assistant end-to-end. The script below creates an assistant for a fictional heating company, teaches it from the company website, and takes it live — the same five-step journey from the welcome page, in about a minute of API calls.

That's a live assistant with a web address. From here you'd give it a phone number (Go Live), connect a calendar (Bookings), and watch the conversations arrive (See What's Happening).

The one concept to internalise before you write real integration code: editing an app changes a draft; taking it live makes those changes real to your callers. Change the prompt, the voice, the branding — nothing your callers experience changes until you take the assistant live again. The rest of this chapter covers the app endpoints in detail.

TOKEN="your_api_token_here"

# 1. Create the app
APP=$(curl -s -X POST https://api.everycallhandled.com/api/v1/apps \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HVAC Receptionist",
    "attributes": {
      "voiceChatPrompt": "You are the AI receptionist for ABC HVAC. Office hours: Mon-Fri 8am-5pm. Always confirm callback number.",
      "voice": "shimmer",
      "voiceChatEnabled": "true",
      "primaryColor": "#1E3A5F"
    }
  }')
APP_ID=$(echo $APP | jq -r .id)
echo "Created app: $APP_ID"

# 2. Teach it from the company website
JOB=$(curl -s -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/knowledge \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "type": "url", "content": "https://abchvac.com/services" }')
JOB_ID=$(echo $JOB | jq -r .jobId)

# Poll until ingestion completes
while true; do
  STATUS=$(curl -s https://api.everycallhandled.com/api/v1/apps/$APP_ID/knowledge/jobs/$JOB_ID \
    -H "Authorization: Bearer $TOKEN" | jq -r .status)
  echo "Knowledge ingest: $STATUS"
  [ "$STATUS" = "completed" ] && break
  [ "$STATUS" = "failed" ] && exit 1
  sleep 3
done

# 3. Take it live
curl -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/publish \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "subdomain": "abc-hvac", "userEmail": "owner@abchvac.com" }'

# 4. Verify the live app
curl https://api.everycallhandled.com/api/v1/apps/$APP_ID \
  -H "Authorization: Bearer $TOKEN" | jq '.publishedAppUrl, .attributes.creditBalance'
Sign in to run this against your account.

An app is one assistant: its instructions, voice, knowledge, branding and (once live) its phone number and web address. Most businesses start with one; you can run many.

Requires create_app.

Request:

All attributes are optional at creation — you can set everything later with an update. The full attribute reference is in Make It Yours.

Response (200):

Save the id — you'll need it for all subsequent operations. (Published state and createdByUserId are returned by GET /apps/{id}, not by create.)

Errors:

  • 409 PHONE_NUMBER_IN_USE if attributes.twilioPhoneNumber is already used by another of your apps.
POST/api/v1/appscreate_app
curl -X POST https://api.everycallhandled.com/api/v1/apps \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Customer Support Bot",
    "description": "AI assistant for customer support",
    "attributes": {
      "assistantName": "Support Assistant",
      "welcomeMessage": "Hello! How can I help you today?",
      "primaryColor": "#4F46E5",
      "textColor": "#FFFFFF",
      "voice": "nova",
      "voiceChatPrompt": "You are a helpful customer support assistant."
    }
  }'
Sign in to run this against your account.
JSON
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "organisationId": "org-abc123",
  "name": "Customer Support Bot",
  "description": "AI assistant for customer support",
  "attributes": { /* echoed back, normalised */ },
  "chatId": "...",
  "createdAt": "2026-05-10T10:30:00Z",
  "updatedAt": "2026-05-10T10:30:00Z"
}

Requires view_settings.

Query params:

  • published — true, false, or all. If omitted, only published apps are returned (pass all to include drafts).
  • limit (default 50, max 200), offset (pagination)

Response (200):

The response is filtered to your account: you see your own apps and those of any customer accounts beneath yours (see For Partners). Credit balances shown for live assistants are always current.

GET/api/v1/apps?published=all&limit=50&offset=0view_settings
JSON
{
  "apps": [ /* array of app objects (same shape as GET /apps/{id}) */ ],
  "isAdmin": false,
  "pagination": { "total": 17, "limit": 50, "offset": 0, "hasMore": false, "nextOffset": null }
}

Available to any signed-in member of the account that owns the app.

Response (200):

hasPendingPublish is true when the app has draft changes that aren't live yet; pendingPublishAttrIds lists which attributes are waiting. Use these to drive an "Apply your changes" prompt in your own UI.

Errors:

  • 404 APP_NOT_FOUND — the app doesn't exist OR your token can't access it (deliberately the same error to avoid existence leaks).
GET/api/v1/apps/{appId}
curl 'https://api.everycallhandled.com/api/v1/apps/{appId}' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Sign in to run this against your account.
JSON
{
  "id": "550e8400-...",
  "name": "Customer Support Bot",
  "description": "...",
  "attributes": { /* full attribute set; balances and usage totals are always current live values */ },
  "createdByUserId": "...",
  "isPublished": true,
  "publishedAppUrl": "https://support.app.everycallhandled.com",
  "subdomain": "support",
  "hasPendingPublish": false,
  "pendingPublishAttrIds": [],
  "createdAt": "...",
  "updatedAt": "..."
}

Requires edit_settings.

PATCH semantics — only fields you supply are changed. Updates are draft changes: take the assistant live again to apply them (see Go Live).

PATCH/api/v1/apps/{appId}edit_settings
curl -X PATCH https://api.everycallhandled.com/api/v1/apps/$APP_ID \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Renamed app",
    "attributes": {
      "voiceChatPrompt": "Updated prompt...",
      "voice": "ballad"
    }
  }'
Sign in to run this against your account.

Requires edit_settings. Permanently removes the app together with its knowledge, conversations and billing history. If the app was live, its site is taken down too. This cannot be undone.

DELETE/api/v1/apps/{appId}edit_settings
curl -X DELETE 'https://api.everycallhandled.com/api/v1/apps/{appId}' \
  -H 'Authorization: Bearer YOUR_TOKEN'
Copy-only. DELETE is destructive — run it from your own terminal with the sample above, not from the docs.

Requires clone_app.

The duplicate inherits the prompt, tools, voice configuration and billing mode. It does not inherit the phone number, live state or credit balance. Duplicating an app is the safe way to experiment with big changes without touching the assistant your callers are using.

POST/api/v1/apps/{appId}/cloneclone_app
curl -X POST https://api.everycallhandled.com/api/v1/apps/$APP_ID/clone \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -d '{ "name": "Copy of My App" }'
Sign in to run this against your account.

Operators only (create_app). Builds a demonstration receptionist for a business from its public website alone: the homepage and up to four service, price, contact and about pages are read, a brief is drawn from what the site states (services and prices only where published, opening hours, the questions a prospective customer would ask), and the same build path as a Builder intake runs from it: a published app, a phone number, a booking page in the business's own logo and colours, web chat, and the website queued into the app's knowledge. The result is stamped as a demonstration (see Demonstration mode below).

Field Required Notes
websiteUrl yes http(s) URL of the business's site.
sector no Short trade label; overrides what the site suggests.
businessName no Overrides the name read from the site.
ownerEmail no The operator who owns the demonstration. Defaults to the caller's email. Nothing personal is ever taken from the website.
creditSeconds no The demonstration's allowance of call time, allocated from the operator's pool as a per-app cap. 300 to 7200, default 1800 (30 minutes). When it is spent the receptionist says so and ends the call.
demoDays no How long the demonstration lives before it is ended. 1 to 60, default 14.

Response (201): appId, businessName, phoneNumber, publishedUrl, bookingPageUrl, landingUrl, pagesRead, the brief's services, hours and callerQuestions, and the seeded bookingTypes. A build takes 30 to 60 seconds, longer than the gateway allows, so call it from a job rather than a browser; backend/scripts/build_demos_from_list.js invokes the function directly for a list of sites.

POST/api/v1/apps/build-from-website
curl -X POST https://api.everycallhandled.com/api/v1/apps/build-from-website \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "websiteUrl": "https://www.example-clinic.co.uk",
    "sector": "dental practice",
    "businessName": "Example Dental",
    "ownerEmail": "you@example.com"
  }'
Sign in to run this against your account.

Every built demonstration also gets a landing page (landingUrl in the build response, a /d/{token} link) that shows the number, the booking page and the web chat with the demonstration notice and the platform's healthcare proof. Resolving the token (GET /api/v1/apps/prospect-demo/{token}) returns mode: "built", a built block (appId, business, websiteUrl, phoneNumber, bookingPageUrl, sector) and the demo notice strings.

Pages report a visit with a public beacon, nothing about the visitor is kept:

Operators read the engagement of every demonstration in one call:

Returns { demos: [...], count }, one row per demonstration: business, sector, phoneNumber, bookingPageUrl, landingUrl, createdAt, visits, returnVisits (visits more than 30 minutes apart), firstVisitAt, lastVisitAt, calls, callSeconds, longestCallSeconds, chats, bookingAttempts, lastActivityAt, stage (no engagement, visited, chatted, called, booking attempted), replied, repliedAt, notes. Calls and chats are the demonstration's own Conversations; booking attempts are its booking rows.

PATCH /api/v1/demos/{appId} with { "replied": true } and/or { "notes": "..." } records the operator's own marks on a row; { "extendDays": 7 } pushes a live demonstration's end date; { "end": true } ends it now.

text
POST /api/v1/public/demo-visit
{ "appId": "...", "path": "landing" }
GET/api/v1/demos/funnel

A CSV for instantly.ai, one row per live demonstration. Standard columns: email, first_name, last_name, company_name, phone, website. Custom variables for the sequence: demo_number, landing_url, booking_url, sector, cohort (dental, aesthetics, veterinary, consultant, general), tailored_question (the question the email invites them to ask, by cohort, or the row's own), company_type, pecr_ok, pecr_reason, opt_out_line, sender_identity, app_id, demo_ends_at, stage.

PECR: a limited company is a corporate subscriber and may be mailed with the sender identified and an opt-out offered; a sole trader or ordinary partnership is an individual subscriber and needs recorded consent; an unknown company type is not mailed until it is known. The contact, company type, company number, consent and tailored question are set at build (contact, companyType, companyNumber, consent, tailoredQuestion on POST /api/v1/apps/build-from-website) or afterwards with PATCH /api/v1/demos/{appId}. The sender identity and opt-out line come from the deployment's DEMO_SENDER_IDENTITY and DEMO_OPT_OUT_LINE; until they are set, visible placeholders ride along.

text
GET /api/v1/demos/export.csv            rows that may be mailed
GET /api/v1/demos/export.csv?all=1      every live demonstration, with the PECR column saying why a row may not be mailed

Every funnel row carries phase (live, ended, quarantine, due-delete), endsAt, endedAt, deleteAfter, daysLeft, creditSeconds and creditBalance. A daily sweep ends demonstrations whose time is up: the number is parked on a short "this demonstration has ended" message, the receptionist is paused (a caller who reaches it hears the same), and the booking page shows the ended notice (config.demo.ended, config.demo.endedText) and takes no bookings. Nothing is deleted by the sweep. Two weeks after ending, the operator is told which demonstrations can be deleted; deleting an app (DELETE /api/v1/apps/{appId}) releases its number. A demonstration that has spent its allowance answers with "this demonstration has used up its allowance of call time" until an operator tops up its cap (POST /api/v1/apps/{appId}/cap-topup).

A built demonstration carries booking.demo in its booking settings:

While it is set: the receptionist opens every call and chat by saying it is a demonstration built from public information and not connected to the business, gives no clinical or treatment advice and no suitability judgement, quotes no price the website does not state, and describes bookings as demonstration bookings that reach nobody; no confirmation, reminder or follow-up messages are sent to anyone; the public booking page shows a banner (config.demo carries title, text and confirmed) and confirms bookings as demonstrations with no payment or manage link. The flag is set at build time and survives dashboard saves; remove it by clearing booking.demo through PUT /api/v1/apps/{appId}/booking.


JSON
"demo": { "enabled": true, "business": "Example Dental", "createdAt": "2026-09-10T09:01:22Z" }