Skip to content

API

Base: https://<your-domain>. Create keys under Settings → API & webhooks; each key belongs to one sub-account.

Machine-readable: OpenAPI 3.1 at GET /api/openapi.json, as a web page at GET /api/docs.html (both without a key). REST hooks, Zapier/Make/n8n, “Webhook received” and the HubSpot import: docs/integrations.md.

Terminal window
KEY=pghl_…
# Create or update a contact (duplicates are merged by email/phone)
curl -X POST https://crm.example.com/api/contacts -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"first_name":"Anna","email":"anna@example.com","tags":["lead"],"custom":{"budget":"5000"}}'
# Search contacts
curl "https://crm.example.com/api/contacts?q=anna&tag=lead&limit=50" -H "Authorization: Bearer $KEY"
# Create an opportunity
curl -X POST https://crm.example.com/api/opportunities -H "Authorization: Bearer $KEY" -H 'content-type: application/json' \
-d '{"pipeline_id":"…","contact_id":"…","value":1500}'

Key endpoints (all JSON):

Area Endpoints
Contacts GET/POST /api/contacts, GET/PATCH/DELETE /api/contacts/:id, POST /api/contacts/:id/tags {add, remove}, POST /api/contacts/:id/notes, POST /api/contacts/import {csv}, GET /api/contacts/export.csv; POST /api/contacts/bulk-owner {ids, assigned_to} (give up to 1,000 contacts to one person, or null = remove the owner; fires “Owner changed” for each contact; record permissions apply; one history entry)
Tasks GET/POST /api/tasks, PATCH/DELETE /api/tasks/:id
Pipelines GET/POST /api/pipelines (lost_reasons = list of loss reasons, empty = default; GET also returns lost_reason_options; stages {id, name, probability?, color?} – color as #rrggbb tints the stage header on the board; order by position), PATCH /api/pipelines/:id, POST /api/pipelines/order {ids} (order of the pipelines for everyone in the sub-account; ones not listed come after), GET/POST /api/opportunities (expected_close_date YYYY-MM-DD, currency ISO code, value in whole units; GET filters with pipeline_id, status, contact_id, q, owner (login ID or none), stage_id (several allowed, comma-separated), tag, filters = JSON list [{field, op, value}] with op eq, neq, contains, not_contains, empty, not_empty, gt, lt, gte, lte, in over the fields name, value, currency, status, pipeline_id, stage_id, assigned_to, source, lost_reason, expected_close_date, created_at, updated_at, stage_changed_at, contact_name, contact_email, contact_phone, contact_company, tags and props.<key> (deal properties), sorted with sort=field or -field, at most limit=2000; every row has props and contact_tags), PATCH/DELETE /api/opportunities/:id – status lost requires lost_reason; POST /api/opportunities/bulk {ids, action: stage|owner|delete, stage_id, pipeline_id?, assigned_to} (selected deals, at most 1,000, one history entry – contact tags via POST /api/contacts/bulk), GET /api/opportunities/export.csv (same filters, optional ids=a,b; with contact, pipeline, stage and deal properties); smart tags: GET/POST /api/deal-smart-tags {name, color, rule: {kind: value_min|overdue|no_close_date|no_owner|stage_days|stale_days, value?, pipeline_id?}}, PATCH/DELETE /api/deal-smart-tags/:id, GET /api/deal-smart-tags/board?pipeline_id= → {dealId: [{id, name, color}]} (open deals only); forecast: GET /api/deal-forecast?pipeline_id=&months=6 → {open, months: [{month, count, value, weighted}], later, risks: {overdue, no_close_date, stale, no_owner}}
Calendar GET/POST /api/calendars, PATCH /api/calendars/:id (duration, interval, buffer, min_notice, max_days, availability, settings; booking rules in settings: limit_day, limit_week, date_overrides: {YYYY-MM-DD: [] | [[from, to]]}, reminders: [{hours, email, sms}], followup, noshow – docs/booking.md), GET/POST /api/appointments, PATCH /api/appointments/:id {status}
Messages POST /api/messages `{contact_id, channel: email
Channels & providers GET/PUT/DELETE /api/settings/channels/telegram|discord|slack, GET/PUT /api/settings/channels/push {enabled, button, title, text} (on/off instead of deleting, no test), POST /api/settings/channels/telegram/webhook, GET /api/settings/channels/telegram/link?campaign=, POST /api/settings/channels/discord/commands, POST /api/settings/channels/telegram|discord|slack/test; email/SMS providers: GET /api/settings/channels/mail|sms, PUT /api/settings/channels/mail/<resend|postmark|ses|brevo|mailgun> or …/sms/<seven|bird|vonage|plivo> {…credentials, activate: true}, DELETE …, POST …/test. Inbound webhooks (public, signed): /hooks/telegram/:loc, /hooks/discord/:loc, /hooks/slack/:loc, /hooks/mail/:provider/:loc/:key, /hooks/sms/:provider/:loc/:key (Channels & providers)
Workflows GET /api/workflows, POST /api/workflows/:id/enroll {contact_ids}; the send_email step optionally takes design (as with templates, rendered into body/text when saved); folders : GET/POST /api/workflow-folders {name, parent_id?} (nested, at most 5 levels), PATCH/DELETE /api/workflow-folders/:id (deleting keeps the workflows and subfolders – they move up one level); GET /api/workflows returns folder_id; POST /api/workflows/bulk {action: move|activate|pause|draft|delete, ids, folder_id?} → {count, failed} (status changes and deletion go through the normal routes – validation, versions, history)
Templates & campaigns GET/POST /api/templates, GET/PATCH/DELETE /api/templates/:id, GET/POST /api/campaigns, PATCH /api/campaigns/:id, POST /api/campaigns/:id/send {scheduled_at?}, POST /api/campaigns/:id/test {contact_id} – emails optionally with design (drag-and-drop JSON, email designer); the server renders body (HTML) and body_text (text version) from it, design: null = plain email
Email designer GET /api/email-builder/starters, POST /api/email-builder/render {design, campaign?} → {html, text, issues}, POST /api/email-builder/test {design, subject, to}, GET/POST /api/email-builder/sections, DELETE /api/email-builder/sections/:id
Email marketing Campaigns additionally with ab (A/B test: {variants: [{subject, sender_name?, design?}], test_percent, metric: opens|clicks|revenue, wait_hours}), resend (reminder to non-openers), send_mode: sto (best time per contact), sender_name, wa_template (WhatsApp channel), filter.segment_id; GET /api/campaigns/:id/ab, POST /api/campaigns/:id/ab/winner, POST/DELETE /api/campaigns/:id/resend, GET /api/campaigns/:id/sto – Email marketing
Segments GET/POST /api/segments, GET/PATCH/DELETE /api/segments/:id, GET /api/segments/meta, POST /api/segments/count {definition}, GET /api/segments/:id/contacts, POST /api/segments/:id/count (conditions)
Template gallery GET /api/email-gallery?q=&category=, GET /api/email-gallery/:key, POST /api/email-gallery/:key/use {target: template|campaign, campaign_id?, name?}
Deliverability GET/POST/PUT/DELETE /api/deliverability/warmup, GET /api/deliverability/reputation?days=, GET/PUT /api/deliverability/settings, GET /api/deliverability/hygiene, POST /api/deliverability/hygiene/suppress|sunset, POST /api/email-validation/check {emails}, GET /api/email-validation/summary|issues, POST /api/email-validation/scan|tag
Forms GET /api/forms, GET /api/forms/:id/submissions
Invoices GET/POST /api/invoices, POST /api/invoices/:id/send, PATCH /api/invoices/:id {status: paid} or `{recurring: {interval: week
Contracts GET/POST /api/contracts (?kind=template, {template_id, contact_id}), PATCH/DELETE /api/contracts/:id, POST /api/contracts/:id/send {channel, subject, body}, several signers: GET/PUT /api/contracts/:id/signers {mode, signers: [{role, name, email}]}, POST /api/contracts/:id/signers/:signerId/remind (sales.md)
Social Planner GET /api/social (channels), GET /api/social/posts?from&to, POST /api/social/posts `{integration_ids, content, type: schedule
Social: approval & evergreen `GET
Calendar sync POST /api/calendars/:id/sync {urls}, GET /api/calendars/:id/busy, GET /api/calendars/:id/feed (iCal); GET /api/calendars/:id/connections (linked Google/Outlook accounts)
Calendar connections (per login, only when signed in in the browser) GET /api/me/calendar-connections (connections with status, log), GET /api/me/calendar-connections/booking-calendars, GET /api/me/calendar-connections/:id/calendars (calendars at Google/Outlook), PUT /api/me/calendar-connections/:id {check_calendars, target_calendar, booking_calendars}, POST /api/me/calendar-connections/:id/sync; connect: POST /api/oauth/start {provider: google|microsoft, purpose: calendar|business, location_id?} → {url} (redirect to Google/Microsoft, back via GET /oauth/:provider/callback), disconnect: DELETE /api/oauth/connections/:id, GET /api/oauth/providers
Tracking GET/POST /api/trigger-links, GET /api/campaigns/:id/engagement (people: opened, clicked, rates; machines apart: machine_opened, machine_clicked, machine[] with reasons – docs/email-tracking.md); public pixel and link GET/HEAD /t/o/:id, /t/c/:id
QR codes GET /api/qr?url=<https link or /path>&format=png|svg&size=120..2048&fg=%23111827&bg=%23ffffff&ec=L|M|Q|H&margin=0..8&download=1&name=<file name> – draws the QR code of a link as PNG (default) or SVG, with download=1 as a file download. Signed-in users only; only an http(s) link is encoded (a path like /book/beratung is completed with the base URL), at most 2000 characters. The UI shows a “QR code” button next to booking pages and review, purchase, funnel and trigger links
Form files GET /api/form-files/:id (only with login/key)
Stripe GET/PUT/DELETE /api/stripe ({secret_key, webhook_secret, options: {auto_send_receipt}}, response without the key), POST /api/stripe/test, GET /api/stripe/payments?invoice_id=, GET /api/stripe/products, PUT /api/stripe/products/:id {slug, tag, redirect_url, enabled}
Purchase extras GET/PUT /api/buy-offers/:productId {bump_product_id, bump_title, bump_text, upsell_product_id, upsell_title, upsell_text, stripe_promo} (response with coupons and upsells: {offered, accepted, declined, fallback}), POST /api/buy-offers/:productId/coupons {code, kind: percent|amount, value (percent, or gross cents), max_uses?, expires_at?}, PATCH /api/buy-coupons/:id {active}, DELETE /api/buy-coupons/:id
A/B tests GET /api/ab-tests?page_id=&funnel_id=&status=draft|running|paused|ended, POST /api/ab-tests {page_id, name?, goal: {type: form|booking|purchase|page, form_id?, calendar_id?, product_id?, page_id?, url?}} (creates A = live page and B = copy), GET/PATCH/DELETE /api/ab-tests/:id ({name, goal, weights: {<variantId>: percent}} – sum 100; ?blocks=0 without page content), POST /api/ab-tests/:id/start|pause|end, POST /api/ab-tests/:id/winner {variant_id} (the winner becomes the live page, the test ends), POST /api/ab-tests/:id/variants {name?, from?} (up to D), PATCH/DELETE /api/ab-variants/:id {name, blocks, seo}, POST /api/ab-variants/:id/preview (HTML). Result per variant: visitors, views, conversions, rate, uplift, z, p_value, significant, prob_beat, verdict: control|too_early|better|worse|no_difference; per test small_sample, leader
Push (per login) GET /api/push, GET /api/push/public-key, POST /api/push/subscribe {subscription}, DELETE /api/push/subscribe?endpoint=, DELETE /api/push/devices/:id, PUT /api/push/prefs {messages, uptime, tasks, payments}, POST /api/push/test, GET /api/push/latest
Telephony POST /api/calls {contact_id, record?} (click-to-call: your phone first, then the contact), `GET /api/calls?contact_id&status=missed,voicemail&direction=in
Google reviews GET /api/google-reviews (stars, count, trend, reviews with reply_text, business: {ready, account_email, location_title}), POST /api/google-reviews/sync, GET/PUT/DELETE /api/google-reviews/settings {api_key, place_id}, POST /api/google-reviews/settings/find-place {query}, AI replies: GET/PUT /api/google-reviews/reply-settings {tone: freundlich|herzlich|professionell|sachlich|locker, formality: sie|du, instructions, signature, auto_draft} (tone: friendly, warm, professional, matter-of-fact, casual; formality: formal Sie or informal du), POST /api/google-reviews/:id/suggest-reply {tone?, instruction?} → {text, tone, model, review} (saves the text as reply_draft and publishes nothing; counts toward the monthly AI limit), PUT /api/google-reviews/:id/reply-draft {text}, DELETE /api/google-reviews/:id/reply-draft
Google Business Profile GET /api/google-reviews/business (connection, selected location), GET /api/google-reviews/business/locations, PUT /api/google-reviews/business {account: "accounts/…", location: "locations/…"} (fetches right away), POST /api/google-reviews/:id/reply {comment} (reply or edit), DELETE /api/google-reviews/:id/reply, GET/POST /api/google-reviews/posts {summary, image_url?, cta_type?: LEARN_MORE|BOOK|ORDER|SHOP|SIGN_UP|CALL, cta_url?}, DELETE /api/google-reviews/posts/:id – needs the sub-account’s OAuth connection (setup)
Browser phone GET /api/phone/status (ready? reachable?), GET /api/phone/token (signed-in users only: {token, identity, edge, ttl} – Twilio access token with VoiceGrant), POST/DELETE /api/phone/presence {available}, POST /api/phone/accepted {call_id, agent_sid}, GET /api/phone/calls/:id (ID or Twilio CallSid), POST /api/phone/calls/:id/hold, POST /api/phone/calls/:id/hangup, GET/PUT /api/phone/settings {enabled, api_key_sid, api_secret, twiml_app_sid, incoming: parallel|browser_only|off, edge} (response without the secret), POST /api/phone/setup (creates the API key/TwiML app at Twilio)
Phone menu, groups, audio GET/POST /api/telephony/ivr, GET/PUT/DELETE /api/telephony/ivr/:id, POST /api/telephony/ivr/:id/simulate {keys, at} (test run), GET /api/telephony/ivr/meta; GET/POST /api/telephony/schedules, PUT/DELETE …/:id, GET /api/telephony/schedules/holidays?year&state; GET/POST /api/telephony/groups, GET/PUT/DELETE …/:id, GET …/:id/stats; GET/POST /api/telephony/audio (raw MP3/WAV with ?name&kind or {name, kind, text}), PATCH/DELETE …/:id – details: docs/telephony.md
Numbers & tracking GET /api/telephony/registry, PATCH /api/telephony/registry/:id {label, kind, route_kind, route_id, pool_id, source, medium, campaign}, GET /api/telephony/numbers/search?country&type&area_code&contains, GET /api/telephony/numbers/pricing, GET /api/telephony/regulatory, POST /api/telephony/numbers/buy {phone_number, confirm: true, …}, POST /api/telephony/numbers/:id/release {confirm: true}; GET/POST /api/telephony/dni/pools, PUT/DELETE …/:id, PUT …/:id/numbers, GET /api/telephony/tracking/report(.csv)?group=source|campaign|number; public GET /a/:loc/dni.js, POST /api/public/dni/:loc/session
During the call GET/POST /api/calls/:id/notes, DELETE /api/calls/:id/notes/:noteId, POST /api/phone/calls/:id/transfer {to: {type: user|number, id}, mode: cold|warm, note}, GET /api/phone/calls/:id/transfer, GET /api/phone/colleagues
Power dialer GET/PUT /api/dialer/settings, GET/POST /api/dialer/lists {name, source: smart_list|tags|manual, smart_list_id|tags|contact_ids, drop_id}, GET/PATCH/DELETE /api/dialer/lists/:id, POST …/sync, GET …/items, POST …/next, POST /api/dialer/items/:id/dial|skip|disposition {code, note, callback_at, callback_user}, POST /api/dialer/calls/:callId/drop, GET /api/dialer/callbacks, GET /api/dialer/stats
Call analysis GET /api/call-intel?contact_id&status&source&sentiment&q&from&to, GET /api/call-intel/stats, GET/PUT /api/call-intel/settings, POST /api/call-intel/upload?title&contact_id&language&consent=1 (raw audio), POST /api/call-intel/text {title, transcript, contact_id}, POST /api/call-intel/calls/:callId/analyze, GET/PATCH/DELETE /api/call-intel/:id, POST …/:id/retry, POST …/:id/action-items/:n/task, PATCH …/:id/action-items/:n; scorecards: GET/POST /api/call-scorecards, PUT/DELETE …/:id, GET …/templates, GET …/:id/report, POST/GET /api/call-intel/:id/scores, PUT /api/call-intel/:id/scores/:sid/review

| AI phone agent | GET/PUT /api/voice-ai/settings {mode: off\|no_answer\|after_hours\|always, greeting, persona, instructions, voice, max_minutes, max_turns, transfer, transfer_number, booking, calendar_ids, booking_sms, sms_link, tasks, pipeline_id, stage_id, notify_email, notify_push, goodbye}, GET /api/voice-ai/calls?limit (AI calls with outcome and summary), GET /api/voice-ai/calls/:callId (with conversation history and actions) | | Blog | GET/POST /api/funnels/:id/posts (?status=draft\|scheduled\|published; POST {title, content (Markdown), excerpt, cover_image, author, tags, status, publish_at, seo: {title, description, og_image, noindex}, slug} – default is draft), GET/PATCH/DELETE /api/funnels/:id/posts/:postId, GET /api/funnels/:id/blog (address, RSS, tags), POST /api/funnels/:id/posts/preview (HTML) | | Courses | GET/POST /api/courses, GET/PATCH/DELETE /api/courses/:id (access: {tags, product_ids, revoke_on_tag_removal, welcome_email, welcome_subject, welcome_body}), POST /api/courses/:id/modules, PATCH/DELETE /api/courses/:id/modules/:mid (title, drip_days, published), POST /api/courses/:id/lessons (module_id, title, video_url, body, downloads: [{upload_id, name}], duration_min, drip_days, published), PATCH/DELETE /api/courses/:id/lessons/:lid, POST /api/courses/:id/reorder {modules: [{id, lessons: [id…]}]} | | Course participants | GET /api/courses/:id/enrollments (with progress), POST /api/courses/:id/enrollments {contact_id} or {email, name}, DELETE /api/courses/:id/enrollments/:contactId (revoke access), POST /api/courses/:id/enrollments/:contactId/login-link, GET /api/contacts/:id/courses, GET/PATCH /api/courses/portal (slug, settings: {title, welcome, color, logo_url, support_email}), POST /api/courses/:id/copy {location_id} (course with modules, lessons and download files into another sub-account, without participants) | | Course video & depth | Videos (/api/courses/videos, chunked upload, subtitles, cover image, HLS), discussion (/api/courses/:id/comments), reviews (/api/courses/:id/reviews), learning paths (/api/courses/paths), cohorts (/api/courses/:id/cohorts), analytics (/api/courses/:id/analytics, .csv) – all routes and fields in docs/courses.md. Lessons additionally take video_id, chapters, complete_percent. | | Forms Pro | Templates (/api/forms/templates), designs (/api/forms/themes), per-field analytics (/api/forms/:id/analytics), Turnstile keys (/api/forms/captcha); fields and settings for layout, jumps, scores, calculators, payment – see docs/forms.md. Workflow triggers form.scored, form.paid. | | Community groups | GET/POST /api/community/groups (POST {name, description, cover_image, access: {mode: all\|courses\|tags\|join, course_ids, tags}, settings, channels?: [{name, admin_only}]}), GET/PATCH/DELETE /api/community/groups/:id (status: active\|archived; settings: {word_filter, filter_mode: mask\|block, posts_per_hour, comments_per_hour, auto_hide_reports, points: {post, comment, like}, levels: [{name, points}], level_unlocks: [{level, course_id}]}), POST /api/community/groups/:id/channels {name, emoji, category, description, admin_only}, PATCH/DELETE /api/community/groups/:id/channels/:cid (?move_to=<channel> moves the posts), POST /api/community/groups/:id/channels/reorder {ids}, GET/PUT /api/community/settings {digest_enabled, digest_hour} | | Community posts | GET /api/community/posts?group_id&channel_id&status=visible\|hidden&limit, GET/POST /api/community/groups/:id/posts (POST as the team: {channel_id, title, body (Markdown), pinned, notify}; GET with ?q&offset), GET/PATCH/DELETE /api/community/posts/:id ({pinned, status, channel_id, title, body}), POST /api/community/posts/:id/comments {body, parent_id}, PATCH/DELETE /api/community/comments/:id ({status}), GET /api/community/files/:id (images in posts) | | Community moderation | GET /api/community/reports?status=open\|resolved\|dismissed\|all&group_id, PATCH /api/community/reports/:id {action: dismiss\|hide\|delete}, GET /api/community/groups/:id/members?q&banned=1 (with points and level), POST/DELETE /api/community/groups/:id/members/:contactId/ban ({reason, hide_content}), GET /api/community/groups/:id/leaderboard?period=week\|month\|all, GET/POST /api/community/groups/:id/events (?past=1; POST {title, starts_at, ends_at, link (https), description, notify}), PATCH/DELETE /api/community/events/:id | | Attribution | GET /api/attribution/report?from&to&group=source\|source_medium\|campaign\|funnel&model=first\|last (leads, won deals, revenue per source), GET /api/attribution/report.csv, GET /api/attribution/sources, GET /api/contacts/:id/attribution (first/last touch + history), GET/PUT /api/attribution/settings {enabled, cookie: consent\|always\|never, cookie_days, banner, banner_text}; filter contacts with GET /api/contacts?attr_source&attr_medium&attr_campaign&attr_touch=first\|last (docs/growth.md) | | Reports | GET /api/reports/sources (sources with fields, operators, measures, ranges), POST /api/reports/run {query: {source, measures: ["count", "sum:value", "calc:win_rate"], group_by: ["stage", "created_at:month"], filters: [{field, op, value}], date_field, range: "last_30_days"\|{from, to}, compare, limit}, scope: location\|all, location_ids} → {dims, measures, rows: [{k, l, v, p}], totals, axis, range, previous}, POST /api/reports/export.csv, GET /api/reports/values?source&field (values for filters), GET/POST /api/reports/library, GET/PATCH/DELETE /api/reports/library/:id {name, description, visibility: private\|team\|admins, config: {viz, query, fixed_range}}, GET/POST /api/reports/dashboards, GET/PATCH/DELETE /api/reports/dashboards/:id {name, visibility, widgets: [{id, title, config\|report_id, x, y, w, h}], settings: {range, scope, refresh, compare}}, GET /api/reports/dashboards/:id/data?range&scope&compare=1, PUT /api/reports/default {dashboard_id}, GET /api/reports/templates, POST /api/reports/templates/:key, GET/POST /api/reports/schedules {dashboard_id\|report_id, frequency: daily\|weekly\|monthly, weekday, monthday, hour, recipients, range}, PATCH/DELETE /api/reports/schedules/:id, POST /api/reports/schedules/:id/send, GET /api/reports/schedules/:id/preview (docs/reports.md) | | Affiliates | GET/POST /api/affiliates/programs ({name, commission_type: percent\|fixed, commission_value, attribution: first\|last, cookie_days, recurring, payout_threshold_cents, hold_days, landing_url, public_signup, auto_approve, terms}), PATCH/DELETE /api/affiliates/programs/:id, GET/POST /api/affiliates ({program_id, name, email, code?, status?, payout_method, iban, bic, paypal_email}), PATCH/DELETE /api/affiliates/:id, POST /api/affiliates/:id/login-link, GET /api/affiliates/overview, GET /api/affiliates/commissions?status&affiliate_id, POST /api/affiliates/commissions (manual credit), POST /api/affiliates/commissions/approve {ids}, POST /api/affiliates/commissions/:id/reverse, GET/POST /api/affiliates/payouts, POST /api/affiliates/payouts/:id/paid, DELETE /api/affiliates/payouts/:id, GET /api/affiliates/payouts/export.csv?ids=, GET /api/affiliates/payouts/sepa.xml?ids= (pain.001) | | REST hooks | GET /api/hooks/me, GET /api/hooks/triggers, GET /api/hooks/triggers/:key/sample, GET/POST /api/hooks {target_url, event, filters, source, label} → {id, secret, …}, GET/PATCH/DELETE /api/hooks/:id ({status: active\|paused, filters, label}), POST /api/hooks/:id/test, GET /api/hooks/deliveries?hook_id&status&event&limit&offset, GET /api/hooks/deliveries/:id, POST /api/hooks/deliveries/:id/retry (details) | | REST hook actions | GET /api/hooks/actions, POST /api/hooks/actions/create_contact\|update_contact\|add_tag\|remove_tag\|create_opportunity\|move_stage\|create_task\|add_note\|send_message\|create_invoice_draft (contact by contact_id, email or phone; pipeline/stage by ID or name), GET /api/hooks/search/contacts?email&phone&q&id, GET /api/hooks/search/opportunities?contact_id&email&status&q&id | | Inbound webhooks | GET/PUT /api/inbound-webhooks/:workflowId {mapping: [{from, to}], tags, capture, rate_limit}, POST /api/inbound-webhooks/:workflowId/secret ({enabled: false} = off), POST …/rotate, POST …/preview, DELETE …/captures | | Integrations | GET /api/integrations/catalog (status per integration in the sub-account), GET /api/agency/integrations/catalog (admins only, summarized per sub-account) | | HubSpot import (UI only) | GET /api/import/hubspot, PUT/DELETE /api/import/hubspot/connection {token}, POST /api/import/hubspot/dry-run {parts}, POST /api/import/hubspot/start {parts}, GET /api/import/hubspot/jobs[/:id], POST /api/import/hubspot/jobs/:id/pause\|resume | | Advertising | GET /api/ads/accounts (without keys), PUT/DELETE /api/ads/accounts/meta\|google (Meta {ad_account_id, token, pixel_id, capi_enabled, capi_token, capi_only_meta_leads, test_event_code}, Google {customer_id, login_customer_id, developer_token, client_id, client_secret, refresh_token}), POST /api/ads/accounts/:platform/test, POST /api/ads/sync {platform?, days?}, GET /api/ads/report?from&to&model (spend, CRM leads, CPL, revenue, ROAS per campaign), GET /api/ads/capi-log | | Academy (per login, only when signed in in the browser) | GET /api/academy (progress per tour, current tour, practice company), PUT /api/academy/tours/:id {step, total, active?, done?}, DELETE /api/academy/tours/:id, DELETE /api/academy/active, POST/DELETE /api/academy/sandbox (admins only: create/delete the practice company with demo data), GET /api/academy/docs, GET /api/academy/docs/:name (guides from docs/; customer logins get only a selection) – details in academy.md | | Tags & Smart Lists | GET/POST /api/tags {name} (list with count per tag), DELETE /api/tags/:id (also removes the tag from all contacts), GET /api/tags/meta → {tags: [{id, name, color, category_id}], categories} (colors and categories for the chips, readable by everyone in the sub-account), PATCH /api/tags/:id {color: "#rrggbb" \| "", category_id} (undo via the history), GET/POST /api/tag-categories {name, color} (list with tag_count), PATCH/DELETE /api/tag-categories/:id {name, color, position} (deleting keeps the tags, they only lose the category; snapshots carry colors and categories along, part tag_meta), Smart Lists : GET /api/smart-lists (shared lists and your own private ones, with columns, sort, sort_dir, visibility, owner_name, mine, can_edit), POST /api/smart-lists {name, filter, columns?, sort?, sort_dir?: asc\|desc, visibility?: shared\|private} (private only when signed in), GET/PATCH/DELETE /api/smart-lists/:id (change: creator, or admin/key for shared lists; visibility: creator only), GET /api/smart-lists/:id/contacts?limit=&offset=&sort=&dir= → {list, total, contacts} (record access applies); sorting of GET /api/contacts: sort=name\|first_name\|last_name\|email\|phone\|company\|city\|postal_code\|country\|website\|source\|date_of_birth\|created\|updated\|activity\|custom.<key>\|score with dir=asc\|desc (unknown keys fall back to newest first; columns are a fixed list, custom fields must exist and go in as parameters); Tags in bulk : GET /api/tags/usage → {tags: [{id, name, color, category_id, contacts, workflows, smart_lists, unused}]} (usage per tag), POST /api/tags/bulk {action: rename\|merge\|delete\|color\|category, ids, name?, into?, color?, category_id?} – rename (exactly one tag, name still free), merge (into = target tag; the others disappear, their contacts get the target without duplicates), delete, color, file into a category; renames the tags on the contacts in SQL without firing tag workflows; response with references (workflows/smart lists that still mention the old name); one history entry | | Custom fields | GET/POST /api/custom-fields {label, key?, type: text\|textarea\|number\|date\|select\|checkbox\|email\|phone\|url, options?, position?} (the key consists of a–z, 0–9 and _), PATCH/DELETE /api/custom-fields/:id; Clean-up : GET /api/custom-fields/usage → {fields: [{id, key, label, contacts, forms, workflows, unused}]} (contacts with a value, forms with custom.<key>, workflows that mention the field), POST /api/custom-fields/bulk-delete {ids, force?} (fields in use only with force; the values on the contacts stay; can be undone) | | Notes & activity | GET /api/notes?contact_id=&q=&pinned=1&limit= (the notes of one contact, or a search across title, text and contact name over all notes; pinned first, each with html – Markdown, sanitized by the server with the wiki renderer), POST /api/notes {contact_id, title, body, color: yellow\|green\|blue\|red\|purple\|gray\|"", pinned}, PATCH /api/notes/:id (undo via the history), DELETE /api/notes/:id (notes can also be created with POST /api/contacts/:id/notes; the workflow step add_note takes title, color, pinned), GET /api/activities?limit= (activity feed of the sub-accounts, at most 200); Documents on a contact / attachments on notes : GET /api/contacts/:id/documents (without content; other people’s “only for me” files are missing), POST /api/contacts/:id/documents {name, data (Base64 or data URL), mime?, note_id?, private?} (images, PDF, text, CSV, Word/Excel/PowerPoint/OpenDocument; the content must match the type; at most 8 MB, 200 per contact, PGHL_CONTACT_DOCS_MB per sub-account, default 2048), GET /api/contacts/:id/documents/:docId (download as an attachment with nosniff and a sandbox CSP; ?inline=1 displays images/PDF), PATCH {name, private} / DELETE on the same address; Share in the customer portal (sales.md): GET /api/contacts/:id/portal-files, PUT /api/contacts/:id/documents/:docId/portal {shared}, GET/PUT /api/portal-files/settings {uploads, max_mb, quota_mb, types, notify}, in the portal /portal/:p/documents (list, download as an attachment, upload via form); Change history : GET /api/contacts/:id/field-history?limit= → [{field, label, old, new, actor_kind: user\|api\|workflow\|system, actor_name, opportunity_id, created_at}] (contact and deal fields; values hidden by record permissions – including individual custom fields – come back empty with hidden: true; changes to deals the person may not open are left out) | | Products | GET/POST /api/products {name, description, price_cents, currency, tax_rate, recurring: month\|year, payment_link}, PATCH/DELETE /api/products/:id – purchase links and Stripe mapping: row Stripe | | Review requests | GET /api/reviews → {requests, stats: {sent, responses, average, distribution}, widget}, POST /api/reviews/request {contact_id, channel: email\|sms}, PATCH /api/reviews/:id {public} (show the response on the website) – Google reviews: row Google reviews | | Search & dashboard | GET /api/search?q= (at least 2 characters) → {contacts, opportunities} (the hits the person may open under the record permissions; at most 8 and 5 respectively), GET /api/dashboard?days=30 (key figures, trend, sources; days 1–365; revenue per currency in revenue.by_currency [{currency, paid, outstanding}] – amounts in different currencies are never added up, revenue.paid/outstanding apply to the main currency revenue.currency) | | Outbound webhooks | GET/POST /api/webhooks {url, events: ["*"] \| ["contact.created", …]} → with a secret for verifying the signature (the list shows it to the owner too), DELETE /api/webhooks/:id. Every event is sent to the URL as JSON {type, locationId, at, data}, with x-pghl-signature-256: sha256=<HMAC-SHA256 of the body with the secret> – retries with a delivery log are offered by the REST hooks | | Uploads | POST /api/uploads {name, data: "data:image/png;base64,…"} (PNG, JPG, GIF, WebP or PDF up to 8 MB, the content is checked) → {id, url, …}, GET /api/uploads; served at /uploads/<file> | | API keys | GET/POST /api/api-keys {name} → {id, name, key} (key pghl_… only in this response; an API key cannot create keys), DELETE /api/api-keys/:id; agency keys: GET/POST /api/agency/api-keys, DELETE /api/agency/api-keys/:id | | Audit log | GET /api/audit?limit=&before=&q= (log of the sub-account, at most 500 rows), for admins GET /api/admin/audit?location_id=&via= across all sub-accounts | | Sub-accounts & permissions | Admins only: GET/POST /api/locations {name, timezone}, DELETE /api/locations/:id (the last one cannot be deleted; deletes all data of the sub-account including files – form uploads, personnel files, receipts, certificates, imports, recordings; the media library and the audit log stay, response {ok, rows, files}). For every login: GET /api/permissions (the areas a customer login can get), GET /api/branding (public, by domain) and GET /api/branding/me (white label of your own login), GET /api/domain-info (server IP and whether a domain agent is running), GET /api/livechat (settings and embed code of the live chat) | | Snapshots (admins only) | GET/POST /api/snapshots {location_id, name, description} (creates a starter pack from a sub-account), POST /api/snapshots/import {name, data}, GET /api/snapshots/:id/download (JSON), POST /api/snapshots/:id/apply {location_id, activate_workflows?}, DELETE /api/snapshots/:id | | Two-factor login (per login, only when signed in) | GET /api/me/2fa → {enabled, backup_left}, POST /api/me/2fa/setup → {secret, uri, qr} (QR code as a data: URL), POST /api/me/2fa/enable {code} → {enabled, backup_codes}, POST /api/me/2fa/backup-codes {code} (new backup codes), POST /api/me/2fa/disable {password, code}; admins: POST /api/users/:id/2fa/reset (for someone who lost their phone) | | Test message | POST /api/settings/test-message {channel: email\|sms, to} really sends a test email or SMS through the sub-account’s configured provider | | Agency IMAP mailboxes (admins only) | GET/POST /api/agency/mail-accounts, PATCH/DELETE /api/agency/mail-accounts/:id, POST /api/agency/mail-accounts/:id/sync – an agency mailbox (e.g. Gmail with an app password) is read via IMAP, rules assign each email to a project; the emails land there in the conversations. Gmail and Outlook through their own APIs: Mailbox sync | | Call analysis per call | GET /api/call-intel/by-call/:callId → the analysis of one call or {id: null} (Telephony) | | More areas | invoice settings GET/PUT /api/invoice-settings (Invoices), GET/PATCH /api/appearance and GET /api/appearance/public (Appearance), /api/ai-connections/* (AI providers), /api/video-studio/* (Video studio), GET /api/connector-apps/google-calendars (Workflow actions), GET /api/video-studio/ugc-voices (voices for UGC videos, Video studio), GET /api/marktplatz/:id/security-reviews (admins only: previous AI security reviews of a plugin, Werkstatt) |

System (public): GET /healthz (ok, otherwise degraded: <check>; ?strict=1 answers 503 on any failing check, ?format=json lists the status of the checks – docs/backups.md), GET /status (public status page, in cloud mode or with PGHL_STATUS_PAGE=1), GET /api/version → {name, version, node, demo, setup_required} (monitoring, deploy check; demo only in demo mode with the demo login), GET /api/tls/ask?domain= (200 = the domain belongs to this installation – for Caddy’s on-demand TLS), POST /api/auth/setup (first-time setup, only while no account exists). Demo mode only: POST /api/demo/login (one-click login), GET /api/demo/outbox (admins: blocked connections).

Public (no key, CORS open): GET /api/public/calendars/:slug/slots, POST /api/public/calendars/:slug/book, POST /api/public/forms/:id/submit (file fields as {name, type, data: "data:…;base64,…"}), GET/POST /api/public/chat/:locationId/messages, POST /api/public/contracts/:token/sign. Meta webhook (WhatsApp, Messenger, Instagram; only with a valid X-Hub-Signature-256): GET/POST /hooks/meta/:locationId.

HyperLevel’s public pages also call (no key, only with the token from their link): POST /api/public/appointments/:token/cancel (cancel an appointment via the manage link), GET /api/public/team-calendars/manage/:token/slots (free times for rescheduling), POST /api/public/invoices/:token/accept (accept a quote), POST /api/public/contracts/:token/decline {reason}, POST /api/public/reviews/:token {rating: 1–5, comment}, GET /api/public/canvas/:token (shared canvas as SVG), GET /api/public/wiki/:token (shared wiki page, …/pdf as PDF), POST /api/public/webinars/:slug/vote {t, poll_id, choice} and POST /api/public/push/:locationId {subscription, attr?} or …/unsubscribe {endpoint} (browser push of a website visitor). The sign-in page asks GET /api/auth/status → {needsSetup}; the domain agent (header x-agent-token) fetches GET /api/agent/domains and reports POST /api/agent/domains/report {domain, status: active|pending_dns|error|removed, message} (example).

Stripe: GET /buy/:product-id-or-slug (purchase page, ?email= pre-fills; form fields bump=1, coupon=<code>), GET /api/public/buy/:key/coupon?code= (check the code → {ok, label, discount_cents, total_cents} or {ok: false, error}), upsell page /upsell/:token with POST /upsell/:token/accept or /decline, POST /hooks/stripe/:locationId (only with a valid Stripe-Signature), return /i/:token/stripe or /buy/:key/danke. Events for workflows/webhooks: invoice.paid (via: "Stripe"), product.purchased (contactId, productId, invoiceId, amount_cents, tag, metadata; order bumps additionally with via: "order_bump"), product.subscription_ended, website.down/website.up.

Twilio (only with a valid X-Twilio-Signature): POST /hooks/twilio/voice/:locationId (incoming call → TwiML), /hooks/twilio/voice-status/:locationId, /hooks/twilio/voice-dial/:locationId, /hooks/twilio/voice-recording/:locationId; AI phone agent: /hooks/twilio/voice-ai/:locationId?t=<round> (<Gather> result with SpeechResult), /hooks/twilio/voice-ai-wait/:locationId, /hooks/twilio/voice-ai-transfer/:locationId; Browser phone: /hooks/twilio/phone-out/:locationId (voice URL of the TwiML app), /hooks/twilio/phone-dial/:locationId, /hooks/twilio/phone-leg/:locationId, /hooks/twilio/phone-held/:locationId.

Public pages: blog at /p/<website>/blog (?tag=, ?page=), /p/<website>/blog/<post> and /p/<website>/blog/rss.xml – on a connected domain as /blog…; posts are included in the website’s sitemap.xml. Member area at /portal/<sub-account-id or short-address> (login by email link, its own cookie per sub-account, separate from admin logins); the community inside it at /portal/<…>/community (groups /g/<group>, posts /g/<group>/p/<id>, calendar, members, leaderboard, profiles /u/<handle>, notifications). Every form there carries a member-session token and is only accepted from the same origin.

Course events (webhooks and workflow triggers, all with contactId and courseId): course.enrolled (source: manual|tag|purchase|workflow|api), course.started, lesson.completed (lessonId, progress: {done, total, percent}), course.completed, course.access_revoked, plus course.comment_posted (lessonId, commentId, parentId, staff), course.review_posted (rating, status), course.path_enrolled / course.path_completed (pathId, path: {id, title}) and course.cohort_joined (cohortId, cohort: {id, name, starts_at}); forms additionally report form.scored (formId, submissionId, values, ending) and form.paid (formId, submissionId, productId, amount_cents); the blog reports blog.post_published. Courses listen to product.purchased {contactId, productId}. Community (all with groupId): community.post_created (contactId for member posts, postId, channelId, post: {title, by_team}), community.member_joined (contactId, when a member first opens the group), community.level_reached (contactId, level, level_name; one event per level reached). Workflow filters: group, and for “Level reached” also the level.

A/B tests on funnel pages: the cookie pghl_ab (random ID, 180 days) pins the variant. Forms, calendar widgets and purchase links on this visitor’s pages carry the ID along (_ab when submitting a form, ab on POST /api/public/calendars/:slug/book or ?ab= on the purchase link) – your own embeds can do the same. Events carry it as submission.meta.ab, meta.ab and metadata.ab respectively.

Attribution & partners (public): GET /a/:locationId/t.js (tracking script for external websites), POST /api/public/attribution/:locationId/touch {url, referrer, token?, consent?} → {token}, GET /a/consent?v=1|0&r=/path or <website>/pghl-consent?v=1|0 (remember consent), partner area /affiliate/<program> with /join (sign-up), /login (link by email), /auth, /logout. Events: affiliate.created, affiliate.commission_created (contactId = the partner’s contact, amount_cents, customerId, invoiceId), payment.refunded (Stripe charge.refunded: contactId, invoiceId, amount_cents, full).

Live chat on any website: <script src="https://<your-domain>/chat/<locationId>/widget.js" async></script> (Settings → Live chat shows the finished code).

Tickets, SLAs, knowledge base, surveys and health score (docs/helpdesk.md). The :id of a ticket may also be the ticket number (/api/tickets/42).

Area Endpoints
Tickets GET /api/tickets?state=open|resolved|closed|done|all&priority=hoch,dringend&owner=me|none|<user-id>&team_id&category&tag&source&contact_id&sla=warning|breached|at_risk&pipeline_id&stage_id&q&sort=updated|created|priority|due|number&limit&offset → {total, tickets} (with status, sla: {first, resolution}), POST /api/tickets {subject, description, contact_id, priority: niedrig|normal|hoch|dringend, category, tags, owner_id, team_id, pipeline_id, status, source, channel, due_at, company_id} (without owner_id the assignment rules apply; the priorities niedrig, normal, hoch, dringend are low, normal, high, urgent), GET /api/tickets/:id (ticket, history, contact, other tickets), PATCH /api/tickets/:id (status = stage ID, name or new|open|waiting|resolved|closed; priority also raise; owner_id also round_robin or ""; note = internal note), DELETE /api/tickets/:id, GET /api/tickets/meta (pipelines, staff, teams, categories, priorities, sources)
Ticket actions POST /api/tickets/:id/reply {body, channel?: email|sms|chat|whatsapp|messenger|instagram, subject?, status?} (really sent, subject with [Ticket #n]), POST /api/tickets/:id/notes {body}, POST /api/tickets/:id/merge {ids}, POST /api/tickets/bulk {ids, action: status|priority|owner|team|category|add_tag|remove_tag|merge|delete, value}, POST /api/tickets/from-conversation {conversation_id, …}, POST /api/tickets/from-call {call_id, …}
Helpdesk GET/PUT /api/helpdesk/settings ({categories, business_hours, holidays, sla: {enabled, warn_percent, policies: {<priority>: {first_response_min, resolution_min, business_hours}}, notify_owner, raise_priority_on_breach, breach_team_id}, assignment: {mode: manual|round_robin|category, default_team_id, rules: [{category, team_id, user_id}]}, email_to_ticket: {mode: off|rules|all, addresses, keywords, domains, include_imported, priority, category}, auto_reply, csat: {enabled, delay_min, channel, subject, body}, portal: {enabled, any_contact}, help_center: {enabled, slug, title, intro, color, domain, seo_title, seo_description, contact_text}, health: {enabled, weights, green, yellow, drop_points, activity_days}}; response with support_hook and help_center_url, area Settings), POST /api/helpdesk/settings/rotate-key, GET/POST /api/helpdesk/pipelines, PATCH/DELETE /api/helpdesk/pipelines/:id (stages: [{id?, name, kind, pause_sla}]), GET/POST /api/helpdesk/teams, PATCH/DELETE /api/helpdesk/teams/:id ({name, members, round_robin}), GET/POST /api/helpdesk/views, DELETE /api/helpdesk/views/:id ({name, shared, filters}), GET/POST /api/helpdesk/canned, PATCH/DELETE /api/helpdesk/canned/:id, POST /api/helpdesk/canned/:id/render {ticket_id}, GET /api/helpdesk/agents, GET /api/helpdesk/dashboard?days=30, GET /api/helpdesk/workload
Knowledge base GET/POST /api/kb/categories, PATCH/DELETE /api/kb/categories/:id, GET /api/kb/articles?status=draft|published&category_id&q, POST /api/kb/articles {title, body (Markdown), category_id, status, tags, excerpt, seo: {title, description, noindex}, slug}, GET/PATCH/DELETE /api/kb/articles/:id (with url, preview_url for drafts), GET /api/kb/search?q&limit&all=1&full=1, POST /api/kb/preview {body}, GET /api/kb/info
Surveys GET /api/surveys/csat?days&score, POST /api/surveys/csat/:ticketId/send, GET/POST /api/surveys/nps ({name, question, channel: email|sms, tags, smart_list_id, subject, body with {{nps_link}}}), PATCH/DELETE /api/surveys/nps/:id, POST /api/surveys/nps/:id/send → {queued, audience}, GET /api/surveys/nps/:id/responses, GET /api/surveys/summary?days
Health score GET /api/health/scores?ids=a,b → {<contactId>: {score, band}}, GET /api/health/contacts/:id (signals), POST /api/health/recompute, GET /api/health/summary, GET /api/contacts/:id/service (health, tickets, CSAT/NPS), contact list/smart lists with health=green|yellow|red|none

Public: support mailbox POST /hooks/:locationId/support-email?key=… ({from, to, subject, text\|html}; every email becomes a ticket or lands in the open ticket); the normal inbound POST /hooks/:locationId/email additionally accepts to (for the rule “to support address”). Help center GET /help/:address (?q= search), /help/:address/:article, /help/:address/k/:category, POST /help/:address/:article/feedback {helpful: 1\|0}, /help/:address/sitemap.xml, GET /api/public/help/:address/search?q (JSON, CORS open); on connected website domains under /help…. Surveys GET/POST /survey/csat/:token {score: 1–5, comment} and GET/POST /survey/nps/:token {score: 0–10, comment} (signed links). Customer portal /portal/:p/tickets (list + new request), /portal/:p/tickets/:number (+ /reply).

Events: ticket.created, ticket.status_changed, ticket.assigned, sla.warning, sla.breached (all with ticketId, ticket and contactId, details in docs/helpdesk.md), csat.received, nps.received, health_score.dropped. Workflow actions: ticket_create {subject, description, priority, category, team_id, owner_id, pipeline_id}, ticket_update {status, priority (also raise), team_id, owner_id (also round_robin, __none), note, tags, notify_owner}.

Bug reports, questions, wish list and automatic error capture (docs/bug-reports.md). Available to every signed-in user (session; not tied to an area); responses contain only your own reports and public wish data. Status object: {key: neu\|bestaetigt\|in_arbeit\|behoben\|ausgeliefert\|abgelehnt\|duplikat, board (wishes: idea\|planned\|in_progress\|done\|declined), label, tone, done} – the key values mean new, confirmed, in progress, fixed, shipped, declined and duplicate.

Area Endpoints
Me GET /api/feedback/me → {prefs: {auto_capture, email}, unread, changelog_new, reports_open, severities, categories, areas, board, screenshot_max, server: {version, web_build}}, PUT /api/feedback/prefs {auto_capture: true|false|null, email}, GET /api/feedback/notifications, POST /api/feedback/notifications/read {ids?}
Reports POST /api/feedback/reports {category: bug|question|wish, what, expected, steps, severity: gering|mittel|hoch|kritisch, title?, area? (wish), location_id (the sub-account the report comes from), screenshot (data: URL, PNG/JPEG/WebP ≤ 3 MB), diagnostics: {url, route, build, user_agent, browser, os, viewport, screen, language, timezone, errors: [{message, stack, source, line, col, at}], api_errors: [{method, path, status, error, at}]} | false} → report with number (ticket number); 15 per 10 minutes. The severity values mean gering low, mittel medium, hoch high, kritisch critical. GET /api/feedback/reports (your own), GET /api/feedback/reports/:id → {report, body, expected, steps, diagnostics, files, thread: [{type: team|user|status, body, author, at}], can_reply, can_reopen}, POST /api/feedback/reports/:id/messages {body, screenshot?}, POST /api/feedback/reports/:id/reopen {reason}, GET /api/feedback/files/:id (screenshot)
Wish list GET /api/feedback/wishes?q&area&status=idea|planned|in_progress|done|declined&sort=votes|new|status&mine=1 → [{id, number, title, body, area, status, votes, my_vote, comments, mine, author, origin, version, reason}] (author/origin for admins only), POST /api/feedback/wishes {title, what, area, screenshot?}, GET /api/feedback/wishes/:id → {wish, duplicate_of, files, thread}, POST/DELETE /api/feedback/wishes/:id/vote → {votes, my_vote}, POST /api/feedback/wishes/:id/comments {body}, GET /api/feedback/changelog → {entries: [{kind, title, note, version, at, url}], new_count}, POST /api/feedback/changelog/seen
Automatic POST /api/feedback/client-errors {errors: [{message, stack, source, line, col}] (max. 5), location_id, diagnostics} → {results: [{id, created} | {skipped}]} – only with capture switched on, 20 per user in 10 minutes. Unexpected server errors answer with 500 {error, request_id} and the header x-request-id.
Agency (admin) GET/PUT /api/feedback/settings {support_location_id ("" = create “<brand> Support” on the first report), capture_server, capture_client} → with support_location_name, pipeline_id, view_id, locations, open, POST /api/feedback/settings/setup, GET /api/feedback/admin/items?kind=reports|bug|question|wish|auto|all&state=open|done|all&status&area&q&sort=updated|new|priority|votes|occurrences&limit, GET /api/feedback/admin/items/:ref (:ref = ID, ticket ID or #number; with diagnostics, files, reporter, affected sub-accounts, votes, duplicates, history, internal notes), GET /api/feedback/admin/by-ticket/:ticketId → {item}, PATCH /api/feedback/admin/items/:ref {status, reason, reply, fixed_in|shipped_in, changelog, shipped, note, public_note, hidden, area, title, duplicate_of}, POST /api/feedback/admin/items/:ref/duplicate {of}, POST /api/feedback/admin/items/:ref/fixed {version, note, shipped}

Push type feedback (“My reports & wishes”), message type feedback in the app feed. MCP: ghl_list_bug_reports, ghl_get_bug_report, ghl_update_bug_report, ghl_list_wishes, ghl_update_wish (agency keys only).

Interface of the native Android app. JSON, money in cents (*_cents) plus currency (EUR), times in ISO 8601 UTC, errors {error} in German. Every list row carries location_id and location_name; ?location=all (default) combines all visible sub-accounts, ?location=<id> takes one (someone else’s → 403, unknown → 404). Areas that a customer login or its SaaS plan does not have are missing for all or return 403 with the reason.

Sign-in: POST /api/mobile/pair only with a session cookie + x-pghl: 1 (login of the app’s web view), body {device_name, platform, app_version} → {token: "ghm_…", device_id, user: {id, name, email, role, permissions}} (the token appears only in this response). All other calls use Authorization: Bearer ghm_… (optional x-app-version); unknown/signed out → 401, more than 300 requests/minute per device → 429. The token is valid only under /api/mobile/*.

Area Endpoints
Devices GET /api/mobile/devices (session or token; current = this device), DELETE /api/mobile/devices/:id (your own), POST /api/mobile/logout (token: signs this device out)
Profile GET /api/mobile/me → {user, agency, locations: [{id, name, timezone, color, status, areas}], server_time, app, device_id}; areas = what the login may do there (dashboard, conversations, calls, contacts, tasks, calendars, opportunities, payments, reviews, social, project), app = current APK or null
Key figures GET /api/mobile/summary?location= → {leads_today, leads_7d, open_deals_count, open_deals_value_cents, won_month_cents, revenue_today_cents, revenue_month_cents, open_invoices_cents, unread_conversations, tasks_due_today, tasks_overdue, appointments_today, missed_calls_today, rating: {avg, count} | null, websites: {up, down}, spark_revenue_30d: [30], spark_leads_14d: [14], currency, updated_at} (trends: oldest day first, today last)
Inbox GET /api/mobile/inbox?location&limit=10&unread=1&starred=1 → [{conversation_id, contact_id, contact_name, channel, last_message, last_direction, last_at, unread (count), starred}], GET /api/mobile/conversations/:id?limit=20 → {conversation_id, contact: {id, name, phone, email}, channels, messages: [{id, direction, channel, subject, body (text), at, status, error}]} (does not mark it as read), POST /api/mobile/conversations/:id/reply {body, channel?, subject?} (default: the channel of the last incoming message; email subject “Re: …”), POST /api/mobile/conversations/:id/read
Team inbox GET /api/mobile/inbox/queue?location&view=open&assignee&status&priority&limit=20 → {view, conversations: [{id, contact_id, contact_name, channel, last_message, last_message_at, unread, starred, status: open|waiting|closed, priority: low|normal|high|urgent, assignee_id, assignee_name, team_id, team_name, snoozed_until, sla: {state: ''|ok|warning|breached, kind: first|next, due_at, since}, link, location_id, location_name}]} (view: all, open, mine, unassigned, team, waiting, done, snoozed, sla, mentions), GET /api/mobile/conversations/:id/team → assignment, status, priority, response deadline and timeline (internal notes and changes), GET /api/mobile/inbox/people → {locations: [{location_id, location_name, people: [{id, name, away, me}], teams}]}, POST /api/mobile/conversations/:id/assign {user_id (ID, “me” or null) | team_id}, POST /api/mobile/conversations/:id/status {status?, snooze_until? (ISO or null), priority?}, POST /api/mobile/conversations/:id/notes {body} (internal note, @name mentions someone; never goes to the contact) – writes go through the same routes as the web app (permissions, audit log with “app”, docs/team-inbox.md)
Tasks GET /api/mobile/tasks?location&filter=open|today|overdue|week|done&limit=20 → [{id, title, description, due_at, done, contact_id, contact_name, assigned_to}], POST /api/mobile/tasks {location_id, title, description?, due_at? (ISO or YYYY-MM-DD), contact_id?}, POST /api/mobile/tasks/:id/complete, POST /api/mobile/tasks/:id/reopen
Project planning GET /api/mobile/pm/tasks?filter=open|today|overdue|done&limit=30 → my tasks from all sub-accounts in which the login has the area contacts: [{id, title, description, due_at, start_at, done, priority (0–4), status, status_category (todo|active|done), status_color, project_id, project_name, parent_id, tags, estimate_minutes, location_id, location_name, link}], GET /api/mobile/pm/tasks/:id → {task, statuses: [{id, name, category, color}], subtasks, comments}, POST /api/mobile/pm/tasks/:id/status {status_id | status (name) | category} → {id, done, status}, POST /api/mobile/pm/tasks/:id/comments {body} (@name mentions someone), GET /api/mobile/pm/timer → {running: {id, location_id, location_name, task_id, task_title, project_name, note, started_at, billable} | null}, POST /api/mobile/pm/timer/start {location_id, task_id?, project_id?, note?} (a running timer is stopped), POST /api/mobile/pm/timer/stop {note?} → {seconds, discarded, task_title}, GET /api/mobile/pm/notifications?limit=30 → {unread, items: [{id, kind: mention|assigned|due|overdue|comment|status|timer|timesheet|watch, title, body, link, task_id, location_id, location_name, read, created_at}]}, POST /api/mobile/pm/notifications/read {ids | all: true} – writes go through the same routes as the web app (permissions, checks, audit log with “app”)
Appointments GET /api/mobile/appointments?location&days=2 (from today, without blockers) → [{id, title, start, end, status, contact_id, contact_name, phone, meeting_url (video link takes precedence over a fixed location), address, calendar_name, host_id}], POST /api/mobile/appointments/:id/status {status: confirmed|cancelled|showed|noshow}
Deals GET /api/mobile/deals?location&limit=10 → {pipelines: [{id, name, stages: [{id, name, count, value_cents}]}], top: [{id, name, value_cents, currency, expected_close_date, lost_reason, status, stage_id, stage_name, pipeline_id, contact_id, contact_name, updated_at}]} (open ones, by value), POST /api/mobile/deals/:id/stage {stage_id}, POST /api/mobile/deals/:id/status {status: won|lost|open, lost_reason?} (without a reason on lost the reason is “Per App”, i.e. “via app”)
Contacts GET /api/mobile/contacts?location&q&limit=20 → [{id, name, first_name, last_name, email, phone, company}], POST /api/mobile/contacts {location_id, first_name, last_name?, email?, phone?, company?, note?, tags?} → contact with created (duplicates are merged by email/phone) and note, POST /api/mobile/notes {contact_id, body}
Calls GET /api/mobile/calls?location&limit=10&missed=1 → [{id, direction, status, missed, from, to, contact_id, contact_name, at, duration_s, has_voicemail, summary}] (summary = AI summary or voicemail text), POST /api/mobile/calls {contact_id} → click-to-call via Twilio (400 with a hint if telephony is not set up)
Undo GET /api/mobile/undo/recent?location=all|<id>&limit=20 → {items: [{id, label, at, entity, entity_id, kind: change|undo, count, changes: [{k, l, from, to}], notice, can_undo, location_id, location_name}], server_time} – my last changes over every sub-account the login can open (the data of an undo snackbar), POST /api/mobile/undo/:id {force?, single?} → {ok, label, redo_id, notices} or 409 {error, conflict: true, diff: [{field, label, entry, now, restore}]} (repeat with force: true after the user confirmed). Writes through the app’s own routes (/api/mobile/tasks/:id/complete …) answer with the headers x-undo-id, x-undo-label (URL-encoded) and x-undo-notice – show “Task ‘…’ done · Undo” and call the undo route with that id – docs/undo.md
More GET /api/mobile/projects → [{location_id, name, status, website, uptime: {up, checked_at, response_ms}, open_tasks, overdue_tasks, unread, claude_url, deep_link}], GET /api/mobile/reviews?location&limit=5 → {avg, count, delta_30d (change in Google stars over 30 days), new_30d, latest: [{id, author, rating, text, at, source: google|intern, replied, link}]}, GET /api/mobile/payments?location&limit=5 → {today_cents, month_cents, open_invoices_cents, overdue_count, latest: [{id, kind, number, title, contact_name, total_cents, currency, status, overdue, paid_at}], currency}, GET /api/mobile/social?location&limit=5 → [{id, text, scheduled_at, status: scheduled|published|draft, channels}] (scheduled first)
Quick capture POST /api/mobile/capture {location_id, text} → {kind: task|note, ai, item} – with the AI assistant switched on, Claude turns it into a task (title, due date, contact) or a note on the named contact (counts toward the AI limit), otherwise – or on errors – a plain task
AI agent GET /api/mobile/agent?location= → {default_agent, agents: [{id, name, emoji, description, mode, starters}], connection: {label, model} | null, conversations: [{id, agent_id, title, status, updated_at}]}, POST /api/mobile/agent {location_id, message, agent_id?, conversation_id?} → {conversation_id, status: done|awaiting|stopped|error|limit, text, error, tools: [{name, status}], confirmation: {id, tool, title, summary, token, args_hash} | null, approvals: [{id, summary}]} (answers only when the run is finished; same permissions, safety modes and limits as on the web – docs/agent.md), POST /api/mobile/agent/confirm {location_id, pending_id, token, approve} → as above (single-use token, bound to the user and the arguments), GET /api/mobile/agent/conversations/:id?location= → {id, title, status, items}
Mailboxes GET /api/mobile/mailboxes → {mailboxes: [{id, name, address, provider: google|microsoft, scope: location|user, location_id, location_name, status, last_sync_at, can_send, can_modify}]} (mailboxes of the visible sub-accounts and your own personal ones), POST /api/mobile/conversations/:id/mail-reply {body, subject?, mailbox_id?, send_at? (ISO), signature?} → replies through the mailbox in the same thread (like POST /api/mail/conversations/:id/reply, see docs/mailbox-sync.md); the messages in the inbox contain the synchronized emails anyway
Notifications GET /api/mobile/feed?since=<cursor>&location= → {cursor, events: [{id, type: message|call_missed|lead|payment|website_down|website_up|task_due|review|feedback|werkstatt, title, body, location_id, location_name, deep_link, at}]} – without since only the cursor; at most 50 events per call (then continue with the new cursor)
Bug reports & wishes GET /api/mobile/bug-reports (your own reports), POST /api/mobile/bug-reports {category: bug|question|wish, what, expected?, steps?, severity, location_id?, screen (name of the screen), screenshot (data: URL), device: {model, manufacturer, android, sdk, app_version}, api_errors: [{method, path, status, error, at}], app_version, diagnostics: false (= no diagnostics)} → report with number, GET /api/mobile/bug-reports/:id, POST /api/mobile/bug-reports/:id/messages {body, screenshot?}, GET /api/mobile/wishes?sort&q&area&status, POST /api/mobile/wishes {title, what, area}, GET /api/mobile/wishes/:id, POST/DELETE /api/mobile/wishes/:id/vote, POST /api/mobile/client-errors (like /api/feedback/client-errors, for app crashes) – formats as under Bug reports & wishes
AI studio POST /api/mobile/genmedia {location_id, capability: image|video|audio_tts|audio_music|stt, prompt, model?, connection_id?, aspect_ratio?, duration?, seed?, voice?, language?, image_url?, audio_url?, params?} → job (like POST /api/genmedia/jobs, source: mobile), GET /api/mobile/genmedia/:id → {id, status: queued|running|succeeded|failed|canceled, progress, results: [{url, download_url, mime, kind, library_url}], url, text, cost, error} – needs an AI connection (BYOK) in the sub-account and one of the areas Social Planner, Funnels, Campaigns or Courses (docs/genmedia.md)
Voice POST /api/mobile/voice/transcribe {location_id, data: "data:audio/webm;base64,…", language?} → {text, provider, model, ms, bytes} (recording up to 15 MB, using the sub-account’s speech recognition), POST /api/mobile/voice/speak {location_id, text} → {audio: "data:audio/mpeg;base64,…", mime, ms, chars} (with the person’s voice) – needs an AI connection with transcription or speech output respectively (docs/voice.md)
Creator & Commerce GET /api/mobile/creator/overview?location= → {payment_plans: {active, defaulted, completed}, dunning: {open_cases, failed_cases, pending_notices, at_risk_cents}, reviews: {quiz_attempts, assignment_submissions}, locations: {payments, courses}} (an area the login does not have counts 0), GET /api/mobile/creator/payment-plans?location&status=active|defaulted|completed|canceled&limit=50 → [{location_id, location_name, id, contact: {id, name}, product_name, plan_name, installments, paid_count, failed_count, amount_cents, paid_cents, currency, status, access, started_at, link}] (area payments), GET /api/mobile/creator/dunning?location&limit=50 → {cases: [{…, product_name, amount_cents, currency, status: open|failed, attempts, next_attempt_at, first_failed_at, access_paused}], notices: [{…, invoice_id, invoice_number, level, label, total_cents, currency, created_at}]} (open payment failures and dunning notices waiting for approval; area payments), GET /api/mobile/creator/reviews?location&limit=50 → [{location_id, location_name, id, kind: quiz|assignment, title, course_title, contact, attempt_no, submitted_at, open_questions | has_file, link}] (quiz short answers and assignment submissions to review, oldest first; area courses). Read only – approving and reviewing happen in the web app; every list runs through the sub-account’s admin route, permissions and tenant separation are the same, a sub-account without permission is skipped (with an explicit location= you get 403)
Back-Office GET /api/mobile/receipts?location&status&limit → {counts: {inbox, review, booked, discarded}, receipts: […, location_id, location_name]} (bookkeeping area), POST /api/mobile/receipts {location_id, data (data: URL, photo or PDF), name?, note?} → receipt in the inbox (source: mobile, duplicate detected by checksum), GET /api/mobile/absences?location&year → [{location_id, location_name, employee, is_manager, balance, absences}] (only sub-accounts in which the login is an employee or a manager), POST /api/mobile/absences {location_id, type: vacation|sick|other, start_date, end_date?, half_start?, half_end?, label?, reason?, certificate?}, POST /api/mobile/absences/:id/cancel, GET /api/mobile/absences/calendar?location&from&to (team calendar, sickness shown only as “absent”), GET /api/mobile/absences/approvals + POST /api/mobile/absences/:id/decide {decision: approved|rejected, note?} (managers), GET /api/mobile/stock/low?location (stock area). Salary and other sensitive HR fields are not available here – a device is not a browser session
Ads GET /api/mobile/ads?location= → {location_id, approvals_pending, totals: {spend_cents, clicks, crm_leads, cpl_cents}, campaigns: [{ref, platform, name, status, budget_cents, budget_type, currency, spend_cents, clicks, crm_leads, cpl_cents}]} (key figures of the last 30 days; ref is the ID for the status call), POST /api/mobile/ads/:ref/status {location_id, status: paused|active, confirm?} (pausing is immediate; activating only with confirm: true after a confirmation prompt on the device), GET /api/mobile/ads/approvals?location=, POST /api/mobile/ads/approvals/:id/approve|reject {location_id} – docs/ads-manager.md
Profit calculator GET /api/mobile/calc/scenarios?location= → {location_id, scenarios: [{id, name, note, updated_at, customers, revenue_net_month, ebit_month, net_profit_month, margin_pct, breakeven_customers}]} (area Invoices & products), POST /api/mobile/calc/scenarios/:id/run {location_id, customers?, input?} → the compact answer of POST /api/calc/run (read-only: no audit entry) – docs/gewinnrechner.md
Living Boards GET /api/mobile/boards → {boards: [{id, location_id, location_name, name, icon, accent, scope: personal|shared, access: owner|edit|view, widgets, version, updated_at, home}]} (the boards of every sub-account the person may open; area Dashboard), GET /api/mobile/boards/:id?location= → the board for one column: {…, widgets: [{id, type, title, layout: {sm, lg}}]} sorted in phone order (sm.y), GET /api/mobile/boards/:id/data?location=&ids= → {version, data: {<widget id>: {view, ms}}} – the views are the standard views of docs/living-boards.md (number, numbers, list, kanban, agenda, markdown, message, locked …) drawn with the caller’s rights; read only, no audit entry

| Webinars | GET /api/mobile/webinars?location=&status=draft\|published&limit=20 → {webinars: [{location_id, location_name, id, title, status, duration_s, next_session, registrations, attended, upcoming_registrations, url, updated_at}]} (soonest next session first), GET /api/mobile/webinars/:id → {location_id, location_name, id, title, status, description, presenter_name, duration_s, time_zone, upcoming: [{start, kind, taken, full}], counts: {registrations, attended, cancelled, questions}, url, problems}, GET /api/mobile/webinars/:id/stats → {location_id, totals, sessions, dropoff} (same as GET /api/webinars/:id/stats) – read only, area Funnels & Websites (docs/webinars.md) | | Community | GET /api/mobile/community?location= → {locations: [{location_id, name, groups: [{id, name, slug, status, members, pending_requests, open_reports}], open_dm_reports, unread_team_messages}], totals: {pending_requests, open_reports, unread_team_messages}}, GET /api/mobile/community/requests?location&limit= → [{id, location_id, location_name, group_id, group_name, contact_id, name, email, tier, requested_at, answers}] (oldest first), POST /api/mobile/community/requests/:id {action: approve\|reject, note?}, GET /api/mobile/community/reports?location&limit= → open reports of posts, comments and (kind: dm) messages: [{id, kind: post\|comment\|dm, location_id, location_name, group_id, group_name, reason, created_at, text, author}], POST /api/mobile/community/reports/:id {action} (dismiss\|hide\|delete; with ?kind=dm: dismiss\|remove\|ban), GET /api/mobile/community/messages?location&limit= → the conversations members have with the team, GET/POST /api/mobile/community/messages/:contactId ({body, from_name?} answers as the team), POST /api/mobile/community/posts {group_id, title, body, channel_id?, pinned?, notify?} → {id, title} (a post as the team). Area Courses & community; every write relays onto the /api/community/* routes, so checks, events and the audit log are the same (docs/community.md) | | App distribution | GET /api/mobile/app (public) → {version_code, version_name, apk_url, sha256, size, changelog, min_supported_code, uploaded_at, page_url} or 404, PUT /api/mobile/app (admin login or agency key; body = APK bytes, headers x-version-code, x-version-name, x-changelog URL-encoded, optional x-min-supported-code), GET /api/mobile/app/releases (admin), GET/PUT /api/agency/mobile-app {cert_sha256, package_name, min_supported_code} (admin) |

App calls that write go internally through the same routes as the UI (POST /api/messages, /api/tasks, PATCH /api/opportunities/:id …) – same checks, events for workflows/webhooks and audit-log entries (via app). Public: GET /android (download page with QR code), GET /downloads/hyperlevel.apk (alias gohigh.apk), GET /.well-known/assetlinks.json.

See team-inbox.md and team-calendars.md. Sub-account routes need the conversations / calendars area; every write goes to the audit log. Times are ISO-8601 UTC, errors {error} in German.

Area Endpoints
Inbox views GET /api/inbox/meta (views, statuses, priorities, people with away, teams, settings summary), GET /api/inbox/counts → {<view>: n}, GET /api/inbox/queue?view&assignee=me|none|<id>&status&priority&channel&q&limit (compact list for the app and MCP). GET /api/conversations also takes view, assignee, status, priority and returns assignee_id, assignee_name, team_name, inbox_status, priority, snoozed, snoozed_until, sla per row
Filters & message search GET /api/conversations also takes tag (the contact has it), pipeline_id / stage_id (the contact has an open deal there) and owner (contact owner: user id, me, none); GET /api/conversations/search?q=&channel=&limit= → {q, fts, hits: [{message_id, conversation_id, contact_id, channel, direction, subject, created_at, snippet}]} – full text over subject and body of every message (word prefixes, accents ignored; SQLite FTS5 kept by triggers, LIKE fallback), the same filters and record access as the list, hit marks \u0001…\u0002 in snippet; MCP ghl_search_messages
One conversation GET /api/inbox/conversations/:id → {assignee_id, assignee_name, team_id, status, snoozed_until, priority, sla, first_response_at, viewers, timeline} (marks your mentions seen), POST …/assign {user_id (id, "me", null) | team_id}, POST …/status {status?, snooze_until?}, POST …/snooze {until | null}, POST …/priority {priority}, GET/POST …/notes {body}, DELETE …/notes/:noteId, POST /api/inbox/bulk {ids (≤200), action: assign|status|priority|snooze|close, …} → {updated, skipped}
Realtime GET /api/inbox/stream?clientId= (server-sent events hello, presence, conversation and bye – the stream ended for good with its login or access, do not reconnect; login only, no API keys), POST /api/inbox/conversations/:id/presence {typing?: true, leaving?: true}
Settings GET/PUT /api/inbox/settings (settings area): assignment {mode, pool, team_id, user_ids, respect_availability, prefer_contact_owner, reassign_when_away, assign_on_reply, rules[]}, sla {enabled, first_response_min, next_response_min, warn_percent, business_hours, notify_assignee, notify_email}, business_hours, holidays, auto_close_days, reply_sets_waiting, notifications
Availability GET /api/team-people, GET/PUT /api/team-people/:userId (me works; {paused, hours, tz}), POST /api/team-people/:userId/away {start_at, end_at, note}, DELETE /api/team-people/away/:awayId
Calendar types GET /api/team-calendars/types, GET /api/team-calendars/meta (kinds, video options, Stripe / Zoom / Google / Microsoft availability, people with connection state), GET/PUT /api/team-calendars/:id ({config, members: [{user_id, weight, hours}]} → warnings[]), POST …/:id/link-me
Booking GET /api/team-calendars/:id/slots?from&days, POST /api/team-calendars/:id/book {start, email, first_name, last_name?, phone?, notes?, answers?, host_id?, payment?: "waive", waitlist?} → {status: confirmed|payment_required|waitlisted, …}, POST /api/team-calendars/appointments/:id/cancel {reason?, refund?} → {refund: refunded|manual|none}, POST …/appointments/:id/reschedule {start}, GET /api/team-calendars/:id/waitlist, DELETE /api/team-calendars/waitlist/:id
Routing forms GET/POST /api/team-routing, PATCH/DELETE /api/team-routing/:id ({name, question, intro, color, options: [{label, calendar_id}], active})
Public GET /book/:slug (page), GET /api/public/team-calendars/:slug/slots, POST …/:slug/book (via = <routing slug>:<option id>), GET /book/manage/:token and POST /api/public/team-calendars/manage/:token/cancel|reschedule (+ …/slots), GET /book/paid/:token and GET /api/public/team-calendars/hold/:token, GET /book/r/:slug, GET /book/waitlist/:token and POST /api/public/team-calendars/waitlist/:token/book
MCP ghl_inbox_queue, ghl_inbox_assign, ghl_inbox_set_status, ghl_inbox_note, ghl_calendar_types, ghl_book_slot
Workflows Triggers conversation.assigned, conversation.status_changed, conversation.sla_breached; steps inbox_assign, inbox_set_status, inbox_note; report sources inbox_conversations, inbox_responses

Calendar view, recurring appointments, AI booking

Section titled “Calendar view, recurring appointments, AI booking”

See booking.md. Calendars area unless noted; times ISO-8601 UTC, drawn in the sub-account’s zone.

Area Endpoints
Calendar view GET /api/calendar-view?from&to&calendars&users&status&layers (grid data: appointments with people and series fields, buffers, bookable hours, busy times, absences; ≤ 62 days), POST /api/calendar-view/check {calendar_id, start, end?, appointment_id?} → {ok, issues}, POST /api/calendar-view/book {calendar_id, start, end?, contact_id | contact | blocked, force?}, POST /api/calendar-view/appointments/:id/move {start?, end?, force?, notify?} (409 with issues unless force), GET /api/calendar-view/appointments/:id (drawer), POST /api/calendar-view/appointments/:id/invoice (payments area)
Series POST /api/appointment-series/preview, POST /api/appointment-series {calendar_id, start, duration?, recurrence (RRULE or object), contact_id | contact | blocked, force?}, GET /api/appointment-series, GET /api/appointment-series/:id, PATCH /api/appointment-series/:id {scope: single|following|all, appointment_id, start_time?, duration?, recurrence?}, POST /api/appointment-series/:id/cancel {scope, appointment_id?, reason?}; appointments carry series_id, recurrence, occurrence_date, series_exception
AI booking GET/PUT /api/ai-booking/settings (settings area) {chat_bot, calendars, per_contact_day}, GET /api/ai-booking/slots?calendar_id&date&days, POST /api/ai-booking/book {calendar_id, contact_id, date, time, notes?, channel?, confirm?} → {status: confirmed, manage_url, reschedule_url, cancel_url, confirmation, confirmed_via}
Public GET /book/move/:token – the reschedule link of a confirmation (appointment page with the free times open)
MCP ghl_booking_slots, ghl_book_appointment (safety level “Book appointments”), ghl_appointment_series_create, ghl_appointment_series_cancel

Native Gmail/Outlook mailboxes over the official APIs – connect, folder/label choice, push, answering through the mailbox, drafts, scheduled send, search and the MCP tools ghl_mailbox_list, ghl_mail_search, ghl_mail_draft, ghl_mail_send. Routes, areas (/api/mailboxes = Settings, /api/mail = Conversations), send body and the public push endpoints (POST /hooks/mailbox/gmail, POST /hooks/mailbox/graph[/lifecycle]) are listed in docs/mailbox-sync.md. GET /api/conversations takes ?mailbox=<id|any>&folder=&label=; conversation rows and thread messages carry a mail object when they come from a mailbox. Personal mailboxes are never reachable with API keys, agency keys or MCP.

AI images, videos, music, speech and transcripts with the sub-account’s own provider keys (BYOK registry, see docs/genmedia.md). Sub-account routes (login with x-location-id, sub-account API key or agency key); client logins need one of the areas social, funnels, campaigns or courses. Without a connection: 409 {error: "Bitte zuerst unter Einstellungen → KI-Anbieter (BYOK) einen Anbieter verbinden"} (German text: “Please connect a provider first under Settings → AI providers (BYOK)”).

Area Endpoints
Catalogue GET /api/genmedia/meta → {available, message, connections: [{id, provider, label, model, capabilities}], providers: [{id, label, capabilities, models: {<capability>: [{id, label, ref}]}}], aspects, callbacks, templates}, GET /api/genmedia/connections?capability=, GET /api/genmedia/voices?connection_id= → {voices: [{id, name, preview}]}
Jobs POST /api/genmedia/jobs {capability: image|video|audio_tts|audio_music|stt, connection_id?, model?, prompt, negative_prompt?, aspect_ratio? (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9), duration? (s), seed?, voice?, language?, image_upload_id|image_file|image_url?, audio_upload_id|audio_file|audio_url?, ref_job_id?+ref_index?, params? (object, passed to the provider, max 20 KB), kie_task? {endpoint, body, poll_path?}, template_id?, library? (copy result into the media library), source?} → job (runs in the background; max 10 running per sub-account → 429); GET /api/genmedia/jobs?capability&status=active|succeeded|…&limit&before, GET /api/genmedia/jobs/:id → {id, capability, provider, model, status, progress, prompt, input, results: [{url, download_url, mime, kind, size, name, upload_id, library_url}], url, text, cost: {amount, unit, note, estimated}, cost_text, error, source, parent_id, target_result, created_at, finished_at}
Actions POST /api/genmedia/jobs/:id/rerun {variation?: true (new seed), prompt?}, POST …/cancel, DELETE /api/genmedia/jobs/:id (finished jobs; media-library copies stay), POST …/library {index?} → {uploads: [{id, url, name, mime, size}]}, POST …/social {integration_ids, content, type?: draft|now|schedule, date?} (images via POST /api/social/posts, videos as signed media link)
Inputs POST /api/genmedia/inputs?name=<file> with the raw file as body (max 25 MB; or JSON {name, data: "data:…;base64,…"}) → {file, name, mime, size, url} – use file as image_file/audio_file
HTTP templates GET/POST /api/genmedia/templates, PUT/DELETE /api/genmedia/templates/:id – {name, capability, config: {url, method, headers, body, result_path, result_type: url|base64, text_path, poll: {url, method, body, done_path, done_values, fail_path, fail_values, error_path, interval_s, result_path}}}
Public POST /hooks/genmedia/:token (provider callbacks; signed ones are verified, others only trigger a poll), GET /genmedia/f/:file (stored results, byte ranges, ?download=1)

Events: genmedia.completed {contactId?, jobId, capability, model, provider, url, urls, text} (workflow trigger “AI media ready”) and genmedia.failed {contactId?, jobId, capability, error}. MCP tools: ghl_generate_image, ghl_generate_video, ghl_generate_audio, ghl_transcribe, ghl_get_media_job.

Text to speech and speech to text with the sub-account’s own providers (BYOK registry), push-to-talk of the HyperLevel Agent, the AI phone agent with its own voice – see docs/voice.md. Sub-account routes; settings, voices, test and latency need the area Settings, the others no extra area.

Area Endpoints
Settings GET/PUT /api/voice/settings (partial patch; {settings, connections, routed, phone: {plan, twilio_ready, agent_ready, public_https}, usage, latency, limits}), GET /api/voice/voices?connection_id=&language=&refresh=1 → {voices: [{id, name, lang, gender, preview}], connection, free_text}, GET /api/voice/latency?limit=
Speak and listen POST /api/voice/test {text?, voice?, model?, connection_id?, language?, speed?, instructions?} → {audio: data-URL, provider, model, voice, chars, bytes, ms}, POST /api/voice/speak[?format=json] {text, voice?, provider?, connection_id?} → audio file (or {audio, mime}; provider/connection_id only of this sub-account’s audio_tts connections – voice.md), POST /api/voice/transcribe?language=&vocabulary= (raw recording as body, or JSON {data: data-URL}; max 15 MB) → {text, provider, model, ms, bytes}, POST /api/voice/transcribe-url {upload_id|url}
Preferences GET/PUT /api/voice/prefs {read_aloud, auto_send, voice, connection_id} (null = sub-account default)
Twilio (public) WebSocket /hooks/twilio/stream/:location (Media Streams, token in the start message), signed POST /hooks/twilio/voice-ai-stream-end/:location, …/voice-ai-turn/:location, …/voice-ai-turn-wait/:location, GET /hooks/voice/play/:token.:ext

Back office (bookkeeping light, stock, HR)

Section titled “Back office (bookkeeping light, stock, HR)”

Sub-account routes, areas accounting, inventory and personal (see backoffice.md for what each does). /api/hr/* needs the area granted explicitly (a login with “all areas” does not get it); the employee’s own record is under /api/me/hr/* without an area. Sensitive HR fields (/api/hr/employees/:id/sensitive, documents marked sensitive) work only in a browser session of an admin or of a person listed in the HR settings – never with API keys, MCP or device tokens.

Group Routes
Accounting GET /api/accounting/status, POST …/setup, GET/PUT …/settings, GET/POST …/accounts, PATCH …/accounts/:id, GET …/tax-codes, GET/POST …/entries, GET …/entries/:id, POST …/entries/:id/storno, GET …/periods, POST …/periods/lock, GET …/audit, GET …/verify, GET …/summary
Receipts GET/POST …/receipts, GET …/receipts/status, GET/PATCH …/receipts/:id, POST …/receipts/:id/extract|discard|book, GET …/documents/:id, GET …/documents/:id/verify, GET …/inbound, POST …/inbound/rotate; public POST /hooks/:locationId/receipts?key= (JSON with attachments)
Bank POST …/bank/import ({name, data | content}, CSV or CAMT.053), GET …/bank/txns, GET …/bank/imports, POST …/bank/rematch, POST …/bank/txns/:id/confirm|book|ignore|reset, GET …/bank/candidates
Reports and export GET …/reports/susa|bwa|euer|ustva|open-items|journal.csv, GET …/datev/preview|export|documents, POST …/autopost/run
Inventory GET /api/inventory/status, GET/PUT …/settings, GET/POST/PATCH/DELETE …/places, GET/POST …/articles, POST …/articles/from-product, GET/PATCH/DELETE …/articles/:id, GET …/stock, GET …/low-stock, GET/POST …/movements, POST …/transfers, GET …/valuation[?asof=&format=csv], GET/POST …/counts, GET …/counts/:id, PUT …/counts/:id/lines, POST …/counts/:id/close|cancel, GET/POST …/orders, GET/PATCH …/orders/:id, POST …/orders/:id/place|send|receive|cancel, POST …/issue-from-invoice/:invoiceId
HR GET /api/hr/access|status|users|holidays|calendar|working-time, GET/PUT …/settings, GET/POST …/employees, GET/PATCH/DELETE …/employees/:id, GET/PUT …/employees/:id/sensitive, GET/POST …/employees/:id/documents, GET/DELETE …/documents/:id, GET …/employees/:id/balance, PUT …/employees/:id/leave-years/:year, GET/POST …/absences, PATCH …/absences/:id, POST …/absences/:id/approve|reject|cancel, GET/POST/PATCH/DELETE …/checklist-templates, GET/POST …/checklists, POST …/checklists/:id/items/:itemId, DELETE …/checklists/:id
Self-service GET /api/me/hr, …/balance, …/absences, POST …/absences, POST …/absences/:id/cancel|decide, …/approvals, …/calendar, …/holidays, …/documents[/:id], …/checklists, POST …/checklists/:id/items/:itemId

MCP tools: ghl_booking_summary, ghl_euer_report, ghl_vat_overview, ghl_open_items, ghl_receipt_status, ghl_stock_levels, ghl_low_stock, ghl_stock_movement, ghl_create_absence_request. Workflow triggers: stock.low, purchase_order.received, receipt.received, receipt.booked, absence.requested, absence.approved, absence.rejected.

Wishes → builds → plugins → Marktplatz, see docs/werkstatt.md. Sub-account routes take the usual sub-account selection (x-location-id); admin routes are marked. Building and installing need a person (session or agency key), read routes also answer sub-account keys. Policy decides who may build (GET /api/werkstatt/me).

Area Routes
Builds POST /api/werkstatt/builds {wish_id? | text, title?} → build (planning), GET /api/werkstatt/builds (own; admins: the sub-account’s), GET …/:id (plan, progress {label, current, total}, log, created, cost), POST …/:id/confirm {accept_scopes, accept_areas} (must equal the plan’s lists), …/cancel, …/undo (stage 1), POST …/:id/approve and …/reject {note} (admin), GET /api/werkstatt/agency (admin: all builds + budgets)
Change POST /api/werkstatt/plugins/:id/change {feedback} (new version, same approval rules; the build’s plan carries diff {from, to, summary[]} and new_scopes), POST /api/werkstatt/builds/:id/replan (plan the same wish again, e.g. after a top model was connected), GET /api/werkstatt/estimate?kind=plan|build|change
Status GET /api/werkstatt/me (may build? why not, AI connection and model, model_gate {ok, model, matched, message, link, effort, review}, license {pro, stub}, budget, approval, pending approvals), GET|POST /api/werkstatt/dry-run {text} (policy, AI, budget, estimate, prompt size, sandbox, GitHub – no AI call, nothing stored), GET /api/werkstatt/notices, POST …/notices/read {ids?}
Policy (admin) GET /api/werkstatt/policy → {policy, defaults, gate_families, license, locations, users, sandbox}, PUT {who, allow_users, allow_locations, approval, budget_usd, budgets, repair_rounds, allowed_hosts, server_logic, kill {errors, window_min, ratio, latency_ms, min_calls}, core, model_gate {enabled, families[], newer, extra[], stage1_normal}, second_opinion, trusted_keys [{id, label, public_key}]}
Plugins GET /api/plugins, /active, /actions (workflow actions), /examples, POST …/examples/:key/install {consent}, GET /api/plugins/:id, POST …/versions/:vid/activate {consent}, …/scope {scope: private|location|agency}, …/toggle {enabled}, …/uninstall {cleanup}, DELETE …/:id, POST …/kill and …/revive (admin), POST …/rollback {version_id?} (author or admin: one click back to the version before; revives a killed plugin; versions carry badges and diff), GET …/health (incl. avg_ms_24h, error_rate_24h), GET …/export (signed when the installation has PGHL_PLUGIN_SIGNING_KEY), POST /api/plugins/import/preview and /import {bundle, consent, accept_unverified?} (answers signature {state: signed|untrusted|invalid|unsigned, key_id, label, gohigh, warning}; invalid is refused, unsigned needs accept_unverified, signed needs Pro), GET /api/plugins/sandbox (admin)
Plugin runtime GET /plugin-host/:id?entry=page|panel:<id>|widget:<id>&entity=contact:<id>&demo=1 (isolated host page), GET /plugin-frame/:versionId, POST /api/plugins/:id/call {op, args} (the bridge; scopes + the user’s rights), POST …/report (UI errors → health)
Marktplatz GET /api/marktplatz?q&category&sort=popular|rating|new, GET …/:id, POST …/:id/install {scope: location|private, consent}, …/update {consent?}, GET|PUT|DELETE …/:id/reviews ({rating 1-5, body}; anyone who has the plugin), GET|POST|DELETE …/:id/screenshots, POST …/screenshots/auto (admin), POST …/:id/security-review (top model only), GET …/:id/publish-check (both verdicts, comparison, second-opinion state), POST …/:id/second-opinion (Pro: another top model) and …/publish {review_id, approve: true, accept_warnings?} (Pro) and …/unpublish (admin), GET /api/marktplatz/admin/plugins (admin, with health)
Core features (admin) GET /api/werkstatt/core, GET …/core/github?repo= (token and repository ready? + hint), POST /api/werkstatt/plugins/:id/core-proposal {agent?, repo?, target_version?, core_action?, create_issue?}, POST …/core/:id/issue, PUT|DELETE …/core/:id, POST …/core/:id/refresh, POST …/core/:id/ship {version, action: migrate|retire}
Feature flags (admin) GET /api/flags, POST /api/flags, PUT|DELETE /api/flags/:name (stage: off|private|location|percent|all, percent, users, locations); GET /api/flags/me for everyone

Workflow action plugin_action {plugin, action, input} runs a plugin’s action (only plugins that are on for the whole sub-account). MCP tools: ghl_list_plugins, ghl_install_plugin, ghl_build_from_wish (plans; the agent chat shows a card), ghl_plugin_status, ghl_change_plugin, ghl_discard_plugin, and for admins ghl_plugin_overview, ghl_plugin_admin.

Reasoning effort (“Denkstufe”): GET /api/ai-effort → {default, builtin_default: 'xhigh', caps [{id, label, strict, effective, source, own, agency}], own, agency}, PUT /api/ai-effort {scope?: 'agency', default?: 'low\|medium\|high\|xhigh\|""', caps?: {agent, werkstatt, review, text, summary, voice}} (agency scope: admin; a sub-account sets its own; the Werkstatt capabilities never go below high). aiRun accepts effort and returns effort {requested, applied, note}.

Infinite boards (whiteboard + design) per sub-account, see docs/canvas.md. Area canvas; access per board (owner, edit, view, private). All routes take the usual sub-account selection; keys and MCP act for the whole sub-account.

Area Endpoints
Boards GET /api/canvas → {boards: [{id, name, access, team_access, element_count, updated_at, updated_by, watching}], mentions}, POST /api/canvas {name, template?, mermaid?, elements?, team_access?: edit|view|none, settings?}, GET/PATCH/DELETE /api/canvas/:id (?outline=1&svg=1 on GET), POST …/duplicate, GET /api/canvas/templates, GET /api/canvas/mentions, GET /api/canvas/media
Elements POST /api/canvas/:id/ops {clientId?, strict?, ops: [{op: add|update|delete, element?|id, props?, baseVersion?}]} → {seq, results: [{id, status: ok|conflict|rejected|locked|deleted|missing|error, version}], current}; POST …/elements {elements: [{ref?, type, at?, in?, from?, to?, …}], layout?, origin?} → {refs, created}; GET …/changes?since=<seq>
Realtime GET …/stream?clientId= (SSE hello, ops, presence, leave, comment, board, reload, deleted, bye – a person’s stream ended with their login or access, do not reconnect), POST …/presence {clientId, x, y, sel}
Generators POST …/template {key}, …/mermaid {source}, …/layout {kind: flow|tree|mindmap|radial|grid, ids?|nodes+edges}, …/cards {refs: [{kind, id}]} (live CRM data, caller’s rights)
Output GET …/svg, GET …/export?format=png|svg|pdf&frame_id&ids&scale&theme&transparent&pages=frames, POST …/export (same, saved to the media library → {id, src, url, format, width, height}), POST …/media {name, data: "data:image/…;base64,…"}
Sharing GET …/people, PUT …/members {user_id, role: edit|view|none}, GET/POST …/shares {label}, DELETE …/shares/:sid, public GET /canvas/s/<token> and GET /api/public/canvas/<token>?seq=
History & comments GET/POST …/snapshots, POST …/snapshots/:sid/restore, GET …/snapshots/:sid/svg, DELETE …/snapshots/:sid, GET/POST …/comments {body, element_id?, x?, y?, parent_id?, mentions?}, PATCH/DELETE …/comments/:cid
CRM & AI POST …/convert {element_ids, to: task|deal, pipeline_id?, …}, …/workflow-diagram {workflow_id}, …/workflow-draft, …/funnel-pages {frame_ids?, funnel_id?|funnel_name?}, …/ai {mode: draw|explain|tasks|tidy, prompt?, kind?, ids?}
Mobile GET /api/mobile/canvases, GET /api/mobile/canvases/:id/svg?theme&max (device token)

MCP tools: ghl_canvas_list, ghl_canvas_create, ghl_canvas_get, ghl_canvas_add_elements, ghl_canvas_update_elements, ghl_canvas_delete_elements, ghl_canvas_layout, ghl_canvas_from_mermaid, ghl_canvas_export, ghl_canvas_comment.

Projects, tasks with subtasks, dependencies, Gantt, time tracking and billing – overview and data model in docs/projects.md. All routes under /api/pm/ need the area “Contacts & tasks” (contacts), /api/pm/billing/* additionally “Invoices & products” (payments); they apply to a session, a sub-account key (x-location-id) and an agency key.

Projects GET/POST /api/pm/projects, GET/PATCH/DELETE /api/pm/projects/:id (DELETE ?tasks=delete deletes the tasks, otherwise they move to “General”), POST /api/pm/projects/:id/duplicate, POST …/save-template, GET /api/pm/templates (built in: kunden-onboarding, website-projekt), statuses/sections/fields: …/statuses, …/sections, /api/pm/custom-fields
Tasks GET /api/pm/tasks?project_id&status_id&category&done&assignee&priority&tag&q&due&from&to&parent&sort&limit (assignee=me|none|<id>, due=overdue|today|week|month|none), POST /api/pm/tasks (also subtasks: [...], nested), GET/PATCH/DELETE /api/pm/tasks/:id, PATCH …/dates {start, due, cascade} (dependent tasks move along), POST …/move, …/duplicate, …/watch, POST /api/pm/tasks/bulk {ids, patch | action: "delete", add_tags}
Collaboration …/comments, …/attachments (link) and …/attachments/file (Base64, up to 8 MB), …/dependencies (depends_on, type FS|SS|FF|SF, lag), …/activity
Views GET /api/pm/projects/:id/gantt (tasks with start/end, buffer, critical path; dependencies with violated), POST/DELETE …/baseline, POST …/auto-schedule {dry_run}, GET /api/pm/workload, GET /api/pm/my-tasks, /api/pm/views (saved views)
Time GET /api/pm/timer, POST /api/pm/timer/start|stop, GET/POST /api/pm/time, PATCH/DELETE /api/pm/time/:id, GET /api/pm/time/report?group=project|user|client|task|day|week, GET /api/pm/time/export.csv, GET /api/pm/timesheet?week&user_id, POST /api/pm/timesheet/submit|decide, GET /api/pm/timesheet/pending
Billing GET /api/pm/billing/unbilled, GET /api/pm/billing/entries, POST /api/pm/billing/invoice {project_id, from?, to?, group_by, title?, include_unapproved?} → invoice draft, GET/PUT /api/pm/billing/rates
Profitability GET/PUT/DELETE /api/pm/billing/budgets/:projectId, GET /api/pm/billing/budgets, GET …/burndown/:projectId, GET/PUT …/cost-rates, GET/POST …/costs, PATCH/DELETE …/costs/:id, GET …/receipts, GET …/profitability[/export.csv]?group=project|client&from&to, GET/POST …/retainers, GET/PATCH/DELETE …/retainers/:id, GET …/retainers/:id/preview, POST …/retainers/:id/close – projects.md
Other GET/PUT /api/pm/settings, /api/pm/automations, GET /api/pm/notifications, POST …/read, GET /api/pm/meta (team, priorities, settings)

MCP: 17 ghl_pm_* tools (including ghl_pm_create_task with nested subtasks, ghl_pm_move_task, ghl_pm_log_time, ghl_pm_timesheet_summary, ghl_pm_invoice_time).

Internal knowledge base and messenger, see docs/wiki.md and docs/team-chat.md. Areas wiki and teamchat; all routes take the usual sub-account selection (x-location-id / ?locationId=). Keys and MCP connections act for the whole sub-account (a read-only key cannot write). Every write is checked by the same guards as the UI and lands in the audit log (page edits and chat posts as one entry per person and ten minutes).

Wiki

Area Endpoints
Spaces GET /api/wiki/spaces → [{id, name, icon, description, team_access: edit|view|none, owner_id, access: owner|edit|view, page_count}], POST /api/wiki/spaces {name, icon?, description?, team_access?}, PATCH/DELETE /api/wiki/spaces/:id (owner or agency), GET /api/wiki/tree → {spaces, pages: [{id, space_id, parent_id, position, title, icon, updated_at, favorite, published}]}
Pages POST /api/wiki/pages {space_id, parent_id?, before_id?, title?, body?, icon?, template?: meeting|sop|brief|handbook|blank}, GET /api/wiki/pages/:id → {…, body, html, toc, tasks, path, children, backlinks, favorite, comments, shares, published, version, access}, PATCH …/:id {title?, body?, icon?, base_version?, force?, note?} (stale base_version → 409), DELETE …/:id (trash; ?purge=1 removes a trashed page for good), POST …/:id/restore, POST …/:id/move {space_id?, parent_id?, before_id?}, POST …/:id/duplicate {children?}, POST …/:id/toggle {line, checked, base_version} (tick a checklist line), GET …/:id/version (cheap change poll), GET /api/wiki/trash, GET /api/wiki/templates
Upsert POST /api/wiki/upsert {page_id|title+space?+parent_id?, body?, mode?: replace|append, base_version?, note?, icon?} → {created, …page} – creates the page (and a first space) if it does not exist; made for agents and scripts
History GET …/:id/versions, GET …/:id/versions/:vid?diff=1 (body, html, line diff against the current text), POST …/:id/versions/:vid/restore
Comments GET/POST …/:id/comments {body, parent_id?, quote?} (mentions as [@Name](https://github.com/BAISESYSTEMS/GoHyperLevel/blob/main/docs/user:ID) or plain @Name), PATCH/DELETE …/:id/comments/:cid {body?, resolved?}
Favourites, search POST/DELETE …/:id/favorite, GET /api/wiki/favorites, GET /api/wiki/search?q&space_id&limit&titles=1 → [{id, title, icon, space_id, space_name, snippet}] (matches wrapped in \u0001…\u0002; no q = latest changes), GET /api/wiki/people
Render, cards, media POST /api/wiki/render {body} → {html, toc}, POST /api/wiki/cards {refs: [{kind, id}]} → {cards: {"kind:id": card}} (live data with the caller’s rights; kinds contact company deal pipeline task ticket canvas doc wiki), GET /api/wiki/pick?kind&q (embed picker), POST /api/wiki/media {name, data: "data:…;base64,…"} → {src, markdown, image}
Import, export POST /api/wiki/import {space_id, parent_id?, files: [{name, content}]} (.md/.markdown/.txt/.html; ≤ 50 files, ≤ 1 MB each) or {markdown, title}, GET …/:id/export?format=md|pdf&children=1
Sharing, help center GET/POST …/:id/shares {label}, DELETE …/:id/shares/:sid, public GET /wiki/s/<token>, GET /api/public/wiki/<token> and …/<token>/pdf; GET/POST/DELETE /api/wiki/pages/:id/publish (knowledge-base article: status, publish/update, withdraw)
AI POST …/:id/ai {action: summarize|rewrite|continue|translate|tasks, instruction?, selection?, language?} → {text} (a suggestion, nothing is saved), POST /api/wiki/ai/create {prompt, space_id, parent_id?} → the new page

MCP tools: ghl_wiki_search, ghl_wiki_get, ghl_wiki_upsert_page.

Team chat

Area Endpoints
Channels GET /api/team-chat/channels → {channels: [{id, kind: public|private|dm|group, name, label, topic, scope: location|agency, role, joined, notify, unread, mentions, last_message_at, preview, archived, slack_mirror, can: {manage, post}}], me, slack}, POST /api/team-chat/channels {name, kind?, topic?, scope?: location|agency, members?}, GET/PATCH/DELETE …/channels/:id (PATCH {name?, topic?, archived?, slack_mirror?, notify?: all|mentions|none}), POST …/:id/join, POST …/:id/leave, POST …/:id/members {user_ids}, DELETE …/:id/members/:userId, POST /api/team-chat/dms {user_ids} (one person = DM, up to 7 = group), GET /api/team-chat/people
Messages GET …/channels/:id/messages?limit&before=<messageId> → {messages, has_more, last_read_at}, POST …/channels/:id/messages {body, thread_id?, files?: [fileId]} (also runs /aufgabe, /kontakt, /agent), GET …/messages/:id/thread, PATCH/DELETE …/messages/:id, POST/DELETE …/messages/:id/pin, GET …/channels/:id/pins, POST …/messages/:id/reactions {emoji} (toggle), POST …/messages/:id/note {contact_id} (keep as internal note), POST …/messages/:id/agent {approve} (the asker confirms or rejects what /agent proposed)
Files POST …/channels/:id/files {name, data: "data:…;base64,…"} → {id, name, mime, size, url} (attach with files when posting), GET /api/team-chat/files/:id
Read state, search POST …/channels/:id/read, GET /api/team-chat/summary → {unread, mentions, channels}, GET /api/team-chat/search?q&channel_id&limit → [{id, channel_id, channel, thread_id, author, snippet}], POST /api/team-chat/cards {refs}
Realtime GET /api/team-chat/stream?clientId= (SSE hello, message, message-edit, message-delete, reaction, typing, presence, channel, read, bye – the stream ended for good with its login or access, do not reconnect), POST …/channels/:id/typing {thread_id?}, POST /api/team-chat/presence {client_id, active}, GET /api/team-chat/presence
Settings GET/PUT /api/team-chat/settings {retention_days: 0 | 30 | 90 | 180 | 365} (?scope=agency for agency-wide channels, admins only; needs the right Settings)
Mobile (device token) GET /api/mobile/chat/channels, GET/POST /api/mobile/chat/channels/:id/messages (before, thread), POST …/channels/:id/read, GET /api/mobile/chat/files/:id, GET /api/mobile/wiki/pages?q, GET /api/mobile/wiki/pages/:id (Markdown, HTML and plain text). Feed events of type chat (GET /api/mobile/feed) announce direct messages and mentions.

MCP tools: ghl_chat_post (channel name or id, text, thread_id?) and ghl_chat_history – public channels of one sub-account only.

Office (documents, spreadsheets, presentations, PDF)

Section titled “Office (documents, spreadsheets, presentations, PDF)”

Office files per sub-account with immutable versions, edit lock and comments, see docs/office.md (DE). Area office; the usual sub-account selection. Every save, restore or agent edit is a new version; writes by somebody else while a person holds the edit lock answer 423 with locked_by/locked_until.

Area Endpoints
Status GET /api/office/status → {kinds, editors: {docx, xlsx, pptx, pdf}, engines: {…}, sandbox, lock_ttl_ms, lock_renew_ms, limits: {max_bytes, quota_bytes, used_bytes}}
Files GET /api/office/files?kind&q&contact_id&deal_id&project_id&limit, POST /api/office/files {kind, name?, text?, rows?, contact_id?, deal_id?, project_id?} (blank or with starting content) or {name, data} (base64) or a raw body (application/octet-stream, ?name=&kind=), GET/PATCH/DELETE /api/office/files/:id (PATCH {name?, contact_id?, deal_id?, project_id?}), GET …/:id/download?version= (attachment, audited as an export), GET …/:id/raw?version= (for the editor)
Versions GET …/:id/versions, POST …/:id/versions (raw ?message=&base_version=&force=1 or JSON {data, message?, base_version?, force?}; stale base_version → 409 with current_version), POST …/:id/versions/convert?from=deckcraft (the presentation editor’s own format → .pptx), POST …/:id/versions/:version/restore
Lock, comments GET/POST/DELETE …/:id/lock (POST {take_over?} – admins only; people only, not keys), GET/POST …/:id/comments {body, version?, parent_id?, mentions?}, PATCH/DELETE …/:id/comments/:cid {body?, resolved?}, GET /api/office/people
Engine GET …/:id/read?mode=both|text|structure&version&range&sheet → {engine, text, structure, version, lock}, POST …/:id/edit {commands?: [{command, params}], writes?: [{range, values, sheet?}], message?, base_version?}, POST …/:id/export-pdf {name?} → the new PDF file
Editors (no sub-account) /office/app/<kind>/ (editor page, signed in), /office/app/<kind>/<file> (build files), /office/bridge.js, /office/blob/<token>/<name> (signed 10-minute link for the PDF editor)

MCP tools: ghl_office_list, ghl_office_read, ghl_office_create, ghl_office_edit, ghl_office_export_pdf.

Create and run Meta and Google Ads campaigns (see docs/ads-manager.md). Sub-account routes (login with x-location-id, sub-account API key or agency key); client logins need the area campaigns (reading insights: dashboard; connection settings: settings). Everything is created paused; switching on and budget changes need confirm: true from a signed-in person – API keys, agency keys, MCP and the agent only create an approval ({status: "approval_required", approval_id, summary, message}), which only a person can approve. Amounts are cents in the account currency.

Area Endpoints
Connections GET /api/ads/connections (per platform: connected, can_manage, account, page, pixel_id, lead_ads, via, token_expires_at, last_error, agency: {ready} – no secrets), POST /api/ads/connect/:platform/start {lead_ads?} → {url} (browser sign-in only) (Meta: ads_management, ads_read, business_management, optional Page and lead scopes; Google: adwords), GET /oauth/ads/:platform/callback (public, signed state, PKCE for Google), GET /api/ads/connections/:platform/options (ad accounts, Pages, pixels / customer accounts), PUT /api/ads/connections/:platform/selection {ad_account_id, page_id, pixel_id} / {customer_id, login_customer_id?}, DELETE /api/ads/connections/:platform (revokes and deletes tokens), GET/PUT /api/ads/settings {max_daily_budget_cents} (default 50000), GET/PUT /api/ads/agency (admin: {meta: {app_id, app_secret, regenerate_verify_token}, google: {developer_token, login_customer_id}}, secrets masked)
Campaigns GET /api/ads/overview (what the wizard needs: accounts, Page, pixel, limit), POST /api/ads/review {spec} → {rows, preview, errors, warnings, ready, hash}, GET/POST /api/ads/campaigns ({spec} → draft), GET/PATCH/DELETE /api/ads/campaigns/:id (delete: drafts only), GET /api/ads/campaigns/:id/review, POST /api/ads/campaigns/:id/publish {review_hash} → creates paused at the platform (409 if the draft changed after the review), POST …/duplicate, POST /api/ads/suggest {platform, goal, brief?, funnel_id?, page_id?, count?} → AI copy within the platform limits (Meta: primary_texts, headlines, descriptions; Google: headlines, long_headlines, descriptions, keywords, negative_keywords)
Spec {platform: meta|google, kind: website|lead_form|search|pmax, name, goal: leads|traffic|conversions|reach, budget: {type: daily|lifetime, amount_cents}, schedule: {start?, end?}, audience: {countries, locations: [{type, key, name}], age_min, age_max, genders, interests: [{id, name}], audience_ids, keywords: [{text, match}], negative_keywords, languages}, creative: {media: [{upload_id, role?}], primary_text, headline, description, cta, link: {funnel_id, page_id, url}, utm, headlines, long_headlines, descriptions, business_name}, lead_form_id, conversion_event, dsa: {beneficiary, payor}}
Status & budget POST /api/ads/campaigns/:id/status {status: paused|active, confirm?} (:id = HyperLevel id or meta:<id> / google:<id> for campaigns created at the platform), POST …/budget {amount_cents, type: daily|lifetime, confirm?} – without confirm a person gets 409 {error: "Bitte bestätigen: …"} (German text: “Please confirm: …”)
Approvals & log GET /api/ads/approvals, POST /api/ads/approvals/:id/approve|reject (persons only; pending 24 h), GET /api/ads/activity
Reporting GET /api/ads/campaigns?from&to&model&refresh=1 (list with metrics, merged with campaigns created at the platform), GET /api/ads/insights?from&to&platform&model (spend, clicks, CTR, platform and CRM leads, CPL, revenue, ROAS), POST /api/ads/campaigns/refresh
Audiences GET /api/ads/audiences, GET /api/ads/audiences/preview?platform&smart_list_id → {total, usable, skipped}, POST /api/ads/audiences {platform, name, smart_list_id, consent: true} (SHA-256 hashed, opted-out contacts left out; persons only), POST …/:id/sync {consent: true}, POST …/:id/lookalike {country, ratio: 0.01…0.2, name?} (Meta), DELETE …/:id, GET /api/ads/targeting/interests?q, GET /api/ads/targeting/locations?platform&q (Google: &country)
Lead Ads GET/POST /hooks/ads/meta (public: handshake with the agency verify token; deliveries verified with X-Hub-Signature-256, 401 otherwise), GET /api/ads/leads/status, POST …/enable {tags?} / disable / poll, PUT /api/ads/leads/settings {tags}, GET /api/ads/leads (last 100), GET/POST /api/ads/leadforms (instant forms: {name, questions: [EMAIL|PHONE|FULL_NAME|FIRST_NAME|… or {key, label}], privacy_url, headline?, description?, thank_you?: {title, body, button_text, url}}), DELETE /api/ads/leadforms/:id
Rules GET/POST /api/ads/rules {name?, platform: all|meta|google, campaign_ref?, metric: cpl|cpc|ctr|spend|leads, op: >|<, value, window_days, min_spend_cents, action: pause|notify, enabled}, PATCH/DELETE /api/ads/rules/:id, POST /api/ads/rules/run {dry_run?, fresh?} → {checked, hits}
Offline conversions (Google) GET/PUT /api/ads/conversions {enabled, config: {enhanced_leads, currency, events: {deal_won|form_submitted|booking|invoice_paid: {enabled, action, value?, form_ids?}}}} (area settings), GET /api/ads/conversions/log, POST /api/ads/conversions/run, POST /api/ads/conversions/:id/retry, POST /api/ads/conversions/validate (dry run with validate_only) – only contacts with marketing consent are uploaded (docs/ads-manager.md)

Events: ads.lead {contactId, contact, created, leadgenId, platform, formId, formName, campaignId, campaignName, adId, adName, adsetName, organic, answers} (workflow trigger “New lead from an ad”, filters form_id, campaign_id, platform), ads.rule_triggered {ruleId, rule, platform, campaignId, campaign, value, action, result}, ads.approval_requested {approvalId, summary}, ads.campaign_activated / ads.campaign_paused {platform, campaignId, name}. MCP tools: ghl_ads_accounts, ghl_ads_create_campaign (paused draft; publish_paused: true also creates it paused at the platform), ghl_ads_update_status (active only files an approval), ghl_ads_update_budget (only files an approval), ghl_ads_insights. Mobile: see the table under Mobile app.

Pure maths on a JSON input – prices, customers, costs, taxes in, key figures out (see docs/gewinnrechner.md). Sub-account routes (login with x-location-id, sub-account API key or agency key); client logins need the area Invoices & products. Calculations (run, breakeven, compare) change nothing and leave no audit entry; scenarios are written to the audit log. Every answer carries disclaimer: "Schätzung, keine Steuerberatung" (German text: “Estimate, not tax advice”).

Area Endpoints
Reference GET /api/calc/schema (JSON Schema with description and default per field; unknown fields are errors), GET /api/calc/presets, GET /api/calc/presets/:key (gohigh-staffel, gohigh-standort, saas-pro-nutzer, agentur-retainer, kurs-community, handwerk; input is the complete input)
Calculate POST /api/calc/run {preset?, scenario_id?, input?, customers?, column?, sensitivity?, compact?, metric?, period?} – input is a partial over the defaults/preset/scenario (objects merge, lists replace) → {headline, warnings, rows, columns, matrix[row][col] (cells with month/year lines and taxes), unit[col] (ARPU, LTV, CAC, payback, break-even), focus: {customers, column, cell, unit, waterfall, projection, sensitivity?, min_price}, input}; compact: true → one matrix metric (revenue_gross|revenue_net|contribution|ebit|net_profit|margin, period: month|year) plus assumptions
Break-even POST /api/calc/breakeven {preset?, scenario_id?, input?, customers?, target_net_profit_month?} → break-even customers per price column (before / after taxes), minimum price for customers, and with a target the customers or the base price needed
Scenarios GET/POST /api/calc/scenarios ({name, note?, preset?, input?}; 200 per sub-account; list items carry summary key figures), GET/PATCH/DELETE /api/calc/scenarios/:id (PATCH {name?, note?, input?}, input = partial over the stored one), POST /api/calc/scenarios/:id/run (temporary changes, not saved), POST /api/calc/compare {ids: [2–6]} → key figures side by side, GET /api/calc/scenarios/:id/export.csv

Errors are German: 400 {error: "Ungültige Eingabe: customers.churn: muss zwischen 0 und 100 liegen"} (“Invalid input: customers.churn: must be between 0 and 100”). MCP tools: ghl_calc_run, ghl_calc_breakeven, ghl_calc_scenarios_list, ghl_calc_scenario_run (with compare_with it compares), ghl_calc_scenario_save. Mobile: see the table under Mobile app.

Workspaces composed from widgets, changed by dragging or by telling the board what to show (docs/living-boards.md). Sub-account routes (login with x-location-id, sub-account API key, agency key); client logins need the area Dashboard – each widget additionally reads its data with the caller’s rights. A key acts like a team member: shared boards only, editing only if the board lets the team edit; the agency key acts like an admin. Every write is audit-logged (way: editor, agent, MCP, API) and creates a version.

Area Endpoints
Catalogue GET /api/boards/widgets → {widgets: [{type, label, description, icon, category, category_label, keywords, access, access_label, size: {w, h, minW, minH, maxW, maxH}, risk: normal|high, fields: [{key, label, type, options?, optionsFrom?, showIf?, default, …}], source}], categories, limits: {widgets, ops}} (modules’ widgets included), GET /api/boards/options/:source?q=&pipeline_id= → {options: [{value, label, hint?}]} for id fields (pipelines, stages, reports, workflows, forms, wiki_pages, scenarios, plugins, plugin_widgets, smart_lists, calendars, projects, tags) – always from the caller’s own data
Boards GET /api/boards?all=1 (own personal + shared; all=1 = every board, admins only) → {boards: [{id, name, icon, accent, scope, team_access, owner_name, mine, access: owner|edit|view, version, home, viewers, widgets, preview}], home, auto_apply, limits}, POST /api/boards {name?, icon?, accent?, scope: personal|shared, team_access: view|edit, template?: solo-cockpit|vertrieb|agentur|service|projekte|<template id>, copy_of?: <board id>} (keys: scope: shared only), GET /api/boards/:id → {…, spec: {v, widgets: [{id, type, title, config, layout: {lg, md, sm}}], custom?}, can_undo, can_redo}, PATCH /api/boards/:id {name?, icon?, accent?, scope?, team_access?} (name/icon/accent are versioned; scope and team access only for the owner or an admin), DELETE /api/boards/:id (owner/admin), POST /api/boards/:id/duplicate {name?, scope?}
Change POST /api/boards/:id/patch {ops: [...], dry_run?, expected_version?, summary?, confirm_external?, strict?} → {ok, unchanged?, version, changes: [{kind, widget_id?, text}], summary, risk: low|normal|high, created: [{ref, id}], board} – the ops are add, remove, move, resize, configure, rename, theme (format in docs/living-boards.md); an error stops all steps (400 with the reasons, nothing saved); a changed board is 409 with expected_version; agents, MCP and keys need confirm_external: true for high-risk steps (embed, quick actions), else 409; POST …/preview {ops} → the same answer without saving (a read), POST …/undo, POST …/redo, POST …/reset-layout {bp: md|sm} (derive tablet or phone from the desktop layout again), GET …/versions?limit → {versions: [{n, parent_n, kind: create|template|edit|agent|api|undo|redo|restore, summary, prompt, actor, via, at, current, widgets}], head, can_undo, can_redo}, GET …/versions/:n → {version, state, ops}, POST …/versions/:n/restore (a new version)
Data GET /api/boards/:id/data?ids=a,b → {data: {<widget id>: {view, ms}}, version, at} – one standard view per widget, computed with the caller’s rights (locked + the missing right instead of values; never an error page), GET /api/boards/:id/stream?clientId= → Server-Sent Events hello {viewers}, viewers, board {version, kind, summary, actor, via, client_id, state, can_undo, can_redo}, access, deleted, bye (the stream ended for good with its login or access – do not reconnect; viewers and their login are re-checked every 20 s, signing out or revoking ends the stream at once; the browser sends locationId as a query parameter because EventSource cannot set headers)
Wishes POST /api/boards/:id/compose {prompt, voice?, preview?} → {ok, applied, proposal: {id, status, message, risk, base_version, expires_at, via, unsupported, changes, summary, after}, message, unsupported, errors?, board?} – the model (needs the AI assistant) answers with a checked patch; a low-risk layout change is applied at once when the person’s auto_apply is layout (not with preview: true), everything else is a proposal; GET …/proposals (the caller’s open proposals), POST …/proposals/:pid/apply / reject (only the person who asked – never a key); 40 wishes per person and hour
Start page, settings GET/PUT /api/boards/home {board_id: <id>|null} → {home: {board_id, location_id, name, icon}|null} (per person), GET/PUT /api/boards/prefs {auto_apply: off|layout}, GET/PUT /api/boards/embed-hosts {hosts: [...]} (PUT admins only: the agency’s own addresses for the embed widget, in addition to the built-in list in defaults)
Templates GET /api/board-templates?q&category → {starters, mine, marketplace, categories, can_publish}, GET /api/board-templates/check?board_id= → the review before saving/publishing ({review: {verdict: ok|warn|fail, checks, needs, areas, hosts, actions}, stripped}), POST /api/board-templates {board_id, name?, description?, category?, scope: location|marketplace, approve?, accept_warnings?} (config only; marketplace: admins, Marktplatz licence, approve: true, accept_warnings: true when the review warns; the same board again = new version), POST …/import {bundle}, GET …/:id (with items and the review), GET …/:id/export → {format: "gohigh-board-template", bundle_version, template, integrity}, POST …/:id/install {name?, scope?, consent?: [needs keys]} (a template of another sub-account needs consent equal to the review’s needs), POST …/:id/unpublish, DELETE …/:id

Errors are German. MCP tools: ghl_board_list, ghl_board_get (with_data), ghl_board_apply_patch (dry_run, expected_version, confirm_external), ghl_board_widgets, ghl_board_create. The HyperLevel Agent has them automatically. Mobile: see the table under Mobile app.

Ask in plain words and get an ephemeral view made of board widgets – focus mode is also the companion’s screen (docs/living-boards.md). Sub-account routes (login with x-location-id, sub-account API key, agency key); no area of its own – every tile reads its data with the caller’s rights (a locked view instead of values). Read only: no CRM data is written and there is no audit entry; an AI call is logged in ai_calls (purpose focus).

Area Endpoints
Ask POST /api/focus/ask {text (2–1000 characters), history?: [earlier questions, oldest first, at most 4 used], lang?: de|en, rules_only?} → {say, source: ai|rules, note: null|rules|off|limit|error|invalid, action: null|<the request, when it asks for something to be done>, tiles: [{id, type, title, config, w, h, source: {label, url, hint}, data: {view, ms}}], at} – say is one sentence plus the key figures the tiles read; the model (with the AI assistant) or the keyword rules only pick widgets, every tile is checked against the catalogue (GET /api/boards/widgets) and read as the caller; 40 AI questions per person and hour (then the rules answer, note: limit), 40 questions per person and minute (429). GET /api/focus/ask?text=&history=&history=&lang=&rules=1 is the same as a read (MCP, read-only keys)
Status GET /api/focus/status → {ai, reason, hourly, used, can_save} – whether the model would answer now (else why not), the hourly limit and its use, and whether the caller may save a view as a board

MCP tool: ghl_focus_view (location_id, question, history?, lang?, rules_only?) → {say, source, note?, action, tiles: [{type, title, config, source, data}], link}; read-only. To keep a view, create a board (ghl_board_create) and add the tiles with ghl_board_apply_patch.

Companion (the person’s companion in focus mode and its proactive check-ins). Sub-account routes for a signed-in person (a sub-account API key gets 403 on the personal ones; the briefing answers keys by the rules, without the signals that need a person). Actions from focus mode go through POST /api/agent/chat with source: "focus" (AI agent).

Area Endpoints
Settings GET /api/companion/prefs → {prefs: {agent_id, name, wake_phrase, shortcut, read_aloud, voice_reply, browser_voice, tts_provider, tts_connection_id, tts_voice, tz, checkin: {mode: off|interval|daily, minutes, times: ["HH:MM"], quiet_from, quiet_to, weekdays_only, only_changed, speak}, conversation_window, barge_in, voice_engine: classic|gemini_live, live_connection_id, initiative: low|balanced|high, events: {on, unanswered_minutes, triggers: {<id>: {on?, priority?: interrupt|digest}}}}, name, wake_phrase, agent: {id, name, emoji}, agent_missing, tz, intervals, next_checkin, unseen} – name and wake_phrase are the effective ones (“Jarvis” / “Hey Jarvis” by default); PUT /api/companion/prefs with any of the prefs fields (checkin partially) → the same; agent_id must be an agent the person may use (else 404), minutes 5–1440, up to 6 times, tz an IANA zone. tts_provider (default google_speech, or browser, account = the sub-account’s voice) and tts_voice (default auto = de-DE-Chirp3-HD-Iapetus / en-US-Chirp3-HD-Iapetus by app language; the 1.0.53 default en-GB-Neural2-B is read as auto) are the companion’s voice; a tts_connection_id must be an audio_tts connection of this sub-account (else 404) and sets tts_provider to its provider. conversation_window 0–30 seconds (default 8), unanswered_minutes 5–1440, trigger ids as in Proactive below (triggers is merged). The wake word itself is switched on per device in the browser and never stored
Briefing GET /api/companion/briefing?lang=de|en&rules=1 → {say, tip, source: ai|rules, hotspots: [{key, title, detail, count, score, url, tile?, step?, sig}], next: [{key, label, prompt?, url?}], sig, tiles: [like focus tiles], at} – keys overdue_tasks, overdue_invoices, dunning, inbox, deals, appointments, tickets, workflow_errors, website_down, low_stock, approvals; each read through the caller’s own routes (a refused one adds nothing), top 5 by score, up to 3 steps; nothing is written. With the AI assistant the model phrases say and tip (purpose companion, 24 per person and day, a number not in the data drops the sentence)
Check-ins GET /api/companion/checkins?unseen=1 → own check-ins of the last 30 days [{id, kind: scheduled|manual, say, tip, source, hotspots, next, seen, created_at}] (newest first, at most 30); GET /api/companion/checkins/:id → one, with tiles read again now; POST /api/companion/checkins/:id/seen; POST /api/companion/checkins/run {lang?} → a check-in now (“Try it now”), with push. Scheduled ones are created by the server once a minute when due (interval outside the quiet hours, fixed times in the person’s zone, weekdays only, skipped when the hotspots did not change) and announced as web push checkin with the link /app/focus?checkin=<id> and in the Android feed (companion_checkin). Since 1.0.54 a check-in also carries digest (collected events), suggestions (the initiative’s, like below) and offer (what the companion adds aloud)
Live (1.0.54) GET /api/companion/live → {events: [{id, kind, title, say, route: speak|card, priority, action: {label, prompt?, url?, entity?}|null, suggestion|null, created_at}], suggestions: [open ones], at} – the open app’s poll (~15 s while visible; it also tells the server the companion is open); each event is returned once. GET /api/companion/events/:id, POST /api/companion/events/:id {status: done|dismissed}
Proactive (1.0.54) Triggers lead_new, message_unanswered, invoice_overdue, appointment_soon, review_negative, deal_won, deal_lost, payment_failed, workflow_failed – routed per person (needs the area; an assigned record only to its owner): interrupt = spoken when open (one per 3 minutes, else a card), push companion_event when closed (link /app/focus?event=<id>), quiet hours → collected; digest = into the next check-in; dedupe per record. GET /api/companion/proactive → {triggers: [{id, label, hint, default_priority, on, priority, allowed}], ai: {ready, reason}, level, levels, live: {available, connections: [{id, label}]}}. GET /api/companion/suggestions → own suggestions (30) [{id, kind: opportunity|risk|growth|task|strategy, title, say, draft_type: reply|email|task_list|social_post|review_reply|none, draft, entity, source: ai|rules, origin, status: offered|accepted|declined|ignored, created_at}]; POST /api/companion/suggestions/:id {status: accepted|declined|ignored} → {ok, suggestion} plus, when accepted, {prompt, entity, label} for POST /api/agent/chat (the draft travels as quoted data; the agent still asks); POST /api/companion/initiative/run {lang?} → {suggestions, source, ai}. AI suggestions are logged in ai_calls (purpose companion_initiative, at most 30 per person and day)
Realtime voice (1.0.54) POST /api/companion/live/session → {ticket, path: "/ws/companion-live?t=…", model, voice, connection: {id, label}} (409 without a Google Gemini connection with a key, 429 when one already runs) – a one-time ticket (30 s) for the WebSocket: binary frames = 16 kHz PCM16 up, 24 kHz PCM16 down; JSON up {type: text|audio_end|tool_result|bye}, down {type: ready|transcript|interrupted|turn_complete|tool|tool_cancel|end|go_away|error, …} (error with fallback: true = use the classic voice). The server relays to the Gemini Live API with the sub-account’s key; sessions end after 10 minutes

MCP tool: ghl_briefing (location_id, lang?, rules_only?) → {say, tip, source, hotspots: [{key, title, detail, count, link}], next, link}; read-only (a key always gets the rules).

Business Developer: GET /api/companion/growth?kind=daily|weekly&lang=de|en → {kind, at, kpis: {revenue, leads, won_count, won_value, win_rate, avg_deal, pipeline, overdue}, prev, deltas, opportunities, risks: [{key, kind: opportunity|risk|growth, area, title, detail, impact_eur, estimate, count, url, sig, action: {label, prompt, draft_type, draft, entity}}], idea, actions, say, sig, fresh, source: rules|ai, goals} – read with the caller’s rights, nothing written; POST /api/companion/growth/run {kind?, lang?} (a person) → {checkin_id, review}, delivered as a check-in of the kind growth / weekly (with growth: {kind, kpis, prev, deltas, idea} in the check-in). Prefs: role: business_developer|assistant (default business_developer), growth: {daily, time: "HH:MM", weekly} (default {false, "08:00", true} – opt-in: daily is the switch, weekly only counts while it is on). Contact counts in the review follow the caller’s record-access rules. MCP tool ghl_growth_review (location_id, kind?, lang?) – the same review as JSON, read-only.

Per-person layouts behind the Raum-Modus (spatial mode) extras (docs/xr.md). They never change a board or a canvas; keys and MCP connections are refused (no person). Writes are audited.

Route
GET/PUT/DELETE /api/boards/:id/xr-layout Where I hung the widgets of a Living Board in my headset. Area Dashboard, access to the board (view is enough). PUT {layout: {v: 1, radius, panels: {<widgetId>: {x, y, z, yaw, scale}}, hud?}} (metres in my own frame; unknown widgets are dropped) → {layout, updated_at}; GET → {layout: … | null, updated_at}
GET/PUT/DELETE /api/canvas/:id/xr-layout The same for a canvas plane (area Canvas): {layout: {v: 1, plane: {x, y, z, yaw, scale, curve}}}
GET/PUT/DELETE /api/xr/workspace My Arbeitsfläche (workspace): {layout: {v: 1, panels: [{route, size}], sync}}, 1–4 panels, routes below /app; GET also returns the default

iPhone, iPad and foldables (installable web app)

Section titled “iPhone, iPad and foldables (installable web app)”

Details, limits and tests: docs/ios-web-app.md.

Area Routes
Manifest (no login) GET /manifest.webmanifest – name, colour and icons of the white label (a client’s own domain gets the client’s branding); display: standalone, scope and start_url /app, orientation: any
Touch icons and launch images (no login) GET /pwa/icon/<icon-120|152|167|180|192|512|1024|maskable-512>.png (the white label’s colour and logo; the shipped HyperLevel icon without branding), GET /pwa/splash/<width>x<height>-<light|dark>.png (only the pixel sizes of server/services/pwa-devices.ts, else 404)
Badge GET /api/app-badge → {unread, total, here}: unread messages in every sub-account the login may see (total, capped at 999 – the icon; 0 without the area Conversations) and in the one named by x-location-id (here – the bottom bar); push payloads carry total as badge

What the community does and how to set it up: docs/community.md. Team routes need a login with x-location-id (or an API key) and the area Courses & community; every write lands in the audit log, everything is scoped to the sub-account. The first routes (groups, channels, posts, comments, reports, events, leaderboard) are in the table above; this section lists what the later parts add. Errors are German.

Part Routes
Public page and joining GET /api/community/groups/:id/join (page, questions, discovery, tiers), PUT /api/community/groups/:id/join/page {slug, enabled, discoverable, indexable, tagline, about, video_url, cover_image, seo_title, seo_description, questions (up to 3), approval, terms_url, privacy_url, consent_text, set_access_join}, POST /api/community/groups/:id/tiers, PATCH/DELETE /api/community/tiers/:tid {name, description, features (up to 8), product_id, annual_product_id, trial_days (0–90), course_ids, tags, highlight, grace_days (0–30), active, position}, GET/POST /api/community/groups/:id/memberships (POST {contact_id|email + name, tier_id?, send_login_link?} adds a person), PATCH /api/community/memberships/:mid {action: approve|reject|end|reactivate|tier, tier_id?, note?, notify?}, GET /api/community/requests (open requests of the sub-account), GET/POST /api/community/groups/:id/invites, PATCH/DELETE /api/community/invites/:iid {label, tier_id, skip_approval, trial_days, max_uses, expires_at, active}, GET/POST /api/community/groups/:id/coupons, PATCH/DELETE /api/community/coupons/:cid {code, label, percent_off|amount_off_cents, duration: once|repeating|forever, months, max_uses, expires_at, tier_id, active}
Public pages (no login) GET /c/:slug (About), /c/:slug/join (GET the form, POST the form fields name, email, tier, billing, coupon, invite, consent, the answers to your questions and the form’s own CSRF token → joined, waiting for approval, or on to the checkout), /c/:slug/i/:code (invite), /c/:slug/willkommen, GET /api/public/communities/:slug/coupon?code=&tier= (checks a code, CORS open), GET /communities (directory; PGHL_COMMUNITY_DISCOVERY=0 switches it off)
Settings and roles GET/PUT /api/community/groups/:id/ext {dms: {enabled, per_hour, new_per_day}, directory, map, online, points: {course, event, challenge_checkin, challenge_complete}, leaderboard: {reset: none|monthly}, start_here: {post_id, label}, welcome: {dm, post, checklist}, live: {jitsi_url}, affiliate: {program_id}, video: {upload}}, GET/POST /api/community/groups/:id/staff {contact_id, role: admin|moderator}, DELETE …/staff/:contactId
Messages GET /api/community/messages (the conversations members have with the team), GET /api/community/messages/:contactId, POST /api/community/messages/:contactId {body, from_name?} (as the team), GET /api/community/dm-reports?status=open|all (reported messages with the four before them), PATCH /api/community/dm-reports/:id {action: dismiss|remove|ban}. Messages between two members are never readable here
Badges and points GET/POST /api/community/groups/:id/badges {name, description, emoji, color (#rrggbb), points (bonus, 0–1000), rule: {type: manual|points|level|posts|comments|likes|courses|events|challenges|days|streak, value}}, PATCH/DELETE /api/community/badges/:bid, GET /api/community/badges/:bid/awards, POST /api/community/badges/:bid/award {contact_id, reason?}, DELETE …/award/:contactId, GET /api/community/groups/:id/badges/preview?type=&value= (who would earn it), POST …/badges/evaluate, POST /api/community/groups/:id/points {contact_id, points (also negative), reason}
Perks and channel rules GET/POST /api/community/groups/:id/perks {level, kind: coupon|file|link|text, title, body, note, file?: {name, data (base64, up to 8 MB)}}, PATCH/DELETE /api/community/perks/:pid, PUT /api/community/perks/:pid/file, GET /api/community/groups/:id/unlocks (what each level opens), GET /api/community/groups/:id/channel-rules, PUT /api/community/channels/:cid/rules {min_level, tier_ids}, GET /api/community/groups/:id/leaderboard/archive (past months)
Challenges GET/POST /api/community/groups/:id/challenges {title, emoji, description, starts_on, ends_on (YYYY-MM-DD, up to 366 days), goal_days, status: draft|active|archived, points_checkin, points_complete, badge_id, require_note, reminder_hour, tasks: [{day, title, body}]}, GET/PATCH/DELETE /api/community/challenges/:cid, GET /api/community/challenges/:cid/participants
Live calls GET /api/community/groups/:id/live (events with their live state), PUT /api/community/events/:id/live {enabled}, GET /api/community/events/:id/attendance, POST /api/community/events/:id/recording {title?, body?, video_url|video_upload, channel_id?, notify?} (publishes the recording as a post)
Numbers and growth GET /api/community/groups/:id/analytics?days=30 (7–366: {members, dau, activity, revenue: {mrr_cents, arr_cents, …, by_tier}, churn, trials, top, engagement, …}), GET /api/community/overview?days= (one line per group), GET/POST /api/community/groups/:id/broadcasts {subject, body (Markdown), channels: [email, bell, push], segment: {tier_ids, min_level, active_days, inactive_days, tags}, scheduled_at?}, POST …/broadcasts/preview (recipient counts), DELETE /api/community/broadcasts/:bid (scheduled ones), GET/PUT /api/community/groups/:id/affiliate (partner programme link)
Import and export POST /api/community/groups/:id/import/members/preview {csv, mapping?, date_order?: auto|dmy|mdy, tier_id?, tier_map?} → counts and rows (nothing is written), POST …/import/members (same body plus confirm_rights: true, send_mail?, trigger_workflows?, grant_tier_access?, import_points?; up to 5,000 rows, 6 MB), POST …/import/posts/preview and POST …/import/posts {files: [{name, content (Markdown)}], channel_id?, keep_dates?} (up to 500 files), GET …/export/members.csv, GET …/export/posts.json
Member app (no login) GET /portal/:key/manifest.webmanifest, /portal/:key/icon/:size.png (192, 512, maskable, apple, favicon), /portal/:key/community-sw.js, /portal/:key/offline, /portal-pwa.js; with a member session, same origin and the header x-community-pwa: 1: POST /portal/:key/community/push {subscription} and POST …/push/unsubscribe {endpoint}

Member pages under /portal/:key/community/… are HTML for browsers (feed, messages, challenges, perks, live room, partner tab); they are not a JSON API. Their forms carry a CSRF token and the member session cookie.

Workflow triggers community.post_created, community.member_joined, community.level_reached, community.member_left, community.trial_started, community.trial_converted, community.join_requested, community.challenge_joined, community.challenge_completed, community.badge_earned, community.dm_received, community.event_attended (event data: docs/community.md) and the steps community_join, community_leave, community_grant_badge, community_send_dm, community_award_points, community_post. Contact filters for campaigns and workflows: community_group, community_min_level, community_active_days, community_inactive_days. MCP tools: ghl_community_members, ghl_community_post, ghl_community_stats, ghl_community_grant (plus the older ghl_list_community_posts, ghl_create_community_post). Mobile: row Community under Mobile app.

Automated webinars per sub-account – what they are and how they behave: docs/webinars.md. Team routes (login with x-location-id, sub-account API key or agency key; member logins need the area Funnels & Websites; every write is in the audit log, e.g. webinars.create, webinars.update, webinars.registrations.create). Errors are German: 400 {error: "Noch nicht bereit zum Veröffentlichen: Eine Videoquelle fehlt; …"} (“Not ready to publish yet: a video source is missing; …”). Times are ISO-8601 in UTC; sessions are identified by their start time.

Area Endpoints
Webinars GET /api/webinars → [{id, slug, title, status: draft|published, presenter_name, duration_s, video_type, registrations, attended, upcoming_registrations, next_session, url, updated_at}], POST /api/webinars {title, description?, presenter_name?, presenter_photo?, video?, duration_s?|duration_min?, schedule?, settings?, slug?} (always created as a draft), GET /api/webinars/:id → the webinar with urls: {page, embed}, tz, upcoming (bookable sessions with taken/full), problems (what is missing to publish), counts, PATCH /api/webinars/:id (any of the fields above plus status: published|draft; settings merges per section; publishing checks video, length and a schedule), DELETE /api/webinars/:id, POST /api/webinars/:id/duplicate (draft; an uploaded file is copied)
video {type: "url", url} (https; MP4/WebM/HLS), {type: "youtube"|"vimeo", id} (a link of any form is accepted and reduced to the id), {type: "upload", upload_id} (set by the upload below), {type: "studio_clip", clip_id}, {type: "studio_upload", upload_id}; GET /api/webinars/studio-assets lists the Video-Studio clips and uploads of the sub-account
schedule {tz, fixed: {enabled, weekdays: ["mon",…], times: ["19:00"]}, dates: ["2026-11-05T18:00"], jit: {enabled, every_min, from, to, weekdays}, ondemand: {enabled}, blackout: ["2026-12-24", "2026-12-27..2027-01-02"], horizon_days, max_per_session, late_join_min} – wall-clock times in tz (empty = the sub-account’s zone); late_join_min 9999 = until the end
settings registration {headline, button_text, consent_text, ask_phone, phone_required, sms_consent_text, thanks_text, redirect_url, tags, form_ids, notify_team}, room {label, replay_label, waiting_text, ended_text, show_counter, questions, scripted_enabled, scripted: [{at_s, name, text}], polls: [{at_s, question, options, multiple, show_results}]}, offer {enabled, at_s, title, text, button_text, target: {type: product|form|url, …}, countdown_s, show_in_replay}, reminders {items: [{key, offset_min, email, sms, subject, body, sms_text}], confirmation_subject, confirmation_body}, replay {enabled, hours}, tags {attended, missed, cta, purchased, replay}. Any wording that claims the webinar is live is refused (400); products, forms and videos must belong to the same sub-account
Sessions GET /api/webinars/:id/sessions → {tz, sessions: [{start, kind: fixed|date|jit|ondemand, taken, full}]}
Registrations GET /api/webinars/:id/registrations?session=<start>|ondemand&status=registered|cancelled&attended=1|0&q=&limit=&offset= → {total, registrations: [{id, contact_id, name, email, phone, session_start, session_kind, status, source, attended, watch_pct, watched_s, left_at, cta_clicked, cta_clicks, purchased, purchase_cents, replay_watched, created_at}]}, POST /api/webinars/:id/registrations {contact_id | email + first_name, last_name?, phone?, session?, silent?} → {id, contact_id, created, session, join_url, replay_url} (no session = the next free one; ondemand = a session starting now; 409 when the session is full or the webinar is a draft), DELETE /api/webinars/:id/registrations/:rid (cancels, frees the seat)
Numbers GET /api/webinars/:id/stats → {totals: {registrations, attended, show_up_rate, avg_watch_s, avg_watch_pct, cta_clicks, cta_unique, purchases, revenue_cents, replay_views, questions, cancelled, conversion_rate}, sessions: [{start, kind, registrations, attended, show_up_rate, avg_watch_s, avg_watch_pct, cta_clicks, cta_unique, purchases, revenue_cents, replay_views, seats}], dropoff: [{at_s, viewers, share}] (21 points), polls: [{id, question, total, options: [{label, count}]}]}, GET /api/webinars/:id/questions → [{id, contact_id, name, email, session_start, offset_s, body, conversation_id, created_at}] (the questions are also messages in Conversations, channel Chat)
Preview POST /api/webinars/:id/preview-link {offset_s?} → {url, expires_at, offset_s} – the room from a chosen second, valid 6 hours, counts nothing, works for drafts
Video upload POST /api/webinars/:id/video/upload {name, size} → {id, chunk_size: 8388608}, then PUT /api/webinars/:id/video/upload/:uid?offset=<bytes> with the raw part as body (application/octet-stream, in order; a repeated part is acknowledged, a wrong offset is 409) → {id, received, complete}; the finished file becomes the webinar’s video. Size limit PGHL_WEBINAR_MAX_MB (default 4096)

Public routes (no login; every viewer action carries the signed token of one registration, ?t= or the body field t):

Route Purpose
GET /w/:slug (?embed=1) Registration page; GET /api/public/webinars/:slug → {title, description, presenter, label, duration_s, sessions: [{start, kind, full}], url} (CORS open)
POST /api/public/webinars/:slug/register {first_name, last_name?, email, phone?, session, tz?, consent: true, sms_consent?} → {ok, created, session: {start, kind}, join_url?} – join_url only for a session that starts right now; for later sessions the personal link goes by e-mail. 400 without consent, 409 when full, 429 over 20 a minute and address
GET /w/:slug/watch?t= · /replay?t= · /ics?t= · /cancel?t= (POST confirms) · /go?t= (the offer: counted, then redirected; 410 after its deadline) · /media?t= (the video bytes with Range, only while the session runs or the recording is open) The room, the recording, the calendar file, cancelling, the offer, the video
POST /api/public/webinars/:slug/ping {t, mode: live|replay, pos?, playing?} Heartbeat every 20 s: books attendance and watch time, returns {server_now, clock: {state: waiting|running|late|ended, starts_in_s, offset_s, ends_in_s}, counter?, replay: {available, reason, until, url}, answers}
POST /api/public/webinars/:slug/question {t, text} · POST …/vote {t, poll_id, choice: [index]} A question to the team (max. 10 per person, 4 s apart); one answer per poll

Workflow triggers webinar.registered, webinar.attended, webinar.missed, webinar.watched_percent (filter min_percent), webinar.cta_clicked, webinar.replay_watched and the action webinar_register (docs/webinars.md). MCP tools: ghl_webinar_list, ghl_webinar_get, ghl_webinar_register, ghl_webinar_stats. Mobile: see the table under Mobile app.

Payment plans, dunning, quizzes/certificates, comment-to-DM and the AI workflow builder. Team routes only (login with x-location-id, sub-account API key or agency key; member logins need the area named in each row; every write that changes money, access or published content is in the audit log). The long versions with all fields: payment-plans, dunning, quizzes-certificates, comment-to-dm, workflow-ai. The phone gets read-only lists under /api/mobile/creator/* (see Mobile app).

Area Routes
Payment plans (payments) GET/POST /api/payment-plans, PATCH/DELETE /api/payment-plans/:id, GET /api/payment-plans/subscriptions[/:id], POST …/subscriptions/:id/access ({on}) and …/cancel
Dunning (payments) GET/PUT /api/dunning/settings, GET /api/dunning/overview, GET /api/dunning/cases[/:id], POST …/cases/:id/remind|retry|close|access, POST /api/dunning/billing-link, GET/PUT /api/dunning/products[/:id], GET /api/dunning/notices, PATCH …/notices/:id, POST …/notices/:id/send|resend|dismiss, GET …/notices/:id/pdf, GET /api/dunning/invoices, POST …/invoices/:id/notice|hold, GET /api/dunning/preview, POST /api/dunning/run
Quizzes & certificates (courses) GET/POST /api/quizzes, GET/PUT/DELETE /api/quizzes/:id, GET …/:id/attempts, POST …/:id/reset, GET /api/quizzes/reviews, POST /api/quizzes/attempts/:id/review, GET/POST /api/assignments, GET/PUT/DELETE /api/assignments/:id, GET …/:id/submissions, POST /api/assignments/submissions/:id/review, GET …/submissions/:id/file, GET/POST /api/certificates/templates, PATCH/DELETE …/templates/:id, POST …/templates/preview, GET /api/certificates, POST /api/certificates/issue, GET …/:id/pdf, POST …/:id/revoke|resend
Comment-to-DM (social) GET /api/ig-comments/status, GET/POST /api/ig-comments/rules, PATCH/DELETE …/rules/:id, POST …/test, GET …/events, GET …/media, POST …/subscribe
AI workflow builder (workflows) GET /api/workflow-ai/status, POST /api/workflow-ai/generate, POST /api/workflow-ai/runs/:id/apply (saves a draft, never publishes), GET /api/workflow-ai/runs[/:id]

Public pages: GET /abo/:token (billing page of one contact, signed link), GET /mahnung/:token/pdf (dunning notice PDF), GET /zertifikat/:id[/pdf] (certificate verification and PDF), GET /portal/:p/abo. MCP tools: ghl_payment_plan_list, ghl_payment_plan_save, ghl_payment_plan_purchases, ghl_dunning_overview, ghl_dunning_send_link, ghl_dunning_run, ghl_dunning_notice, ghl_quiz_list, ghl_quiz_save, ghl_quiz_reviews, ghl_quiz_review, ghl_certificate_issue, ghl_certificate_list, ghl_ig_comment_rules, ghl_workflow_generate.

Mandates, collection runs (pain.008.001.08), returns and transfer files (pain.001.001.09) without a payment provider – area payments, details in docs/sepa.md.

Area Routes
Settings GET /api/sepa/overview, GET/PUT /api/sepa/settings (creditor identifier with check digits, account, pre-notification 1–60 days)
Mandates GET/POST /api/sepa/mandates (paper), POST /api/sepa/mandates/request (online link, send), GET/PATCH /api/sepa/mandates/:id, POST …/:id/revoke|reactivate|link
Collection runs GET /api/sepa/candidates, POST /api/sepa/runs/preview, POST /api/sepa/runs (sends the pre-notification), GET /api/sepa/runs[/:id], GET …/:id/xml, POST …/:id/submit|notify|cancel|paid, POST /api/sepa/items/:id/paid, POST /api/sepa/items/:id/return (code, fee_cents, fee_invoice)
Transfers GET /api/sepa/payables (open supplier invoices), POST /api/sepa/transfers (receipt_ids, lines)

Public page: GET/POST /sepa/m/:token (online mandate, one random token per mandate). Workflow triggers sepa.mandate_signed, sepa.returned. MCP tools (read-only): ghl_sepa_mandates, ghl_sepa_runs.

Online store, gift cards and payment providers

Section titled “Online store, gift cards and payment providers”

Online store with cart and checkout, gift cards with a balance ledger, and PayPal / Mollie next to Stripe – store and gift cards in the area payments, provider settings in settings. Details: docs/store.md, docs/payment-providers.md.

Store GET/PUT /api/store/settings, GET /api/store/products, PUT /api/store/products/:id (published, kind: physical|digital|gift_card, images, weight_g, collection_ids, variant_prices), GET/POST /api/store/collections, PATCH/DELETE /api/store/collections/:id
Orders GET /api/store/orders?status=&fulfilment=&q=, GET/PATCH /api/store/orders/:id (fulfilment, carrier, tracking_number, notify), POST /api/store/orders/:id/refund, POST /api/store/orders/:id/resend
Gift cards GET/POST /api/gift-cards, GET/PATCH /api/gift-cards/:id, POST /api/gift-cards/:id/adjust|send, POST /api/gift-cards/check, POST /api/gift-cards/redeem (code, amount_cents or invoice_id)
Payment providers GET /api/pay/providers, PUT/DELETE /api/pay/providers/:id (paypal, mollie – credentials checked, stored sealed), POST /api/pay/providers/:id/test, GET /api/pay/sessions[/:id], POST /api/pay/sessions/:id/refund
Public POST /api/public/store/<key>/cart|quote|checkout, pages /store/<key>/checkout, /store/<key>/order/<token>, pay page /pay/<id>, webhooks /hooks/paypal/<locationId>, /hooks/mollie/<locationId>

Workflow triggers store.order_placed, store.order_paid, store.order_shipped; abandoned carts fire checkout.abandoned. MCP tools: ghl_store_orders, ghl_store_order_status, ghl_gift_card_create.

Journal of every change to the main records, with undo, redo and a 30-day trash – docs/undo.md for what is covered. Sub-account routes (x-location-id or the sub-account key); the person undoes their own changes, admins anyone’s; another sub-account always gets 404.

Route What
GET /api/undo?scope=mine|all&entity=&actor=&q=&include=undone&before=&limit= Changes, newest first, one item per request (a bulk action is one item with count). actor: me, user, agent, api_key, mcp, agency_key, app. Item: {id, at, label, entity, entity_id, action, actor, actor_kind, count, items?, changes: [{k, l, from, to}], notice, kind: change|undo, undone_at, can_undo, restorable, reason, removes, expires_at}; the answer adds has_more, retention_days
GET /api/undo/last {undo, redo} – what Ctrl+Z would take back and the undo it could repeat
GET /api/undo/trash?entity=&q= Deleted records that can still be restored: items plus kids_text, expires_at, can_purge
GET /api/undo/:id/preview Dry run → {conflict, conflicts: [{id, label, diff}], notice}
POST /api/undo/:id {force?, single?} Restore in one transaction → {ok, label, restored, redo_id, notices}; 409 with {conflict, diff} if the record changed since; 403 not yours / no area; 404; 409 already undone; 410 expired; 422 not restorable
DELETE /api/undo/:id, DELETE /api/undo/trash Admins: remove one record / all from the trash for good
GET/PUT /api/undo/settings {retention_days} (1–365, default 30)
GET /api/undo/done-today Tasks completed today

Journaled writes answer with x-undo-id, x-undo-label and x-undo-notice (URL-encoded). MCP tools: ghl_undo_list, ghl_undo. Every undo is in the audit log (undo.create).

Import from Mailchimp, ActiveCampaign, Kit, Kajabi, Teachable, Keap, Pipedrive, systeme.io, Klaviyo, Brevo, KlickTipp, ManyChat, Zendesk, Intercom, Salesforce, Zoho CRM, monday.com, ClickUp, Notion, Airtable, Typeform, Calendly, Lexware Office and sevDesk – what moves and how it behaves: docs/umzug.md. Team routes (login with x-location-id, sub-account API key or agency key; member logins need the area Settings; every import, dry run and undo is in the audit log, umzug.*):

Area Endpoints
Overview GET /api/umzug → {sources: [{key, label, tagline, credentials: [{key, label, secret, placeholder, hint, optional}], howto, limits, parts: [{key, label, on?, note?}], exports}], file_kinds, connections: {<source>: {connected, label?, hint?, account?, updated_at?}}, jobs (the last 10), files, max_upload_mb}
Connection PUT /api/umzug/connections/:source {<credential fields>} → {connected: true, label, hint, account} – the vendor is called once to validate, the key is sealed and never returned; only for logged-in people in the UI (403 with an API key, agency key or MCP). DELETE /api/umzug/connections/:source
Export files POST /api/umzug/files?source=<source>&name=<file name>&kind=<contacts|deals|courses|students|templates> with the raw file as body (CSV, TSV, JSON, HTML, ZIP; up to PGHL_UMZUG_MAX_MB, default 50) → {id, source, kind, name, size, detected, rows, tables: [{name, columns, rows, header}]}; PATCH /api/umzug/files/:id {kind}; DELETE /api/umzug/files/:id. Files are deleted after a successful import and after 14 days
Preview POST /api/umzug/preview {source, origin?: "api"|"file", file_ids?, parts?, mapping?} → {source, label, origin, counts: [{part, key, label, total, error?}], fields: [{ref, label, type, std?, sample?, group?}], mapping: {<field ref>: <target>}, custom_fields, existing: {<kind>: n}, warnings, parts, file_ids}. Reads, writes nothing; 30 per hour per sub-account
Mapping targets "" (skip), a standard field (first_name, last_name, email, phone, company, address, city, postal_code, country, website, date_of_birth, source), custom:<key>, new:<type>:<label> (creates the custom field; types text, textarea, number, date, select, checkbox, email, phone, url), tag / tag:<prefix>, note
Run POST /api/umzug/dry-run and POST /api/umzug/jobs {source, origin?, file_ids?, parts?, mapping?, options?: {merge: "fill"|"overwrite", tag?, keep_files?}} → the job (a background job; one per sub-account at a time). Without mapping the automatic one is used. A dry run writes nothing
Jobs GET /api/umzug/jobs?limit=, GET /api/umzug/jobs/:id → {id, source, source_label, kind: "import"|"dry_run", status: running|done|failed|interrupted|undoing|undone, origin, parts, error, step, step_label, processed, total, percent, steps: [{id, label, done}], report: [{key, label, created, updated, skipped, failed}], undo: {deleted, restored, kept, notes} | null, warnings, log, created_at, finished_at, undone_at}
Control POST /api/umzug/jobs/:id/pause (stops after the current page), POST …/resume (failed or paused jobs, continues at the saved position), POST …/undo (deletes what the import created, restores merged contacts)

MCP tools: ghl_umzug_sources, ghl_umzug_preview, ghl_umzug_start (dry_run: true is a read), ghl_umzug_status. Hooks and test hooks: docs/architecture.md.

SPF, DKIM, DMARC and MX of a sending domain, with the DNS records to create: docs/deliverability.md. Team routes (area Settings, except status: Campaigns; writes are in the audit log):

Area Endpoints
Domains GET /api/mail-domains → {domains: [{id, domain, provider, provider_label, selector, status: new|ok|warn|fail|unknown, checked_at, summary, checks: [{key, label, status, summary, optional}]}], sender: {from_email, domain, provider, provider_label, active, added, configured_provider}, providers, smtp_hint, rules}, POST /api/mail-domains {domain, provider?: ses|postmark|resend|brevo|mailgun|smtp, selector?} (up to 20), GET /api/mail-domains/:id (adds result), PATCH /api/mail-domains/:id {provider?, selector?} (clears the verdict), DELETE /api/mail-domains/:id
result {domain, provider, checked_at, status, summary, checks: [{key: spf|dkim|dmarc|mx|return_path, label, status, summary, found, problems, steps, records: [{type: TXT|CNAME|MX, host, fqdn, value, priority?, note?}], optional?}], notes}
Check POST /api/mail-domains/:id/check (at most every 3 s per domain, 40 per hour per sub-account) · POST /api/mail-domains/check {domain?} adds the domain if needed and checks it (without domain: the sender’s)
Send dialog GET /api/mail-domains/status[?refresh=1] → {status: none|log|new|ok|warn|fail|unknown, domain, summary, issues: [{key, label, status, summary}], checked_at, stale} – the verdict for the sender’s domain (checks it when older than 24 h and refresh=1)
Unsubscribe GET /u/:contact/:token (page with the button Ja, abmelden (“Yes, unsubscribe”) – opening it changes nothing), POST /u/:contact/:token (the button, confirm=1: the confirmation page; RFC 8058 one-click: 400 for a wrong token, otherwise {ok: true}); campaign, workflow and NPS mails carry List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click

MCP tool: ghl_mail_domain_check.

Details and examples: docs/objects.md. Filters for lists: filters as a JSON list [{field, op, value}] with op = eq, neq, contains, not_contains, empty, not_empty, gt, lt, gte, lte, in; sort=field or sort=-field.

Area Endpoints
Companies GET /api/companies?q&filters&sort&industry&lifecycle_stage&owner_id&limit&offset (with contacts_count, deals_count, deals_value; fields: standard fields, custom.<key>, created_at, updated_at), POST /api/companies {name, domain, industry, size, phone, website, address, postal_code, city, country, owner_id, lifecycle_stage, description, custom, contact_ids} (same domain = update, response with created), GET/PATCH/DELETE /api/companies/:id (GET with contacts, deals, deals_value, notes, tasks, contact_tasks, timeline), POST /api/companies/:id/notes {body}, POST /api/companies/:id/tasks {title, due_at, contact_id?}, GET /api/companies/:id/activities, POST /api/companies/:id/merge {from_id}, GET /api/companies/duplicates, POST /api/companies/import {csv, mapping?, triggerWorkflows?}, GET /api/companies/export.csv, GET/PUT /api/companies/settings {auto_associate, create_from_domain, create_from_text}, POST /api/companies/sync; POST /api/companies/bulk {action: owner|delete|enroll, ids, owner_id?, workflow_id?} → {count, failed} (each company goes through the normal route – permissions, history; enroll enrolls the linked contacts in an active workflow; MCP ghl_companies_bulk)
Custom objects GET /api/objects (types with properties, groups, count), GET/POST /api/object-types {singular, plural, icon, description, primary_label, primary_unique, properties?: [{label, type, options, required, unique}]}, PATCH /api/object-types/:id {singular, plural, icon, description, primary_property}, DELETE /api/object-types/:id?force=1; records: GET /api/objects/:key/records?q&filters&sort&owner_id&limit&offset, POST /api/objects/:key/records {id?, properties, owner_id?, associations?: [{type, id, label?, primary?}]} (unique property = update), GET/PATCH/DELETE /api/objects/:key/records/:id, POST /api/objects/:key/records/:id/notes, POST /api/objects/:key/import {csv, mapping?}, GET /api/objects/:key/export.csv
Properties GET /api/properties?object= (objects, types, standard properties, groups, properties), POST /api/properties {object: contact|company|opportunity|p_…, label, key?, type, options, group_key, description, required, unique}, PATCH/DELETE /api/properties/:id, POST /api/property-groups {object, label}, PATCH/DELETE /api/property-groups/:id, deal values: GET/PUT /api/opportunities/:id/properties {properties}, the sub-account’s team: GET /api/location-users
Associations GET /api/associations?type&id&to_type&label, GET /api/associations/types, GET /api/associations/search?type&q, POST /api/associations {from_type, from_id, to_type, to_id, labels?, primary?}, PATCH /api/associations/:id {labels, primary_for}, DELETE /api/associations/:id, Labels: GET/POST /api/association-labels {a_type, b_type, name}, PATCH/DELETE /api/association-labels/:id
Views & notes GET /api/object-views?object=, POST /api/object-views {object, name, shared, config: {columns, filters, sort, q}}, PATCH/DELETE /api/object-views/:id (your own only), DELETE /api/record-notes/:id
Lead scoring GET/POST /api/lead-scoring/models {name, active, is_primary, rules: [{kind: property, field, op, value, points} | {kind: event, event, points, max_points?, within_days?}], thresholds: [{label, min}], decay: {percent, days}, max_score}, GET/PATCH/DELETE /api/lead-scoring/models/:id, POST /api/lead-scoring/models/:id/recalculate, GET /api/lead-scoring/meta, GET /api/lead-scoring/contacts/:id (scores, breakdown, history), POST /api/lead-scoring/contacts/:id/recalculate, GET /api/lead-scoring/scores?ids= (primary score for lists); filter/sort contacts: GET /api/contacts?score_min&score_max&score_label&sort=score, by company: GET /api/contacts?company.industry=…&company.custom.<key>=…&company.id=…&company.q=…

Events (webhooks, workflows): company.created, company.updated (changed), company.property_changed (property, value, previous), company.deleted, record.created, record.updated, record.property_changed, record.deleted (each with recordId, objectKey, objectLabel, record), association.created/association.removed (from, to, labels, contactId if a contact is involved), lead_score.changed (contactId, modelId, score, previous, label, direction), lead_score.threshold_reached (contactId, modelId, threshold, score).

Agency endpoints (admin or agency key), details in docs/saas.md:

Area Endpoints
Settings GET/PUT /api/saas/settings ({secret_key, webhook_secret, grace_days, default_snapshot_id, mail_location_id, activate_workflows, signup_enabled, signup_title, signup_text, terms_url, unit_cost: {emails, sms, ai} (cents), markup_percent}, response without keys), POST /api/saas/settings/test, DELETE /api/saas/settings/stripe
Plans GET/POST /api/saas/plans, PATCH/DELETE /api/saas/plans/:id (`{name, description, price_month, price_year (€) or price_month_cents/price_year_cents, currency, trial_days, features: [areas from /api/permissions], limits: {contacts, users, funnels, courses, emails, sms, ai} (null = unlimited), overage: {emails
Customer subscriptions GET /api/saas/locations, GET /api/saas/locations/:id, PUT /api/saas/locations/:id `{plan_id, interval: month
Overview GET /api/saas/overview (MRR, ARR, status counts, 30-day churn, per plan, renewals, accounts, events), GET /api/saas/signups, POST /api/saas/signups/:id/invite (new invite link), POST /api/saas/usage/report (report overage to Stripe now), GET /api/saas/usage/reports

In the sub-account (login or API key): GET /api/saas/account (plan, status, usage this month, current counts, limits, hidden areas blocked, banner), POST /api/saas/account/portal → {url} (Stripe customer portal), POST /api/saas/account/cancel {undo?} (at the end of the term). If an area is not in the plan, its endpoints answer 403; at the limit, creating records returns 403 with a hint; paused or ended accounts are read-only for customers.

Public: GET/POST /signup (sign-up page, ?plan=<id> preselects – also hidden plans, never the cloud plans cloud-*), GET /signup/done?session_id= (return from Stripe), GET/POST /invite/:token (set a password, signs in). The agency’s Stripe webhook (only with a valid Stripe-Signature): POST /hooks/stripe-agency – handles checkout.session.completed, customer.subscription.created|updated|deleted|paused|resumed, invoice.paid, invoice.payment_failed.

Only with PGHL_CLOUD_MODE=1 (otherwise 404). Setup, lifecycle and limits: docs/cloud.md.

Area Endpoints
Public GET /cloud (prices), GET/POST /start (signup form; `?plan=solo
Customer (login) GET /api/cloud/me (tenant, plan, phase, meters, support consent, pending legal texts), GET /api/cloud/plans, POST /api/cloud/plan {plan, interval} (prorated), POST /api/cloud/subscribe {plan, interval} → {url} (Checkout), POST /api/cloud/subscribe/confirm {session_id}, POST /api/cloud/portal → {url}, POST /api/cloud/cancel {reason?, comment?, undo?}, GET /api/cloud/invoices, GET /api/cloud/onboarding
Team and clients GET/POST /api/cloud/team, PATCH/DELETE /api/cloud/team/:id, POST /api/cloud/team/:id/invite; agency: GET/POST /api/cloud/clients, PATCH/DELETE /api/cloud/clients/:id, POST /api/cloud/clients/:id/delete {confirm}, GET/PUT /api/cloud/branding, PUT /api/cloud/domain
Data and contracts GET /api/cloud/export (ZIP, owner), POST /api/cloud/delete {confirm, password?}, POST /api/cloud/delete/undo, PUT /api/cloud/support {hours} (0 = revoke), GET /api/cloud/avv[?format=html], GET /api/cloud/legal, POST /api/cloud/legal/accept
Operator (admin) GET /api/cloud/console/overview (MRR, ARR, churn, trial conversion, health), GET /api/cloud/console/tenants[.csv], GET/PATCH /api/cloud/console/tenants/:id, `POST …/tenants/:id/extend-trial
Abuse (admin) GET /api/cloud/console/abuse (notices, accounts with measures, settings), PATCH /api/cloud/console/abuse/:id `{status: new
Abuse (public) GET/POST /abuse (DSA notice form: category, urls, explanation, name, email, good_faith)
Customer sites GET /api/site-domain-check?domain= (TXT record that verifies a site’s custom domain), POST /api/site-domain-check {domain} (check now) – docs/cloud.md
Waitlist mode Public (also on a landing-page instance without cloud mode when PGHL_CLOUD_WAITLIST=1): GET /api/public/cloud-waitlist → {enabled, via, token, plans, consent, privacy_url, need_name, doi}, POST /api/public/cloud-waitlist {email, name?, plan?, consent: true, lang?, wt} → {ok, state, message} (400/429/404 with {error, state}; a form post gets a 303 back to back). Admin: GET /api/cloud/waitlist[?q=], GET /api/cloud/waitlist.csv, DELETE /api/cloud/waitlist/:id. While it is on, POST /start, the verification link, the Google return and /signup answer 403 – docs/cloud.md

Errors are German {error} strings with the usual status codes. Customers get 403 on /api/cloud/console/*; operator admins get 403 on a tenant’s sub-accounts unless the customer allowed support access.

Admins only; setup in docs/backups.md.

Area Endpoints
Off-site backup GET /api/admin/offsite-backup (configuration, last backup, last restore test, runs), POST /api/admin/offsite-backup/run, POST /api/admin/offsite-backup/restore-test (both start in the background, 409 while one runs), GET /api/admin/offsite-backup/snapshots (the bucket’s snapshots)
Health and alerts GET /api/admin/ops (checks with details, alert channels, open and recent alerts), POST /api/admin/ops/test-alert
Status page GET /api/admin/status-page (components, uptime, incidents), POST /api/admin/status-page/incidents `{title, impact: minor

Login for people in the browser (not for API keys); setup and security model: docs/auth.md. All public endpoints are throttled per IP; responses of sign-in steps always take one of these forms: {done: true, user, next} (session cookie set), {second_factor: {token, methods: [code, passkey], passkey?: {challenge_id, options}}}, {link_confirm: {token, email, provider}}, {error}.

Area Endpoints
Login page (public) GET /api/auth/methods (per domain: {password, magic_link, passkey, providers: [{id, label, icon}]}), POST /api/auth/discover {email} → {sso: {id, label} | null, password} (SSO required for the domain), POST /api/auth/login {email, password, code?, passkey?: {challenge_id, credential}} (with 2FA {needs_code: true, methods?, passkey?}), POST /api/auth/second-factor {token, code | passkey}
SSO (public) POST /api/auth/sso/start {provider: google|microsoft|github|apple|oidc:<id>, next} → {url} (sets a flow cookie, send the browser there), GET /auth/sso/:slug?next= (direct link), callback GET|POST /auth/sso/:slug/callback, POST /api/auth/sso/poll (only with the flow cookie and the header x-pghl: 1: {pending} or the result), POST /api/auth/sso/link-confirm {token, password, code?}
Passkeys POST /api/auth/passkey/options → {challenge_id, options}, POST /api/auth/passkey/login {challenge_id, credential, next}; per login: GET /api/me/passkeys, POST /api/me/passkeys/options, POST /api/me/passkeys {challenge_id, name, credential}, PATCH /api/me/passkeys/:id {name}, DELETE /api/me/passkeys/:id; agency: POST /api/users/:id/passkeys/reset
Email link POST /api/auth/magic-link {email, next} (the response is always the same), POST /api/auth/magic-link/peek {token} → {email, invite}, POST /api/auth/magic-link/consume {token}
My account GET /api/me/identities ({identities, available, password_set, passkeys, enforced}), POST /api/me/identities/link {provider} → {url}, DELETE /api/me/identities/:id, POST /api/me/password/set {password} (only accounts without a password), GET /api/auth/gate ({require_2fa, has_2fa})
Agency GET/PUT /api/agency/login ({password, magic_link, passkey, social: {google: {enabled}, microsoft: {enabled}, github: {enabled, client_id, client_secret}, apple: {enabled, client_id, team_id, key_id, private_key}}, signup: {mode: closed|invite|domains, domains, locations, permissions}, link_by_email, require_2fa_admins, session_days, mail_location_id, clients: {<locationId>: {password, magic_link, passkey, providers} | null}} – secrets come back as ••••••), POST /api/agency/login/invites {email, locations, permissions, days, send}, DELETE /api/agency/login/invites/:email, GET/POST /api/agency/sso, PATCH/DELETE /api/agency/sso/:id ({name, slug, preset, issuer, client_id, client_secret, scopes, location_id, enabled, config: {claims: {email, name, groups}, trust_email, use_userinfo, jit, default_role, default_locations, default_permissions, group_rules: [{group, role?, locations?, permissions?}], require_group, sync_roles, enforce_domains, allowed_domains, skip_2fa}}), POST /api/agency/sso/:id/test

Outbound: POST to your URL with {type, locationId, at, data} and the headers x-pghl-signature-256: sha256=<HMAC-SHA256(secret, body) hex> (recommended) and x-pghl-signature: sha256(secret + body) (older method, stays for existing receivers). New: call.missed, call.voicemail, call.completed ({contactId, callId, direction, status, from, to, duration, transcription}), voice_ai.completed – AI call ended ({contactId, callId, sessionId, outcome: booked|transferred|message|opportunity|task|sms_link|answered|hangup|voicemail|limit|error, outcomes, summary, next_step, appointmentId, taskId, opportunityId, message, transferred, turns, duration, from}) and google_review.received ({contactId?, rating, author, text, link, low}). If an appointment is deleted in the connected Google/Outlook calendar, appointment.status_changed arrives with status: "cancelled" and source: "google"|"microsoft". Without a workflow trigger of their own, but delivered to webhooks with events: ["*"] and REST hooks “Every event”: contact.deleted ({contactId, erased?} – also when several contacts are deleted, erased: true for GDPR erasure), contact.merged ({contactId, contact, fromId}), from project planning task.updated ({contactId?, task, projectId, changed}), task.deleted ({contactId?, taskId, title, projectId, by}), task.commented ({contactId?, task, comment: {id, body}, mentions}), pm.project.deleted ({projectId, name}), timer.started, timer.stopped, time.logged (each {entry}), timesheet.submitted ({userId, week}), timesheet.decided ({userId, week, status}), plus sequence.enrolled ({contactId, sequenceId, enrollmentId, userId}), billing.cancel_requested ({contactId, subscription, productId, immediate}) and billing.plan_switched ({contactId, subscription, fromProductId, productId, product}) from the customer’s subscription page (/abo/…, docs/dunning.md).

Inbound (workflow trigger “Inbound webhook”): call the URL shown in the workflow with JSON {email, phone, first_name, …, tags} (400 without email, phone or name). Trigger “Webhook received”: POST /hooks/in/<token> with any JSON (field mapping in the workflow, optional X-Signature-256: sha256=<hmac>, limit per URL, 429 above it) – details. Repeated deliveries are processed only once: for both workflow webhooks via the sender’s delivery ID (webhook-id, svix-id, Idempotency-Key, X-Idempotency-Key, X-Webhook-Id, X-Delivery-Id, X-GitHub-Delivery, X-Shopify-Webhook-Id – response {ok: true, duplicate: true}), for SMS/email inbound (/hooks/:locationId/sms|email?key=) and for the support mailbox via the message ID (MessageSid, payload.messageId, Message-Id, also in headers). Stripe, Meta, Twilio calls, Telegram, Slack and the SMS/email providers already recognize repeats by their event IDs.

REST hooks (Zapier/Make/n8n): deliveries as {id, event, type, location_id, occurred_at, contact_id, contact, data} with x-pghl-signature-256 = sha256=<HMAC-SHA256(secret, body)>, x-pghl-signature = sha256(secret + body), x-pghl-event, x-pghl-delivery, x-pghl-attempt; retry after 1 min / 5 min / 30 min / 2 h / 6 h, subscription switched off after three 410 responses. New event task.completed ({contactId?, task}) when a task is completed.

Created under Agency → API & MCP. They apply to all sub-accounts; the x-location-id header selects the sub-account. “Read only” keys may not make changes. Users, sign-ins, sessions and the keys themselves, personal connections (OAuth, push, app pairing) and database backups are blocked for them (403).

Terminal window
curl -H "Authorization: Bearer pghla_…" https://crm.example.com/api/agency/projects
curl -H "Authorization: Bearer pghla_…" -H "x-location-id: <id>" https://crm.example.com/api/contacts

Agency OAuth apps (admin login only): GET/PUT /api/agency/oauth {google: {client_id, client_secret}, microsoft: {client_id, client_secret, tenant}} – secrets are never returned (client_secret: "••••••", secret_set), plus the redirect_uris to register.

More endpoints: GET/PUT /api/project (project profile, incl. claude_sessions: [{label, url}]), GET /api/agency/projects, GET /api/agency/contacts?q=&location_id= and GET /api/agency/opportunities?status=open|won|lost|all&q= (across projects), GET/POST /api/docs (documents of the project), GET/POST /api/agency/docs (all documents, ?unassigned=1 = inbox; POST {docs:[{url,title,kind,location_id?}]} files by URL, manual assignments stay), PATCH /api/agency/docs/:id (location_id, pinned), PUT /api/invoices/:id/external (provider: norman|lexware|other, external_id, number, status, url), GET /api/invoice-links, GET /api/agency/invoices?unsynced=1 (sent/paid invoices without a bookkeeping link), POST /api/conversations/import (emails/drafts, see the tool ghl_import_emails), GET /api/conversations/drafts, GET /api/admin/audit, GET /api/admin/metrics (admins only: event loop, memory, slowest routes and requests, job times, SQLite settings, row counts – docs/performance.md).

Todoist sync (docs/todoist.md, admins/agency keys only): GET /api/agency/todoist (settings without token, status, counters, mapping per sub-account, log), PATCH /api/agency/todoist {token, enabled, layout: sections|projects, parent_name, home_project_id, home_project_name, unsectioned_location_id, direction: both|push, scope: all|mine, default_mode: auto|off, label, webhook_secret} (a new token is checked at once; with the first token the sync starts by itself only with layout: sections), DELETE /api/agency/todoist (disconnect), GET /api/agency/todoist/projects (projects from Todoist), GET /api/agency/todoist/sections?project_id= (sections, default: the project of the sections layout), PUT /api/agency/todoist/mapping/:locationId {mode: auto|existing|off} plus {section_id, section_name} (sections) or {project_id, project_name} (one project per sub-account), POST /api/agency/todoist/sync {full} (also retries after a Todoist limit; limits are listed in state.limit_error). Webhook from Todoist (only with a valid X-Todoist-Hmac-SHA256): POST /hooks/todoist.

AI assistant (docs/ai-assistant.md): POST /api/ai/suggest {conversation_id | contact_id, channel?, instruction?} → {text, subject, channel} (sends nothing), POST /api/ai/summarize {conversation_id}, GET/PATCH /api/ai/conversations/:id (bot status, summary), GET/PUT /api/ai/settings, POST /api/ai/test, GET /api/ai/usage, GET /api/ai/status.

Finalised invoices and credit notes go to the connected tool by themselves; payments come back (docs/integrations.md). Sub-account routes, area settings:

Area Endpoints
Connection GET /api/books-sync (provider, options, test result, counts – never the key), PUT /api/books-sync {provider: lexware|sevdesk, api_key?, enabled?, options?}, DELETE /api/books-sync, POST /api/books-sync/test
Queue GET /api/books-sync/jobs?status=pending|retry|done|failed|skipped, POST /api/books-sync/jobs/:id/retry, POST /api/books-sync/invoices/:id/push (finalised documents only), POST /api/books-sync/backfill {since} → {queued}
Payments & log POST /api/books-sync/pull → {checked, updated}, GET /api/books-sync/log

The tool’s id is stored in invoice_links (GET /api/invoice-links, provider lexware / sevdesk).

Details and setup: docs/connectors.md. Keys, catalog, connections and approvals belong to the area Settings; app actions and app events to Automations; Google Sheets export/import to Contacts.

Area Endpoints
Composio GET /api/composio (status without keys: configured, source: agency|location|env, key_hint, override, user_id, webhook_url, allow, connections), PUT /api/composio/settings {api_key} (the sub-account’s own key, null = remove), POST /api/composio/test, PUT /api/composio/allow {allow: ["SLACK_SEND_MESSAGE", "notion:*"]}, GET /api/composio/calls (recent app actions)
Agency GET/PUT/DELETE /api/agency/composio {api_key} (admins only, response without keys), POST /api/agency/composio/test
Catalog GET /api/composio/toolkits?search&category&cursor → {items: [{slug, name, logo, description, categories, tools_count, triggers_count, no_auth, connection}], next_cursor}, GET /api/composio/categories
Connections GET /api/composio/connections (?refresh=1 syncs with Composio), POST /api/composio/connections {toolkit} → {url} (only when signed in in the browser; return via GET /composio/callback), DELETE /api/composio/connections/:id (at Composio too)
Actions GET /api/composio/tools?toolkit&search, GET /api/composio/tools/:slug (with input_parameters), POST /api/composio/execute {tool, arguments, contact_id?} → {successful, data, error, log_id} – placeholders are filled from contact_id; with an API key/MCP only approved actions
AI agent POST /api/composio/ai {prompt, contact_id?} → {answer, steps: [{action: search|execute|answer, tool, ok, detail}]} (uses the AI assistant and only approved actions)
App events GET /api/composio/triggers/types?toolkit=, GET/POST /api/composio/triggers {toolkit, trigger_slug, config, mapping: [{from, to}], tags, name}, PATCH /api/composio/triggers/:id {status: active|paused, mapping, tags, name}, DELETE /api/composio/triggers/:id, GET /api/composio/triggers/:id/events, POST /api/composio/triggers/:id/preview {payload?, mapping?}
Direct connections GET /api/connectors (without secrets), PUT /api/connectors/:key {values} (slack, discord: webhook_url; notion: token, page_id), DELETE /api/connectors/:key, POST /api/connectors/:key/test
Google Sheets GET /api/sheets, POST /api/sheets/connect → {url} (return via GET /oauth/google/sheets/callback), DELETE /api/sheets, POST /api/sheets/export {tag?, spreadsheet?} → {spreadsheet_id, url, rows}, POST /api/sheets/import {spreadsheet, sheet?, tag?, dry_run?, trigger_workflows?}

Public: POST /hooks/composio/agency or /hooks/composio/:locationId (only with a valid webhook-signature from Composio; repeats of the same webhook-id are ignored). Event for workflows/webhooks: composio.event (contactId, subscriptionId, app, trigger + all fields of the app event). Workflow actions: composio_action {toolkit, tool, arguments, map: [{from, to}]} (result as {{event.app_result.…}}), composio_ai {prompt, save_field}, chat_notify {service: slack\|discord, title, text, webhook_url?}, notion_append {heading, text, page_id?}. MCP tool: ghl_app_action {location_id, mode: connections\|search\|execute, query, toolkit, tool, arguments}.

Workflows: catalogue, test run, versions, import/export

Section titled “Workflows: catalogue, test run, versions, import/export”

Details: docs/workflow-actions.md. All routes belong to the area Automations and are scoped to the sub-account.

Area Endpoints
Catalogue GET /api/workflow-catalog?q&category&kind=actions|triggers|all&compact=1 → {categories, trigger_categories, actions: [{type, label, category, summary, doc, fields, provides, pure, needs, app, sample}], triggers: [{type, label, category, summary, doc, fields, sample}], counts, functions}
Test run POST /api/workflows/:id/test-run {contact_id?, sample?: {contact, event, vars}, workflow?: {trigger, steps, settings}, max_steps?} → {status, ms, contact, event, log, vars, summary} – nothing is sent or changed
Run log GET /api/workflow-runs/:id → the run with the full log (input, output, set, ms per step) and variables; GET /api/workflows/:id/split-stats
Versions GET /api/workflows/:id/versions, GET …/versions/:n, POST …/versions/:n/restore (fills the draft)
Draft GET/PUT/DELETE /api/workflows/:id/draft, POST /api/workflows/:id/publish {note?}
Export/import GET /api/workflows/:id/export, GET /api/workflow-export, POST /api/workflow-import {data, name?} → {imported: [{id, name, steps, warnings}]} (created as drafts)
Copy and paste steps POST /api/workflows/:id/copy {steps} → {format: 'gohigh-steps', version: 1, steps, references, source} (HTTP secrets blanked), POST /api/workflows/:id/paste {data, existing_ids?} → {steps, warnings, count} (new ids, references checked like the import; nothing is saved)
Validation GET/PATCH /api/workflows/:id answer with issues: [{step, code, field?, params?, message}] (missing required settings, gone jump targets – saving is never refused); POST /api/workflows/:id/validate {steps?} → {ok, issues}. Paused steps carry disabled: true and are skipped by the engine (log line with skipped: true)
Custom events POST /api/custom-events {name, contact_id | email | phone, create?, data?} → fires the trigger Custom event (event.custom)

MCP tools: ghl_workflow_catalog, ghl_workflow_test_run, ghl_workflow_export, ghl_workflow_import, ghl_emit_event.

Area Endpoints
Agency GET/PUT/DELETE /api/agency/nango {base_url, secret_key, webhook_key?, connect_url?, allow_private?} (admins only, response without keys), POST /api/agency/nango/test → {integrations, names}, GET /api/agency/nango/integrations
Status GET /api/nango → {configured, end_user_id, allow, connections, pending}, GET /api/nango/integrations (?refresh=1) → [{key, name, provider, logo, supported, connection}]
Connect POST /api/nango/connect {integration} → {url, mode: oauth|connect_ui, attempt_id} (only when signed in in the browser; redirect, no popup), GET /api/nango/attempts/:id (syncs with Nango)
Connections GET /api/nango/connections (?refresh=1), DELETE /api/nango/connections/:id (at Nango too)
Calls POST /api/nango/request {integration, method, path, query?, body?, headers?, contact_id?} → {status, ok, data, ms} – with an API key/MCP only within the approvals; PUT /api/nango/allow {allow: [{integration, methods: ["GET"]|["*"], path?: "/repos/*"}]} (signed in only), GET /api/nango/calls

Public: POST /hooks/nango (only with a valid X-Nango-Hmac-Sha256). Workflow action nango_request {integration, method, path, query, body, map: [{from, to}], fail_on_error} (response as {{event.nango_result.…}}). MCP tool ghl_nango_request {location_id, mode: integrations\|request, integration, method, path, query, body}. Details: nango.md.

Self-hosted Cognee server as a knowledge-graph memory, opt-in per sub-account and data type – docs/cognee.md for the governance model, what is sent and erasure.

Area Endpoints
Agency GET/PUT/DELETE /api/agency/cognee {base_url, auth_mode: password|api_key, email, password|api_key, allow_private, locations_may_enable, allow_open_server} (signed-in admins only, response without credentials; DELETE only without shared data or with ?force=1), POST /api/agency/cognee/test → {ok, steps: [{key, label, ok, warn?, detail}], version, posture, datasets}, GET /api/agency/cognee/overview → all sub-accounts
Own access PUT/DELETE /api/agency/cognee/locations/:id/account, POST …/test (only while Cognee holds no data of this sub-account), PUT …/settings (as below), POST …/purge
Approvals GET /api/cognee/admin/settings → {enabled, types, redact, members_ask, catalog, connection}, PUT {enabled?, types?: {wiki|notes|tickets|courses|conversations|contacts|memory: bool}, redact?: {…}, members_ask?} → {settings, turned_on, turned_off, offer_delete} (area Settings, signed in only)
Status GET /api/cognee/admin/status → state per data type, warnings, access_control, pending_deletions, the latest cognee.* audit-log entries; POST /api/cognee/admin/sync {full?}; POST /api/cognee/admin/purge {types: [...] | "all", confirm: true} (“Delete data in Cognee”; only for types that are no longer approved)
Questions GET /api/cognee/info → {enabled, connected, can_ask, can_remember, types}; GET /api/cognee/recall?q=&limit=&types=a,b&mode=answer&compact=1 → {searched, mode, answer?, results: [{type, type_label, title, snippet, link, score}], truncated, note?} – only types that are approved and match the caller’s areas; compact=1 (the tools) trims to at most 4 KB
Remember POST /api/cognee/remember {text} (≤ 4000 characters), GET /api/cognee/memories, DELETE /api/cognee/memories/:id

MCP tools (also in the AI agent): ghl_memory_recall {location_id, query, limit?, types?} (read only, compact), ghl_memory_remember {location_id, text}.

Area Endpoints
Presets GET /api/mcp-presets → {presets, callback_url}
Agency GET/POST /api/agency/mcp-servers, GET/PATCH/DELETE /api/agency/mcp-servers/:id, POST …/:id/test, GET …/:id/tools?refresh=1, PUT …/:id/tools {enabled: [names]}, POST …/:id/oauth/start → {url}, DELETE …/:id/oauth
Sub-account the same under /api/mcp-servers (own servers; agency servers can only be read/connected), GET /api/mcp-servers/tools (approved tools with schema for the workflow editor), POST /api/mcp-servers/call {server_id, tool, arguments, contact_id?} → {ok, result, text, truncated, ms, untrusted: true} – with an API key/MCP only when set to “Über HyperLevels /mcp” (“via HyperLevel’s /mcp”)

Create/edit a server: {name, url, transport: auto\|http\|sse, auth: none\|bearer\|oauth, header_name, secret, token_scope: server\|location\|user, oauth_scope, oauth_client_id, oauth_client_secret, allow_private (agency admins only), share: "*"\|[ids] (agency), expose_agent, expose_mcp} – responses come without tokens and with a masked address; create, edit, delete and approvals only when signed in (not with API keys). Public: GET /mcp-client/oauth/callback. Workflow action mcp_tool {server_id, tool, arguments, map} (result as {{event.mcp_result.…}}); on /mcp as ext_<server>_<tool>. Details: mcp-client.md.

POST /mcp speaks the Model Context Protocol (Streamable HTTP, JSON responses, stateless). Sign in with Authorization: Bearer <agency key> (all projects) or a sub-account key (this project only) – or with an OAuth token (pghlo_…, see OAuth for MCP clients) with exactly the same reach. Without valid credentials /mcp answers 401 with WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://<domain>/.well-known/oauth-protected-resource". Setup for Codex, Claude Code, Cursor, ChatGPT and Claude.ai: docs/ai-tools.md.

Terminal window
# Claude Code
claude mcp add --transport http ghl https://crm.example.com/mcp --header "Authorization: Bearer pghla_…"
# Codex (~/.codex/config.toml: [mcp_servers.gohigh] url = "https://crm.example.com/mcp", bearer_token_env_var = "GOHIGH_API_KEY")
codex mcp add gohigh --url https://crm.example.com/mcp --bearer-token-env-var GOHIGH_API_KEY

Tools (prefix ghl_): list_projects, get_project, create_project, update_project (also claude_sessions and codex_sessions – links to claude.ai and chatgpt.com/codex respectively), search_contacts, get_contact, upsert_contact, add_note, list_pipelines, create_pipeline, list_opportunities, create_opportunity, update_opportunity, list_tasks, list_all_tasks, create_task, complete_task, list_conversations, get_conversation, list_drafts, import_emails, send_message (also WhatsApp/Messenger/Instagram/Telegram/Discord/push), telegram_link (campaign link to the Telegram bot), list_whatsapp_templates, send_whatsapp, dashboard, list_social_channels, list_social_posts, create_social_post, list_docs, save_docs, assign_doc, list_invoices, link_invoice, list_blog_posts, create_blog_post (draft unless status says otherwise), list_courses, grant_course_access (revoke with revoke: true), list_calls, google_reviews, ab_test_results (A/B tests with rate, uplift, p-value and Bayesian chance; test_id or the filters funnel_id, page_id, status), saas_overview (SaaS: MRR, status, churn, plans, renewals), set_location_plan (set/change/remove a sub-account’s plan; agency key only), reply_google_review (set or change a reply, or delete it with delete: true), suggest_review_reply (AI reply draft for a Google review in the project’s tone and brand voice – only creates a draft, publishes nothing), create_google_post (publish immediately), todoist_sync_status (status of the Todoist sync incl. layout and limits), todoist_sync (sync now, full: true = complete), list_email_templates, save_email_template (text, builder design or starter), audit_log, ai_suggest_reply (AI reply suggestion, sends nothing, counts toward the project’s monthly limit), list_community_posts, create_community_post (as the team, optionally pinned and with a notification to all members), attribution_report (leads, deals, revenue per source/campaign/funnel), list_affiliates (partners with clicks, leads, sales and commission, without bank details), run_report (free-form report: source, measures, groupings, filters, period, previous period, agency view), list_dashboards, get_dashboard (all widgets with numbers), list_tickets, create_ticket (contact by contact_id or email), update_ticket (status, priority also raise, assignment, note; ticket by ID or number), reply_ticket (really sent, or internal: true = note), search_kb (published help articles), list_companies, upsert_company (by domain without duplicates, contact_ids links), list_object_types (custom objects with properties and labels), search_records (companies, contacts or custom objects with filters), upsert_record, associate (labels, primary, remove: true removes), lead_score (score, breakdown, history of a contact), create_shorts (Video studio: shorts from a video link), list_shorts_jobs (jobs; with job_id the clips with score), schedule_clip (schedule a clip in the Social Planner, default draft – docs/video-studio.md), recycle_video (content recycling: blog, newsletter, social texts, YouTube description and more from a finished job or link – stays a suggestion until you adopt it; list: true shows the status), provider_signup_link (the “create account” link of a provider as /go/<id>, with is_referral – marks partner links when recommending, docs/referrals.md), upload_files, list_files, read_file, delete_file (project files), import_claude_project (take over a claude.ai project with instructions, knowledge files and chat links as a sub-account; agency access only – moving Claude projects over), api (any other REST route with the rights of the connection; sign-in, people, keys, tokens, sessions, OAuth and backups are blocked – ghl_api), api_routes (finds the matching route by keyword), plus everyday work (coverage): update_task, list_appointments, create_appointment, update_appointment, cancel_appointment, update_contact, tag_contact, merge_contacts, draft_email (draft, sends nothing), create_invoice (draft), list_campaigns, create_campaign (draft), list_workflows, workflow_enroll, workflow_set_status, list_forms, form_submissions, survey_results, list_reviews, settings_overview. Every tool calls the REST API internally – permissions, checks and the audit log apply unchanged (path “MCP”). The tools of the other areas (among them project planning ghl_pm_*, Canvas ghl_canvas_*, wiki, team chat, invoices, quotes, sequences, webinars, Living Boards, undo, Umzug, Werkstatt) are listed with their features; the full, current list (currently over 240 tools, somewhat fewer with a sub-account key) is returned by tools/list.

Arguments and reach: every tool checks its arguments against the inputSchema – types that are only slightly off are converted (a number for text, "5" for a number, "false" for a boolean, a list or object as JSON text), everything else is a JSON-RPC error -32602 with a German message (e.g. “Parameter „ids“ muss eine Liste sein”, i.e. parameter ids must be a list). A sub-account key (or an OAuth connection for one sub-account) with a foreign location_id gets 403 on every tool; an agency login without location_id on a tool that needs a sub-account gets the message “location_id fehlt” (“location_id is missing”). With a “Read only” login all read-only tools run, including those that compute via POST internally (run_report, calc_*, workflow_test_run …). get_conversation leaves the conversation unread for the team (only people mark it as read when they open it, in the connected mailbox too).

AI agents via MCP: list_agents (a sub-account’s own agents) and run_agent (location_id, agent_id, prompt – runs with the rights of the person who created the agent, only in this sub-account; actions that need confirmation land under AI agents → Approvals) – docs/agent.md.

Files of a project (instructions, knowledge files, repository files – text and binary), see ai-tools.md. Sub-account routes, area Settings.

Upload POST /api/project-files {files: [{path, content (UTF-8)} | {path, data (Base64)}, mime?], source?} → {created, updated, unchanged, files: [{id, path, size, sha256, status}]} – upsert by path, all or nothing; 400 bad path or shape, 413 over 10 MB per file, 200 files / 25 MB per call or the sub-account’s cap
List GET /api/project-files?prefix=docs/ → {files: [{id, path, mime, size, sha256, source, created_at, updated_at}], total, bytes, limit_bytes}
Read GET /api/project-files/:id → metadata + text, content (UTF-8 text up to 1 MB, else null)
Download GET /api/project-files/:id/raw – always content-disposition: attachment, x-content-type-options: nosniff, content-security-policy: sandbox; HTML, SVG, XML and scripts as application/octet-stream; audit-logged
Delete DELETE /api/project-files/:id

MCP: ghl_upload_files, ghl_list_files, ghl_read_file, ghl_delete_file, ghl_import_claude_project (agency only – Moving Claude projects over) and ghl_api (every other route).

ChatGPT (connectors in developer mode), Claude.ai / Claude Desktop custom connectors, codex mcp login and Claude Code connect via OAuth 2.1 instead of a key. Everything a client needs is discoverable:

| Endpoint | Purpose |

Section titled “Consent & legal (consent, cookie banner, legal texts)”

What it does and its limits (no legal advice): docs/consent.md. Routes need login with x-location-id or a sub-account API key (permission area Settings); everything is limited to the sub-account.

Area Endpoints
Settings GET/PUT /api/consent/settings (policy: legacy | confirmed, doi, footer_link), GET /api/consent/overview, GET /api/consent/log?action=&channel=&limit=, GET/POST /api/consent/texts
Contact GET /api/contacts/:id/consent (status per channel and purpose, events, open requests), POST …/consent {action: granted | revoked | reset, channel, purposes, note, text, proof_url, doi}, GET …/consent/proof, POST …/consent/doi, GET …/consent/export.csv, …/export.pdf
Contact create POST /api/contacts with consent: true or {channels, purposes, doi, text, proof_url, note}; CSV import consent (legacy | confirmed | doi), consent_note, source_label
Cookie banner GET/PUT /api/cmp/settings, POST /api/cmp/reask, POST /api/cmp/preview (HTML), GET /api/cmp/log
Legal texts GET/PUT /api/legal/profile, GET /api/legal/services, POST /api/legal/generate {kind: impressum | datenschutz | widerruf | avv | vvt, …}, GET /api/legal/export?kind=&format=pdf | html | md | txt, POST /api/legal/funnels/:id/apply {kinds, overwrite}
Checkout GET/PUT /api/checkout-law/settings, GET /api/checkout-law/acceptances
Public GET/POST /consent/confirm/:token, GET/POST /consent/p/:contact/:token, GET /legal/:subAccount/impressum | datenschutz | widerruf

MCP: ghl_contact_consent (read-only proof). Workflow triggers: consent.confirmed, consent.revoked; workflow steps Send email / Send SMS accept purpose: "transactional" (service message, exempt from the send rule; the mail gets no “Abmelden” (unsubscribe) footer and its open pixel follows settings.email.track_business_opens).

What it does: docs/sites.md. All routes belong to the area Funnels & Websites, need a login with x-location-id or a sub-account key, and are limited to the sub-account; keys with read permissions may only read. Existing routes (/api/funnels, /api/funnel-pages/:id …) are listed further up or in the README; what is new on them: PATCH /api/funnel-pages/:id accepts layout ({header: false, footer: false, lang, group}), blocks may contain sections ({type: "section", rows: [{cols: [{span, blocks}]}]}), PATCH /api/funnels/:id checks the design keys in settings (tokens, header, footer, menus, languages, search, not_found, analytics), and POST /api/funnel-pages/:id/preview renders unsaved blocks, layout and settings.

Area Endpoints
Design GET /api/funnels/:id/design (tokens, defaults, fonts, contrast check, header/footer, menus, languages, 404, page list)
Components GET/POST /api/site-components (?blocks=1 includes the blocks), GET/PATCH/DELETE /api/site-components/:id (deleting turns every use into normal blocks; a component cannot contain itself)
Templates GET /api/site-templates (?kind=website|funnel), GET /api/site-templates/:id, GET /api/site-templates/:id/preview?page=<path> (HTML), POST /api/site-templates/:id/apply {name, form_id, calendar_slug, product_id, legal, samples} → website with wiring, legal, notes
Collections GET /api/funnels/:id/collection-presets, GET/POST /api/funnels/:id/collections ({preset, name, slug, singular, fields, settings, with_samples}), GET/PATCH/DELETE /api/site-collections/:id ({name, slug, singular, fields, settings, detail_blocks}), GET /api/site-collections/:id/detail-default, POST /api/site-collections/:id/preview (HTML)
Items GET/POST /api/site-collections/:id/items (?status=&q=; POST {data, slug, status}), PATCH/DELETE /api/site-items/:id, POST /api/site-items/:id/duplicate, PUT /api/site-collections/:id/order {ids}
Redirects GET/POST /api/funnels/:id/redirects (POST {source, target, code: 301|302|307|308, keep_query, note}; GET also returns misses), PATCH/DELETE /api/site-redirects/:id, POST …/redirects/test {path}, POST …/redirects/import {text}, DELETE …/redirects/misses[?path=]
Translation POST /api/funnel-pages/:id/translation {lang, slug?}
Accessibility GET /api/funnels/:id/a11y[?page=], POST /api/funnel-pages/:id/a11y (draft: {blocks, layout, seo, settings}), POST /api/funnels/:id/a11y/statement {apply: true} (draft of the statement, with apply as the page “barrierefreiheit” (accessibility))
Statistics GET /api/funnels/:id/analytics?from=&to= (steps, totals, devices, sources, days, A/B, attribution), GET /api/funnels/:id/analytics.csv, GET /api/funnel-analytics?days= (all websites of a sub-account)
Public GET /p/:site/:a[/:b[/:c[/:d]]] (collection items, search /p/:site/search?q=, redirects, 404), POST /api/public/site/beat (time on page, without cookies), GET /uploads-opt/:width/:file (scaled images: 320, 640, 960, 1280, 1920)

MCP: ghl_site_templates (list or create templates), ghl_site_a11y_check (check a website), ghl_funnel_analytics (results per step). Event site.item_published (collection item published) for workflow triggers.

Funnel & Website AI (site-ai.md, area funnels): GET /api/site-ai/status, POST /api/site-ai/estimate, POST /api/site-ai/plan {brief}, POST /api/site-ai/generate {brief, plan?}, POST /api/site-ai/page {page_id, prompt, current_blocks?}, POST /api/site-ai/section {page_id, section_id, command|prompt, current_blocks?}, GET /api/site-ai/runs, GET /api/site-ai/runs/:id, GET /api/site-ai/runs/:id/preview?page=, POST /api/site-ai/runs/:id/apply (new website as an unpublished draft, or a page/section with a restore point into the editor), GET /api/site-ai/drafts, GET /api/site-ai/drafts/:funnelId, POST /api/site-ai/drafts/:funnelId/publish, GET /api/site-ai/restore-points?page_id=, GET /api/site-ai/restore-points/:id, POST /api/site-ai/restore-points/:id/restore {save?}, POST /api/site-ai/write {kind: blog|email, topic, …}. MCP: ghl_site_generate.

Global placeholders {{custom_values.key}} per sub-account (custom-values.md, area settings, list readable by everyone): GET /api/custom-values, POST /api/custom-values {name, key?, value, folder?, description?, in_snapshot?}, PATCH /api/custom-values/:id, DELETE /api/custom-values/:id, PUT /api/custom-values/by-key/:key {value, name?, folder?}, POST /api/custom-values/preview {text}, GET /api/custom-values/form-targets, POST /api/custom-values/form-targets {form_id, field_id, value_key}, DELETE /api/custom-values/form-targets/:id. MCP: ghl_custom_values, ghl_custom_value_set.

  • GET /api/contacts/:id/privacy-export – all stored data about one contact in this sub-account as a JSON file (contact, notes, tasks, messages, appointments, forms, tickets, courses, community, attribution, partner data with the same email …; without tokens and secrets; other people’s “only for me” documents only for admins and API keys). | — | — | | GET /.well-known/oauth-protected-resource (also …/oauth-protected-resource/mcp) | RFC 9728: {resource: "https://<domain>/mcp", authorization_servers: ["https://<domain>"], scopes_supported: ["mcp", "mcp:read"]} | | GET /.well-known/oauth-authorization-server | RFC 8414 metadata: endpoints below, code_challenge_methods_supported: ["S256"], grant types authorization_code, refresh_token | | POST /oauth/register | RFC 7591 dynamic client registration {client_name, redirect_uris, token_endpoint_auth_method?: none \| client_secret_post \| client_secret_basic, grant_types?, response_types?} → 201 {client_id, client_secret?}. Redirect URIs: https, http only for localhost/127.0.0.1, or a private scheme; no fragments | | GET /oauth/authorize | response_type=code, client_id, redirect_uri (exact match), code_challenge + code_challenge_method=S256 (required), state, scope (mcp or mcp:read), resource (optional, must be this server’s /mcp). Shows the consent screen (/app/oauth-consent); members choose one of their sub-accounts (needs the settings area), admins may choose the whole agency. Back to the client with code, state, iss – or error | | POST /oauth/token | grant_type=authorization_code (code, redirect_uri, code_verifier, client auth) or grant_type=refresh_token → {access_token: "pghlo_…", token_type: "Bearer", expires_in: 3600, refresh_token: "pghlr_…", scope}. Codes: 5 minutes, single use. Refresh tokens rotate; reusing an old one revokes the connection | | POST /oauth/revoke | RFC 7009 {token, client_id} (+ secret for confidential clients). A refresh token ends the whole connection, an access token only itself. Always 200 |

Access tokens work on /mcp only, with the reach of a sub-account key (connection to one sub-account) or an agency key (agency connection), optionally read only. Consent screen API (signed-in browser only): GET /api/mcp-oauth/requests/:id, POST /api/mcp-oauth/requests/:id/approve {target: <location_id> | "agency", access: write | read} → {redirect}, POST /api/mcp-oauth/requests/:id/deny.

Area Endpoints
AI tools (sub-account, settings area, browser login for changes) GET /api/ai-tools → {mcp_url, keys (with tool, last_used_at), apps (OAuth connections), oauth}, POST /api/ai-tools/keys {tool: codex | claude-code | cursor | other, label} → {key: "pghl_…", name: "Codex – Laptop", mcp_url} (always a sub-account key, shown once), DELETE /api/ai-tools/keys/:id, DELETE /api/ai-tools/apps/:id, POST /api/ai-tools/test {key} → MCP initialize + tools/list result
AI tools (agency, admins only) GET /api/agency/ai-tools, POST /api/agency/ai-tools/keys {tool, label, scope: write | read} → {key: "pghla_…"}, DELETE /api/agency/ai-tools/keys/:id, DELETE /api/agency/ai-tools/apps/:id, POST /api/agency/ai-tools/test {key}
AI provider (docs/ai-assistant.md) GET/PUT /api/ai/provider {provider: "" | anthropic | openai, openai_model, openai_key (null = remove)} – the key is never returned (key_set, key_hint, key_source), POST /api/ai/provider/test {openai_key?, model?}; agency default: GET/PUT /api/agency/ai-provider {provider, openai_model, openai_key}, POST /api/agency/ai-provider/test
GitHub for Codex (docs/ai-tools.md) GET/PUT /api/github-codex/settings {token (null = remove), default_repo} (token never returned), POST /api/github-codex/test {repo?}; workflow action github_issue {repo?, title, body?, prompt, labels?}
  • POST /api/contacts/:id/erase {confirm: "<the contact's email or ID>"} – deletes the contact in all areas including uploaded files; invoices, contracts and sent dunning notices (statutory retention) as well as projects, retainers and payment plans (the agency’s records) stay, without a link. Response: {erased: {table: count}, retained: [...]}.
  • Agency (admin): GET /api/agency/privacy/export?email= and POST /api/agency/privacy/erase {email, confirm: email} – across all sub-accounts.
  • MCP: ghl_contact_privacy_export, ghl_contact_erase (with confirm).
  • Exports, downloads and backups appear in the audit log (export:<route>).

Sequences from the personal mailbox, deal positions, quotes, duplicate contacts, quotas and record-level access – what they do and the rules: docs/sales.md. Routes need the area Opportunities (login with x-location-id, sub-account API key or agency key). A key sees every record regardless of the record-access rules; a restricted member does not.

Area Endpoints
Sequences GET/POST /api/sequences ({name, mode: marketing|individual, legal_ack?, steps: [{type: email|task|call|linkedin, delay_days, subject?, body?, variants?}], settings?}; individual needs legal_ack: true), GET/PATCH /api/sequences/:id (status: draft|active|paused), POST …/:id/enroll {contact_ids?|tag?|smart_list_id?, deal_id?, user_id?} → {enrolled, skipped: [{contact_id, reason}], contact_ids}, GET …/:id/enrollments, POST …/:id/preview {contact_id, step_id}, GET /api/sequence-enrollments?contact_id=, POST /api/sequence-enrollments/:id/stop, GET/PUT /api/sequence-settings (daily_limit, user_limits)
Positions GET/PUT /api/opportunities/:id/lines {lines: [{product_id?, name, qty, price_cents, discount_type, discount_value, tax_rate}]} → {lines, totals, amount, locked}; with positions the deal’s value is their net sum and PATCH of it answers 409
Quotes GET /api/quotes[?deal_id=&status=], POST /api/quotes {deal_id}, GET/PATCH /api/quotes/:id (title, intro, terms, valid_until, items, options, payment: {mode: none|deposit|full, type: percent|amount, value} (amount = net cents), deal_on_accept: none|won; after sending only valid_until), POST …/:id/send {deliver?, subject?, body?, channel?}, …/void, …/duplicate, …/invoice (final invoice after a deposit); public: GET /q/:token, POST /api/public/quotes/:token/accept {name, signature (PNG data URL), agree: true, option_id?, optional_ids?}, …/decline {reason?}
Quote approval GET/PUT /api/quote-approvals/settings, GET /api/quote-approvals, GET /api/quote-approvals/quotes/:quoteId, POST …/quotes/:quoteId/request, POST /api/quote-approvals/:id/approve|reject|withdraw; a quote that needs approval answers POST /api/quotes/:id/send with 409 {approval_required: true, reasons} – sales.md
Duplicates GET /api/contact-duplicates[?min=high], GET …/for?contact_id=, GET …/preview?keep_id=&from_id=, POST …/merge {keep_id, from_id, picks: {field: keep|from}, confirm_detach?} (one undo entry), POST …/ignore|unignore {contact_ids}
Quotas GET /api/quotas?period=month|quarter&metric=won_value|won_count&start=YYYY-MM-DD, PUT /api/quotas {period, start, metric, quotas: [{scope: user|team, subject_id, target}]}, POST /api/quotas/copy, GET /api/leaderboard[?scope=team], GET/PUT /api/quota-settings (leaderboard: all|own|off|null, currency)
Notes GET /api/opportunities/:id/meeting-notes, POST /api/meeting-notes {contact_id, deal_id?, summary, title?, external_id?, source?, action_items?, create_tasks?} (the same source + external_id twice changes nothing), DELETE /api/meeting-notes/:id
Access GET/PUT /api/sales/access ({enabled, unassigned: show|hide, rules: {default, users: {<id>: set|null}, teams: {<id>: set|null}}} with set = {levels: {contacts|companies|deals|tasks|conversations: all|team|own}, hidden: {<kind>: [field, …]}}), GET /api/sales/access/me, GET/POST /api/sales/teams {name, description?, members: [user_id | {user_id, role: member|lead}]}, PATCH/DELETE /api/sales/teams/:id

Workflow triggers sequence.replied, sequence.finished, sequence.stopped, quote.sent, quote.viewed, quote.accepted, quote.declined, quote.expired, quote.paid, meeting_note.added; action sequence_enroll {sequence, user_id?}. MCP tools ghl_sequence_*, ghl_deal_lines, ghl_deal_set_lines, ghl_quote_*, ghl_contact_duplicates, ghl_quota_*, ghl_leaderboard, ghl_deal_meeting_note, ghl_sales_access.

Requests without valid credentials are rejected at the headers. Body size: 1 MB on public routes (form uploads, inbound emails, community images and /mcp with a valid key: 12 MB), 12 MB with a login or key. 429 above 900 API requests or 120 public writes per minute and address. limit parameters are clamped to 1…maximum.

GET /api/referrals (any signed-in user) → { "<id>": { name, url: "/go/<id>", is_referral, sponsor? } }. Admins: GET/PUT /api/agency/referrals ({ enabled?, links?: { "<id>": "<https-url>" | "" } }), DELETE /api/agency/referrals/clicks. Public: GET /go/:provider → 302 to the resolved address (catalogue ids only, counts clicks per provider). Details: docs/referrals.md.

The HyperLevel Agent and custom agents. Session login with x-location-id unless noted; writes need x-pghl: 1. Streams are text/event-stream with data: {json} events (start, text, tool, tool_result, confirm, navigate, usage, bridge, error, done).

Area Endpoints
Chat POST /api/agent/chat {message, conversation_id?, agent_id?, connection?: <ai-connection id> | "bridge" | "sub:chatgpt" (your own ChatGPT sign-in, browser session only) | "", files?: [file ids], ui?: {route, label, entity: {type, id, label}}, source?: "focus" (focus mode with companion: short spoken answers, history label *Focus*)} → SSE, POST /api/agent/confirm {pending_id, token, approve, args_hash?} → SSE (token single use, bound to user, conversation, tool call and argument hash; 30 minutes), POST /api/agent/conversations/:id/stop, GET /api/agent/bootstrap (agents, default agent, model, models to switch to, bridge status, open approvals), GET /api/agent/context?route=contacts/<id> (label for the context chip)
History & files GET /api/agent/conversations, GET /api/agent/conversations/:id → {items, confirm} (a waiting confirmation gets a fresh token), DELETE /api/agent/conversations/:id, POST /api/agent/files {name, mime, data: base64} (images, CSV, text, JSON, max. 8 MB) → {id, name, mime, image, preview}, GET /api/agent/files/:id (own files only), GET/DELETE /api/agent/memory[/:id]?agent_id=
Without a session POST /api/agent/run {agent_id, prompt} (sub-account or agency key, MCP) → {conversation_id, status, text, pending, tools} – runs as the agent’s owner in that sub-account, confirmations become approvals
Approvals GET /api/agent/approvals?status=all, POST /api/agent/approvals/:id {approve} (owner or admin; executes the stored tool call with the decider’s rights)
Agents GET /api/agents?usable=automation, GET /api/agents/templates, POST /api/agents {name, emoji, color, description, instructions, connection_id, model, tools: [names] | null, mode: read|ask|auto, confirm_sensitive (admins only false), knowledge: {sources: [agent, ai, kb, docs, project], text}, starters, visibility: private|team|location, web_fetch, max_steps, memory, responder: {enabled, channels, mode: draft|send, outside_hours}, schedules: [{cadence: daily|weekdays|weekly|monthly, weekday, day, time, prompt, deliver: notification|email|task}]} or {template: sales|support|social|projects|invoices|onboarding|cleanup|research}, GET/PATCH/DELETE /api/agents/:id, POST /api/agents/:id/clone, GET /api/agents/:id/export, POST /api/agents/import {format: "gohigh-agent", version: 1, agent}, GET /api/agents/:id/runs, POST /api/agents/:id/schedules/:sid/run
Tools & settings GET /api/agent/tools (catalogue with group and class read|write|sensitive|internal), GET/PUT /api/agent/settings {enabled, default_mode, web_fetch} (area Settings), GET/PUT /api/agent/agency-settings {bridge_enabled, gpt_actions_enabled, web_fetch_allowed} (admins), GET /api/agent/usage (month per agent and person, last runs; area Settings)
Custom GPT actions GET/POST /api/agent/gpt-actions/keys {name, tools, allow_write, allow_sensitive (admins)} → {key: "gha_…", openapi_url} (key shown once), DELETE /api/agent/gpt-actions/keys/:id, GET /api/agent/gpt-actions/tools; public: GET /api/agent/gpt-actions/openapi.json?k=<key id> (OpenAPI 3.1, ≤ 30 operations), POST /api/agent/gpt-actions/tools/:tool with Authorization: Bearer gha_… → {result}
Connect info GET /api/agent/connect → {mcp_url, oauth, ai_tools_page, gpt_actions, bridge}
Bridge Browser: GET /api/agent/bridge, POST /api/agent/bridge/code → {code, expires_at, expires_in_s, command, command_oneline, command_windows, qr_svg}, PATCH /api/agent/bridge/devices/:id {cli?: codex|claude, model?}, DELETE /api/agent/bridge/devices/:id. Public: GET /api/agent/bridge/script, POST /api/agent/bridge/pair {code, name, clis, version} → {token: "ghb_…"}. Bridge (Bearer ghb_…): GET /api/agent/bridge/poll?wait= (long poll, 204 = nothing), POST /api/agent/bridge/jobs/:id/events {events: [{kind: text|status, text}]} → {stop}, POST /api/agent/bridge/jobs/:id/done {text, error?}. CLI (Bearer ghbj_…, per job): POST /api/agent/bridge/mcp (MCP JSON-RPC: initialize, tools/list, tools/call)
Build with AI GET /api/agent-ai/status, POST /api/agent-ai/generate {prompt} → {run_id, config, tools, summary, notes, warnings, cost_usd}, POST /api/agent-ai/runs/:id/apply → the new agent as a draft (private, schedule off) – agent.md; MCP ghl_agent_generate

Workflow action agent_run {agent_id, prompt, target: log|note|field, field: custom.<key>}. Events: agent.approval_needed (pendingId, agentId, tool, summary), agent.run_finished (agentId, source, status, conversationId), agent.replied (contactId, conversationId, agentId, mode: draft|sent).

Registry of AI connections for every text and media provider – endpoints, presets, routing and app.services.aiConnections in docs/ai-providers.md. The personal subscription sign-in (/api/ai-subscriptions/*, MCP tool ghl_ai_providers) is listed there too.