API
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
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.
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 contactscurl "https://crm.example.com/api/contacts?q=anna&tag=lead&limit=50" -H "Authorization: Bearer $KEY"
# Create an opportunitycurl -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).
Service (Helpdesk)
Section titled “Service (Helpdesk)”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 & wishes
Section titled “Bug reports & wishes”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).
Mobile app
Section titled “Mobile app”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.
Team inbox and team calendars
Section titled “Team inbox and team calendars”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 |
Mailbox sync (Gmail & Outlook)
Section titled “Mailbox sync (Gmail & Outlook)”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 studio (media generation)
Section titled “AI studio (media generation)”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.
Voice layer
Section titled “Voice layer”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.
Werkstatt, plugins and Marktplatz
Section titled “Werkstatt, plugins and Marktplatz”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}.
Canvas
Section titled “Canvas”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.
Project planning & time tracking
Section titled “Project planning & time tracking”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).
Wiki & team chat
Section titled “Wiki & team chat”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.
Ads manager (Meta & Google Ads)
Section titled “Ads manager (Meta & Google Ads)”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.
Gewinnrechner (profit calculator)
Section titled “Gewinnrechner (profit calculator)”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.
Living Boards
Section titled “Living Boards”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.
Focus mode
Section titled “Focus mode”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.
XR layouts
Section titled “XR layouts”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 |
Community
Section titled “Community”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.
Webinars (automated webinars)
Section titled “Webinars (automated webinars)”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.
Creator & Commerce
Section titled “Creator & Commerce”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.
SEPA (direct debit and transfer files)
Section titled “SEPA (direct debit and transfer files)”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.
Undo, history and trash
Section titled “Undo, history and trash”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).
Umzug (switch-over importers)
Section titled “Umzug (switch-over importers)”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.
Email delivery (mail domains)
Section titled “Email delivery (mail domains)”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.
Companies, objects & associations
Section titled “Companies, objects & associations”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.
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).
SaaS mode
Section titled “SaaS mode”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.
Cloud mode
Section titled “Cloud mode”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.
Operations (backups, alerts, status page)
Section titled “Operations (backups, alerts, status page)”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 |
Sign-in (SSO & passkeys)
Section titled “Sign-in (SSO & passkeys)”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 |
Webhooks
Section titled “Webhooks”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.
Agency keys
Section titled “Agency keys”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).
curl -H "Authorization: Bearer pghla_…" https://crm.example.com/api/agency/projectscurl -H "Authorization: Bearer pghla_…" -H "x-location-id: <id>" https://crm.example.com/api/contactsAgency 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.
Lexware Office / sevDesk live sync
Section titled “Lexware Office / sevDesk live sync”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).
Apps & Connectors (Composio)
Section titled “Apps & Connectors (Composio)”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.
Knowledge memory (Cognee)
Section titled “Knowledge memory (Cognee)”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}.
External MCP servers (MCP client)
Section titled “External MCP servers (MCP client)”| 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.
MCP server
Section titled “MCP server”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.
# Claude Codeclaude 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_KEYTools (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.
Project files
Section titled “Project files”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).
OAuth for MCP clients
Section titled “OAuth for MCP clients”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 |
Consent & legal (consent, cookie banner, legal texts)
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).
Website builder (Funnels & Websites)
Section titled “Website builder (Funnels & Websites)”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.
Custom Values
Section titled “Custom Values”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.
Privacy (GDPR)
Section titled “Privacy (GDPR)”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 typesauthorization_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(mcpormcp: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 withcode,state,iss– orerror| |POST /oauth/token|grant_type=authorization_code(code,redirect_uri,code_verifier, client auth) orgrant_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. Always200|
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.
AI tools, AI provider, GitHub for Codex
Section titled “AI tools, AI provider, GitHub for Codex”| 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=andPOST /api/agency/privacy/erase{email, confirm: email}– across all sub-accounts. - MCP:
ghl_contact_privacy_export,ghl_contact_erase(withconfirm). - 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.
Limits
Section titled “Limits”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.
Provider sign-up links (partner links)
Section titled “Provider sign-up links (partner links)”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.
AI agent
Section titled “AI agent”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).
AI providers (BYOK)
Section titled “AI providers (BYOK)”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.