{"version":"1.0","title":"Salesbase CRM API — Integration Reference","baseUrl":"/api/v1","sections":{"auth":{"overview":{"title":"Authentication","description":"Integration endpoints use a workspace API key (Bearer sb_…). API keys are created in the Salesbase app under Settings → Workspace → Automation → API Keys. Mobile app endpoints (voice, notifications, timers, assistant, devices, workspaces switch) require JWT login and are not part of this integration reference."},"apiKey":{"title":"API Key Authentication","description":"Use your workspace API key in the Authorization header","format":"Authorization: Bearer <YOUR_API_KEY>","alternativeFormat":"Query parameter: ?api_key=<YOUR_API_KEY> (for tools that cannot set headers)","keyPrefix":"API keys start with 'sb_' prefix","note":"The full API key is only shown once when created. It is stored as a SHA-256 hash — Salesbase cannot recover lost keys."},"mcpOAuth":{"title":"MCP OAuth (AI agents)","description":"Claude and other MCP clients can connect via OAuth (Settings → Workspace → Automation → MCP). The consent screen grants scopes that gate entire API groups. Call GET /api/v1/whoami (or the MCP whoami tool) to see the workspace name, user, and active scopes for the current session.","scopes":{"read":"Read CRM data (companies, people, leads, deals, activities, notes, tasks, search, docs, …)","write":"Create/update/delete on CRM resources","inbound":"POST /api/v1/inbound","webhooks":"Webhook subscription management","sms":"SMS send/list/threads (required — SMS is not included in read)","export":"CSV/export endpoints","import":"Import endpoints"},"note":"Missing a scope returns 403 for that entire group. Re-connect the MCP app to grant additional scopes. List endpoints reject unknown query parameters with HTTP 400 so agents can tell missing filters from empty results. Prefer view=summary (MCP default) and updatedSince for incremental sync. CRM writes (POST/PUT on deals, leads, companies, people, projects, support tickets) accept preview:true (body or ?preview=true) and return current/proposed/diff without saving — call again without preview to persist."},"rateLimits":{"title":"Rate Limits","description":"All API keys share fixed rate limits to ensure fair usage and platform stability","defaults":{"minuteLimit":"120 requests per minute","dailyLimit":"50,000 requests per day"},"headers":{"X-RateLimit-Limit-Minute":"Maximum requests allowed per minute (120)","X-RateLimit-Remaining-Minute":"Remaining requests in the current minute window","X-RateLimit-Limit-Day":"Maximum requests allowed per day (50,000)","X-RateLimit-Remaining-Day":"Remaining requests in the current day window"},"exceeded":"Returns HTTP 429 Too Many Requests when limits are exceeded"},"errors":{"title":"Error Responses","codes":{"400":{"description":"Bad Request — invalid parameters or missing required fields"},"401":{"description":"Unauthorized — missing or invalid API key","body":"{ \"error\": \"Invalid API key\" }"},"403":{"description":"Forbidden — API key is revoked or workspace is suspended"},"404":{"description":"Not Found — resource does not exist or belongs to another workspace"},"429":{"description":"Too Many Requests — rate limit exceeded"},"500":{"description":"Internal Server Error — unexpected server error"}}},"responseFormat":{"title":"Standard Response Format","success":{"description":"Successful responses wrap data in a 'data' key","single":"{ \"data\": { \"_id\": \"...\", \"name\": \"...\", ... } }","list":"{ \"data\": [...], \"page\": 1, \"limit\": 20, \"total\": 150 }"},"error":{"description":"Error responses include an 'error' key with a message string","format":"{ \"error\": \"Description of what went wrong\" }"}},"quickStart":{"title":"Quick Start","steps":[{"step":1,"title":"Create an API key","description":"Go to Settings → Workspace → Automation → API Keys and create a new key. Copy the key immediately — it won't be shown again."},{"step":2,"title":"Test the connection","description":"Make a GET request to /api/v1/whoami to verify your key and see workspace name, user, and scopes","example":{"curl":"curl -H \"Authorization: Bearer sb_your_api_key_here\" https://app.salesbase.dk/api/v1/leads"}},{"step":3,"title":"Explore the API","description":"Use GET /api/v1/docs to get the full API documentation as JSON"}]},"bestPractices":["Store API keys in environment variables, never in code","Use the minimum required permissions/scopes for each key","Rotate keys periodically and revoke unused keys","Handle rate limit errors (429) with exponential backoff","Always include error handling for network failures","Use the workspace-scoped endpoints — all data is automatically filtered by your workspace"],"inboundWebhooks":{"title":"Inbound webhooks (external → Salesbase)","description":"These endpoints receive data from external systems. They are outside /api/v1 but documented here for integrators.","endpoints":[{"method":"POST","path":"/api/webhooks/automation/:automationId","description":"Trigger a webhook-type automation. JSON body is available as webhookData.* placeholders in the flow.","authentication":"Header x-webhook-secret must match the secret configured on the automation trigger node.","example":{"curl":"curl -X POST -H \"Content-Type: application/json\" -H \"x-webhook-secret: YOUR_SECRET\" -d '{\"email\":\"lead@example.com\"}' https://app.salesbase.dk/api/webhooks/automation/AUTOMATION_ID"}},{"method":"POST","path":"/api/v1/webhooks/zapier","description":"Zapier-specific inbound — creates person, lead, and deal. Uses Bearer API key (inbound scope).","authentication":"Authorization: Bearer sb_…"},{"method":"POST","path":"/api/v1/inbound","description":"Unified inbound creation for forms and integrations. Uses Bearer API key (inbound scope)."}]}},"automations":{"overview":{"title":"Automations","description":"Automations are event-driven flows that execute actions when specific triggers fire. Each automation has exactly one trigger node and one or more action/condition/delay/AI nodes connected by edges. Automations are stored as ReactFlow graphs (nodes + edges) and executed by a cron-based step processor.","concepts":{"flow":"An automation flow is a directed graph of nodes connected by edges. It always starts with a single trigger node.","trigger":"The entry point — fires when a specific event occurs (e.g. lead_created, deal_stage_changed).","action":"Performs an operation. Action settings live under node.data.config.","condition":"Branches via sourceHandle 'true'|'false'.","delay":"Pauses execution (rounded to 5-minute cron intervals).","ai":"AI model step; output as {{ai_text}} / {{ai_json.*}}.","lookup":"Queries records; sourceHandle found/empty/error. mode=all fans out one child execution per match.","edge":"Condition/AI condition: true|false. Lookup: found|empty|error. Else null."}},"endpoints":[{"method":"GET","path":"/api/v1/automations","description":"List all automations in the workspace","queryParams":{"page":{"type":"number","default":1,"description":"Page number"},"limit":{"type":"number","default":20,"max":100,"description":"Items per page"},"isActive":{"type":"string","enum":["true","false"],"description":"Filter by active status"}},"response":{"data":"Automation[]","page":"number","limit":"number","total":"number"}},{"method":"POST","path":"/api/v1/automations","description":"Create a new automation","body":{"name":{"type":"string","required":true,"description":"Automation name"},"description":{"type":"string","required":false,"description":"Description of what this automation does"},"nodes":{"type":"Node[]","required":false,"description":"Array of flow nodes (see Node Structure below)"},"edges":{"type":"Edge[]","required":false,"description":"Array of edges connecting nodes (see Edge Structure below)"}},"response":{"data":"Automation"},"statusCode":201},{"method":"GET","path":"/api/v1/automations/:id","description":"Get a single automation by ID","response":{"data":"Automation"}},{"method":"PUT","path":"/api/v1/automations/:id","description":"Update an automation","body":{"name":{"type":"string","required":false},"description":{"type":"string","required":false},"nodes":{"type":"Node[]","required":false},"edges":{"type":"Edge[]","required":false},"executionSettings":{"type":"ExecutionSettings","required":false},"rateLimits":{"type":"RateLimits","required":false}},"response":{"data":"Automation"}},{"method":"DELETE","path":"/api/v1/automations/:id","description":"Delete an automation. Fails if there are running executions.","response":{"data":{"deleted":true}}},{"method":"POST","path":"/api/v1/automations/:id/toggle","description":"Toggle automation active/inactive","response":{"data":{"_id":"string","isActive":"boolean"}}},{"method":"GET","path":"/api/v1/automations/:id/executions","description":"List executions for an automation","queryParams":{"page":{"type":"number","default":1},"limit":{"type":"number","default":20,"max":100},"status":{"type":"string","enum":["running","completed","failed","cancelled"]}},"response":{"data":"AutomationExecution[]","page":"number","limit":"number","total":"number"}},{"method":"GET","path":"/api/v1/automations/executions/:executionId","description":"Get a single execution with full step log","response":{"data":"AutomationExecution"}},{"method":"GET","path":"/api/v1/automations/stats","description":"Get aggregate automation statistics for the workspace","response":{"data":{"totalAutomations":"number","activeAutomations":"number","totalExecutions":"number","executionsByStatus":"{ running: number, completed: number, failed: number, cancelled: number }","avgDuration":"number|null (milliseconds)"}}},{"method":"POST","path":"/api/v1/automations/:id/duplicate","description":"Duplicate an automation (creates inactive copy)","response":{"data":"Automation"}},{"method":"POST","path":"/api/v1/automations/:id/test","description":"Run automation in test mode with sample context","body":{"testContext":{"type":"object","description":"Optional context overrides"}}},{"method":"POST","path":"/api/v1/automations/:id/trigger","description":"Manually trigger an active automation","body":{"context":{"type":"object","description":"Trigger context payload"}}},{"method":"POST","path":"/api/v1/automations/executions/:executionId/cancel","description":"Cancel a running automation execution","response":{"data":{"cancelled":true}}}],"nodeStructure":{"description":"Every node in the flow has this base structure:","schema":{"id":{"type":"string","required":true,"description":"Unique node ID. Convention: {type}_{timestamp}_{index}"},"type":{"type":"string","required":true,"enum":["trigger","action","condition","delay","ai","lookup"],"description":"Canvas node type"},"position":{"type":"object","required":true,"description":"Canvas position { x, y }. Vertical flows: x=500, increment y by 200 per row."},"data":{"type":"object","required":true,"description":"Node config. Triggers: triggerType + config. Actions: actionType + config."}}},"triggerTypes":{"description":"Each automation must have exactly ONE trigger node. Set data.triggerType and optional data.config on the trigger node.","uiGuide":"Triggeren er den øverste node. Klik på den → panelet til højre viser 'Trigger type' øverst. Vælg event i dropdownen; eventuelle ekstra felter (fx tidsplan, keyword, webhook) vises under.","types":{"lead_created":{"label":"Lead oprettet","description":"Starter når et nyt lead oprettes.","category":"leads","placeholders":["lead.*","person.*","company.*","owner.*"]},"lead_updated":{"label":"Lead opdateret","description":"Starter når et lead opdateres.","category":"leads","placeholders":["lead.*","person.*","company.*","owner.*"]},"company_updated":{"label":"Virksomhed opdateret","description":"Starter når en virksomhed opdateres.","category":"general","placeholders":["company.*","owner.*"]},"person_updated":{"label":"Person opdateret","description":"Starter når en person opdateres.","category":"general","placeholders":["person.*","company.*","owner.*"]},"deal_created":{"label":"Deal oprettet","description":"Starter når en deal oprettes.","category":"deals","placeholders":["deal.*","company.*","person.*","owner.*"]},"deal_updated":{"label":"Deal opdateret","description":"Starter når en deal opdateres.","category":"deals","placeholders":["deal.*","company.*","person.*","owner.*"]},"deal_won":{"label":"Deal vundet","description":"Starter når en deal markeres som vundet.","category":"deals","placeholders":["deal.*","company.*","person.*","owner.*"]},"deal_lost":{"label":"Deal tabt","description":"Starter når en deal markeres som tabt.","category":"deals","placeholders":["deal.*","company.*","person.*","owner.*"]},"deal_stage_changed":{"label":"Deal stage ændret","description":"Starter når en deal flyttes til et nyt stage.","category":"deals","config":{"stageId":{"type":"string","description":"Begræns til et bestemt stage (valgfrit — tom = alle stage-skift)","storage":"node.data.config"}},"placeholders":["deal.*","company.*","person.*","owner.*"]},"call_completed":{"label":"Opkald afsluttet","description":"Starter når et opkald afsluttes.","category":"calls","placeholders":["call.*","lead.*","deal.*","owner.*"]},"call_answered":{"label":"Opkald besvaret","description":"Starter når et opkald bliver besvaret.","category":"calls","placeholders":["call.*","lead.*","deal.*","owner.*"]},"email_received":{"label":"Email modtaget","description":"Starter når en email modtages.","category":"email","placeholders":["email.*","lead.*","person.*"]},"email_opened":{"label":"Email åbnet","description":"Starter når en sendt email åbnes.","category":"email","placeholders":["email.*","lead.*","person.*"]},"email_replied":{"label":"Email besvaret","description":"Starter når en email besvares.","category":"email","placeholders":["email.*","lead.*","person.*"]},"sms_received":{"label":"SMS modtaget","description":"Starter når en SMS modtages.","category":"sms","config":{"keyword":{"type":"string","description":"Valgfrit keyword-filter — kun beskeder der indeholder dette ord udløser","storage":"node.data.config"}},"placeholders":["sms.*","lead.*","person.*"]},"sms_replied":{"label":"SMS besvaret","description":"Starter når en SMS besvares.","category":"sms","config":{"keyword":{"type":"string","description":"Valgfrit keyword-filter","storage":"node.data.config"}},"placeholders":["sms.*","lead.*","person.*"]},"event_created":{"label":"Begivenhed oprettet","description":"Starter når en kalenderbegivenhed oprettes.","category":"calendar","placeholders":["event.*","owner.*"]},"agent_field_change":{"label":"Agent: feltændring","description":"Starter når en AI-agent ændrer et felt.","category":"agents","config":{"agentId":{"type":"string","description":"Hvilken agent","storage":"node.data.config"}},"placeholders":["agent.*","company.*","person.*","owner.*"]},"agent_new_company":{"label":"Agent: ny virksomhed","description":"Starter når en AI-agent opretter en ny virksomhed.","category":"agents","config":{"agentId":{"type":"string","description":"Hvilken agent","storage":"node.data.config"}},"placeholders":["agent.*","company.*","owner.*"]},"agent_any":{"label":"Agent: enhver hændelse","description":"Starter ved enhver AI-agent-hændelse.","category":"agents","placeholders":["agent.*","company.*","person.*","owner.*"]},"sales_analytics_done":{"label":"Salgsanalyse færdig","description":"Starter når en salgsanalyse er færdig.","category":"analytics","placeholders":["call.*","analytics.*","owner.*"]},"transcription_ready":{"label":"Transskription klar","description":"Starter når en opkaldstransskription er klar.","category":"analytics","placeholders":["call.*","transcript.*"]},"offer_created":{"label":"Tilbud oprettet","description":"Starter når et tilbud oprettes.","category":"offers","placeholders":["offer.*","deal.*","company.*"]},"offer_sent":{"label":"Tilbud sendt","description":"Starter når et tilbud sendes.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"offer_viewed":{"label":"Tilbud set","description":"Starter når et tilbud åbnes af modtageren.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"offer_accepted":{"label":"Tilbud accepteret","description":"Starter når et tilbud accepteres.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"offer_rejected":{"label":"Tilbud afvist","description":"Starter når et tilbud afvises.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"offer_expired":{"label":"Tilbud udløbet","description":"Starter når et tilbud udløber.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"offer_pdf_downloaded":{"label":"Tilbuds-PDF hentet","description":"Starter når tilbuds-PDF'en downloades.","category":"offers","placeholders":["offer.*","deal.*","company.*","owner.*"]},"contract_sent":{"label":"Kontrakt sendt","description":"Starter når en kontrakt sendes til underskrift.","category":"general","placeholders":["contract.*","offer.*","deal.*","company.*","owner.*"]},"contract_viewed":{"label":"Kontrakt åbnet","description":"Starter når en underskriver åbner kontrakten.","category":"general","placeholders":["contract.*","signer.*","offer.*","deal.*","company.*","owner.*"]},"contract_signed":{"label":"Underskrift modtaget","description":"Starter hver gang en underskriver har underskrevet.","category":"general","placeholders":["contract.*","signer.*","offer.*","deal.*","company.*","owner.*"]},"contract_completed":{"label":"Kontrakt underskrevet","description":"Starter når alle parter har underskrevet.","category":"general","placeholders":["contract.*","offer.*","deal.*","company.*","owner.*"]},"contract_declined":{"label":"Kontrakt afvist","description":"Starter når en underskriver afviser kontrakten.","category":"general","placeholders":["contract.*","signer.*","offer.*","deal.*","company.*","owner.*"]},"contract_expired":{"label":"Underskriftsfrist udløbet","description":"Starter når fristen for underskrift udløber.","category":"general","placeholders":["contract.*","offer.*","deal.*","company.*","owner.*"]},"contract_voided":{"label":"Kontrakt annulleret","description":"Starter når en kontrakt annulleres.","category":"general","placeholders":["contract.*","offer.*","deal.*","company.*","owner.*"]},"contract_period_ending":{"label":"Kontrakt udløber snart","description":"Starter når aftaleperioden nærmer sig sin slutdato.","category":"general","placeholders":["contract.*","offer.*","deal.*","company.*","owner.*"]},"booking_created":{"label":"Booking oprettet","description":"Starter når en booking oprettes.","category":"bookings","config":{"bookingConfigId":{"type":"string","description":"Begræns til en bestemt bookingside (valgfrit)","storage":"node.data.config"}},"placeholders":["booking.*","person.*"]},"booking_cancelled":{"label":"Booking aflyst","description":"Starter når en booking aflyses.","category":"bookings","config":{"bookingConfigId":{"type":"string","description":"Bookingside (valgfrit)","storage":"node.data.config"}},"placeholders":["booking.*","person.*"]},"booking_rescheduled":{"label":"Booking flyttet","description":"Starter når en booking flyttes.","category":"bookings","config":{"bookingConfigId":{"type":"string","description":"Bookingside (valgfrit)","storage":"node.data.config"}},"placeholders":["booking.*","person.*"]},"booking_approved":{"label":"Booking godkendt","description":"Starter når en booking godkendes.","category":"bookings","placeholders":["booking.*","person.*"]},"booking_rejected":{"label":"Booking afvist","description":"Starter når en booking afvises.","category":"bookings","placeholders":["booking.*","person.*"]},"calculator_submission":{"label":"Beregner indsendt","description":"Starter når en beregner indsendes.","category":"calculators","config":{"calculatorTemplateId":{"type":"string","description":"Begræns til en bestemt beregner (valgfrit)","storage":"node.data.config"}},"placeholders":["calculator.*","lead.*"]},"calculator_submission_processed":{"label":"Beregner behandlet","description":"Starter når en beregner-indsendelse er færdigbehandlet.","category":"calculators","placeholders":["calculator.*","lead.*"]},"reply_intent_detected":{"label":"Svar-intention registreret","description":"Starter når AI registrerer en intention i et svar.","category":"ai","placeholders":["email.*","lead.*","person.*","ai.*"]},"lifecycle_stage_changed":{"label":"Livscyklus-stage ændret","description":"Starter når en kontakts livscyklus-stage ændres.","category":"ai","placeholders":["person.*","company.*","lead.*","owner.*"]},"engagement_risk_detected":{"label":"Engagement-risiko","description":"Starter når der registreres risiko for lavt engagement.","category":"ai","placeholders":["person.*","company.*","lead.*","owner.*"]},"winback_opportunity":{"label":"Winback-mulighed","description":"Starter når der opstår en winback-mulighed.","category":"ai","placeholders":["person.*","company.*","lead.*","owner.*"]},"support_ticket_created":{"label":"Supportsag oprettet","description":"Starter når en supportsag oprettes.","category":"support","placeholders":["ticket.*","person.*","company.*"]},"support_ticket_updated":{"label":"Supportsag opdateret","description":"Starter når en supportsag opdateres.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"support_ticket_resolved":{"label":"Supportsag løst","description":"Starter når en supportsag løses.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"support_ticket_closed":{"label":"Supportsag lukket","description":"Starter når en supportsag lukkes.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"support_ticket_reopened":{"label":"Supportsag genåbnet","description":"Starter når en supportsag genåbnes.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"support_ticket_stage_changed":{"label":"Supportsag stage ændret","description":"Starter når en supportsag skifter stage.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"support_ticket_assigned":{"label":"Supportsag tildelt","description":"Starter når en supportsag tildeles en bruger.","category":"support","placeholders":["ticket.*","person.*","company.*","owner.*"]},"tag_added":{"label":"Tag tilføjet","description":"Starter når et tag tilføjes til en entity.","category":"general","config":{"entityType":{"type":"enum","description":"Hvilken entitetstype skal lyttes på","enum":["all","Lead","People","Company"],"default":"all","storage":"node.data.config"},"tagId":{"type":"string","description":"Specifikt tag-id, eller 'any' for alle tags","storage":"node.data.config"}},"placeholders":["lead.*","person.*","company.*","owner.*"]},"tag_removed":{"label":"Tag fjernet","description":"Starter når et tag fjernes fra en entity.","category":"general","config":{"entityType":{"type":"enum","description":"Entitetstype","enum":["all","Lead","People","Company"],"default":"all","storage":"node.data.config"},"tagId":{"type":"string","description":"Tag-id eller 'any'","storage":"node.data.config"}},"placeholders":["lead.*","person.*","company.*","owner.*"]},"user_joined_workspace":{"label":"Bruger tilføjet","description":"Starter når en bruger tilføjes til workspacet.","category":"users","placeholders":["owner.*"]},"meta_lead_form_submitted":{"label":"Meta lead-formular","description":"Starter når en Meta/Facebook lead-formular indsendes. Lead oprettes automatisk medmindre »Opret leads automatisk« er slået fra under Meta-indstillinger.","category":"integrations","placeholders":["lead.*","meta.*","person.*","company.*"]},"custom_object_created":{"label":"Brugerdefineret objekt oprettet","description":"Starter når en record oprettes i et brugerdefineret objekt.","category":"custom_objects","config":{"customObjectId":{"type":"string","description":"Begræns til specifikt objekt (valgfrit)","storage":"node.data.config"}},"placeholders":["custom_record.*","owner.*"]},"custom_object_updated":{"label":"Brugerdefineret objekt opdateret","description":"Starter når en record opdateres i et brugerdefineret objekt.","category":"custom_objects","config":{"customObjectId":{"type":"string","description":"Begræns til specifikt objekt (valgfrit)","storage":"node.data.config"}},"placeholders":["custom_record.*","owner.*"]},"record_created":{"label":"Record oprettet","description":"Starter når en valgt record-type oprettes (lead, virksomhed, person, deal, tilbud, event, support, booking eller brugerdefineret objekt).","category":"deals","config":{"entityTypes":{"type":"enum","description":"Hvilke record-typer der overvåges ved oprettelse","enum":["all","lead","company","person","deal","offer","event","support_ticket","booking","custom_object"],"default":"all","storage":"node.data.config"}},"placeholders":["lead.*","company.*","person.*","deal.*","offer.*","event.*","ticket.*"],"note":"Preferred over legacy single-entity triggers (lead_created, deal_created, etc.) for new flows."},"record_changed":{"label":"Record ændret","description":"Starter når en valgt record opdateres eller tags ændres.","category":"leads","config":{"entityTypes":{"type":"enum","description":"Hvilke entiteter der overvåges","enum":["all","lead","deal","company","person"],"default":"all","storage":"node.data.config"},"changeTypes":{"type":"enum","description":"Hvilke ændringstyper der udløser","enum":["all","updated","tag_added","tag_removed"],"default":"all","storage":"node.data.config"},"tagId":{"type":"string","description":"Specifikt tag-id ved tag-ændringer, eller 'any'","storage":"node.data.config"}},"placeholders":["lead.*","deal.*","company.*","person.*","owner.*"],"note":"Preferred over legacy single-entity triggers (lead_created, deal_created, etc.) for new flows."},"scheduled":{"label":"Tidsplan","description":"Starter på en fast tidsplan (cron-lignende).","category":"scheduling","config":{"frequency":{"type":"enum","description":"Hvor ofte den kører. interval = hvert 5., 10., 15. eller 30. minut","enum":["interval","hourly","daily","weekly","monthly"],"default":"daily","storage":"node.data.config"},"intervalMinutes":{"type":"number","description":"Minutter mellem kørsler (5, 10, 15 eller 30). Kun ved frequency=interval","storage":"node.data.config"},"dayOfWeek":{"type":"number","description":"Ugedag 0-6 (kun weekly)","storage":"node.data.config"},"dayOfMonth":{"type":"number","description":"Dag i måneden 1-28 (kun monthly)","storage":"node.data.config"},"time":{"type":"string","description":"Tidspunkt HH:mm (ved hourly bruges kun minuttet. Ved interval ignoreres feltet)","default":"09:00","storage":"node.data.config"}},"example":"Hvert 5. minut: frequency=interval, intervalMinutes=5. Daglig kl. 09:00: frequency=daily, time=09:00. Kombinér interval med HTTP-kald for at polle et API.","note":"No entity context by itself — pair with a lookup node (optionally changeDetection) to load records."},"webhook":{"label":"Webhook","description":"Starter når en ekstern webhook kalder automationens unikke URL.","category":"scheduling","config":{"secret":{"type":"string","description":"Valgfri hemmelighed til at validere kald","storage":"node.data.config"},"entityMapping":{"type":"object","description":"Mapping af payload til entity: { entityType: Lead|People|Company|Deal|none, entityIdField }","storage":"node.data.config"}},"placeholders":["webhookData.*"],"example":"Webhook-URL'en findes i trigger-panelet og kaldes udefra. Payload-felter bliver til {{webhookData.<felt>}}.","note":"Webhook URL: POST /api/webhooks/automation/{automationId}. Payload fields available as {{webhookData.fieldName}}."}}},"actionTypes":{"description":"Action nodes perform operations. Set data.actionType and data.config on the node.","types":{"send_email":{"label":"Send email","description":"Sender en email til en kontakt på entityen.","group":"communication","config":{"subject":{"type":"string","description":"Emnelinje — kritisk indhold brugeren skal angive (medmindre template vælges)","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"body":{"type":"string","description":"Email-brødtekst (HTML) — kritisk indhold (medmindre template vælges)","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"emailMode":{"type":"enum","description":"Sæt til template når templateId bruges. Hvis kun templateId er sat (uden emailMode), bruges skabelonen alligevel.","enum":["template","compose"],"storage":"node.data.config"},"templateId":{"type":"string","description":"Brug en eksisterende email-template i stedet for at skrive emne/body. Alone (uden emailMode) is enough to enter template mode.","storage":"node.data.config"},"recipientField":{"type":"enum","description":"Hvilken email-adresse: primary=Primær (første tilgængelige), secondary=Sekundær, work/privat/cvr/ai/faktura/support=type-markeret email, person/company=via forbindelse, priority=Person→Virksomhed, any=første tilgængelige, all=alle (multi-send), custom=fast adresse i feltet 'to'.","enum":["primary","secondary","work","privat","cvr","ai","faktura","support","person","company","priority","any","all","custom"],"default":"primary","storage":"node.data.config"},"recipientType":{"type":"enum","description":"KUN relevant når entityen er et Lead: 'auto'=kontakt først, ellers firma; 'contact'=personRef; 'company'=companyRef. Bestemmer om mailen går til kontakten eller firmaet.","enum":["auto","contact","company"],"default":"auto","storage":"node.data.config"},"to":{"type":"string","description":"KUN når recipientField=custom: fast email eller {{placeholder}}.","supportsPlaceholders":true,"storage":"node.data.config"},"senderAccountId":{"type":"string","description":"Afsenderkonto — auto-detekteres hvis tom; spørg ikke","storage":"node.data.config"}},"produces":["emailSent.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"Spørg ALTID om emne+indhold hvis ingen template er valgt. Slå afsender op — spørg aldrig om den. recipientType gælder kun Leads (kontakt vs. firma)."},"send_sms":{"label":"Send SMS","description":"Sender en SMS til et telefonnummer på entityen.","group":"communication","config":{"message":{"type":"string","description":"SMS-tekst — kritisk indhold brugeren skal angive","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"phoneField":{"type":"enum","description":"Hvilket telefonfelt der bruges","enum":["primary","mobile","work","all","custom"],"default":"primary","storage":"node.data.config"},"fromNumber":{"type":"string","description":"Afsendernummer — bruger workspace-default hvis tom","storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"Spørg om beskedteksten. Slå afsendernummer op via listTwilioNumbers."},"create_task":{"label":"Opret kalenderbegivenhed","description":"IMPORTANT: Despite the key name create_task, this creates a calendar Event (møde/opkald/opgave i kalenderen) — not a timeline log. Use create_activity to log on the entity timeline without a calendar slot.","group":"crm","config":{"title":{"type":"string","description":"Begivenhedens titel (NOT taskTitle — that field is sequences-only)","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"description":{"type":"string","description":"Beskrivelse","supportsPlaceholders":true,"storage":"node.data.config"},"activityType":{"type":"enum","description":"Type i kalenderen","enum":["task","call","meeting","follow_up","email","deadline","reminder"],"default":"meeting","storage":"node.data.config"},"priority":{"type":"enum","description":"Prioritet","enum":["low","medium","high"],"default":"medium","storage":"node.data.config"},"dateScheduleType":{"type":"enum","description":"Hvordan dato/tid vælges","enum":["relative","fixed","placeholder"],"default":"relative","storage":"node.data.config"},"durationMinutes":{"type":"number","description":"Varighed i minutter","default":30,"storage":"node.data.config"},"assignTo":{"type":"string","description":"Hvem begivenheden tildeles: owner | creator | placeholder | konkret bruger-ID fra teamlisten","default":"owner","storage":"node.data.config"},"assignToPlaceholder":{"type":"string","description":"Kun når assignTo=placeholder: bruger-ID","supportsPlaceholders":true,"storage":"node.data.config"},"calendarAccountId":{"type":"string","description":"Kalenderkonto — tom = auto (ansvarliges egen eller første tilgængelige). Listen viser brugernavn + kalender.","storage":"node.data.config"},"conflictPolicy":{"type":"enum","description":"Ved dobbeltbooking: flyt til ledig, undlad oprettelse, eller opret alligevel","enum":["find_next","skip","create_anyway"],"default":"find_next","storage":"node.data.config"}},"produces":["taskCreated.*","taskCreated.date","taskCreated.time","taskCreated.created"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"Spørg om titel og tidspunkt. Ved dobbeltbooking anbefales find_next. Downstream kan bruge {{taskCreated.date}} / {{taskCreated.time}}. Do NOT confuse with create_activity (timeline-only)."},"create_recall":{"label":"Opret genopkald","description":"Opretter et planlagt genopkald (recall).","group":"crm","config":{"title":{"type":"string","description":"Recall-titel","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"message":{"type":"string","description":"Noter til opkaldet","storage":"node.data.config"},"phoneField":{"type":"enum","description":"Telefonfelt","enum":["primary","mobile","work","custom"],"default":"primary","storage":"node.data.config"},"delayDays":{"type":"number","description":"Dage til genopkald","default":0,"storage":"node.data.config"},"assignTo":{"type":"string","description":"Tildeles til — default ejer","storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"create_note":{"label":"Opret note","description":"Tilføjer en note på entityen.","group":"crm","config":{"noteContent":{"type":"string","description":"Notens indhold. Field name is noteContent (NOT content) — the executor reads config.noteContent.","required":true,"supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"config: { noteContent: \"Lead score: {{lead.score}}\" }"},"create_activity":{"label":"Opret aktivitet","description":"Logs an Activity on the entity timeline only — does NOT create a calendar Event. Use create_task for calendar meetings/calls/tasks.","group":"crm","config":{"activityType":{"type":"string","description":"Aktivitetstype (timeline)","storage":"node.data.config"},"description":{"type":"string","description":"Beskrivelse af aktiviteten","required":true,"supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"update_record":{"label":"Opdater felt","description":"Opdaterer ét eller flere felter på entityen via fieldUpdates-arrayet.","group":"crm","config":{"recordType":{"type":"enum","description":"Hvilken record-type der opdateres (resolver entityId fra trigger-kontekst). custom_object kræver customRecord i kontekst.","enum":["lead","deal","company","person","custom_object"],"default":"lead","storage":"node.data.config"},"fieldUpdates":{"type":"array","description":"Array of { id?, fieldKey, value, isCustomField?, customFieldType? }. fieldKey is the field name (e.g. status, score) — NOT fieldPath. Legacy single field+value is still accepted by the executor but do not use it for new flows.","required":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"config: { recordType: \"lead\", fieldUpdates: [{ fieldKey: \"score\", value: \"{{ai_json.score}}\" }] }"},"assign_user":{"label":"Tildel adgang (legacy)","description":"Legacy — brug set_access i stedet.","group":"crm","config":{"assignmentType":{"type":"enum","description":"","enum":["owner","ownerGroup","share","shareGroup"],"default":"owner","storage":"node.data.config"},"userId":{"type":"string","description":"Bruger","storage":"node.data.config"},"groupId":{"type":"string","description":"Gruppe","storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"set_access":{"label":"Tildel adgang","description":"Sæt ejer, synlighed (gruppe/privat) og deling.","group":"crm","config":{"recordType":{"type":"enum","description":"","enum":["trigger","lead","deal","company","person","project","supportTicket","customObject"],"default":"trigger","storage":"node.data.config"},"accessState":{"type":"object","description":"Adgangskonfiguration (ejer, synlighed, delinger)","required":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"move_deal_stage":{"label":"Flyt deal stage","description":"Flytter dealen til et nyt pipeline-stage. Efter Slå op (Find alle) bruges den fundne deal automatisk.","group":"crm","config":{"flowRef":{"type":"string","description":"Pipeline (flowRef)","storage":"node.data.config"},"stageId":{"type":"string","description":"Mål-stage","storage":"node.data.config"},"dealId":{"type":"string","description":"Valgfri deal-id eller {{placeholder}} fra Slå op. Tom = deal fra trigger/opslag.","supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["deal.*"],"uiGuide":"Klik på Flyt deal stage → vælg Pipeline og Stage. Deal-feltet kan stå tomt efter en Slå op-node, eller du kan indsætte en placeholder via {}."},"create_lead":{"label":"Opret lead","description":"Opretter et nyt lead. (Deprecated — use create_record.)","group":"crm","config":{"title":{"type":"string","description":"Lead-titel/navn","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"email":{"type":"string","description":"Email","supportsPlaceholders":true,"storage":"node.data.config"},"phone":{"type":"string","description":"Telefon","supportsPlaceholders":true,"storage":"node.data.config"},"status":{"type":"string","description":"Status — default workspace-standard","storage":"node.data.config"}},"produces":["createdLead.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","deprecated":true},"create_deal":{"label":"Opret deal","description":"Opretter en ny deal i en pipeline. (Deprecated — use create_record.)","group":"crm","config":{"title":{"type":"string","description":"Deal-titel","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"value":{"type":"number","description":"Værdi","storage":"node.data.config"},"flowRef":{"type":"string","description":"Pipeline (flowRef)","storage":"node.data.config"},"stageId":{"type":"string","description":"Start-stage","storage":"node.data.config"}},"produces":["createdDeal.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","deprecated":true},"create_contact":{"label":"Opret kontakt","description":"Opretter en ny person/kontakt. (Deprecated — use create_record.)","group":"crm","config":{"firstName":{"type":"string","description":"Fornavn","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"lastName":{"type":"string","description":"Efternavn","supportsPlaceholders":true,"storage":"node.data.config"},"email":{"type":"string","description":"Email","supportsPlaceholders":true,"storage":"node.data.config"}},"produces":["createdContact.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","deprecated":true},"add_to_sequence":{"label":"Tilmeld sequence","description":"Tilmelder entityen til en sequence. There is no remove_from_sequence action — use sequence exit conditions or manual unenroll. When already enrolled: forceRestart=false skips (already_enrolled); forceRestart=true restarts. Sequence-level duplicateHandling is a separate enrollment-API setting.","group":"communication","config":{"sequenceId":{"type":"string","description":"Hvilken sequence","storage":"node.data.config"},"entityType":{"type":"enum","description":"'trigger' = use the triggering entity; otherwise override target type (ID inferred from context).","enum":["trigger","Lead","People","Company","Deal"],"default":"trigger","storage":"node.data.config"},"forceRestart":{"type":"boolean","description":"If true and an active/paused enrollment exists, restart it. If false (default), skip when already enrolled. Independent of sequence.duplicateHandling.","default":false,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"create_offer":{"label":"Opret tilbud","description":"Opretter et tilbud. (Deprecated — use create_record.)","group":"crm","config":{"templateId":{"type":"string","description":"Tilbudsskabelon","storage":"node.data.config"},"title":{"type":"string","description":"Tilbudstitel","required":true,"supportsPlaceholders":true,"storage":"node.data.config"}},"produces":["createdOffer.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","deprecated":true},"http_request":{"label":"HTTP-kald","description":"Sender et HTTP-request til et eksternt endpoint og gør svaret tilgængeligt som variabler.","group":"general","config":{"method":{"type":"enum","description":"HTTP-metode","enum":["GET","POST","PUT","PATCH","DELETE"],"default":"GET","storage":"node.data.config"},"url":{"type":"string","description":"Endpoint-URL — kritisk","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"headers":{"type":"object","description":"HTTP-headers som key/value","storage":"node.data.config"},"body":{"type":"json","description":"Request-body (JSON, kan indeholde placeholders)","supportsPlaceholders":true,"storage":"node.data.config"},"responseMapping":{"type":"object","description":"Map felter fra svaret til navngivne variabler","storage":"node.data.config"}},"produces":["httpResponse.*","httpResponse.status","httpResponse.body"],"consumes":["lead.*","deal.*","ai_json.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"Svaret tilgås som {{httpResponse.body.<felt>}}. Spørg om URL og metode; resten kan defaultes."},"send_webhook":{"label":"Send webhook","description":"Sender en webhook (POST) til en URL.","group":"general","config":{"url":{"type":"string","description":"Webhook-URL — kritisk","required":true,"supportsPlaceholders":true,"storage":"node.data.config"},"payload":{"type":"json","description":"Payload (JSON)","supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"ordrestyring_create_debtor":{"label":"Opret debitor","description":"Opretter en debitor i økonomisystemet.","group":"integrations","config":{"mapping":{"type":"object","description":"Mapping af entity-felter til debitor-felter (navn, CVR, email)","storage":"node.data.config"}},"produces":["debtor.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"ordrestyring_update_debtor":{"label":"Opdater debitor","description":"Opdaterer en eksisterende debitor.","group":"integrations","config":{"mapping":{"type":"object","description":"Felter der opdateres","storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"ordrestyring_create_case":{"label":"Opret sag","description":"Opretter en sag i økonomisystemet.","group":"integrations","config":{"mapping":{"type":"object","description":"Sags-felter","storage":"node.data.config"}},"produces":["case.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"ordrestyring_create_order_confirmation":{"label":"Opret ordrebekræftelse","description":"Opretter en ordrebekræftelse i økonomisystemet.","group":"integrations","config":{"mapping":{"type":"object","description":"Ordre-felter og linjer","storage":"node.data.config"}},"produces":["orderConfirmation.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"create_record":{"label":"Opret record","description":"Opretter lead, deal, kontakt, tilbud eller brugerdefineret objekt-record i ét samlet trin.","group":"crm","config":{"recordType":{"type":"enum","description":"Hvilken record-type der oprettes","enum":["lead","deal","contact","offer","custom_object"],"default":"lead","required":true,"storage":"node.data.config"},"fieldValues":{"type":"object","description":"Standardfelter for record-typen (fx title, email, value, stageId)","supportsPlaceholders":true,"storage":"node.data.config"},"customFields":{"type":"array","description":"Brugerdefinerede felter [{ name, value, type }]","storage":"node.data.config"},"customObjectId":{"type":"string","description":"Påkrævet når recordType=custom_object — ID på objektdefinition","storage":"node.data.config"}},"produces":["createdRecord.*","createdLead.*","createdDeal.*","createdContact.*","createdOffer.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder.","example":"Brug create_record i stedet for legacy create_lead/create_deal/create_contact/create_offer."},"add_to_list":{"label":"Tilføj til liste","description":"Tilføjer entityen til en statisk liste.","group":"lists","config":{"listId":{"type":"string","description":"Mål-liste (statisk liste-ID)","required":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"remove_from_list":{"label":"Fjern fra liste","description":"Fjerner entityen fra en statisk liste.","group":"lists","config":{"listId":{"type":"string","description":"Liste entityen fjernes fra","required":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"billy_create_contact":{"label":"Billy: Opret kontakt","description":"Opretter en kontakt i Billy.","group":"integrations","config":{"mapping":{"type":"object","description":"Mapping af entity-felter til Billy-kontakt","storage":"node.data.config"}},"produces":["billyContact.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"apacta_create_contact":{"label":"Apacta: Opret kontakt","description":"Opretter en kontakt i Apacta.","group":"integrations","config":{"mapping":{"type":"object","description":"Mapping af entity-felter til Apacta-kontakt","storage":"node.data.config"}},"produces":["apactaContact.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"economic_create_customer":{"label":"e-conomic: Opret kunde","description":"Opretter en kunde i e-conomic.","group":"integrations","config":{"mapping":{"type":"object","description":"Mapping til e-conomic kunde-felter (navn, CVR, email)","storage":"node.data.config"}},"produces":["economicCustomer.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"economic_create_invoice":{"label":"e-conomic: Opret faktura","description":"Opretter en faktura i e-conomic.","group":"integrations","config":{"mapping":{"type":"object","description":"Fakturalinjer og kunde-reference","storage":"node.data.config"}},"produces":["economicInvoice.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"economic_book_invoice":{"label":"e-conomic: Bogfør faktura","description":"Bogfører en faktura i e-conomic.","group":"integrations","config":{"invoiceId":{"type":"string","description":"Faktura-ID eller {{placeholder}}","supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"uniconta_create_customer":{"label":"Uniconta: Opret kunde","description":"Opretter en kunde i Uniconta.","group":"integrations","config":{"mapping":{"type":"object","description":"Mapping til Uniconta kunde-felter","storage":"node.data.config"}},"produces":["unicontaCustomer.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"uniconta_create_invoice":{"label":"Uniconta: Opret faktura","description":"Opretter en faktura i Uniconta.","group":"integrations","config":{"mapping":{"type":"object","description":"Fakturalinjer og kunde-reference","storage":"node.data.config"}},"produces":["unicontaInvoice.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"uniconta_book_invoice":{"label":"Uniconta: Bogfør faktura","description":"Bogfører en faktura i Uniconta.","group":"integrations","config":{"invoiceId":{"type":"string","description":"Faktura-ID eller {{placeholder}}","supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"dinero_create_contact":{"label":"Dinero: Opret kontakt","description":"Opretter en kontakt i Dinero.","group":"integrations","config":{"contactName":{"type":"string","description":"Kontaktnavn i feltet Navn (config.contactName). Alternativt config.mapping.name.","supportsPlaceholders":true,"storage":"node.data.config"},"contactCountryKey":{"type":"string","description":"Valgfri landekode til Dinero CountryKey (ISO-2). Tomt bliver DK.","supportsPlaceholders":true,"storage":"node.data.config"},"mapping":{"type":"object","description":"Valgfri mapping (name/email/cvr/countryKey) hvis contact*-felter mangler","storage":"node.data.config"}},"produces":["dineroContact.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"dinero_create_invoice":{"label":"Dinero: Opret faktura","description":"Opretter en kladdefaktura i Dinero.","group":"integrations","config":{"useOfferLines":{"type":"boolean","description":"Sæt true for at kopiere linjer fra det udløsende tilbud (kræver en tilbuds-trigger). Uden linje-config falder runtime også tilbage til tilbuddets linjer.","storage":"node.data.config"},"lines":{"type":"array","description":"Statiske fakturalinjer: [{ description, quantity, unitPrice, accountNumber?, unit? }]. Placeholders tilladt i description. Alternativt config.mapping.lines/lineItems/invoiceLines.","storage":"node.data.config"},"contactGuid":{"type":"string","description":"Dinero kontakt-GUID. Tomt bliver {{dinero_contact_guid}} fra et tidligere Dinero: Opret kontakt-trin. Alternativt config.mapping.contactGuid.","supportsPlaceholders":true,"storage":"node.data.config"},"mapping":{"type":"object","description":"Valgfri mapping-aliaser: mapping.lines/lineItems/invoiceLines (array eller '{{offer.lineItems}}'), mapping.contactGuid, mapping.useOfferLines","storage":"node.data.config"}},"produces":["dineroInvoice.*"],"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."},"dinero_book_invoice":{"label":"Dinero: Bogfør faktura","description":"Bogfører en faktura i Dinero.","group":"integrations","config":{"invoiceId":{"type":"string","description":"Faktura-ID eller {{placeholder}}","supportsPlaceholders":true,"storage":"node.data.config"}},"consumes":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Klik på action-noden → panelet til højre viser action-typen øverst og dens felter nedenunder."}}},"conditionNode":{"description":"Condition nodes evaluate rules and branch the flow. Edges use sourceHandle 'true' (met) or 'false' (not met).","dataSchema":{"label":{"type":"string","description":"Display label"},"conditions":{"type":"array","description":"Array of { id, field, operator, value, dataType? }"},"logic":{"type":"string","enum":["and","or"],"description":"Combine multiple conditions"}},"operators":[{"key":"equals","label":"equals"},{"key":"not_equals","label":"not equals"},{"key":"contains","label":"contains"},{"key":"not_contains","label":"not contains"},{"key":"starts_with","label":"starts with"},{"key":"ends_with","label":"ends with"},{"key":"matches_regex","label":"matches regex"},{"key":"greater_than","label":"greater than"},{"key":"less_than","label":"less than"},{"key":"greater_than_or_equal","label":"greater than or equal"},{"key":"less_than_or_equal","label":"less than or equal"},{"key":"between","label":"between"},{"key":"is_empty","label":"is empty"},{"key":"is_not_empty","label":"is not empty"},{"key":"exists","label":"exists"},{"key":"not_exists","label":"not exists"},{"key":"is_true","label":"is true"},{"key":"is_false","label":"is false"},{"key":"array_contains","label":"array contains"},{"key":"array_length_equals","label":"array length equals"},{"key":"array_length_greater","label":"array length greater"},{"key":"date_before","label":"date before"},{"key":"date_after","label":"date after"},{"key":"date_equals","label":"date equals"},{"key":"date_within_days","label":"date within days"},{"key":"date_in_exactly_days","label":"date in exactly days"}],"fields":{"conditions":{"type":"array","description":"Liste af { id, field, operator, value }. field er en sti (fx lead.score, deal.value, ai_json.intent).","required":true,"storage":"node.data"},"logic":{"type":"enum","description":"Hvordan flere betingelser kombineres","enum":["and","or"],"default":"and","storage":"node.data"}},"uiGuide":"Klik på betingelses-noden → panelet 'Betingelser' åbner. Hver betingelse har felt-vælger, operator-dropdown og værdi. Husk at trække BÅDE den grønne (true) og røde (false) udgang videre."},"delayNode":{"description":"Delay nodes pause execution for a specified duration before continuing.","dataSchema":{"label":{"type":"string","description":"Display label"},"delayAmount":{"type":"number","required":true,"description":"Amount of time to wait"},"delayUnit":{"type":"string","required":true,"enum":["minutes","hours","days","weeks"],"description":"Time unit (stored in data.config)"}},"note":"Delays are rounded to 5-minute cron intervals.","fields":{"delayAmount":{"type":"number","description":"Antal tidsenheder","default":1,"storage":"node.data.config"},"delayUnit":{"type":"enum","description":"Tidsenhed","enum":["minutes","hours","days","weeks"],"default":"days","storage":"node.data.config"}}},"aiNode":{"description":"AI nodes send execution context to an AI model. Settings live on node.data (not under config). Output available as {{ai_text}} and {{ai_json.*}}.","dataSchema":{"label":{"type":"string","description":"Display label"},"prompt":{"type":"string","required":true,"description":"AI prompt with {{placeholders}}"},"outputMode":{"type":"string","enum":["text","json_schema"],"default":"text"},"reasoningEffort":{"type":"string","enum":["none","low","medium","high"],"default":"low"},"schema":{"type":"object","description":"JSON Schema when outputMode is json_schema"}},"outputContext":{"ai_text":"string","ai_json.*":"structured fields from schema or freeform JSON"},"fields":{"prompt":{"type":"string","description":"Instruktion til AI'en — kritisk. Kan referere {{lead.*}} osv.","required":true,"supportsPlaceholders":true,"storage":"node.data"},"outputMode":{"type":"enum","description":"'text' = fritekst, 'json_schema' = struktureret output der kan forgrenes på","enum":["text","json_schema"],"default":"text","storage":"node.data"},"reasoningEffort":{"type":"enum","description":"Hvor meget AI'en 'tænker' — højere = langsommere/dyrere","enum":["none","low","medium","high"],"default":"low","storage":"node.data"},"schema":{"type":"json","description":"JSON schema for struktureret output (kun ved json_schema). Hvert top-level felt bliver til {{ai_json.<felt>}}. Brug schema-builderen eller en SCHEMA_TEMPLATE.","storage":"node.data"}},"uiGuide":"Klik på AI-noden → skriv prompten i det store felt. For struktureret output: skift 'Output-format' til JSON og byg felterne i schema-sektionen. Output bruges senere som {{ai_json.<felt>}}."},"lookupNode":{"description":"Lookup nodes query records and branch via sourceHandle found/empty/error. Settings live on node.data (NOT under config). mode=first continues once with the record; mode=count only counts; mode=all FANS OUT — one child AutomationExecution per match (downstream runs per record). Fan-out caps are plan-based (no user-editable limit field).","dataSchema":{"label":{"type":"string","description":"Display label; also slugified into an optional lookup.<alias> placeholder namespace"},"entityType":{"type":"string","required":true,"enum":["Lead","Deal","Company","People","Offer","Activity","Task","Booking","Event","SupportTicket","CustomObjectRecord"],"description":"Entity type to query"},"customObjectId":{"type":"string","description":"Required when entityType is CustomObjectRecord"},"mode":{"type":"string","enum":["first","count","all"],"default":"first","description":"first=findOne; count=countDocuments; all=fan-out child executions per entity (plan-capped)"},"filters":{"type":"array","description":"Filter rules [{ field, operator, value }] relative to trigger context"},"sortField":{"type":"string","default":"createdAt","description":"Sort field for first/all"},"sortOrder":{"type":"number","enum":[-1,1],"default":-1,"description":"Mongo sort order (-1 desc, 1 asc). Named sortOrder, not sortDirection."},"changeDetection":{"type":"boolean","default":false,"description":"Snapshot watched fields; unchanged records take the empty handle"},"watchedFields":{"type":"array","description":"Field paths watched when changeDetection is true"}},"entityTypes":["Lead","Deal","Company","People","Offer","Activity","Task","Booking","Event","SupportTicket","CustomObjectRecord"],"branching":{"handles":["found","empty","error"],"describe":"Connect edges with sourceHandle 'found', 'empty', and optionally 'error'. With changeDetection enabled, unchanged records emit 'empty' so the found branch only runs on real changes."},"iteration":"mode=all = fan-out: N child AutomationExecutions (one per record); parent completes. mode=first/count continue once in the same execution.","fanOutLimits":"Effective cap = min(planLimit, 1000). Plan table: Trial/Free 100 · Basic/Starter 500 · Pro+ 1000 (hard safety). No per-node limit field.","outputPlaceholders":["lookup.<nodeId>.<field>","lookup.<nodeId>.count","lookup.<nodeId>.found","lookup.<alias>.<field>","lookup.<nodeId>.changed","lookup.<nodeId>.previous.*"],"fields":{"entityType":{"type":"enum","description":"Hvilken type records der slås op","enum":["Lead","Deal","Company","People","Offer","Activity","Task","Booking","Event","SupportTicket","CustomObjectRecord"],"storage":"node.data"},"customObjectId":{"type":"string","description":"Påkrævet når entityType er CustomObjectRecord — ID på brugerdefineret objekt","storage":"node.data"},"mode":{"type":"enum","description":"first = findOne + continue once with that record in context. count = count only (found if count>0). all = FAN-OUT: creates one child AutomationExecution per matching record (parent completes); downstream nodes run PER record, not once with an array. Cap is plan-based (Free/Trial 100, Basic/Starter 500, Pro 5000, Enterprise 50000), hard-capped at 1000 except higher plans.","enum":["first","count","all"],"default":"first","storage":"node.data"},"filters":{"type":"array","description":"Filtre der afgrænser opslaget ({ field, operator, value }) relative to trigger context","storage":"node.data"},"sortField":{"type":"string","description":"Sort field for first/all modes (entity-specific; default createdAt)","default":"createdAt","storage":"node.data"},"sortOrder":{"type":"number","description":"Mongo sort order: -1 = descending (newest first), 1 = ascending. Field name is sortOrder (NOT sortDirection).","default":-1,"storage":"node.data"},"changeDetection":{"type":"boolean","description":"When true (typically with scheduled + mode=first), compares watchedFields against a stored snapshot. Unchanged → outputHandle empty; changed → found + previous values in context.","default":false,"storage":"node.data"},"watchedFields":{"type":"array","description":"Field paths to watch when changeDetection is on. Empty = first 10 non-internal keys on the document.","storage":"node.data"}},"uiGuide":"Klik på opslag-noden → vælg entityType, mode og filtre. Forbind found/empty (og evt. error) handles. mode=all faner ud til N child-executions — efterfølgende noder kører pr. record. Placeholders: {{lookup.<nodeId>.field}} eller via label-alias."},"flowBuildingGuide":{"layout":"Automations flow top-down (vertical). Sequences flow left-to-right (horizontal).","nodeIds":"Use stable IDs: trigger_1, action_1, condition_1, delay_1, lookup_1, ai_1","branching":"Condition / AI condition: sourceHandle 'true'|'false'. Lookup: sourceHandle 'found'|'empty'|'error'.","storageConvention":"Automation action settings live under node.data.config. Logic nodes (condition, delay, ai, lookup) store settings on node.data (delay also accepts config.*). Sequences store ALL step settings on node.data — never under config.","placeholders":"Use {{entity.field}} syntax. AI output: {{ai_json.field}}, {{ai_text}}. Lookup: {{lookup.<nodeId>.field}} or {{lookup.<alias>.field}}.","compositeTriggers":"Prefer record_created and record_changed over legacy lead_created/deal_created/tag_added triggers.","createRecord":"Prefer create_record action over legacy create_lead/create_deal/create_contact/create_offer.","namingGotcha":"create_task = calendar Event. create_activity = timeline Activity only. create_note uses config.noteContent (not content). update_record uses config.fieldUpdates[{fieldKey,value}] + recordType (not fieldPath+value).","knownLimitations":["No remove_from_sequence (exit via sequence exit conditions).","No merge node; failureBehavior is global (no per-node catch edge).","No delete/archive entity action.","Lookup mode=all is fan-out to child executions — not an in-graph array iterator.","No business-hours gate on automation sends (use sequences for outreach timing)."]},"edgeStructure":{"description":"Edges connect nodes in the flow. Branching nodes require sourceHandle.","schema":{"id":{"type":"string","required":true,"description":"Unique edge ID, e.g. 'edge_trigger_1-action_1'"},"source":{"type":"string","required":true,"description":"Source node ID"},"target":{"type":"string","required":true,"description":"Target node ID"},"sourceHandle":{"type":"string|null","required":false,"description":"Branching handle. Condition / AI condition: 'true' | 'false'. Lookup: 'found' | 'empty' | 'error'. null for non-branching nodes.","enum":["true","false","found","empty","error",null]},"targetHandle":{"type":"string|null","required":false,"description":"Target handle (usually null)"}},"examples":[{"id":"e_cond_true","source":"condition_1","target":"action_1","sourceHandle":"true"},{"id":"e_lookup_found","source":"lookup_1","target":"action_1","sourceHandle":"found"},{"id":"e_lookup_empty","source":"lookup_1","target":"action_2","sourceHandle":"empty"}]},"executionSettings":{"description":"Configure how the automation handles failures","schema":{"failureBehavior":{"type":"string","enum":["stop","continue","retry"],"default":"stop","description":"'stop' halts the flow, 'continue' skips the failed node, 'retry' retries"},"retryMaxAttempts":{"type":"number","default":3,"min":1,"max":10},"retryIntervalMinutes":{"type":"number","default":5,"min":1,"max":1440}}},"rateLimits":{"description":"Limit how often an automation can run","schema":{"maxRunsPerHour":{"type":"number","default":0,"description":"0 = unlimited"},"maxRunsPerDay":{"type":"number","default":0,"description":"0 = unlimited"},"maxRunsPerWeek":{"type":"number","default":0,"description":"0 = unlimited"},"maxTotalRuns":{"type":"number","default":0,"description":"0 = unlimited. Auto-deactivates after this many total runs."}}},"placeholders":{"description":"Placeholders use double curly braces: {{field.path}}. They are resolved from the execution context at runtime.","categories":{"lead":["lead._id","lead.title","lead.email","lead.phone","lead.status","lead.source","lead.score","lead.value","lead.notes","lead.customFields.*"],"company":["company._id","company.name","company.cvr","company.email","company.phone","company.address","company.city","company.zip","company.industry","company.employees","company.website"],"contact":["contact._id","contact.firstName","contact.lastName","contact.fullName","contact.email","contact.phone","contact.mobile","contact.title"],"deal":["deal._id","deal.title","deal.value","deal.stage","deal.probability","deal.priority","deal.customFields.*"],"owner":["owner._id","owner.firstName","owner.lastName","owner.fullName","owner.email","owner.phone"],"call":["call._id","call.duration","call.outcome","call.direction","call.notes"],"email":["email.subject","email.from","email.fromName","email.body"],"sms":["sms.body","sms.from"],"webhook":["webhookData.* (any field from the incoming webhook payload)"],"lookup":["lookup.<nodeId>.items","lookup.<nodeId>.count","lookup.<nodeId>.<field>"],"system":["triggeredAt","entityId","entityType","workspaceId"],"previousSteps":["httpResponse.status","httpResponse.data.*","ai_text","ai_json.*","aiOutput","aiOutput.*"]}},"examples":[{"name":"New lead welcome email + follow-up task","description":"When a lead is created, send a welcome email and create a follow-up task in 3 days","flow":{"name":"Welcome & Follow-up","description":"Sends welcome email on lead creation and creates a 3-day follow-up task","nodes":[{"id":"trigger_1","type":"trigger","position":{"x":500,"y":50},"data":{"triggerType":"lead_created","label":"Lead created"}},{"id":"action_1","type":"action","position":{"x":500,"y":250},"data":{"actionType":"send_email","label":"Send welcome email","config":{"subject":"Welcome {{contact.firstName}}!","body":"<p>Hi {{contact.firstName}},</p><p>Thank you for your interest. We will be in touch soon.</p><p>Best regards,<br>{{owner.firstName}}</p>"}}},{"id":"action_2","type":"action","position":{"x":500,"y":450},"data":{"actionType":"create_task","label":"Follow-up task","config":{"taskTitle":"Follow up with {{contact.firstName}} {{contact.lastName}}","taskDescription":"New lead from {{lead.source}}. Score: {{lead.score}}","dueInDays":3,"assignTo":"owner"}}}],"edges":[{"id":"e1","source":"trigger_1","target":"action_1"},{"id":"e2","source":"action_1","target":"action_2"}]}},{"name":"Deal stage change with condition","description":"When a deal moves to 'Proposal' stage, check if value > 50000 and send different emails","flow":{"name":"High-value deal notification","description":"Notifies the owner differently based on deal value when deal reaches proposal stage","nodes":[{"id":"trigger_1","type":"trigger","position":{"x":500,"y":50},"data":{"triggerType":"deal_stage_changed","label":"Deal stage changed"}},{"id":"condition_1","type":"condition","position":{"x":500,"y":250},"data":{"label":"Deal value > 50000?","conditions":[{"id":"c1","field":"deal.value","operator":"greater_than","value":"50000"}],"logic":"and"}},{"id":"action_1","type":"action","position":{"x":300,"y":450},"data":{"actionType":"send_email","label":"High-value alert","config":{"to":"{{owner.email}}","subject":"High-value deal: {{deal.title}}","body":"<p>Deal {{deal.title}} worth {{deal.value}} has moved to proposal stage!</p>"}}},{"id":"action_2","type":"action","position":{"x":700,"y":450},"data":{"actionType":"create_task","label":"Standard follow-up","config":{"taskTitle":"Follow up on {{deal.title}}","dueInDays":2,"assignTo":"owner"}}}],"edges":[{"id":"e1","source":"trigger_1","target":"condition_1"},{"id":"e2","source":"condition_1","target":"action_1","sourceHandle":"true"},{"id":"e3","source":"condition_1","target":"action_2","sourceHandle":"false"}]}},{"name":"Webhook-triggered lead creation","description":"External system sends webhook data, automation creates a lead and notifies the team","flow":{"name":"Webhook lead import","description":"Creates leads from incoming webhook data and sends a Slack/webhook notification","nodes":[{"id":"trigger_1","type":"trigger","position":{"x":500,"y":50},"data":{"triggerType":"webhook","label":"Webhook received"}},{"id":"action_1","type":"action","position":{"x":500,"y":250},"data":{"actionType":"create_lead","label":"Create lead","config":{"title":"{{webhookData.company_name}} — {{webhookData.contact_name}}","email":"{{webhookData.email}}","phone":"{{webhookData.phone}}","source":"webhook"}}},{"id":"action_2","type":"action","position":{"x":500,"y":450},"data":{"actionType":"send_webhook","label":"Notify team","config":{"webhookUrl":"https://hooks.slack.com/services/YOUR/WEBHOOK/URL","webhookBody":"{\"text\": \"New lead from webhook: {{webhookData.company_name}}\"}"}}}],"edges":[{"id":"e1","source":"trigger_1","target":"action_1"},{"id":"e2","source":"action_1","target":"action_2"}]}},{"name":"AI-powered email response handler","description":"When an email reply is received, use AI to analyze sentiment and route accordingly","flow":{"name":"AI Email Router","description":"Analyzes email replies with AI and creates appropriate tasks based on sentiment","nodes":[{"id":"trigger_1","type":"trigger","position":{"x":500,"y":50},"data":{"triggerType":"email_replied","label":"Email replied"}},{"id":"ai_1","type":"ai","position":{"x":500,"y":250},"data":{"label":"Analyze reply","prompt":"Analyze this email reply and determine the sentiment and intent.\n\nFrom: {{email.fromName}}\nSubject: {{email.subject}}\nBody: {{email.body}}\n\nReturn JSON with sentiment (positive/negative/neutral) and intent (interested/not_interested/question/meeting_request).","outputMode":"json_schema","schema":{"type":"object","properties":{"sentiment":{"type":"string","enum":["positive","negative","neutral"]},"intent":{"type":"string","enum":["interested","not_interested","question","meeting_request"]},"summary":{"type":"string"}},"required":["sentiment","intent","summary"]}}},{"id":"condition_1","type":"condition","position":{"x":500,"y":450},"data":{"label":"Positive sentiment?","conditions":[{"id":"c1","field":"aiOutput.sentiment","operator":"equals","value":"positive"}],"logic":"and"}},{"id":"action_1","type":"action","position":{"x":300,"y":650},"data":{"actionType":"create_task","label":"Priority follow-up","config":{"taskTitle":"Priority: {{contact.firstName}} replied positively — {{aiOutput.summary}}","dueInDays":0,"assignTo":"owner"}}},{"id":"action_2","type":"action","position":{"x":700,"y":650},"data":{"actionType":"create_task","label":"Review reply","config":{"taskTitle":"Review reply from {{contact.firstName}}: {{aiOutput.intent}}","dueInDays":1,"assignTo":"owner"}}}],"edges":[{"id":"e1","source":"trigger_1","target":"ai_1"},{"id":"e2","source":"ai_1","target":"condition_1"},{"id":"e3","source":"condition_1","target":"action_1","sourceHandle":"true"},{"id":"e4","source":"condition_1","target":"action_2","sourceHandle":"false"}]}}],"webhookTriggerUrl":{"description":"To trigger a webhook-based automation externally, send a POST request to:","url":"POST /api/webhooks/automation/{automationId}","body":"Any JSON payload — all fields become available as webhookData.* placeholders","note":"The automation must be active and have a webhook trigger type configured."}},"sequences":{"overview":{"title":"Sequences","description":"Sequences are entity-centric outreach flows for personalized sales communication. Unlike automations (which are event-triggered), sequences are started by enrolling an entity (Lead, People, Deal, or Company). Each sequence has a Start node, communication nodes (email, SMS), timing nodes (wait), action nodes (tasks, recalls, field updates), logic nodes (conditions, AI), and Stop nodes.","sequencesVsAutomations":{"sequences":{"start":"Enrollment (manual or automatic)","purpose":"Outreach campaigns & nurturing","focus":"Email/SMS communication sequences","exit":"Exit conditions (reply, meeting booked, etc.)","timing":"Smart scheduling with business hours"},"automations":{"start":"Trigger (event-based)","purpose":"React to CRM events","focus":"Broad automation of any action","exit":"Runs to completion","timing":"Event-driven, immediate execution"}},"entityTypes":{"Lead":"Leads in the system — cold outreach, nurturing","People":"Contact persons — personal outreach, follow-up","Company":"Companies — account-based outreach","Deal":"Deals in pipeline — deal acceleration, engagement"}},"endpoints":[{"method":"GET","path":"/api/v1/sequences","description":"List all sequences in the workspace","queryParams":{"page":{"type":"number","default":1},"limit":{"type":"number","default":20,"max":100},"isActive":{"type":"string","enum":["true","false"],"description":"Filter by active status"}},"response":{"data":"Sequence[]","page":"number","limit":"number","total":"number"}},{"method":"POST","path":"/api/v1/sequences","description":"Create a new sequence","body":{"name":{"type":"string","required":true,"description":"Sequence name"},"description":{"type":"string","required":false},"nodes":{"type":"Node[]","required":false,"description":"Array of step nodes (see Step Types below). A default Start node is created if nodes are omitted."},"edges":{"type":"Edge[]","required":false,"description":"Array of edges connecting nodes"}},"response":{"data":"Sequence"},"statusCode":201},{"method":"GET","path":"/api/v1/sequences/:id","description":"Get a single sequence by ID","response":{"data":"Sequence"}},{"method":"PUT","path":"/api/v1/sequences/:id","description":"Update a sequence","body":{"name":{"type":"string"},"description":{"type":"string"},"nodes":{"type":"Node[]"},"edges":{"type":"Edge[]"},"allowedEntityTypes":{"type":"string[]","description":"Array of entity types that can be enrolled: 'Lead', 'People', 'Deal', 'Company'"},"exitConditions":{"type":"ExitConditions","description":"See Exit Conditions section"},"sendSettings":{"type":"SendSettings","description":"See Send Settings section"},"trackingSettings":{"type":"TrackingSettings"},"aiSettings":{"type":"AiSettings"},"executionSettings":{"type":"ExecutionSettings"},"triggers":{"type":"Trigger[]","description":"Auto-enrollment triggers"},"defaultSenderType":{"type":"string","enum":["fixed","owner"]},"defaultSenderId":{"type":"string","description":"Email account ID for fixed sender"},"defaultSmsNumber":{"type":"string","description":"Phone number for SMS"}},"response":{"data":"Sequence"}},{"method":"DELETE","path":"/api/v1/sequences/:id","description":"Delete a sequence. Fails if there are active enrollments.","response":{"data":{"deleted":true}}},{"method":"POST","path":"/api/v1/sequences/:id/toggle","description":"Toggle sequence active/inactive. Activation requires a start node and at least one action node.","response":{"data":{"_id":"string","isActive":"boolean"}}},{"method":"POST","path":"/api/v1/sequences/:id/duplicate","description":"Duplicate a sequence (creates inactive copy)","response":{"data":"Sequence"}},{"method":"GET","path":"/api/v1/sequences/:id/enroll-eligibility","description":"Check if an entity can be enrolled before calling enroll","queryParams":{"entityType":{"type":"string","required":true,"enum":["Lead","People","Deal","Company"]},"entityId":{"type":"string","required":true}},"response":{"data":"{ eligible: boolean, reason?: string }"}},{"method":"POST","path":"/api/v1/sequences/:id/enroll","description":"Enroll one or more entities into the sequence","body":{"entityType":{"type":"string","required":true,"enum":["Lead","People","Deal","Company"]},"entityId":{"type":"string","description":"Single entity ID (for single enrollment)"},"entityIds":{"type":"string[]","description":"Array of entity IDs (for bulk enrollment)"},"source":{"type":"string","default":"api","description":"Enrollment source identifier"},"forceRestart":{"type":"boolean","default":false,"description":"Re-enroll even if entity was previously enrolled"}},"response":{"singleEnrollment":{"data":"{ ok: boolean, action: string, enrollmentId: string }"},"bulkEnrollment":{"data":"{ enrolled: number, failed: number, results: [{ entityId, success, error? }] }"}},"statusCode":201,"note":"Use entityId for single enrollment, entityIds (array) for bulk enrollment. The sequence must be active."},{"method":"GET","path":"/api/v1/sequences/:id/enrollments","description":"List enrollments for a sequence","queryParams":{"page":{"type":"number","default":1},"limit":{"type":"number","default":20,"max":100},"status":{"type":"string","enum":["active","paused","completed","exited","switched"]}},"response":{"data":"SequenceEnrollment[]","page":"number","limit":"number","total":"number"}},{"method":"GET","path":"/api/v1/sequences/:id/stats","description":"Get aggregate statistics for a sequence","response":{"data":{"stats":"Sequence.stats object (totalEnrolled, active, completed, exited, etc.)","enrollmentCounts":"[{ _id: status, count: number }]"}}},{"method":"POST","path":"/api/v1/sequences/enrollments/:enrollmentId/pause","description":"Pause an active enrollment","body":{"durationDays":{"type":"number","description":"Optional: auto-resume after X days"}},"response":{"data":{"paused":true}}},{"method":"POST","path":"/api/v1/sequences/enrollments/:enrollmentId/resume","description":"Resume a paused enrollment","response":{"data":{"resumed":true}}},{"method":"POST","path":"/api/v1/sequences/enrollments/:enrollmentId/unenroll","description":"Remove an entity from the sequence (only if active or paused)","response":{"data":{"unenrolled":true}}}],"stepTypes":{"description":"Each sequence node has a type on node.type. Settings live on node.data (not nested under config). Flows are horizontal (left to right).","nodeWidths":{"start":200,"stop":180,"wait":220,"send_email":280,"send_sms":280,"create_task":280,"create_recall":280,"update_field":280,"condition":280,"ai_analyzer":320,"ai_condition":280},"types":{"start":{"label":"Start","description":"Indgangspunktet for sequencen. Præcis én pr. flow.","produces":["lead.*","person.*","company.*","deal.*","owner.*"],"uiGuide":"Start-noden er fast og kan ikke fjernes. Træk den første step ud fra dens højre udgang."},"send_email":{"label":"Send email","description":"Sender en email-step i sequencen.","dataSchema":{"subject":{"type":"string","description":"Emnelinje — kritisk (medmindre template)","required":true},"body":{"type":"string","description":"Email-brødtekst (HTML) — kritisk (medmindre template). Brug rich text med toolbar: vedhæft fil, indsæt billede, variabel, AI Composer.","required":true},"aiBlocks":{"type":"array","description":"Inline AI-blokke [{ id, prompt }] — indsættes som {{ai:id}} i body og genereres pr. modtager ved afsendelse. Prompt kan indeholde variabler."},"attachments":{"type":"array","description":"Vedhæftede filer [{ id, name, s3Key, workspaceFileId }]"},"templateId":{"type":"string","description":"Brug template i stedet for emne/body"},"recipientType":{"type":"enum","description":"Trin 1 'Hvem sender du til?': 'contact'=Person, 'company'=Virksomhed, 'auto'=prøv kontaktperson først, ellers virksomhed. KUN relevant når sequencen accepterer Lead/Deal eller en blanding (Company+People) — Company-only låses til virksomhed, People-only til person. Bekræft valget med brugeren for Lead/Deal/blandede sequences.","enum":["contact","company","auto"],"default":"contact"},"companyRecipientTarget":{"type":"enum","description":"KUN ved virksomheds-flow (recipientType company/auto eller Company-entity). 'contact_person'=send til en person tilknyttet virksomheden, 'company_direct'=send til virksomhedens egen email (fx info@, salg@).","enum":["contact_person","company_direct"],"default":"contact_person"},"companyContactSelection":{"type":"object","description":"KUN når companyRecipientTarget=contact_person. Objekt: { mode: 'first'|'beslutningstagere'|'all'|'byTitle'|'byTag', titleFilter, tagFilter, fallback: 'company_email'|'skip' }. mode vælger hvilke(n) kontaktperson(er) (byTitle kræver titleFilter, byTag kræver tagFilter); fallback bestemmer hvad der sker hvis ingen kontaktperson matcher."},"recipientField":{"type":"enum","description":"Hvilken email-adresse på den valgte modtager: primary=Første email (uanset type), work=Arbejde, cvr=CVR, privat=Privat, faktura=Faktura, support=Support, ai=AI-fundet, all=send separat til ALLE emails, custom=fast adresse/placeholder i customRecipient.","enum":["primary","work","cvr","privat","faktura","support","ai","all","custom"],"default":"primary"},"customRecipient":{"type":"string","description":"KUN når recipientField=custom: fast email eller {{placeholder}} (fx {{contact.email}} eller salg@firma.dk)."},"senderType":{"type":"enum","description":"'fixed' = bestemt konto, 'owner' = entity-ejers konto. Default = sequencens afsender","enum":["fixed","owner"]},"senderAccountId":{"type":"string","description":"Afsenderkonto — sæt via setEmailSender eller emailAccountId i updateNodeConfig (auto-resolver email-adresse)"},"emailMode":{"type":"enum","description":"'manual' = emne+body, 'template' = brug skabelon","enum":["manual","template"],"default":"manual"},"ignoreBusinessHours":{"type":"boolean","description":"Send uden for åbningstid hvis true","default":false},"sendCondition":{"type":"object","description":"Valgfrit: spring email over hvis felt matcher { enabled, field, operator: equals|not_equals, value }"},"unsubscribeLinkEnabled":{"type":"boolean","description":"Tilføj afmeldingslink i footer","default":false},"unsubscribeLinkPreset":{"type":"string","description":"Footer-preset for afmelding","default":"attio_default"}},"produces":["stepHistory.lastEmail.*"],"uiGuide":"Lineære sekvenser vises som vertikal trin-liste (Attio-stil): emne + rich body-kort med bund-toolbar (Vedhæft, Billede, Variabel, AI Composer). Brug AI Composer til personaliserede snippets med {{ai:id}}-markører. Modtageren konfigureres i dialogen 'Opsæt modtager' (åbnes via ⋯ → Leveringsindstillinger): trinnene er 1) 'Hvem sender du til?' (Person/Virksomhed/Auto — kun for Lead/Deal/blandet), 2) ved virksomhed 'Hvem modtager emailen?' (virksomhedens email vs. en kontaktperson), 3) 'Hvilken kontaktperson?' + fallback, 4) 'Hvilken email bruges?'. Højre side viser en live forhåndsvisning når man søger en rigtig person/virksomhed.","example":"Spørg om emne+indhold hvis ingen template. Brug aiBlocks til dynamisk tekst — ikke skriv alt manuelt. Afsender slås op — spørg ikke. Modtager: for Lead/Deal/blandede sequences, bekræft recipientType (person/virksomhed/auto) med brugeren og forklar den beregnede modtager."},"send_sms":{"label":"Send SMS","description":"Sender en SMS-step i sequencen.","dataSchema":{"message":{"type":"string","description":"SMS-tekst — kritisk. Kan indeholde {{ai:id}} AI-blokke.","required":true},"aiBlocks":{"type":"array","description":"Inline AI-blokke [{ id, prompt }] for SMS — genereres pr. modtager"},"recipientType":{"type":"enum","description":"Trin 1 'Hvem sender du til?': 'contact'=Person, 'company'=Virksomhed, 'auto'=prøv kontaktperson først, ellers virksomhed. KUN relevant for Lead/Deal/blandede sequences (Company-only=virksomhed, People-only=person). Bekræft valget med brugeren ved Lead/Deal/blandet.","enum":["contact","company","auto"],"default":"contact"},"companyRecipientTarget":{"type":"enum","description":"KUN ved virksomheds-flow. 'contact_person'=brug telefon fra en person tilknyttet virksomheden, 'company_direct'=brug virksomhedens eget telefonnummer.","enum":["contact_person","company_direct"],"default":"contact_person"},"companyContactSelection":{"type":"object","description":"KUN når companyRecipientTarget=contact_person. { mode: 'first'|'beslutningstagere'|'all'|'byTitle'|'byTag', titleFilter, tagFilter, fallback: 'company_phone'|'skip' }."},"phoneField":{"type":"enum","description":"Hvilket telefonnummer: primary=Første telefon, mobile=Mobil, work=Arbejde, privat=Privat, cvr=CVR, direkte=Direkte, all=alle numre, custom=fast nummer/placeholder i customPhone.","enum":["primary","mobile","work","privat","cvr","direkte","all","custom"],"default":"primary"},"customPhone":{"type":"string","description":"KUN når phoneField=custom: fast nummer eller {{placeholder}} (fx {{contact.phone}} eller +4512345678)."},"smsMode":{"type":"enum","description":"'manual' = besked, 'template' = SMS-skabelon","enum":["manual","template"],"default":"manual"},"templateId":{"type":"string","description":"SMS-skabelon (template mode)"},"ignoreBusinessHours":{"type":"boolean","description":"Send uden for åbningstid hvis true","default":false},"sendCondition":{"type":"object","description":"Valgfrit: spring SMS over hvis felt matcher { enabled, field, operator, value }"},"senderType":{"type":"enum","description":"Afsender-type for SMS","enum":["fixed","owner","alphanumeric"],"default":"fixed"},"fromNumber":{"type":"string","description":"Telefonnummer (fast afsender)"},"senderId":{"type":"string","description":"Alfanumerisk afsender-ID (max 11 tegn)"}},"produces":["stepHistory.lastSms.*"],"uiGuide":"SMS vises som kompakt kort i trin-listen med besked + toolbar (Variabel, AI Composer — ingen vedhæftninger). Modtager konfigureres i 'Opsæt modtager (telefon)' (⋯ → Leveringsindstillinger): samme trin-flow som email, men med telefonnumre og fallback til virksomhedens telefon. Højre side viser live forhåndsvisning."},"wait":{"label":"Vent","description":"Venter et tidsrum før næste step. Canonical fields: delayAmount + delayUnit on node.data (NOT under config). Aliases accepted by the executor: waitAmount/waitUnit, duration/unit, amount, and waitDays/waitHours.","dataSchema":{"delayAmount":{"type":"number","description":"Antal tidsenheder (canonical)","default":1},"delayUnit":{"type":"enum","description":"Tidsenhed (canonical)","enum":["minutes","hours","days","weeks"],"default":"days"}},"uiGuide":"Klik på vent-step'et → angiv delayAmount + delayUnit. Do not use nested config.","example":"data: { delayAmount: 3, delayUnit: \"days\" }"},"create_task":{"label":"Opret kalenderbegivenhed","description":"Creates a calendar Event for the enrolled entity (same calendar model as automation create_task). Field names differ from automations: use taskTitle / taskDescription on node.data (NOT config, NOT title — though title is accepted as a legacy alias).","dataSchema":{"taskTitle":{"type":"string","description":"Event title. Canonical key is taskTitle (legacy alias: title).","required":true},"taskDescription":{"type":"string","description":"Beskrivelse (canonical; not description)"},"activityType":{"type":"enum","description":"Kalender-type","enum":["task","call","meeting","follow_up","email","deadline","reminder"],"default":"task"},"priority":{"type":"enum","description":"Prioritet","enum":["low","medium","high"],"default":"medium"},"dueInDays":{"type":"number","description":"Dage til deadline. Default/runtime: 1. Note: stored 0 currently coerces to 1 (historical || 1) — use 1+ explicitly; do not rely on 0 for 'today'.","default":1},"assignTo":{"type":"enum","description":"Hvem begivenheden tildeles","enum":["owner","creator"],"default":"owner"}},"uiGuide":"Klik på opgave-step'et → udfyld taskTitle, type og dueInDays. Settings on node.data.","example":"data: { taskTitle: \"Ring til {{contact.firstName}}\", dueInDays: 1, priority: \"high\" }"},"create_recall":{"label":"Opret genopkald","description":"Planlægger et genopkald som en step.","dataSchema":{"title":{"type":"string","description":"Recall-titel","required":true},"message":{"type":"string","description":"Noter"},"phoneField":{"type":"enum","description":"Telefonfelt","enum":["primary","mobile","work","custom"],"default":"primary"},"recipientType":{"type":"enum","description":"Modtagertype","enum":["contact","company","auto"],"default":"auto"},"delayDays":{"type":"number","description":"Dage til genopkald","default":0},"callTime":{"type":"string","description":"Tidspunkt HH:mm"},"assignTo":{"type":"string","description":"Tildeles til"}},"uiGuide":"Klik på genopkalds-step'et → udfyld titel, telefonfelt og forsinkelse."},"update_field":{"label":"Opdater felt","description":"Opdaterer ét felt på den enrolled entity. Uses fieldPath + value on node.data (sequences do NOT use automation's fieldUpdates array).","dataSchema":{"fieldPath":{"type":"string","description":"Felt der opdateres (fx status, customFields.score)","required":true},"value":{"type":"string","description":"Ny værdi","required":true}},"uiGuide":"Klik på step'et → vælg felt og angiv ny værdi. Settings on node.data — not under config."},"condition":{"label":"Betingelse","description":"Forgrener sequencen så den udfører FORSKELLIGE handlinger (fx ud fra lead-score eller åben deal). For blot at stoppe ved svar/booking, brug exit conditions i stedet — ikke denne node.","dataSchema":{"field":{"type":"string","description":"Felt der tjekkes (fx lead.score, deal.stage)","required":true},"operator":{"type":"enum","description":"Operator","enum":["equals","not_equals","contains","not_contains","starts_with","ends_with","matches_regex","greater_than","less_than","greater_than_or_equal","less_than_or_equal","between","is_empty","is_not_empty","exists","not_exists","is_true","is_false","array_contains","array_length_equals","array_length_greater","date_before","date_after","date_equals","date_within_days","date_in_exactly_days"],"default":"is_true"},"value":{"type":"string","description":"Sammenligningsværdi (hvis relevant)"}},"branching":{"handles":["true","false"],"describe":"Begge grene bør forbindes."},"uiGuide":"Klik på betingelses-step'et → vælg felt, operator og værdi. Forbind både true- og false-grenen.","example":"field=lead.score, operator=greater_than, value=70 (forgren på score). Brug IKKE til at stoppe ved svar — det gør exit conditions automatisk."},"ai_analyzer":{"label":"AI-analyse","description":"Analyserer kontekst med AI (fx et email-svar) og producerer output til efterfølgende betingelser.","dataSchema":{"prompt":{"type":"string","description":"Analyse-prompt — kritisk","required":true},"outputMode":{"type":"enum","description":"Output-format","enum":["text","json_schema"],"default":"json_schema"},"reasoningEffort":{"type":"enum","description":"Reasoning effort","enum":["none","low","medium","high"],"default":"low"},"schema":{"type":"json","description":"Schema for struktureret output → {{ai_json.<felt>}}"}},"produces":["ai_json.*","ai_text"],"uiGuide":"Klik på AI-analyse-step'et → skriv prompt og byg schema. Forgren derefter med en betingelse på ai_json.<felt>."},"ai_condition":{"label":"AI-betingelse","description":"AI træffer en ja/nej-beslutning og forgrener sequencen. (Manglede tidligere helt i flow-schemas.)","dataSchema":{"prompt":{"type":"string","description":"Kriterie AI skal vurdere — kritisk","required":true},"confidenceThreshold":{"type":"number","description":"Sikkerhedstærskel (0-1)","default":0.7},"useKnowledgeBase":{"type":"boolean","description":"Brug vidensbase","default":false}},"produces":["ai_json.decision","ai_json.confidence"],"branching":{"handles":["true","false"],"describe":"Begge grene bør forbindes."},"uiGuide":"Klik på AI-betingelse-step'et → formulér kriteriet. Forbind true- og false-grenen."},"set_access":{"label":"Tildel adgang","description":"Sætter ejer, synlighed og deling på den enrolled entity.","dataSchema":{"recordType":{"type":"enum","description":"","enum":["trigger"],"default":"trigger"},"accessState":{"type":"object","description":"Adgangskonfiguration","required":true}}},"stop":{"label":"Stop","description":"Afslutter sequencen for entityen.","uiGuide":"Forbind et step til stop-noden for eksplicit at afslutte den gren."}}},"sequenceSettingsGuide":{"exitConditions":"onEmailReply, onSmsReply, onMeetingBooked, onDealStageChange.{enabled,stages[]}, onLeadStatusChange.{enabled,statuses[]}","sendSettings":"businessHoursOnly, businessStart, businessEnd, timezone, skipWeekends, smsQuietHours*, emailIntervalSeconds. Defaults if omitted: businessHoursOnly=false, skipWeekends=false (emails can send on weekends). Recommended DK defaults: businessHoursOnly=true, businessStart=08:00, businessEnd=16:30, timezone=Europe/Copenhagen, skipWeekends=true.","autoEnrollTriggers":"field_change | sequence_event | entity_created — see triggers[] on sequence document","duplicateHandling":"block | block_with_restart (default) | allow_multiple. Independent of automation add_to_sequence.forceRestart: when forceRestart=false and an enrollment is active, the automation skips; sequence duplicateHandling applies to enrollment API / auto-enroll paths.","storageConvention":"ALL sequence step settings live on node.data (never nested under config). Automations put action settings under node.data.config — opposite convention.","waitFields":"Canonical: delayAmount + delayUnit. Aliases: waitAmount/waitUnit, duration/unit, waitDays/waitHours.","createTaskFields":"Canonical: taskTitle, taskDescription, dueInDays, assignTo, priority, activityType. Legacy alias for title: title.","placeholders":"Namespaced only: {{contact.firstName}} — bare {{firstName}} does not resolve."},"exitConditions":{"description":"Exit conditions automatically remove an entity from the sequence when certain events occur.","schema":{"onEmailReply":{"type":"boolean","default":true,"description":"Exit when the entity replies to a sequence email"},"onSmsReply":{"type":"boolean","default":true,"description":"Exit when the entity replies to a sequence SMS"},"onMeetingBooked":{"type":"boolean","default":true,"description":"Exit when a meeting/booking is created for the entity"},"onDealStageChange":{"enabled":{"type":"boolean","default":false},"stages":{"type":"number[]","description":"Array of stage IDs that trigger exit"}},"onLeadStatusChange":{"enabled":{"type":"boolean","default":false},"statuses":{"type":"string[]","description":"Array of status values that trigger exit (e.g. ['won', 'lost'])"}}},"exitReasons":["email_reply","sms_reply","meeting_booked","deal_stage","lead_status","manual","ai_decision"]},"sendSettings":{"description":"Control when messages are sent in the sequence","schema":{"businessHoursOnly":{"type":"boolean","default":false,"description":"Only send during business hours"},"businessStart":{"type":"string","default":"08:00","description":"Business hours start (HH:mm)"},"businessEnd":{"type":"string","default":"16:30","description":"Business hours end (HH:mm)"},"timezone":{"type":"string","default":"Europe/Copenhagen","description":"Timezone for scheduling"},"skipWeekends":{"type":"boolean","default":false,"description":"Skip Saturday and Sunday"},"smsQuietHoursEnabled":{"type":"boolean","default":false,"description":"Enforce SMS quiet hours"},"smsQuietHoursStart":{"type":"string","default":"20:00","description":"SMS quiet hours start"},"smsQuietHoursEnd":{"type":"string","default":"08:00","description":"SMS quiet hours end"}}},"trackingSettings":{"schema":{"trackOpens":{"type":"boolean","default":true,"description":"Track email opens via pixel"},"trackClicks":{"type":"boolean","default":false,"description":"Track link clicks"}}},"executionSettings":{"schema":{"failureBehavior":{"type":"string","enum":["stop","continue","retry"],"default":"stop"},"retryMaxAttempts":{"type":"number","default":3,"min":1,"max":10},"retryIntervalMinutes":{"type":"number","default":5,"min":1,"max":1440}}},"autoEnrollmentTriggers":{"description":"Sequences can automatically enroll entities when specific conditions are met. Configure via the 'triggers' array on the sequence.","triggerTypes":{"field_change":{"description":"Enroll when a field on an entity changes to a specific value","config":{"entityType":{"type":"string","enum":["Lead","People","Deal","Company"]},"field":{"type":"string","description":"Field path to watch"},"operator":{"type":"string","enum":["equals","not_equals","contains"]},"value":{"type":"string"}}},"entity_created":{"description":"Enroll when a new entity is created matching filters","config":{"entityType":{"type":"string","enum":["Lead","People","Deal","Company"]},"filters":{"type":"object","description":"Field-value filters the new entity must match"}}},"sequence_event":{"description":"Enroll when an entity completes or exits another sequence","config":{"sourceSequenceId":{"type":"string","description":"The other sequence ID"},"event":{"type":"string","enum":["completed","exited","any"],"description":"Which event to trigger on"}}}}},"duplicateHandling":{"description":"Controls what happens when an entity is already enrolled","options":{"block":"Reject the enrollment if the entity is already active in this sequence","block_with_restart":"Unenroll from the current run and start fresh (default)","allow_multiple":"Allow multiple concurrent enrollments of the same entity"}},"placeholders":{"description":"Placeholders use double curly braces: {{namespace.field}}. Available in subject, body, message, titles, and descriptions. IMPORTANT: bare {{firstName}} does NOT resolve — use {{contact.firstName}} (or company.*/lead.*/deal.*/owner.*). This differs from some email-template shorthand; sequence context is namespaced.","categories":{"contact":["contact.firstName","contact.lastName","contact.fullName","contact.email","contact.phone","contact.mobile","contact.title"],"company":["company.name","company.cvr","company.email","company.phone","company.address","company.city","company.zip","company.industry","company.employees","company.website"],"lead":["lead._id","lead.title","lead.email","lead.phone","lead.status","lead.source","lead.score","lead.value"],"deal":["deal._id","deal.title","deal.value","deal.stage","deal.probability","deal.priority"],"owner":["owner.firstName","owner.lastName","owner.fullName","owner.email","owner.phone"],"workspace":["workspace.name"],"system":["currentDate","currentTime","enrolledAt"],"customFields":["customFields.field_key (any custom field defined on the entity)"],"aiOutput":["aiOutput (full AI response)","aiOutput.fieldName (structured field from AI json_schema output)"],"stepHistory":["stepHistory.lastEmail.replied","stepHistory.lastEmail.opened","stepHistory.lastSms.replied"]},"formatting":{"description":"Placeholders support default values and formatting","syntax":["{{contact.firstName}} — basic placeholder (required namespace)","{{contact.firstName|default:Customer}} — fallback value if empty","{{deal.value|format:currency}} — format as currency","{{currentDate|format:date}} — format as date"]},"commonMistakes":["{{firstName}} → use {{contact.firstName}}","Do not use automation-style {{person.firstName}} unless that namespace is present in enrollment context"]},"examples":[{"name":"3-email cold outreach","description":"Send 3 emails spaced 3 days apart with a stop at the end","flow":{"name":"Cold Outreach 3-Touch","description":"3-email cold outreach sequence with 3-day intervals","nodes":[{"id":"start_1","type":"start","position":{"x":50,"y":300},"data":{"label":"Start"}},{"id":"email_1","type":"send_email","position":{"x":330,"y":300},"data":{"label":"First email","subject":"Hi {{contact.firstName}} — quick question","body":"<p>Hi {{contact.firstName}},</p><p>I noticed {{company.name}} might benefit from our solution. Would you be open to a quick chat this week?</p><p>Best,<br>{{owner.firstName}}</p>"}},{"id":"wait_1","type":"wait","position":{"x":690,"y":300},"data":{"label":"Wait 3 days","delayAmount":3,"delayUnit":"days"}},{"id":"email_2","type":"send_email","position":{"x":990,"y":300},"data":{"label":"Follow-up","subject":"Re: Hi {{contact.firstName}} — quick question","body":"<p>Hi {{contact.firstName}},</p><p>Just following up on my last email. I'd love to show you how we've helped similar companies in {{company.industry}}.</p><p>Best,<br>{{owner.firstName}}</p>"}},{"id":"wait_2","type":"wait","position":{"x":1350,"y":300},"data":{"label":"Wait 3 days","delayAmount":3,"delayUnit":"days"}},{"id":"email_3","type":"send_email","position":{"x":1650,"y":300},"data":{"label":"Final attempt","subject":"Last try — {{contact.firstName}}","body":"<p>Hi {{contact.firstName}},</p><p>I don't want to be a bother. If now isn't the right time, no worries at all. Just let me know if you'd like to connect in the future.</p><p>Best,<br>{{owner.firstName}}</p>"}},{"id":"stop_1","type":"stop","position":{"x":2010,"y":300},"data":{"label":"Stop"}}],"edges":[{"id":"e1","source":"start_1","target":"email_1"},{"id":"e2","source":"email_1","target":"wait_1"},{"id":"e3","source":"wait_1","target":"email_2"},{"id":"e4","source":"email_2","target":"wait_2"},{"id":"e5","source":"wait_2","target":"email_3"},{"id":"e6","source":"email_3","target":"stop_1"}],"exitConditions":{"onEmailReply":true,"onMeetingBooked":true},"sendSettings":{"businessHoursOnly":true,"skipWeekends":true,"timezone":"Europe/Copenhagen"}}},{"name":"Multi-channel with condition","description":"Email then SMS with a condition check for email open","flow":{"name":"Multi-channel Follow-up","description":"Sends email, waits 2 days, checks if opened, then sends SMS or second email","nodes":[{"id":"start_1","type":"start","position":{"x":50,"y":300},"data":{"label":"Start"}},{"id":"email_1","type":"send_email","position":{"x":330,"y":300},"data":{"label":"Initial email","subject":"{{contact.firstName}}, quick intro","body":"<p>Hi {{contact.firstName}},</p><p>I wanted to introduce myself and our solution for {{company.name}}.</p><p>{{owner.firstName}}</p>"}},{"id":"wait_1","type":"wait","position":{"x":690,"y":300},"data":{"label":"Wait 2 days","delayAmount":2,"delayUnit":"days"}},{"id":"condition_1","type":"condition","position":{"x":990,"y":300},"data":{"label":"Email opened?","conditions":[{"field":"stepHistory.lastEmail.opened","operator":"is_true"}],"logic":"and"}},{"id":"sms_1","type":"send_sms","position":{"x":1350,"y":150},"data":{"label":"SMS follow-up","message":"Hi {{contact.firstName}}, I sent you an email about {{company.name}}. Would love to chat briefly — {{owner.firstName}}","phoneField":"primary"}},{"id":"email_2","type":"send_email","position":{"x":1350,"y":450},"data":{"label":"Re-engage email","subject":"Did you see my email, {{contact.firstName}}?","body":"<p>Hi {{contact.firstName}},</p><p>I sent an email a couple of days ago but wanted to make sure it didn't get lost. Happy to chat whenever works for you.</p><p>{{owner.firstName}}</p>"}},{"id":"task_1","type":"create_task","position":{"x":1710,"y":300},"data":{"label":"Manual follow-up","taskTitle":"Call {{contact.firstName}} at {{company.name}}","taskDescription":"Sequence complete — time for a personal touch","dueInDays":1,"assignTo":"owner"}},{"id":"stop_1","type":"stop","position":{"x":2070,"y":300},"data":{"label":"Stop"}}],"edges":[{"id":"e1","source":"start_1","target":"email_1"},{"id":"e2","source":"email_1","target":"wait_1"},{"id":"e3","source":"wait_1","target":"condition_1"},{"id":"e4","source":"condition_1","target":"sms_1","sourceHandle":"true"},{"id":"e5","source":"condition_1","target":"email_2","sourceHandle":"false"},{"id":"e6","source":"sms_1","target":"task_1"},{"id":"e7","source":"email_2","target":"task_1"},{"id":"e8","source":"task_1","target":"stop_1"}],"exitConditions":{"onEmailReply":true,"onSmsReply":true,"onMeetingBooked":true},"sendSettings":{"businessHoursOnly":true,"skipWeekends":true}}},{"name":"AI-powered reply analysis","description":"Send email, wait for reply, use AI to analyze and route","flow":{"name":"AI Reply Analyzer","description":"Sends outreach email, waits for reply, uses AI to determine next action","nodes":[{"id":"start_1","type":"start","position":{"x":50,"y":300},"data":{"label":"Start"}},{"id":"email_1","type":"send_email","position":{"x":330,"y":300},"data":{"label":"Outreach email","subject":"{{contact.firstName}} — idea for {{company.name}}","body":"<p>Hi {{contact.firstName}},</p><p>I have an idea that could help {{company.name}} save time. Worth a 15-min call?</p><p>{{owner.firstName}}</p>"}},{"id":"wait_1","type":"wait","position":{"x":690,"y":300},"data":{"label":"Wait 5 days","delayAmount":5,"delayUnit":"days"}},{"id":"ai_1","type":"ai_condition","position":{"x":990,"y":300},"data":{"label":"Got a reply?","prompt":"Has the contact {{contact.firstName}} at {{company.name}} replied to our last email? Check the step history for any email reply.","reasoningEffort":"low"}},{"id":"ai_2","type":"ai_analyzer","position":{"x":1350,"y":150},"data":{"label":"Analyze reply","prompt":"Analyze the reply from {{contact.firstName}}. Determine if they are interested, need more info, or are declining. Reply history: {{stepHistory.lastEmail.replyBody}}","outputMode":"json_schema","schema":{"type":"object","properties":{"intent":{"type":"string","enum":["interested","needs_info","declining","out_of_office"]},"suggestedAction":{"type":"string"}},"required":["intent","suggestedAction"]}}},{"id":"task_1","type":"create_task","position":{"x":1750,"y":150},"data":{"label":"Act on reply","taskTitle":"{{contact.firstName}} replied: {{aiOutput.intent}} — {{aiOutput.suggestedAction}}","dueInDays":0,"assignTo":"owner"}},{"id":"email_2","type":"send_email","position":{"x":1350,"y":450},"data":{"label":"Bump email","subject":"Re: {{contact.firstName}} — idea for {{company.name}}","body":"<p>Hi {{contact.firstName}},</p><p>Just bumping this to the top of your inbox. Let me know if you'd like to chat.</p><p>{{owner.firstName}}</p>"}},{"id":"stop_1","type":"stop","position":{"x":2110,"y":300},"data":{"label":"Stop"}}],"edges":[{"id":"e1","source":"start_1","target":"email_1"},{"id":"e2","source":"email_1","target":"wait_1"},{"id":"e3","source":"wait_1","target":"ai_1"},{"id":"e4","source":"ai_1","target":"ai_2","sourceHandle":"true"},{"id":"e5","source":"ai_1","target":"email_2","sourceHandle":"false"},{"id":"e6","source":"ai_2","target":"task_1"},{"id":"e7","source":"task_1","target":"stop_1"},{"id":"e8","source":"email_2","target":"stop_1"}],"exitConditions":{"onEmailReply":false,"onMeetingBooked":true}}}],"completeWorkflow":{"title":"Building a sequence via API — step by step","steps":[{"step":1,"title":"Create the sequence","description":"POST /api/v1/sequences with name and the full flow (nodes + edges)","note":"Include all nodes and edges in the initial creation, or create empty and update later."},{"step":2,"title":"Configure settings","description":"PUT /api/v1/sequences/:id with exitConditions, sendSettings, allowedEntityTypes","note":"Set exit conditions (onEmailReply, onMeetingBooked) and send settings (business hours, timezone)."},{"step":3,"title":"Activate the sequence","description":"POST /api/v1/sequences/:id/toggle to activate","note":"The sequence must have a start node and at least one action node."},{"step":4,"title":"Enroll entities","description":"POST /api/v1/sequences/:id/enroll with entityType and entityId or entityIds","note":"Entities begin at the start node and progress through the flow automatically."},{"step":5,"title":"Monitor progress","description":"GET /api/v1/sequences/:id/enrollments and /api/v1/sequences/:id/stats","note":"Check enrollment statuses and aggregate statistics."}]}},"endpoints":{"overview":{"baseUrl":"/api/v1","authentication":"All endpoints require Authorization: Bearer <API_KEY> header (or ?api_key= query param)","responseFormat":{"success":"{ data: <result>, page?, limit?, total? }","error":"{ error: <message string>, ...details }"},"pagination":"Most list endpoints accept page (default 1) and limit (default 20, max 100) query params","integrationOnly":"Denne reference dækker workspace API-nøgler til integrationer. Mobilapp-, session- og platform-endpoints er ikke dokumenteret her."},"groups":{"inbound":{"title":"Inbound (unified creation)","description":"Create companies, people, leads, and deals in one request. Ideal for form submissions and integrations.","endpoints":[{"method":"POST","path":"/api/v1/inbound","description":"Create or find-and-update company/person/lead/deal from a single payload. Supports JSON, form-urlencoded, and Elementor webhook formats.","body":{"companyName":{"type":"string","description":"Company name (for company lookup/creation)"},"companyCvr":{"type":"string","description":"Danish CVR number (for exact company match)"},"companyEmail":{"type":"string"},"companyPhone":{"type":"string"},"personFirstName":{"type":"string"},"personLastName":{"type":"string"},"personEmail":{"type":"string"},"personPhone":{"type":"string"},"personTitle":{"type":"string"},"leadTitle":{"type":"string","description":"Lead title (auto-generated if not provided)"},"leadSource":{"type":"string"},"leadStatus":{"type":"string"},"leadNotes":{"type":"string"},"dealTitle":{"type":"string"},"dealValue":{"type":"number"},"dealFlowId":{"type":"string","description":"Pipeline ID"},"dealStageId":{"type":"string","description":"Stage ID within pipeline"},"customFields":{"type":"object","description":"Key-value pairs for custom fields"},"tags":{"type":"string[]","description":"Array of tag names to apply"}},"response":"{ data: { company, person, lead, deal, flow }, created: { company: bool, person: bool, lead: bool, deal: bool } }"},{"method":"GET","path":"/api/v1/inbound","description":"Returns API documentation for the inbound endpoint"}]},"session":{"title":"Session","description":"Identity for the authenticated API key or MCP session.","endpoints":[{"method":"GET","path":"/api/v1/whoami","description":"Return workspace name, user name, API key prefix, and effective scopes for the current session. Any valid key or MCP token works (no extra scope required). MCP clients should call this first.","response":"{ data: { workspaceId, workspaceName, workspace: { id, name, companyName, plan }, userId, userName, userEmail, user: { id, firstName, lastName, name, email }, apiKeyPrefix, apiKeyName, authType, scopes, scopeLabels, scopeDescriptions, rateLimits } }"},{"method":"GET","path":"/api/v1/docs","description":"API documentation index or a section. Query: section, subsection, type, index=true, full=true. No authentication required."},{"method":"GET","path":"/api/v1/docs/markdown","description":"Full API documentation as Markdown."}]},"search":{"title":"Search","endpoints":[{"method":"GET","path":"/api/v1/search","description":"Search across CRM entities, projects, offers, and tasks","queryParams":{"q":{"type":"string","required":true,"description":"Search query (minimum 2 characters)"},"types":{"type":"string","description":"Comma-separated entity types: company,person,lead,deal,support-ticket,project,offer,task (default: all)"},"limit":{"type":"number","default":10,"description":"Max results per entity type (max 50)"}}}]},"cvr":{"title":"CVR","endpoints":[{"method":"GET","path":"/api/v1/cvr/match","description":"Match a Danish CVR company by LinkedIn URL, domain, or exact name (Chrome extension).","queryParams":{"linkedin":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}]},"extension":{"title":"Chrome extension","endpoints":[{"method":"GET","path":"/api/v1/extension/contribution","description":"Get workspace Bidrag til Salesbase-data status."},{"method":"PUT","path":"/api/v1/extension/contribution","description":"Admin: enable or disable contribution.","body":{"enabled":{"type":"boolean","required":true}}},{"method":"POST","path":"/api/v1/extension/observations","description":"Ingest observation batches (contribute scope).","body":{"observations":{"type":"array","required":true}}},{"method":"POST","path":"/api/v1/extension/captures/presign","description":"Presign S3 uploads for a page capture (contribute scope)."},{"method":"GET","path":"/api/v1/extension/network","description":"Give-to-get network contacts and signals (contributing workspaces only).","queryParams":{"linkedin":{"type":"string"},"handle":{"type":"string"}}},{"method":"GET","path":"/api/v1/extension/domains","description":"Known company domains for the extension local filter."}]},"companies":{"title":"Companies","endpoints":[{"method":"GET","path":"/api/v1/companies","description":"List companies. view=full returns stored fields including integration ids (ordrestyringCustomerNumber, economicCustomerNumber, …). fields= projects any schema field at Mongo. limit max 500. MCP defaults to view=summary.","queryParams":{"page":{},"limit":{},"q":{"type":"string"},"view":{"type":"string","enum":["summary","full"],"default":"full"},"fields":{"type":"string","description":"Comma-separated stored field names"},"updatedSince":{"type":"string"},"dateFrom":{"type":"string"},"dateTo":{"type":"string"}}},{"method":"GET","path":"/api/v1/companies/lookup","description":"Look up a company by LinkedIn URL or website domain (Chrome extension).","queryParams":{"linkedin":{"type":"string"},"domain":{"type":"string"}}},{"method":"POST","path":"/api/v1/companies","description":"Create a company","body":{"name":{"type":"string","required":true},"cvr":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"website":{"type":"string"},"industry":{"type":"string"},"employees":{"type":"number"},"customFields":{"type":"array"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/companies/:id","description":"Get company by ID. Optional fields= query projects stored fields (including integration ids).","queryParams":{"fields":{"type":"string"}}},{"method":"PUT","path":"/api/v1/companies/:id","description":"Update company. May set website, avatar (logo) and logoUrl. avatar/logoUrl accept an https image URL (re-hosted as a workspace image) or a data:image/...;base64 URL (max 2 MB). customFields is a partial key→value merge (definitions: GET /api/v1/custom-fields?entityType=company).","body":{"website":{"type":"string"},"avatar":{"type":"string","description":"Image URL (re-hosted as a workspace image), data:image/...;base64 URL (max 2 MB), or empty to clear"},"logoUrl":{"type":"string","description":"Image URL (re-hosted as a workspace image), data:image/...;base64 URL (max 2 MB), or empty to clear"},"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/companies/:id","description":"Delete company"},{"method":"POST","path":"/api/v1/companies/:id/find-web-presence","description":"Find and fill empty website, logo/avatar and LinkedIn from CVR, the company e-mail domain and a web search (page verified by name or CVR). Same as CRM action Find hjemmeside og logo. Does not overwrite filled fields. Response data.status: updated | already_filled | not_found; data.hint says what to do next when nothing was found."},{"method":"GET","path":"/api/v1/companies/:id/people","description":"List people connected to company"},{"method":"GET","path":"/api/v1/companies/:id/deals","description":"List deals connected to company"},{"method":"GET","path":"/api/v1/companies/:id/activities","description":"List activities for company"},{"method":"GET","path":"/api/v1/companies/:id/activity-feed","description":"Unified activity feed for company"},{"method":"POST","path":"/api/v1/companies/:id/people","description":"Link person to company"},{"method":"DELETE","path":"/api/v1/companies/:id/people","description":"Unlink person from company"}]},"people":{"title":"People (contacts)","endpoints":[{"method":"GET","path":"/api/v1/people","description":"List people. view=full returns stored fields including integration ids. fields= projects any schema field at Mongo. limit max 500. MCP defaults to view=summary."},{"method":"GET","path":"/api/v1/people/lookup","description":"Look up a person by LinkedIn URL (Chrome extension).","queryParams":{"linkedin":{"type":"string","required":true}}},{"method":"POST","path":"/api/v1/people","description":"Create a person","body":{"firstName":{"type":"string","required":true},"lastName":{"type":"string"},"emails":{"type":"array","description":"[{ email, label }]"},"phones":{"type":"array","description":"[{ phone, label }]"},"title":{"type":"string"},"companyId":{"type":"string"},"customFields":{"type":"array"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/people/:id","description":"Get person by ID. Optional fields= query.","queryParams":{"fields":{"type":"string"}}},{"method":"PUT","path":"/api/v1/people/:id","description":"Update person. May set avatar: an https image URL (re-hosted as a workspace image) or a data:image/...;base64 URL (max 2 MB). customFields is a partial key→value merge (definitions: GET /api/v1/custom-fields?entityType=person).","body":{"avatar":{"type":"string","description":"Image URL (re-hosted as a workspace image), data:image/...;base64 URL (max 2 MB), or empty to clear"},"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/people/:id","description":"Delete person"},{"method":"POST","path":"/api/v1/people/:id/find-avatar","description":"Find and fill an empty avatar from public sources (Gravatar, LinkedIn, company site). Same as CRM action Find billede. Does not overwrite an existing avatar. Response data.status: updated | already_filled | not_found, plus data.hint."},{"method":"GET","path":"/api/v1/people/:id/companies","description":"List companies connected to person"},{"method":"GET","path":"/api/v1/people/:id/deals","description":"List deals connected to person"},{"method":"GET","path":"/api/v1/people/:id/activities","description":"List activities for person"},{"method":"GET","path":"/api/v1/people/:id/activity-feed","description":"Unified activity feed for person"}]},"leads":{"title":"Leads","endpoints":[{"method":"GET","path":"/api/v1/leads","description":"List leads. view=full returns stored documents. fields= projects any schema field. limit max 500. MCP defaults to view=summary.","queryParams":{"page":{},"limit":{},"status":{"type":"string"},"source":{"type":"string"},"search":{"type":"string"},"view":{"type":"string","enum":["summary","full"]},"fields":{"type":"string"}}},{"method":"POST","path":"/api/v1/leads","description":"Create a lead. Score and scoreBreakdown are calculated automatically from active lead-scoring rules after create (do not rely on a client-supplied score). Score 0 with empty breakdown means no active rules matched; configure rules via GET/POST /api/v1/lead-scoring/rules.","body":{"title":{"type":"string","required":true},"email":{"type":"string"},"phone":{"type":"string"},"status":{"type":"string"},"source":{"type":"string"},"score":{"type":"number","description":"Ignored for scoring: server recalculates from rules after create"},"value":{"type":"number"},"notes":{"type":"string"},"companyRef":{"type":"string","description":"Company ID"},"personRef":{"type":"string","description":"Person ID"},"customFields":{"type":"object"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/leads/:id","description":"Get lead by ID (includes score and scoreBreakdown from the last automatic calculation)"},{"method":"PUT","path":"/api/v1/leads/:id","description":"Update lead. customFields is a partial key→value merge (definitions: GET /api/v1/custom-fields?entityType=lead). Score is recalculated automatically after update from active lead-scoring rules; score 0 means no rules matched.","body":{"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/leads/:id","description":"Delete lead"},{"method":"GET","path":"/api/v1/leads/:id/activity-feed","description":"Unified activity feed for lead"},{"method":"GET","path":"/api/v1/leads/:id/activities","description":"List activities for lead"}]},"deals":{"title":"Deals","endpoints":[{"method":"GET","path":"/api/v1/deals","description":"List deals. Unknown query params return 400. Use view=summary for slim payloads; updatedSince for incremental sync.","queryParams":{"page":{},"limit":{},"q":{"type":"string"},"status":{"type":"string","enum":["open","won","lost"]},"flowId":{"type":"string","description":"Pipeline id (alias: flowRef)"},"stageId":{"type":"string","description":"Stage id/number (alias: stage)"},"ownerId":{"type":"string","description":"Owner user id (alias: owner)"},"sortBy":{"type":"string","description":"title|value|updatedAt|createdAt|status|lostDate|wonDate|stage"},"sortOrder":{"type":"string","enum":["asc","desc"]},"view":{"type":"string","enum":["summary","full"],"default":"full"},"fields":{"type":"string"},"updatedSince":{"type":"string","description":"ISO — updatedAt >="},"createdAfter":{"type":"string"},"dateFrom":{"type":"string","description":"createdAt >= (aliases: startDate, from)"},"dateTo":{"type":"string","description":"createdAt <= (aliases: endDate, to)"}},"response":"{ data, page, limit, total, view }"},{"method":"POST","path":"/api/v1/deals","description":"Create a deal","body":{"title":{"type":"string","required":true},"value":{"type":"number"},"flowId":{"type":"string"},"stageId":{"type":"string"},"priority":{"type":"string","enum":["low","medium","high"]},"probability":{"type":"number"},"company":{"type":"string","description":"Company ID"},"personRef":{"type":"string","description":"Person ID"},"leadRef":{"type":"string","description":"Lead ID"},"customFields":{"type":"object"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/deals/:id","description":"Get deal by ID"},{"method":"PUT","path":"/api/v1/deals/:id","description":"Update deal (logs field/stage changes to activity feed, same as UI). customFields is a partial key→value merge (definitions: GET /api/v1/custom-fields?entityType=deal).","body":{"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/deals/:id","description":"Delete deal"},{"method":"POST","path":"/api/v1/deals/:id/move","description":"Move deal to a different stage","body":{"stageId":{"type":"string","required":true},"flowId":{"type":"string"}}},{"method":"GET","path":"/api/v1/deals/:id/activities","description":"List activities for deal"},{"method":"GET","path":"/api/v1/deals/:id/activity-feed","description":"Unified activity feed for deal"}]},"activities":{"title":"Activities","description":"Aktiviteter / activity feed — list and create feed rows (noter, opkald, deals m.m.)","endpoints":[{"method":"GET","path":"/api/v1/activities","description":"List activities (aktiviteter). Unknown query params return 400.","queryParams":{"page":{},"limit":{},"type":{"type":"string"},"companyId":{},"personId":{},"dealId":{},"leadId":{},"dateFrom":{"type":"string","description":"Activity date >= (aliases: startDate, from)"},"dateTo":{"type":"string","description":"Activity date <= (aliases: endDate, to)"},"updatedSince":{},"createdAfter":{},"view":{"type":"string","enum":["summary","full"]},"q":{},"ownerId":{}},"response":"{ data, page, limit, total, view }"},{"method":"POST","path":"/api/v1/activities","description":"Create activity / aktivitet (creates a feed row). Body: type (required), summary or title, description, companyId/personId/dealId/leadId, entityType+entityId, noteRef, date, metadata"},{"method":"GET","path":"/api/v1/activities/:id","description":"Get activity by ID (aktivitet)"}]},"tasks":{"title":"Tasks","endpoints":[{"method":"GET","path":"/api/v1/tasks","description":"List tasks. Coarse status is to_do, in_progress, blocked, or done (not open or cancelled). The task list Status column and done checkbox follow stage from GET /api/v1/task-flow.","queryParams":{"page":{},"limit":{},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"]},"priority":{},"ownerId":{"description":"Assignee (aliases: owner, assignee)"},"project":{},"q":{},"view":{},"updatedSince":{},"dateFrom":{},"dateTo":{}}},{"method":"POST","path":"/api/v1/tasks","description":"Create a task (Mine opgaver or project task). Requires title plus exactly one parent: project, customObjectRecord (+ customObjectSlug), company, personRef, leadRef, or dealRef. Field name is company (not companyRef).","body":{"title":{"type":"string","required":true},"project":{"type":"string","description":"Project ID (omit when linking only to company/person/lead/deal)"},"customObjectRecord":{"type":"string","description":"Custom object record ID; requires customObjectSlug"},"customObjectSlug":{"type":"string"},"company":{"type":"string","description":"Company ID (standalone task on company; not companyRef)"},"personRef":{"type":"string","description":"Person ID (standalone task on person)"},"leadRef":{"type":"string","description":"Lead ID"},"dealRef":{"type":"string","description":"Deal ID"},"description":{"type":"string"},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"]},"priority":{"type":"string"},"assignees":{"type":"string[]","description":"User IDs; defaults to API caller"},"deadline":{"type":"string","description":"ISO date"},"tags":{"type":"string[]"},"customFields":{"type":"object"},"estimatedHours":{"type":"number"},"taskValue":{"type":"number"},"owner":{"type":"string","description":"User ID; defaults to API caller"}}},{"method":"GET","path":"/api/v1/tasks/:id","description":"Get task"},{"method":"PUT","path":"/api/v1/tasks/:id","description":"Update task. status done also sets stage to completedStageId from GET /api/v1/task-flow. stage sets status. The list Status column and done checkbox read stage.","body":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"],"description":"Coarse status. done moves stage to the flow completedStageId so the list and checkbox show Færdig."},"stage":{"type":"number","description":"Task-flow stage id from GET /api/v1/task-flow. Wins over status when both are sent. completedStageId marks the task done."},"priority":{"type":"string","enum":["low","medium","high","critical"]},"assignees":{"type":"string[]"},"deadline":{"type":"string","description":"ISO date"},"tags":{"type":"string[]"},"estimatedHours":{"type":"number"},"taskValue":{"type":"number"},"owner":{"type":"string","description":"User ID"},"customFields":{"type":"object","description":"Partial key → typed value merge. Definitions: GET /api/v1/project-custom-fields?entityType=task"}}},{"method":"DELETE","path":"/api/v1/tasks/:id","description":"Delete task"}]},"notes":{"title":"Notes","endpoints":[{"method":"GET","path":"/api/v1/notes","description":"List notes. Supports q search; unknown params → 400.","queryParams":{"page":{},"limit":{},"q":{},"personRef":{},"company":{},"leadRef":{},"dealRef":{},"dateFrom":{},"dateTo":{},"updatedSince":{},"createdAfter":{},"view":{"type":"string","enum":["summary","full"]}}},{"method":"POST","path":"/api/v1/notes","description":"Create note (also creates linked activity for Noter tab / activity feed). Requires entity ref: company, personRef, leadRef, dealRef, or customObjectRecord"},{"method":"GET","path":"/api/v1/notes/:id","description":"Get note"},{"method":"PUT","path":"/api/v1/notes/:id","description":"Update note (syncs linked activity)"},{"method":"DELETE","path":"/api/v1/notes/:id","description":"Delete note (removes linked activity)"}]},"calls":{"title":"Calls","endpoints":[{"method":"GET","path":"/api/v1/calls","description":"List calls"},{"method":"POST","path":"/api/v1/calls","description":"Create call record (also creates call activity in feed)"},{"method":"GET","path":"/api/v1/calls/:id","description":"Get call by ID including transcriptText, transcriptSummary, and slim conversation turns (speaker/text/timing). Recording download URLs are JWT/session-only.","response":"{ data: { _id, subject, phone, status, direction, duration, transcriptText, transcriptSummary, transcriptStatus, hasTranscript, conversation[], conversationTruncated, languageCode, hasRecording, recordingPending, … } }"},{"method":"PUT","path":"/api/v1/calls/:id","description":"Update call (syncs linked activity)"},{"method":"DELETE","path":"/api/v1/calls/:id","description":"Delete call (removes linked activity)"}]},"events":{"title":"Events (calendar)","endpoints":[{"method":"GET","path":"/api/v1/events","description":"List events"},{"method":"POST","path":"/api/v1/events","description":"Create event"},{"method":"GET","path":"/api/v1/events/:id","description":"Get event"},{"method":"PUT","path":"/api/v1/events/:id","description":"Update event"},{"method":"DELETE","path":"/api/v1/events/:id","description":"Delete event"}]},"tags":{"title":"Tags","endpoints":[{"method":"GET","path":"/api/v1/tags","description":"List unique tags","queryParams":{"entityType":{"type":"string","enum":["company","person","deal","lead"]}}},{"method":"POST","path":"/api/v1/tags/bulk","description":"Bulk add/remove tags on multiple entities"}]},"customFields":{"title":"Custom Fields","description":"Three field systems: (1) CRM defs via /api/v1/custom-fields for lead/deal/company/person (and support ticket values via entity PUT). (2) Project/task defs via GET /api/v1/project-custom-fields. (3) Custom object fields via GET /api/v1/custom-objects/:slug. Entity values: PUT customFields:{key:value} on the entity (partial merge). Custom object values: PATCH record with field-slug keys (or customFields alias).","endpoints":[{"method":"GET","path":"/api/v1/custom-fields","description":"List CRM custom field definitions (lead, deal, company, person)","queryParams":{"entityType":{"type":"string","enum":["lead","deal","company","person"]}}},{"method":"POST","path":"/api/v1/custom-fields","description":"Create custom field definition"},{"method":"GET","path":"/api/v1/custom-fields/:id","description":"Get custom field"},{"method":"PUT","path":"/api/v1/custom-fields/:id","description":"Update custom field"},{"method":"DELETE","path":"/api/v1/custom-fields/:id","description":"Delete custom field"},{"method":"POST","path":"/api/v1/custom-fields/search","description":"Search entities by custom field values"},{"method":"GET","path":"/api/v1/custom-fields/search","description":"Returns search endpoint documentation"},{"method":"GET","path":"/api/v1/project-custom-fields","description":"List project/task custom field definitions (CustomFieldDefinition). Use keys when setting customFields on project/task PUT.","queryParams":{"entityType":{"type":"string","enum":["project","task"],"description":"Optional filter"},"forCreateDialog":{"type":"string","enum":["true","false"]}}}]},"flows":{"title":"Flows & Stages (Pipelines)","endpoints":[{"method":"GET","path":"/api/v1/flows","description":"List pipelines"},{"method":"POST","path":"/api/v1/flows","description":"Create pipeline"},{"method":"GET","path":"/api/v1/flows/:id","description":"Get pipeline"},{"method":"PUT","path":"/api/v1/flows/:id","description":"Update pipeline"},{"method":"DELETE","path":"/api/v1/flows/:id","description":"Delete pipeline"},{"method":"POST","path":"/api/v1/flows/:id/stages","description":"Create stage in pipeline"},{"method":"PUT","path":"/api/v1/flows/:id/stages","description":"Update stage","body":{"id":{"type":"string","required":true},"title":{"type":"string"},"order":{"type":"number"}}},{"method":"DELETE","path":"/api/v1/flows/:id/stages","description":"Delete stage","body":{"id":{"type":"string","required":true}}}]},"agents":{"title":"AI agents","description":"Custom AI agents. They do not own a cron. Start them from Automations with the Kør agent action. Schedule on create/update is always Via automation.","endpoints":[{"method":"GET","path":"/api/v1/agents","description":"List AI agents in the workspace"},{"method":"POST","path":"/api/v1/agents","description":"Create an AI agent. Enable it, then start it from an automation.","body":{"name":{"type":"string","required":true},"description":{"type":"string"},"goal":{"type":"string"},"systemPrompt":{"type":"string"},"icon":{"type":"string"},"modelTier":{"type":"string","description":"fast, balanced or thorough"},"enabled":{"type":"boolean"},"permissions":{"type":"object","description":"trustLevel, allowedModules, webAccess, maxActionsPerRun, maxDailyActions"}}},{"method":"GET","path":"/api/v1/agents/:id","description":"Get one AI agent"},{"method":"PUT","path":"/api/v1/agents/:id","description":"Replace agent fields"},{"method":"PATCH","path":"/api/v1/agents/:id","description":"Update agent fields"},{"method":"DELETE","path":"/api/v1/agents/:id","description":"Delete an AI agent and its runs"}]},"lists":{"title":"Lists & Items","endpoints":[{"method":"GET","path":"/api/v1/lists","description":"List all lists"},{"method":"POST","path":"/api/v1/lists","description":"Create list"},{"method":"GET","path":"/api/v1/lists/:id","description":"Get list"},{"method":"PUT","path":"/api/v1/lists/:id","description":"Update list"},{"method":"DELETE","path":"/api/v1/lists/:id","description":"Delete list"},{"method":"GET","path":"/api/v1/lists/:id/items","description":"List items in a list"},{"method":"POST","path":"/api/v1/lists/:id/items","description":"Add items to list"},{"method":"DELETE","path":"/api/v1/lists/:id/items","description":"Remove items from list"}]},"emails":{"title":"Emails","description":"List and send mailbox emails. Single send uses html and/or text (not bodyHtml). Bulk send is POST /api/v1/emails/bulk (queues by default). There is no signatureId on send: fetch a signature via GET /api/v1/email-signatures and inline its HTML into html.","endpoints":[{"method":"GET","path":"/api/v1/emails","description":"List emails with inbox account access enforced. Unknown query params return 400. Use view=summary for slim payloads."},{"method":"POST","path":"/api/v1/emails","description":"Send one email from a connected mailbox. Required: accountId, to, subject; html and/or text for content. bodyHtml is rejected on single send. No signatureId: inline signature HTML in html.","body":{"accountId":{"type":"string","required":true,"description":"Connected email account ID (GET /api/v1/email-accounts)"},"to":{"type":"string|string[]","required":true,"description":"Recipient address or list of addresses"},"subject":{"type":"string","required":true},"html":{"type":"string","description":"HTML body. Preferred for rich mail. Not bodyHtml (templates only on single send)."},"text":{"type":"string","description":"Plain-text body (optional; use with or instead of html)"},"cc":{"type":"string|string[]","description":"CC recipients"},"bcc":{"type":"string|string[]","description":"BCC recipients"},"linkPersonId":{"type":"string","description":"Link sent message to person"},"linkCompanyId":{"type":"string","description":"Link sent message to company"},"linkDealId":{"type":"string","description":"Link sent message to deal"},"trackOpens":{"type":"boolean","description":"Enable open tracking when supported"},"replyToMessageId":{"type":"string","description":"Existing message ID when this send is a reply"}},"response":"{ data } with HTTP 201 on success"},{"method":"POST","path":"/api/v1/emails/bulk","description":"Bulk email. Requires write scope, email.create, and admin/owner. Queues by default (process-email-queue cron). Set immediate=true for sync send (max 10). Max 500 recipients. Prefer html/text; bodyHtml is accepted as an html alias here.","body":{"accountId":{"type":"string","required":true,"description":"Connected email account ID"},"subject":{"type":"string","required":true},"html":{"type":"string","description":"Shared HTML body (alias: bodyHtml)"},"text":{"type":"string","description":"Shared plain-text body"},"recipients":{"type":"array","required":true,"description":"Email strings and/or { email, name?, personId?, companyId?, personalizedSubject?, personalizedHtml? } (to is accepted as email alias)"},"trackOpens":{"type":"boolean"},"immediate":{"type":"boolean","description":"If true and recipients <= 10, send synchronously; otherwise queue"}},"response":"{ data: { queued, queueId, summary } } or { data: { immediate, summary, results } } with HTTP 201"},{"method":"GET","path":"/api/v1/emails/:id","description":"Get email by ID"},{"method":"GET","path":"/api/v1/emails/:id/calendar-invite","description":"Get calendar invite for email"},{"method":"POST","path":"/api/v1/emails/:id/calendar-invite","description":"RSVP to calendar invite"},{"method":"GET","path":"/api/v1/emails/threads/:threadId","description":"Get email thread"}]},"emailAccounts":{"title":"Email Accounts","endpoints":[{"method":"GET","path":"/api/v1/email-accounts","description":"List connected email accounts"}]},"emailTemplates":{"title":"Email Templates","description":"Create and manage email templates. Send bodyHtml (+ optional bodyCss and layoutSettings); Salesbase renders the full document. compiledHtml is accepted as a legacy full-document input. Supports {{contact.*}} placeholders and default filter syntax {{field | default:\"fallback\"}}.","endpoints":[{"method":"GET","path":"/api/v1/email-templates","description":"List email templates"},{"method":"POST","path":"/api/v1/email-templates","description":"Create email template (admin scope)","body":{"name":{"type":"string","required":true},"category":{"type":"string","enum":["marketing","transactional","reminder","follow-up","notification","quote","newsletter","other"],"description":"Invalid values return 400 (not 500). Default when omitted: other."},"bodyHtml":{"type":"string","description":"Body content with {{placeholders}} (preferred; no document wrapper)"},"bodyCss":{"type":"string","description":"Optional CSS rules for bodyHtml"},"compiledHtml":{"type":"string","description":"Legacy: full HTML document with {{placeholders}}"},"plainText":{"type":"string"},"metadata":{"type":"object","description":"{ subject, preheader }"},"layoutSettings":{"type":"object","description":"width (full|fixed), align (left|center|right, fixed only), containerWidth, textAlign, padding*, backgroundColor, outerBackground, fullWidthBackground. Deprecated aliases accepted: layoutMode (minimal|styled), useTableWrapper, containerAlign, outerBackgroundColor"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/email-templates/:id","description":"Get template by ID (includes bodyHtml, bodyCss, compiledHtml, layoutSettings, projectData)"},{"method":"PATCH","path":"/api/v1/email-templates/:id","description":"Update template (admin scope)"},{"method":"DELETE","path":"/api/v1/email-templates/:id","description":"Delete template (admin scope)"}],"aiGuide":"/api/v1/docs?section=offerTemplates"},"sms":{"title":"SMS","description":"Requires API key scope `sms` (not covered by `write` alone). Message text field is body (aliases message and text). Sender field is fromNumber (alias from). Bulk send queues by default.","endpoints":[{"method":"GET","path":"/api/v1/sms","description":"List SMS messages. Unknown query params return 400. Use view=summary for slim payloads. Requires scope: sms."},{"method":"POST","path":"/api/v1/sms","description":"Send one SMS. Requires scope: sms. Canonical body fields: to, body; optional fromNumber, linkPersonId, linkCompanyId, linkDealId.","body":{"to":{"type":"string","required":true,"description":"Recipient phone (E.164 preferred)"},"body":{"type":"string","required":true,"description":"Plain-text SMS. Aliases: message, text. Not html/bodyHtml."},"fromNumber":{"type":"string","description":"Workspace Twilio number (alias: from). Defaults to workspace default."},"linkPersonId":{"type":"string"},"linkCompanyId":{"type":"string"},"linkDealId":{"type":"string"},"isAlphanumericSender":{"type":"boolean"}},"response":"{ data } with HTTP 201 on success"},{"method":"GET","path":"/api/v1/sms/:id","description":"Get SMS by ID (Mongo ObjectId). Requires scope: sms."},{"method":"PUT","path":"/api/v1/sms/:id","description":"Update SMS flags (unread, starred). Requires scope: sms."},{"method":"GET","path":"/api/v1/sms/conversations","description":"List SMS conversation threads (one row per counterparty phone). Alias of /sms/threads. Requires scope: sms."},{"method":"GET","path":"/api/v1/sms/threads","description":"List SMS conversation threads (same as /sms/conversations). Requires scope: sms."},{"method":"POST","path":"/api/v1/sms/bulk","description":"Bulk SMS. Requires scope: sms and admin/owner. Queues by default (process-sms-queue cron). Set immediate=true for sync send (max 50). Max 100 recipients.","body":{"body":{"type":"string","required":true,"description":"Shared SMS text. Aliases: message, text."},"recipients":{"type":"array","required":true,"description":"Phone strings and/or { phone, personId?, companyId? } objects (to is accepted as phone alias)"},"fromNumber":{"type":"string","description":"Alias: from"},"immediate":{"type":"boolean","description":"If true and recipients <= 50, send synchronously; otherwise queue"}},"response":"{ data: { queued, queueId, summary } } or { data: { immediate, summary, results } } with HTTP 201"},{"method":"POST","path":"/api/v1/sms/schedule","description":"Schedule SMS for later. Requires scope: sms.","body":{"to":{"type":"string","required":true},"body":{"type":"string","required":true,"description":"Aliases: message, text"},"scheduledAt":{"type":"string","required":true,"description":"Future ISO datetime"},"fromNumber":{"type":"string","description":"Alias: from"},"linkPersonId":{"type":"string"},"linkCompanyId":{"type":"string"},"linkDealId":{"type":"string"},"isAlphanumericSender":{"type":"boolean"}}},{"method":"GET","path":"/api/v1/sms/scheduled","description":"List scheduled SMS. Requires scope: sms."},{"method":"DELETE","path":"/api/v1/sms/scheduled/:id","description":"Cancel scheduled SMS. Requires scope: sms."},{"method":"GET","path":"/api/v1/sms/threads/:phoneNumber","description":"Get SMS thread by phone number (messages + resolved contact). Requires scope: sms."},{"method":"PUT","path":"/api/v1/sms/threads/:phoneNumber","description":"Mark inbound unread messages in the thread as read. Body: { unread: false }. Requires scope: sms.","body":{"unread":{"type":"boolean","description":"Set false to mark read"}}}]},"smsTemplates":{"title":"SMS Templates","endpoints":[{"method":"GET","path":"/api/v1/sms-templates","description":"List SMS templates"}]},"webhooks":{"title":"Webhook Subscriptions","description":"Subscribe to CRM events via outbound webhooks. See the Webhooks section for available events.","endpoints":[{"method":"GET","path":"/api/v1/webhooks/subscriptions","description":"List webhook subscriptions","response":"{ data: items[], total, availableEvents: string[] }"},{"method":"POST","path":"/api/v1/webhooks/subscriptions","description":"Create webhook subscription","body":{"url":{"type":"string","required":true},"events":{"type":"string[]","required":true,"description":"Array of event names to subscribe to"},"name":{"type":"string"},"active":{"type":"boolean","default":true}},"response":"{ data: { _id, url, events, name, active, secret, createdAt }, warning: 'Store the secret...' }"},{"method":"GET","path":"/api/v1/webhooks/subscriptions/:id","description":"Get subscription (secret masked)"},{"method":"PUT","path":"/api/v1/webhooks/subscriptions/:id","description":"Update subscription"},{"method":"DELETE","path":"/api/v1/webhooks/subscriptions/:id","description":"Delete subscription"},{"method":"POST","path":"/api/v1/webhooks/zapier","description":"Zapier inbound webhook — creates person, lead, and deal in one request (Bearer API key)","body":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"leadTitle":{"type":"string"},"dealTitle":{"type":"string"},"dealValue":{"type":"number"},"source":{"type":"string","default":"zapier"}},"response":"{ data: { person, lead, deal } }"}]},"crmConnect":{"title":"CRM Connect","description":"Lookup and connect CRM data with external systems","endpoints":[{"method":"GET","path":"/api/v1/crm-connect/lookup","description":"Look up entity by phone/email"},{"method":"POST","path":"/api/v1/crm-connect/calls","description":"Log or connect call events for integration"}]},"workspaceOfferSettings":{"title":"Workspace offer settings","description":"Workspace sender profile and offer defaults used by {{company.*}} and {{bank.*}} placeholders, plus communication, numbering, units, and contract defaults. PATCH merges nested objects and replaces arrays. Write requires a workspace admin or owner (write scope). Products, offer types, templates, and offers are separate resources.","endpoints":[{"method":"GET","path":"/api/v1/workspace/offer-settings","description":"Read workspace offer settings. Creates defaults on first read."},{"method":"PATCH","path":"/api/v1/workspace/offer-settings","description":"Update companyInfo (name, cvr, address.street/postalCode/city, phone, email, website, logo), bankInfo (bankName, registrationNumber, accountNumber, iban, swift), defaults, branding, emailSettings, smsSettings, acceptanceSettings, trackingSettings, sequenceSettings, orderConfirmationSettings, numbering, contractSettings, defaultTerms, defaultPaymentTerms, lineItemUnits, billingFrequencies, setupCompleted. Admin or owner.","body":{"companyInfo":{"type":"object","description":"Sender profile. Maps to {{company.name}}, {{company.cvr}}, {{company.phone}}, {{company.email}}, {{company.street}}, {{company.postalCode}}, {{company.city}}, {{company.logo}}"},"bankInfo":{"type":"object","description":"bankName, registrationNumber, accountNumber, iban, swift. Maps to {{bank.name}}, {{bank.reg}}, {{bank.account}}, {{bank.iban}}, {{bank.swift}}"},"defaults":{"type":"object","description":"vatRate, currency, defaultValidityDays, paymentTerms, defaultTemplateId, defaultOfferTypeId"},"numbering":{"type":"object","description":"{ prefix, nextNumber }"},"lineItemUnits":{"type":"array","description":"[{ value, label, sortOrder }]. Replaces the list."},"billingFrequencies":{"type":"array","description":"Replaces workspace billing periods."}}}]},"offers":{"title":"Offers (quotes/proposals)","description":"Create draft offers linked to offer types and templates. The visual layout comes from the template (native elements, not a HTML page on the offer). Prices are lineItems, rendered by the template line_items and totals elements. Send creates an immutable snapshot and public link.","endpoints":[{"method":"GET","path":"/api/v1/offers","description":"List offers"},{"method":"POST","path":"/api/v1/offers","description":"Create offer","body":{"title":{"type":"string","required":true},"offerTypeId":{"type":"ObjectId"},"templateId":{"type":"ObjectId"},"companyRef":{"type":"ObjectId"},"personRef":{"type":"ObjectId"},"dealRef":{"type":"ObjectId"},"quoteInput":{"type":"object","description":"{ contact, address, facts, calculatorInputs }"},"lineItems":{"type":"array","description":"Preferred write field for offer lines. Each item: name (or description), quantity, unitPrice, optional unit/vatRate. Server generates id and totals."},"calculatorResult":{"type":"object","description":"Readback/result shape { lineItems, subtotal, vatAmount, total }. lineItems without id/name are normalized on write (same as top-level lineItems)."},"validUntil":{"type":"Date"}}},{"method":"GET","path":"/api/v1/offers/:id","description":"Get offer (by _id or publicId)"},{"method":"PUT","path":"/api/v1/offers/:id","description":"Update draft offer","body":{"title":{"type":"string"},"templateId":{"type":"ObjectId"},"offerTypeId":{"type":"ObjectId"},"quoteInput":{"type":"object"},"lineItems":{"type":"array","description":"Replace draft offer lines. Same shape as POST."},"calculatorResult":{"type":"object"}}},{"method":"DELETE","path":"/api/v1/offers/:id","description":"Delete draft offer"},{"method":"POST","path":"/api/v1/offers/:id/send","description":"Send offer — snapshot, activity, automations, offer.sent webhook. Returns publicId.","response":"{ data: { _id, status: 'sent', sentAt, publicId, snapshotId } }"},{"method":"POST","path":"/api/v1/offers/:id/cancel","description":"Cancel offer"},{"method":"POST","path":"/api/v1/offers/:id/send-email","description":"Send or resend offer email to customer","body":{"emailTo":{"type":"string","description":"Optional recipient override"},"senderAccountId":{"type":"ObjectId","description":"Optional sender email account"},"templateId":{"type":"ObjectId","description":"Optional email template"}}},{"method":"POST","path":"/api/v1/offers/:id/send-sms","description":"Send or resend offer link via SMS","body":{"phoneTo":{"type":"string","description":"Optional phone override"}}},{"method":"GET","path":"/api/v1/offers/:id/preview-token","description":"Preview URL for draft offers (short-lived token) or public link when sent"}],"aiGuide":"/api/v1/docs?section=offerTemplates"},"contracts":{"title":"Contracts (e-signature envelopes)","description":"Contracts are offers with kind=contract and a multi-signer envelope. Send delivers per-signer links (sequential or parallel). Status accepted means fully signed. Signer tokens are never exposed.","endpoints":[{"method":"GET","path":"/api/v1/contracts","description":"List contracts"},{"method":"POST","path":"/api/v1/contracts","description":"Create draft contract with signers","body":{"title":{"type":"string"},"templateId":{"type":"ObjectId","description":"Contract template (kind=contract)"},"companyRef":{"type":"ObjectId"},"personRef":{"type":"ObjectId"},"dealRef":{"type":"ObjectId"},"signers":{"type":"array","required":true,"description":"[{ name, email, phone?, title?, role?: 'signer'|'cc' }] in signing order"},"mode":{"type":"string","description":"sequential (default) | parallel"},"signatureLevel":{"type":"string","description":"standard (default) | mitid"},"deadlineAt":{"type":"Date","description":"Signing deadline"},"contract":{"type":"object","description":"{ startDate, endDate } agreement period"},"lineItems":{"type":"array","description":"Optional price lines"},"sourceOfferId":{"type":"ObjectId","description":"Accepted offer this contract was generated from"}}},{"method":"GET","path":"/api/v1/contracts/:id","description":"Get contract incl. envelope status and signer progress"},{"method":"POST","path":"/api/v1/contracts/:id/send","description":"Send to signers (snapshot + links). Webhooks: contract.sent, contract.signed, contract.completed, contract.declined, contract.expired, contract.voided","body":{"channels":{"type":"object","description":"{ email: true, sms: false }"},"message":{"type":"string"}}},{"method":"POST","path":"/api/v1/contracts/:id/void","description":"Void (annullér) a sent contract","body":{"reason":{"type":"string"},"notifySigners":{"type":"boolean"}}}]},"offerTemplates":{"title":"Offer Templates (tilbudsskabeloner)","description":"Visual page-builder templates with pages[], headerElements, footerElements, and branding. Build pages from native elements (text, heading, image, table, columns, line_items, totals, button, divider, spacer, signer fields). See ?section=offerTemplates&subsection=nativeElements before writing layout. type html is a last resort.","endpoints":[{"method":"GET","path":"/api/v1/offer-templates","description":"List templates (summary without full pages)"},{"method":"POST","path":"/api/v1/offer-templates","description":"Create template with full layout (admin scope)","body":{"name":{"type":"string","required":true},"pages":{"type":"array","description":"Page[] with native elements. Catalog: ?section=offerTemplates&subsection=nativeElements. Do not default pages to html."},"headerElements":{"type":"array"},"footerElements":{"type":"array"},"branding":{"type":"object","description":"colors, typography, logo"},"defaultTerms":{"type":"string"},"defaultPaymentTerms":{"type":"string"},"isDefault":{"type":"boolean"}}},{"method":"GET","path":"/api/v1/offer-templates/:id","description":"Get full template including pages and elements"},{"method":"PATCH","path":"/api/v1/offer-templates/:id","description":"Update template (admin scope)"},{"method":"DELETE","path":"/api/v1/offer-templates/:id","description":"Delete template (admin scope)"},{"method":"POST","path":"/api/v1/offer-templates/:id/duplicate","description":"Duplicate template (admin scope)"}],"aiGuide":"/api/v1/docs?section=offerTemplates"},"offerTypes":{"title":"Offer Types (Prisregler)","description":"Prisregler: seller questions (requiredInputFields, incl. computed m² fields) and Hvis … så … pricing rules. Workflow: rule-reference → draft → validate → simulate → POST/PATCH. Full guide: ?section=pricingRules.","endpoints":[{"method":"GET","path":"/api/v1/offer-types","description":"List prisregler (offer types)","queryParams":{"page":{"type":"number"},"limit":{"type":"number","description":"max 100"},"includeInactive":{"type":"boolean","description":"true to include inactive"}}},{"method":"POST","path":"/api/v1/offer-types","description":"Create prisregler (admin). Validated like /offer-types/validate; 400 returns details[] with path + error. Rule id/order/phase are filled in when omitted.","body":{"name":{"type":"string","required":true},"description":{"type":"string"},"isDefault":{"type":"boolean","description":"Preselect on new offers"},"defaultTemplateId":{"type":"ObjectId","description":"OfferTemplate _id"},"requiredInputFields":{"type":"array","description":"[{ fieldId, label, type: number|text|select|checkbox|radio|slider|computed, unit?, options?, required?, expression? }]. See ?section=pricingRules&subsection=fieldTypes"},"pricingRules":{"type":"object","description":"{ rules: [{ name, when: { all, any }, then }], vatRate?, currency?, vatDisplayMode? }. See ?section=pricingRules&subsection=actionTypes"}}},{"method":"GET","path":"/api/v1/offer-types/rule-reference","description":"Builder vocabulary for this workspace: products (_id, name, unitPrice), subjects + keys (BBR only when the module is on), operators, action types, expression ops"},{"method":"GET","path":"/api/v1/offer-types/rule-templates","description":"Starter rules (Start fra skabelon) with fresh ids, the fields each needs, and placeholders to fill (product ids)","queryParams":{"industry":{"type":"string","description":"IT | Håndværk | Generelt"}}},{"method":"POST","path":"/api/v1/offer-types/validate","description":"Read-only. Validate a draft { requiredInputFields, pricingRules } and get errors[], warnings[] and Danish ruleSummaries. Nothing is saved.","body":{"requiredInputFields":{"type":"array"},"pricingRules":{"type":"object"}}},{"method":"POST","path":"/api/v1/offer-types/simulate","description":"Read-only dry run of the engine. Returns lines, subtotal, computedFields, warnings, blocks and trace. Nothing is saved.","body":{"offerTypeId":{"type":"ObjectId","description":"Saved prisregler (or send offerType)"},"offerType":{"type":"object","description":"Unsaved draft { requiredInputFields, pricingRules }"},"fields":{"type":"object","description":"{ fieldId: value } answers"},"lines":{"type":"array","description":"[{ offerProductId, quantity, unitPrice? } | { name, unitPrice, quantity }]"},"customer":{"type":"object","description":"{ industry, country, postalCode, isExisting }"},"offer":{"type":"object","description":"{ manualDiscountPercent }"}}},{"method":"GET","path":"/api/v1/offer-types/:id","description":"Get prisregler. ?describe=true adds ruleSummaries (Danish Hvis … så … text per rule)"},{"method":"PATCH","path":"/api/v1/offer-types/:id","description":"Update prisregler (admin). pricingRules merges with existing VAT/currency settings; pricingRules.rules and requiredInputFields replace the whole array. Validated when either is sent."},{"method":"DELETE","path":"/api/v1/offer-types/:id","description":"Delete prisregler (admin)"}],"aiGuide":"/api/v1/docs?section=pricingRules"},"products":{"title":"Products & Catalog","endpoints":[{"method":"GET","path":"/api/v1/products","description":"List products"},{"method":"POST","path":"/api/v1/products","description":"Create product"},{"method":"GET","path":"/api/v1/products/:id","description":"Get product"},{"method":"PUT","path":"/api/v1/products/:id","description":"Update product"},{"method":"DELETE","path":"/api/v1/products/:id","description":"Delete product"},{"method":"GET","path":"/api/v1/catalog","description":"List catalog items"},{"method":"POST","path":"/api/v1/catalog","description":"Create catalog item"},{"method":"GET","path":"/api/v1/catalog/:id","description":"Get catalog item"},{"method":"PUT","path":"/api/v1/catalog/:id","description":"Update catalog item"},{"method":"DELETE","path":"/api/v1/catalog/:id","description":"Delete catalog item"}]},"projects":{"title":"Projects","endpoints":[{"method":"GET","path":"/api/v1/projects","description":"List projects"},{"method":"POST","path":"/api/v1/projects","description":"Create project"},{"method":"GET","path":"/api/v1/projects/:id","description":"Get project"},{"method":"PUT","path":"/api/v1/projects/:id","description":"Update project. customFields is a partial key→value merge (definitions: GET /api/v1/project-custom-fields?entityType=project).","body":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"owner":{"type":"string"},"tags":{"type":"string[]"},"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/projects/:id","description":"Delete project"},{"method":"POST","path":"/api/v1/projects/:id/move","description":"Move project to different stage"},{"method":"GET","path":"/api/v1/projects/:id/stats","description":"Get project statistics"},{"method":"GET","path":"/api/v1/projects/:id/tasks","description":"List project tasks"},{"method":"POST","path":"/api/v1/projects/:id/tasks","description":"Create project task"},{"method":"GET","path":"/api/v1/projects/tasks/:taskId","description":"Get project task"},{"method":"PUT","path":"/api/v1/projects/tasks/:taskId","description":"Update project task. Send stage (from GET /api/v1/task-flow) or status. status done sets stage to completedStageId. The task list Status column and done checkbox read stage, not status alone. customFields is a partial key→value merge.","body":{"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"],"description":"Coarse status. done also sets stage to completedStageId."},"stage":{"type":"number","description":"Stage id from GET /api/v1/task-flow. Wins over status when both are sent. Use completedStageId to mark Færdig."},"priority":{"type":"string","enum":["low","medium","high","critical"]},"owner":{"type":"string","description":"User ID"},"assignees":{"type":"string[]"},"startDate":{"type":"string"},"deadline":{"type":"string"},"estimatedHours":{"type":"number"},"taskType":{"type":"string"},"billable":{"type":"boolean"},"tags":{"type":"string[]"},"order":{"type":"number"},"customFields":{"type":"object","description":"Partial key → typed value merge. Definitions: GET /api/v1/project-custom-fields?entityType=task"}}},{"method":"DELETE","path":"/api/v1/projects/tasks/:taskId","description":"Delete project task"},{"method":"POST","path":"/api/v1/projects/tasks/:taskId/move","description":"Move a project task to another task-flow stage. stage or status is enough. status done sets stage to completedStageId from GET /api/v1/task-flow.","body":{"stage":{"type":"number","description":"Stage id from GET /api/v1/task-flow. Sets status (completedStageId becomes done)."},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"],"description":"Coarse status. Also sets stage. Ignored for status when stage is sent."},"order":{"type":"number","description":"Order within the stage"}}},{"method":"GET","path":"/api/v1/task-flow","description":"Get the workspace task flow (opgavestatusser), including stages and completedStageId. Use completedStageId as stage when marking a project task Færdig so the list Status column and done checkbox update.","response":"{ data: { name, stages: [{ id, title, order, color }], completedStageId } }"},{"method":"GET","path":"/api/v1/projects/tasks/:taskId/comments","description":"List comments on a project task, including internal team notes. Each item has internal (boolean) and visibility (customer or internal). Internal notes are never shown in the customer portal. Task must belong to a project.","queryParams":{"visibility":{"type":"string","enum":["customer","internal"],"description":"Optional. customer returns the portal thread only. internal returns team notes only. Omit to return both."}}},{"method":"POST","path":"/api/v1/projects/tasks/:taskId/comments","description":"Create a team comment on a project task. Without internal, it is the same thread as CRM Kommentarer and the customer portal when comments are enabled. With internal: true or visibility: internal, the note is team-only: hidden in the customer portal and not emailed to the customer.","body":{"content":{"type":"string","required":true,"description":"Comment text. HTML is stripped. Max 5000 characters."},"internal":{"type":"boolean","description":"True saves an internal team note. False or omitted is visible to the customer when portal comments are on."},"visibility":{"type":"string","enum":["customer","internal"],"description":"customer (default) or internal. Same as internal: true when set to internal. Do not contradict the internal flag."}}},{"method":"PATCH","path":"/api/v1/projects/tasks/:taskId/comments/:commentId","description":"Update a project task comment (admin/owner). Change content and/or move between customer thread and internal notes via internal/visibility. Requires content and/or internal.","body":{"content":{"type":"string","description":"New text. HTML stripped. Max 5000 characters."},"internal":{"type":"boolean","description":"true = internal team note; false = customer-visible thread"},"visibility":{"type":"string","enum":["customer","internal"],"description":"Same meaning as internal flag"}}},{"method":"DELETE","path":"/api/v1/projects/tasks/:taskId/comments/:commentId","description":"Delete a project task comment (admin/owner)."},{"method":"GET","path":"/api/v1/projects/:id/time-entries","description":"List time entries on a project.","queryParams":{"taskId":{"type":"string","description":"Only entries attached to this project task"},"page":{"type":"number"},"limit":{"type":"number"}}},{"method":"POST","path":"/api/v1/projects/:id/time-entries","description":"Register time on a project (and optionally a task). Uses the same rate, clip-card, and min-words rules as the UI timer.","body":{"hours":{"type":"number","required":true,"description":"Hours to log. Can be combined with minutes. Must be greater than 0."},"minutes":{"type":"number","description":"Optional extra minutes added to hours (e.g. 30 = 0.5 hours)."},"taskId":{"type":"string","description":"Optional project task to attach the time entry to."},"description":{"type":"string","description":"Work description. Required when the project has timeEntryMinWords set."},"date":{"type":"string","description":"ISO date of the work. Defaults to now."},"billable":{"type":"boolean","description":"Default true. Non-billable entries still consume clip-card hours."}}},{"method":"PUT","path":"/api/v1/projects/time-entries/:entryId","description":"Update time entry"},{"method":"DELETE","path":"/api/v1/projects/time-entries/:entryId","description":"Delete time entry"},{"method":"GET","path":"/api/v1/projects/:id/expenses","description":"List expenses"},{"method":"POST","path":"/api/v1/projects/:id/expenses","description":"Create expense"}]},"bookings":{"title":"Bookings","description":"Authenticated workspace API (API key). Create/cancel/reschedule/slots use the same pipeline as the public booking page. For a custom website UI without an API key, see GET /api/v1/docs?section=publicBooking.","endpoints":[{"method":"GET","path":"/api/v1/bookings","description":"List bookings. dateFrom/dateTo filter createdAt. startFrom/startTo filter startDateTime (when the meeting starts).","queryParams":{"status":{"type":"string"},"dateFrom":{"type":"string","description":"createdAt >= ISO"},"dateTo":{"type":"string","description":"createdAt <= ISO"},"startFrom":{"type":"string","description":"startDateTime >= ISO"},"startTo":{"type":"string","description":"startDateTime <= ISO"},"sortBy":{"type":"string","enum":["startDateTime","endDateTime","createdAt","status"]},"view":{"type":"string","enum":["summary","full"]}}},{"method":"POST","path":"/api/v1/bookings","description":"Create booking (slot check, calendar, CRM, emails, automations, webhooks)","body":{"configId":{"type":"string","required":true},"slotStart":{"type":"string","required":true,"description":"ISO datetime"},"attendeeInfo":{"type":"object","required":true,"description":"{ name, email, phone?, notes? }"},"meetingTypeId":{"type":"string"},"selectedAgentId":{"type":"string"},"customFieldsData":{"type":"object"},"invitedParticipants":{"type":"array"}}},{"method":"GET","path":"/api/v1/bookings/:id","description":"Get booking by ID or publicId"},{"method":"POST","path":"/api/v1/bookings/:id/cancel","description":"Cancel booking (calendar + notifications)"},{"method":"POST","path":"/api/v1/bookings/:id/approve","description":"Approve pending booking","body":{"notes":{"type":"string"}}},{"method":"POST","path":"/api/v1/bookings/:id/reject","description":"Reject pending booking","body":{"reason":{"type":"string"}}},{"method":"POST","path":"/api/v1/bookings/:id/reschedule","description":"Reschedule booking","body":{"newSlotStart":{"type":"string","required":true}}},{"method":"GET","path":"/api/v1/bookings/available-slots","description":"Get available time slots for a booking config","queryParams":{"configId":{"type":"string","required":true},"startDate":{"type":"string","required":true},"endDate":{"type":"string","required":true},"meetingTypeId":{"type":"string"}}},{"method":"GET","path":"/api/v1/booking-configs","description":"List booking configurations"},{"method":"GET","path":"/api/v1/booking-configs/:id","description":"Get booking configuration"},{"method":"PATCH","path":"/api/v1/booking-configs/:id","description":"Update booking configuration (admin scope)"}]},"reports":{"title":"Reports","endpoints":[{"method":"GET","path":"/api/v1/reports/pipeline","description":"Pipeline report. byStage entries include stageId + stageName (stage keeps the name for back-compat).","response":"{ data: { total, totalValue, byStage: [{ stageId, stageName, stage, count, value }], byStatus, winRate, avgDealValue, avgDaysToClose } }"},{"method":"GET","path":"/api/v1/reports/leads","description":"Leads report"},{"method":"GET","path":"/api/v1/reports/offers","description":"Offers report"},{"method":"GET","path":"/api/v1/reports/support","description":"Support report"},{"method":"GET","path":"/api/v1/reports/history","description":"History/activity report (daily counts)","queryParams":{"stat":{"type":"string","required":true,"enum":["deals","leads","activities","companies"]},"startDate":{"type":"string","required":true,"description":"ISO date (aliases: dateFrom, from)"},"endDate":{"type":"string","required":true,"description":"ISO date (aliases: dateTo, to)"},"userId":{"type":"string"}}},{"method":"GET","path":"/api/v1/reports/organization","description":"Organization overview"},{"method":"GET","path":"/api/v1/reports/users","description":"Users report"},{"method":"GET","path":"/api/v1/reports/users/:userId","description":"Individual user report"}]},"leadScoring":{"title":"Lead Scoring","description":"Rules that compute lead.score (0-100) on create/update. Score is not writable on POST/PUT /leads. Admin required to create/update/delete rules.","endpoints":[{"method":"GET","path":"/api/v1/lead-scoring/rules","description":"List lead scoring rules (sorted by sortOrder)","response":"{ data: LeadScoringRule[] }"},{"method":"POST","path":"/api/v1/lead-scoring/rules","description":"Create scoring rule. Admin/owner required.","body":{"name":{"type":"string","required":true,"description":"Unique rule name in the workspace"},"description":{"type":"string"},"field":{"type":"string","required":true,"description":"Eval field: hasEmail, hasPhone, hasValue, source, contactType, value, tags, customFields.<key>, contact.<field>, etc."},"condition":{"type":"string","required":true,"enum":["exists","not_exists","equals","not_equals","contains","not_contains","greater_than","less_than","in","not_in"]},"value":{"type":"mixed","description":"Comparison value (omit for exists/not_exists). Arrays for in/not_in."},"points":{"type":"number","required":true,"description":"Points to add (positive) or subtract (negative). Total clamped 0-100."},"isActive":{"type":"boolean","default":true},"sortOrder":{"type":"number","default":0}},"response":"{ data: LeadScoringRule }"},{"method":"PUT","path":"/api/v1/lead-scoring/rules/:id","description":"Update scoring rule (partial). Admin/owner required.","body":{"name":{"type":"string"},"description":{"type":"string"},"field":{"type":"string"},"condition":{"type":"string"},"value":{"type":"mixed"},"points":{"type":"number"},"isActive":{"type":"boolean"},"sortOrder":{"type":"number"}},"response":"{ data: LeadScoringRule }"},{"method":"DELETE","path":"/api/v1/lead-scoring/rules/:id","description":"Delete scoring rule. Admin/owner required.","response":"{ data: { deleted: true } }"}]},"leadRouting":{"title":"Lead Routing","endpoints":[{"method":"GET","path":"/api/v1/lead-routing/rules","description":"List lead routing rules"},{"method":"POST","path":"/api/v1/lead-routing/rules","description":"Create routing rule"},{"method":"PUT","path":"/api/v1/lead-routing/rules/:id","description":"Update routing rule"},{"method":"DELETE","path":"/api/v1/lead-routing/rules/:id","description":"Delete routing rule"}]},"importExport":{"title":"Import & Export","description":"Import requires API key scope `import`; export requires `export`. Neither is covered by `write` alone. Admin/owner role is also required.","endpoints":[{"method":"POST","path":"/api/v1/import/companies","description":"Import companies from CSV/data. Requires scope: import."},{"method":"POST","path":"/api/v1/import/people","description":"Import people. Requires scope: import."},{"method":"POST","path":"/api/v1/import/leads","description":"Import leads. Requires scope: import."},{"method":"POST","path":"/api/v1/import/deals","description":"Import deals. Requires scope: import."},{"method":"POST","path":"/api/v1/import/validate","description":"Validate import data before importing. Requires scope: import."},{"method":"GET","path":"/api/v1/import/history","description":"List import history. Requires scope: import."},{"method":"POST","path":"/api/v1/export/leads","description":"Export leads (POST only). Body: limit (max 10000), page, skip, cursor (_id), sortBy, sortOrder, fields, dateFrom, dateTo, filters. total is workspace match count. Requires scope: export. Admin/owner role required."},{"method":"POST","path":"/api/v1/export/deals","description":"Export deals (POST only). Body: limit, page, skip, cursor, sortBy, sortOrder, fields, dateFrom, dateTo, filters. Requires scope: export. Admin/owner role required."},{"method":"POST","path":"/api/v1/export/people","description":"Export people (POST only). Body: limit, page, skip, cursor, sortBy, sortOrder, fields, dateFrom, dateTo, filters. Requires scope: export. Admin/owner role required."},{"method":"POST","path":"/api/v1/export/companies","description":"Export companies (POST only). Body: limit (max 10000), page, skip, cursor (_id of last row), sortBy, sortOrder, fields (any stored field), dateFrom, dateTo, filters. Returns total, hasMore, nextCursor. Requires scope: export. Admin/owner role required."},{"method":"POST","path":"/api/v1/export/lists/:listId","description":"Export list items (POST only). Requires scope: export. Admin/owner role required."},{"method":"POST","path":"/api/v1/export/custom-objects/:slug","description":"Export custom object records (POST only). Requires scope: export. Admin/owner role required."}]},"files":{"title":"Files & Folders","endpoints":[{"method":"GET","path":"/api/v1/files","description":"List files"},{"method":"POST","path":"/api/v1/files","description":"Upload file"},{"method":"GET","path":"/api/v1/files/:id","description":"Get file metadata"},{"method":"DELETE","path":"/api/v1/files/:id","description":"Delete file"},{"method":"POST","path":"/api/v1/files/:id/link","description":"Link file to entity"},{"method":"DELETE","path":"/api/v1/files/:id/link","description":"Unlink file from entity"},{"method":"GET","path":"/api/v1/files/cloud/accounts","description":"List accessible Google Drive / OneDrive accounts"},{"method":"GET","path":"/api/v1/files/cloud/browse","description":"Browse or search cloud files (parentId, driveId, query, recent, roots=true for shared drives, withPath)"},{"method":"POST","path":"/api/v1/files/cloud/link","description":"Link cloud files to an entity"},{"method":"POST","path":"/api/v1/files/cloud/create","description":"Create a cloud document in Salesbase/<entity> and link it"},{"method":"GET","path":"/api/v1/folders","description":"List folders"},{"method":"POST","path":"/api/v1/folders","description":"Create folder"},{"method":"DELETE","path":"/api/v1/folders/:id","description":"Delete folder"}]},"users":{"title":"Users (Team Members)","description":"Manage workspace members","endpoints":[{"method":"GET","path":"/api/v1/users","description":"List workspace members","queryParams":{"page":{},"limit":{},"search":{"type":"string"}}},{"method":"GET","path":"/api/v1/users/:id","description":"Get user by ID"},{"method":"PUT","path":"/api/v1/users/:id","description":"Update user (name, phone)","body":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phone":{"type":"string"},"defaultUserPhone":{"type":"string"}}}]},"supportTickets":{"title":"Support Tickets","description":"Authenticated workspace API (API key) for the agent inbox. For a custom website chat UI without an API key, see GET /api/v1/docs?section=publicChat. Category is a workspace-defined string from the support portal Kategori field (GET /api/v1/support-tickets/categories). Public visitors use the portal form, not this API.","endpoints":[{"method":"GET","path":"/api/v1/support-tickets","description":"List support tickets","queryParams":{"page":{},"limit":{},"status":{"type":"string"},"priority":{"type":"string"},"assignee":{"type":"string"},"category":{"type":"string","description":"Workspace-defined category value"},"search":{"type":"string"}}},{"method":"GET","path":"/api/v1/support-tickets/categories","description":"List ticket category options for this workspace (from support portal Felter → Kategori). Use these values in POST/PUT category.","response":"{ data: [{ value: string, label: string }] }"},{"method":"POST","path":"/api/v1/support-tickets","description":"Create support ticket. Requires an existing support flow. category is a workspace-defined string (GET /categories). Defaults to the first portal category, or general.","body":{"title":{"type":"string","required":true},"description":{"type":"string"},"priority":{"type":"string","enum":["low","medium","high","urgent"]},"category":{"type":"string","description":"Workspace-defined category. Use GET /api/v1/support-tickets/categories."},"assignee":{"type":"string","description":"User ID"},"company":{"type":"string","description":"Company ID"},"personRef":{"type":"string","description":"Person ID"},"tags":{"type":"string[]"}}},{"method":"GET","path":"/api/v1/support-tickets/:id","description":"Get support ticket by ID"},{"method":"PUT","path":"/api/v1/support-tickets/:id","description":"Update support ticket. customFields is a partial key→value merge.","body":{"title":{"type":"string"},"description":{"type":"string"},"priority":{"type":"string"},"status":{"type":"string","enum":["open","in_progress","waiting","resolved","closed"]},"category":{"type":"string","description":"Workspace-defined category. Use GET /api/v1/support-tickets/categories."},"assignee":{"type":"string"},"tags":{"type":"string[]"},"resolution":{"type":"string"},"customFields":{"type":"object","description":"Partial key → typed value merge"}}},{"method":"DELETE","path":"/api/v1/support-tickets/:id","description":"Delete support ticket"},{"method":"POST","path":"/api/v1/support-tickets/:id/reply","description":"Add reply to ticket","body":{"content":{"type":"string","required":true},"type":{"type":"string","enum":["agent_reply","internal_note"],"default":"agent_reply"},"visibility":{"type":"string","enum":["public","internal"],"default":"public"}}},{"method":"POST","path":"/api/v1/support-tickets/:id/move","description":"Move ticket to kanban stage","body":{"stageId":{"type":"number","required":true},"flowId":{"type":"string","description":"Optional support flow ID"}}},{"method":"GET","path":"/api/v1/support-tickets/:id/messages","description":"List ticket messages","queryParams":{"limit":{"type":"number","default":50},"visibility":{"type":"string","enum":["public","internal"]}}},{"method":"GET","path":"/api/v1/support-tickets/:id/time-entries","description":"List time entries on ticket"},{"method":"POST","path":"/api/v1/support-tickets/:id/time-entries","description":"Create time entry on ticket"},{"method":"GET","path":"/api/v1/support-flow","description":"Get (or lazily create) the workspace's default support flow (kanban stages)"},{"method":"GET","path":"/api/v1/support-customers","description":"List support customers (grouped identity across tickets by person, email, or chat visitor)","queryParams":{"page":{},"limit":{},"search":{"type":"string"},"q":{"type":"string","description":"Alias for search"},"sortBy":{"type":"string","enum":["lastActivityAt","totalTickets","openTickets","unread"],"default":"lastActivityAt"},"sortOrder":{"type":"string","enum":["asc","desc"],"default":"desc"}}},{"method":"GET","path":"/api/v1/support-customers/detail","description":"Get full detail for one support customer: tickets, time stats, and time entries","queryParams":{"type":{"type":"string","required":true,"enum":["person","email","visitor"]},"value":{"type":"string","required":true,"description":"personRef ID, email address, or chatVisitorRef ID depending on type"}}}]},"filterPresets":{"title":"Filter Presets","description":"Saved filter configurations for lists and views","endpoints":[{"method":"GET","path":"/api/v1/filter-presets","description":"List filter presets","queryParams":{"area":{"type":"string","description":"Filter by area (e.g. companies, leads, deals)"},"page":{},"limit":{}}},{"method":"POST","path":"/api/v1/filter-presets","description":"Create filter preset","body":{"name":{"type":"string","required":true},"area":{"type":"string"},"columnFilters":{"type":"object"},"filterTree":{"type":"object"},"filterMode":{"type":"string","enum":["and","or"]},"sorting":{"type":"array"},"visibility":{"type":"string","enum":["workspace","private","group","user"]}}},{"method":"GET","path":"/api/v1/filter-presets/:id","description":"Get filter preset"},{"method":"PUT","path":"/api/v1/filter-presets/:id","description":"Update filter preset"},{"method":"DELETE","path":"/api/v1/filter-presets/:id","description":"Delete filter preset"}]},"emailSignatures":{"title":"Email Signatures","endpoints":[{"method":"GET","path":"/api/v1/email-signatures","description":"List email signatures"},{"method":"POST","path":"/api/v1/email-signatures","description":"Create signature","body":{"name":{"type":"string","required":true},"content":{"type":"string","required":true,"description":"HTML content"},"textContent":{"type":"string"}}},{"method":"GET","path":"/api/v1/email-signatures/:id","description":"Get signature"},{"method":"PUT","path":"/api/v1/email-signatures/:id","description":"Update signature"},{"method":"DELETE","path":"/api/v1/email-signatures/:id","description":"Delete signature"}]},"clipCards":{"title":"Clip Cards (Prepaid Hours)","description":"Manage prepaid service hour cards for projects","endpoints":[{"method":"GET","path":"/api/v1/clip-cards","description":"List clip cards","queryParams":{"page":{},"limit":{},"status":{"type":"string","enum":["active","depleted","expired","cancelled"]},"projectId":{"type":"string"}}},{"method":"POST","path":"/api/v1/clip-cards","description":"Create clip card","body":{"project":{"type":"string","required":true,"description":"Project ID"},"title":{"type":"string"},"totalHours":{"type":"number","required":true},"hourlyRate":{"type":"number","required":true},"allowOverdraft":{"type":"boolean"},"overdraftRate":{"type":"number"},"expiresAt":{"type":"string","description":"ISO date"},"notes":{"type":"string"}}},{"method":"GET","path":"/api/v1/clip-cards/:id","description":"Get clip card"},{"method":"PUT","path":"/api/v1/clip-cards/:id","description":"Update clip card"},{"method":"DELETE","path":"/api/v1/clip-cards/:id","description":"Delete clip card"}]},"dashboardLayouts":{"title":"Dashboard Layouts","description":"Manage saved dashboard configurations","endpoints":[{"method":"GET","path":"/api/v1/dashboard-layouts","description":"List dashboard layouts"},{"method":"POST","path":"/api/v1/dashboard-layouts","description":"Create layout","body":{"name":{"type":"string","required":true},"layout":{"type":"object"},"visibleComponents":{"type":"string[]"},"chartWidgets":{"type":"array"},"visibility":{"type":"string","enum":["workspace","private","group","user"]}}},{"method":"GET","path":"/api/v1/dashboard-layouts/:id","description":"Get layout"},{"method":"PUT","path":"/api/v1/dashboard-layouts/:id","description":"Update layout"},{"method":"DELETE","path":"/api/v1/dashboard-layouts/:id","description":"Delete layout"}]},"customObjects":{"title":"Custom Objects","description":"Workspace-defined object types and records. Schema/field mutations require admin scope; record CRUD uses write scope.","endpoints":[{"method":"GET","path":"/api/v1/custom-objects","description":"List custom object definitions"},{"method":"POST","path":"/api/v1/custom-objects","description":"Create custom object definition (admin scope). Sending color without useAccentNavIcon enables accent nav icons.","body":{"slug":{"type":"string","required":true},"singular":{"type":"string","required":true},"plural":{"type":"string","required":true},"description":{"type":"string"},"icon":{"type":"string"},"color":{"type":"string","description":"Accent hex. Implies useAccentNavIcon true unless that flag is set explicitly."},"useAccentNavIcon":{"type":"boolean","description":"Show object color on sidebar icons. Defaults true when color is set."}}},{"method":"GET","path":"/api/v1/custom-objects/search","description":"Search records across all searchable objects","queryParams":{"q":{"type":"string","required":true,"minLength":2},"limit":{"type":"number","default":10}}},{"method":"GET","path":"/api/v1/custom-objects/:slug","description":"Get definition with field schemas"},{"method":"PATCH","path":"/api/v1/custom-objects/:slug","description":"Update definition metadata (admin scope). Supports singular/plural/icon/color/sidebar fields plus titleField, favoriteFieldKeys, searchableFields, views, piiFields. Patching color without useAccentNavIcon enables accent nav icons.","body":{"color":{"type":"string","description":"Accent hex. Implies useAccentNavIcon true unless that flag is set explicitly."},"useAccentNavIcon":{"type":"boolean"},"icon":{"type":"string"},"singular":{"type":"string"},"plural":{"type":"string"},"showInSidebar":{"type":"boolean"},"titleField":{"type":"string","description":"Field slug used as display title"},"favoriteFieldKeys":{"type":"string[]","description":"Up to 5 field slugs for previews"},"searchableFields":{"type":"string[]"},"views":{"type":"array"},"piiFields":{"type":"string[]"}}},{"method":"DELETE","path":"/api/v1/custom-objects/:slug","description":"Delete object and all records (admin scope)"},{"method":"POST","path":"/api/v1/custom-objects/:slug/fields","description":"Add field (admin scope). Relation type is record_reference (not reference/relation). select/multi_select options must be { value, label } objects. See schema CustomObjectField for full type enum.","body":{"slug":{"type":"string","required":true},"label":{"type":"string","required":true},"type":{"type":"string","required":true,"enum":["text","number","currency","date","datetime","select","multi_select","checkbox","url","email","phone","file","record_reference","json","formula","rollup"]},"options":{"type":"array","description":"[{ value, label, color? }] for select/multi_select"},"referenceType":{"type":"string","enum":["custom","native"]},"referenceObject":{"type":"string","description":"Custom object slug when referenceType=custom"},"nativeEntity":{"type":"string","description":"Native entity when referenceType=native"}}},{"method":"PATCH","path":"/api/v1/custom-objects/:slug/fields/:fieldSlug","description":"Update field (admin scope)"},{"method":"DELETE","path":"/api/v1/custom-objects/:slug/fields/:fieldSlug","description":"Delete field (admin scope)"},{"method":"GET","path":"/api/v1/custom-objects/:slug/records","description":"List records with pagination or cursor search","queryParams":{"page":{"type":"number","default":1},"limit":{"type":"number","default":30,"max":100},"q":{"type":"string","description":"Search query (alias: search)"},"search":{"type":"string","description":"Alias for q"},"cursor":{"type":"string","description":"Cursor for next page when searching"},"sort":{"type":"string","description":"Field slug to sort by"},"dir":{"type":"string","enum":["asc","desc"],"default":"desc"}}},{"method":"POST","path":"/api/v1/custom-objects/:slug/records","description":"Create record. Body is field-slug keys → values (flattened). Optional wrappers: { data: {…} } or { customFields: {…} }. Unknown or read-only slugs return 400.","body":{"type":"object","description":"Field slug keys → values, or { data } / { customFields } wrappers"}},{"method":"POST","path":"/api/v1/custom-objects/:slug/records/bulk","description":"Bulk update records. Unknown field slugs in patch/operations return 400.","body":{"ids":{"type":"string[]","required":true},"patch":{"type":"object","required":true,"description":"Field slug → value"}}},{"method":"GET","path":"/api/v1/custom-objects/:slug/records/:id","description":"Get record by ID. Response is flattened field slugs (not nested under data)."},{"method":"PATCH","path":"/api/v1/custom-objects/:slug/records/:id","description":"Partial update. Body: field-slug map, or { data: {…} } / { customFields: {…} }. Unknown or read-only slugs return 400 with unknown/denied. Top-level avatar sets the record image: an https image URL (re-hosted) or a data:image/...;base64 URL (max 2 MB); empty clears it.","body":{"type":"object","description":"Field slug keys → values (partial merge)"}},{"method":"PUT","path":"/api/v1/custom-objects/:slug/records/:id","description":"Alias of PATCH (partial merge, not a full replace).","body":{"type":"object","description":"Field slug keys → values (partial merge)"}},{"method":"DELETE","path":"/api/v1/custom-objects/:slug/records/:id","description":"Delete record"},{"method":"GET","path":"/api/v1/custom-objects/:slug/records/:id/activity-feed","description":"Activity feed for a record"},{"method":"GET","path":"/api/v1/custom-objects/:slug/records/:id/related","description":"Related custom object records linked via native entity refs"},{"method":"POST","path":"/api/v1/export/custom-objects/:slug","description":"Export records (export + admin scope)","body":{"from":{"type":"string"},"to":{"type":"string"},"limit":{"type":"number"}}},{"method":"POST","path":"/api/v1/import/custom-objects/:slug","description":"Import records from rows array (import + admin scope)","body":{"rows":{"type":"array","required":true}}}]},"calculators":{"title":"Calculators","description":"Read calculator templates tied to offer types and run stateless price calculations.","endpoints":[{"method":"GET","path":"/api/v1/calculator-templates","description":"List calculator templates"},{"method":"GET","path":"/api/v1/calculator-templates/:id","description":"Get template with pricing rules"},{"method":"POST","path":"/api/v1/calculator-templates/:id/calculate","description":"Run calculation without persisting","body":{"inputs":{"type":"object","required":true}}},{"method":"GET","path":"/api/v1/calculator-submissions","description":"List calculator submissions","queryParams":{"page":{},"limit":{},"templateId":{"type":"string"},"offerId":{"type":"string"}}}]},"integrations":{"title":"Integrations","description":"Read integration connection status. OAuth connect flows remain in-app only.","endpoints":[{"method":"GET","path":"/api/v1/integrations","description":"List connected providers and sync health (no secrets)"},{"method":"POST","path":"/api/v1/integrations/:provider/sync","description":"Trigger manual sync (admin scope)","body":{"fullSync":{"type":"boolean"}}}]},"metrics":{"title":"Metrics","description":"Read every statistic the dashboard and reports can show (calls, SMS, e-mail, pipeline, revenue, leads, offers, support, projects, custom objects, external series) and push your own time series. Custom-object metrics are keyed custom.{slug}.* and follow the object's view permission. Reads require the reports module; writes require write scope.","endpoints":[{"method":"GET","path":"/api/v1/metrics","description":"Catalog of metrics available in the workspace. Each entry: { key, label, description, category, format (integer|currency|percent|duration|decimal), mode (flow|snapshot), dimensions[], measureType }. Includes custom.{slug}.* families and external:{seriesKey} series.","queryParams":{"category":{"type":"string","description":"Filter by category key (e.g. calls, sms, custom:{slug}, external)"}},"response":"{ data: CatalogMetric[], categories: [{ key, label }], meta: { total } }"},{"method":"GET","path":"/api/v1/metrics/query","description":"Query one metric. Returns total, optional period-over-period delta and a bucketed series. Snapshot metrics (mode snapshot) report the current state and ignore the window.","queryParams":{"metricKey":{"type":"string","required":true,"description":"Key from GET /api/v1/metrics"},"from":{"type":"string","required":true,"description":"ISO date/datetime (inclusive)"},"to":{"type":"string","description":"ISO date/datetime (inclusive). Default now"},"granularity":{"type":"string","enum":["day","week","month","quarter","year"],"description":"Bucket size. Default day"},"dimension":{"type":"string","description":"Group by one of the metric's dimensions (breakdown). Points get dimension + dimensionLabel"},"compareTo":{"type":"string","enum":["previous","previousYear"],"description":"Add compareTotal, compareSeries and delta { abs, pct }"},"tz":{"type":"string","description":"IANA timezone for bucketing. Default Europe/Copenhagen"},"format":{"type":"string","enum":["json","csv"],"description":"csv returns a UTF-8 CSV file with one row per point: metricKey, label, granularity, period (current|compare), bucket, dimension, dimensionValue, dimensionLabel, value, format. Default json"}},"response":"{ data: { metricKey, label, format, mode, from, to, granularity, dimension, compareTo, total, compareTotal, delta: { abs, pct } | null, series: [{ bucket, value, dimension?, dimensionLabel? }], compareSeries } }"},{"method":"POST","path":"/api/v1/metrics/query","description":"Batch query up to 25 metrics in one call. Same fields per request as GET /api/v1/metrics/query. Results are returned in request order; an unknown or forbidden metric yields { metricKey, error, code } for that entry only. Add ?format=csv (or \"format\": \"csv\" in the body) to download all series as one CSV file.","body":{"requests":{"type":"array","required":true,"description":"Array of { metricKey, from, to?, granularity?, dimension?, compareTo?, tz? }"},"format":{"type":"string","enum":["json","csv"],"description":"Response format. Default json"}},"response":"{ data: MetricResult[] } or text/csv when format=csv"},{"method":"POST","path":"/api/v1/metrics/points","description":"Idempotent upsert of daily points for an external series. The series appears in the catalog as external:{seriesKey} and can be shown on dashboards and reports. Max 5000 points per request. Requires write scope.","body":{"seriesKey":{"type":"string","required":true,"description":"Stable series id (letters, digits, '_', '.', '-'). Stored as external:{seriesKey}."},"label":{"type":"string","description":"Display name shown in pickers (set once or update any time)"},"description":{"type":"string","description":"Short help text for the picker"},"format":{"type":"string","enum":["integer","decimal","currency","percent","duration"],"description":"Number format. Default decimal"},"points":{"type":"array","required":true,"description":"Array of { date (ISO), value (number), dimensions? (object) }"}},"response":"{ data: { ok: true, upserted: number } }"}]},"projectTemplates":{"title":"Project templates","description":"Reusable project blueprints (tasks, milestones, portal/invoice defaults). Create a project with POST /api/v1/projects/from-template. On offer accept, set OfferType.createProjectOnAccept + projectTemplateId instead of an automation create_project action.","endpoints":[{"method":"GET","path":"/api/v1/project-templates","description":"List project templates. includeInactive=true includes soft-deleted (isActive=false).","queryParams":{"includeInactive":{"type":"boolean"}}},{"method":"POST","path":"/api/v1/project-templates","description":"Create a project template (requires projects create).","body":{"name":{"type":"string","required":true},"description":{"type":"string"},"flowRef":{"type":"string","description":"ProjectFlow id"},"taskTemplates":{"type":"array","description":"[{ title, description?, stage?, priority?, … }]"},"milestoneTemplates":{"type":"array"},"projectData":{"type":"object"},"portalSettings":{"type":"object"},"isDefault":{"type":"boolean"}}},{"method":"GET","path":"/api/v1/project-templates/:id","description":"Get one template"},{"method":"PATCH","path":"/api/v1/project-templates/:id","description":"Update template fields"},{"method":"DELETE","path":"/api/v1/project-templates/:id","description":"Soft-delete (isActive=false)"},{"method":"POST","path":"/api/v1/projects/from-template","description":"Instantiate a project from a template (tasks/milestones). Prefer OfferType createProjectOnAccept for accept-time creation.","body":{"templateId":{"type":"string","required":true},"title":{"type":"string"},"company":{"type":"string","description":"Company id"},"personRef":{"type":"string"},"offerData":{"type":"object","description":"Optional { offerId, offerTitle, lineItems, dealRef, leadRef }"}}}]}}},"schemas":{"overview":{"title":"Data Schemas","description":"Salesbase CRM entities and their field structures. All entities are workspace-scoped. Custom fields can be added to any entity type via the Custom Fields API."},"entities":{"Company":{"description":"A business entity. Can be linked to people, leads, and deals.","fields":{"_id":{"type":"ObjectId","description":"Unique identifier"},"name":{"type":"string","required":true,"description":"Company name"},"cvr":{"type":"string","description":"Danish CVR number (unique within workspace)"},"email":{"type":"string","description":"Primary email"},"phone":{"type":"string","description":"Primary phone"},"address":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"website":{"type":"string"},"industry":{"type":"string"},"employees":{"type":"number"},"description":{"type":"string"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string). Responses never include customFieldsMap."},"tags":{"type":"ObjectId[]","description":"Array of tag IDs"},"owner":{"type":"ObjectId","description":"Assigned user ID"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"People":{"description":"A contact person. Can be linked to companies, leads, and deals.","fields":{"_id":{"type":"ObjectId"},"firstName":{"type":"string","required":true},"lastName":{"type":"string"},"emails":{"type":"array","description":"[{ email: string, label: string }]"},"phones":{"type":"array","description":"[{ phone: string, label: string }]"},"title":{"type":"string","description":"Job title"},"description":{"type":"string"},"companyIds":{"type":"ObjectId[]","description":"Connected company IDs"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string). Responses never include customFieldsMap."},"tags":{"type":"ObjectId[]"},"owner":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Lead":{"description":"A sales lead. Links to a company and/or person, and can have deals.","fields":{"_id":{"type":"ObjectId"},"title":{"type":"string","required":true,"description":"Lead title (typically company/person name)"},"email":{"type":"string"},"phone":{"type":"string"},"status":{"type":"string","description":"Lead status (e.g. 'new', 'contacted', 'qualified', 'won', 'lost')"},"source":{"type":"string","description":"Where the lead came from"},"score":{"type":"number","description":"Lead score (0-100)"},"value":{"type":"number","description":"Estimated value"},"notes":{"type":"string"},"companyRef":{"type":"ObjectId","description":"Connected company"},"personRef":{"type":"ObjectId","description":"Connected person"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string)"},"tags":{"type":"ObjectId[]"},"owner":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Deal":{"description":"A deal/opportunity in a pipeline. Linked to a company, person, and/or lead.","fields":{"_id":{"type":"ObjectId"},"title":{"type":"string","required":true},"value":{"type":"number","description":"Deal monetary value"},"flow":{"type":"ObjectId","description":"Pipeline (flow) ID"},"stage":{"type":"number","description":"Stage ID within the pipeline"},"status":{"type":"string","enum":["open","won","lost"]},"probability":{"type":"number","description":"Win probability (0-100)"},"priority":{"type":"string","enum":["low","medium","high"]},"expectedCloseDate":{"type":"Date"},"wonDate":{"type":"Date","description":"Set when status is won (synced with won stage)"},"lostDate":{"type":"Date","description":"Set when status is lost (synced with lost stage)"},"closeDate":{"type":"Date","description":"Set with wonDate for won deals"},"lostReason":{"type":"string","description":"Dialer loss-reason value (e.g. no_budget). Copied from call outcomes when deals are created from dialer."},"company":{"type":"ObjectId","description":"Connected company"},"personRef":{"type":"ObjectId","description":"Connected person"},"leadRef":{"type":"ObjectId","description":"Connected lead"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string)"},"tags":{"type":"ObjectId[]"},"owner":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Project":{"description":"A project. Custom field definitions: GET /api/v1/project-custom-fields?entityType=project.","fields":{"_id":{"type":"ObjectId"},"title":{"type":"string","required":true},"description":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"owner":{"type":"ObjectId"},"company":{"type":"ObjectId"},"personRef":{"type":"ObjectId"},"dealRef":{"type":"ObjectId"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string). Partial merge on PUT."},"tags":{"type":"string[]"},"workspace":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Task":{"description":"A task (standalone or on a project). Custom field definitions: GET /api/v1/project-custom-fields?entityType=task.","fields":{"_id":{"type":"ObjectId"},"title":{"type":"string","required":true},"description":{"type":"string"},"status":{"type":"string","enum":["to_do","in_progress","blocked","done"]},"stage":{"type":"number"},"priority":{"type":"string"},"project":{"type":"ObjectId"},"owner":{"type":"ObjectId"},"assignees":{"type":"ObjectId[]"},"customFields":{"type":"object","description":"Key → typed value (boolean/number/string). Partial merge on PUT."},"tags":{"type":"string[]"},"workspace":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Automation":{"description":"An event-driven automation flow (see Automations section for full detail)","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"description":{"type":"string"},"isActive":{"type":"boolean"},"nodes":{"type":"Node[]","description":"ReactFlow nodes (see Automation docs)"},"edges":{"type":"Edge[]","description":"ReactFlow edges"},"triggers":{"type":"Trigger[]","description":"Extracted trigger configs for quick lookup"},"executionSettings":{"type":"ExecutionSettings"},"rateLimits":{"type":"RateLimits"},"stats":{"type":"Stats","description":"{ totalRuns, successfulRuns, failedRuns, lastRunAt }"},"createdBy":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Sequence":{"description":"A multi-step outreach sequence (see Sequences section for full detail)","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"description":{"type":"string"},"isActive":{"type":"boolean"},"nodes":{"type":"Node[]","description":"ReactFlow nodes (see Sequence docs)"},"edges":{"type":"Edge[]"},"allowedEntityTypes":{"type":"string[]","description":"['Lead', 'People', 'Deal', 'Company']"},"exitConditions":{"type":"ExitConditions"},"sendSettings":{"type":"SendSettings"},"trackingSettings":{"type":"TrackingSettings"},"executionSettings":{"type":"ExecutionSettings"},"stats":{"type":"Stats"},"createdBy":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"Activity":{"description":"An activity log entry (call, meeting, email, note, task)","fields":{"_id":{"type":"ObjectId"},"type":{"type":"string","enum":["call","meeting","email","note","task","other"]},"title":{"type":"string"},"description":{"type":"string"},"entityType":{"type":"string","description":"Related entity type"},"entityId":{"type":"ObjectId","description":"Related entity ID"},"dueDate":{"type":"Date"},"completed":{"type":"boolean"},"owner":{"type":"ObjectId"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"}}},"Tag":{"description":"A label that can be applied to any entity","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"color":{"type":"string"},"workspaceId":{"type":"ObjectId"}}},"WebhookSubscription":{"description":"An outbound webhook subscription that fires on CRM events","fields":{"_id":{"type":"ObjectId"},"url":{"type":"string","required":true},"events":{"type":"string[]","required":true},"name":{"type":"string"},"active":{"type":"boolean"},"secret":{"type":"string","description":"HMAC signing secret (returned on creation only)"},"workspaceId":{"type":"ObjectId"},"createdAt":{"type":"Date"}}},"CustomObjectDefinition":{"description":"A workspace-defined object type (schema). Managed via admin-scoped API key.","fields":{"_id":{"type":"ObjectId"},"slug":{"type":"string","required":true,"description":"URL-safe identifier, e.g. contracts"},"singular":{"type":"string","required":true},"plural":{"type":"string","required":true},"description":{"type":"string"},"icon":{"type":"string"},"color":{"type":"string","description":"Hex accent for sidebar"},"searchable":{"type":"boolean"},"showInSidebar":{"type":"boolean"},"workspace":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"CustomObjectField":{"description":"Field definition on a custom object. Types mirror Attio-style schema fields.","fields":{"_id":{"type":"ObjectId"},"slug":{"type":"string","required":true},"label":{"type":"string","required":true},"type":{"type":"string","enum":["text","number","currency","date","datetime","select","multi_select","checkbox","url","email","phone","file","record_reference","json","formula","rollup"],"description":"Relation fields use type record_reference (not reference/relation). There is no textarea type; use text. multi_select not multiselect."},"isRequired":{"type":"boolean"},"isUnique":{"type":"boolean"},"options":{"type":"array","description":"For select/multi_select: array of { value, label, color? } objects (bare strings are invalid and may 400/500)"},"referenceObject":{"type":"string","description":"Custom object slug when referenceType=custom"},"referenceType":{"type":"string","enum":["custom","native"]},"nativeEntity":{"type":"string","description":"company, person, deal, lead, project, offer when referenceType=native"},"order":{"type":"number"}}},"CustomObjectRecord":{"description":"A record instance for a custom object. API responses flatten field values to top-level field-slug keys (plus id, _recordNumber, _displayTitle). Mongo stores values under data.","fields":{"id":{"type":"ObjectId","description":"Record id (also as _id)"},"object":{"type":"ObjectId","description":"CustomObjectDefinition _id"},"_recordNumber":{"type":"number","description":"Auto-increment per object type"},"_displayTitle":{"type":"string","description":"Computed display title"},"(fieldSlug)":{"type":"mixed","description":"Each writable field appears as a top-level key on the API response"},"workspace":{"type":"ObjectId"},"createdBy":{"type":"ObjectId"},"updatedBy":{"type":"ObjectId"},"createdAt":{"type":"Date"},"updatedAt":{"type":"Date"}}},"OfferTemplate":{"description":"Tilbudsskabelon page builder. Prefer native elements so the user can edit them in the editor. Full catalog: /api/v1/docs?section=offerTemplates&subsection=nativeElements. type html is a last resort.","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"description":{"type":"string"},"pages":{"type":"array","description":"[{ id, order, title, elements[], settings }]"},"headerElements":{"type":"array","description":"Elements on every page header"},"footerElements":{"type":"array","description":"Elements on every page footer"},"branding":{"type":"object","description":"{ colors, typography, logo }"},"defaultTerms":{"type":"string"},"defaultPaymentTerms":{"type":"string"},"defaultValidity":{"type":"number","description":"Days"},"isDefault":{"type":"boolean"},"isActive":{"type":"boolean"},"defaultOfferTypeId":{"type":"ObjectId","ref":"OfferType","description":"Price rules selected automatically when this template is used on a new offer. Null clears the link."},"workspaceId":{"type":"ObjectId"}},"elementTypes":["text","heading","image","video","table","button","line_items","totals","divider","spacer","columns","signature_field","initials_field","date_field","text_field","checkbox_field","dropdown_field","html"],"elementRule":"Use a native type whenever one fits. html cannot be edited as structured fields afterwards.","aiGuide":"/api/v1/docs?section=offerTemplates"},"EmailTemplate":{"description":"Email template rendered from bodyHtml + bodyCss + layoutSettings (single document model). compiledHtml is the derived/legacy full document. Optional GrapesJS projectData.","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"category":{"type":"string","enum":["marketing","transactional","reminder","follow-up","notification","quote","newsletter","other"]},"bodyHtml":{"type":"string","description":"Body content with {{contact.firstName}} placeholders (preferred)"},"bodyCss":{"type":"string","description":"CSS rules applied to bodyHtml"},"compiledHtml":{"type":"string","description":"Legacy/derived full HTML document with placeholders"},"plainText":{"type":"string"},"metadata":{"type":"object","description":"{ subject, preheader }"},"layoutSettings":{"type":"object","description":"{ width: full|fixed, align, containerWidth, textAlign, paddingTop/Right/Bottom/Left, backgroundColor, outerBackground, fullWidthBackground }. Deprecated aliases: layoutMode, useTableWrapper, containerAlign, outerBackgroundColor"},"requiredVars":{"type":"array"},"tags":{"type":"string[]"},"workspace":{"type":"ObjectId"}},"aiGuide":"/api/v1/docs?section=offerTemplates"},"OfferType":{"description":"Tilbudstype — pricing rules, BBR mapping, default template, CRM automations on accept/submit.","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"slug":{"type":"string"},"defaultTemplateId":{"type":"ObjectId","ref":"OfferTemplate"},"requiredInputFields":{"type":"array","description":"Input and computed fields. type computed uses expression { left, op, right, round }"},"pricingRules":{"type":"object","description":"v2: schemaVersion 2, rules[]. Actions may use mode expression and quantity.kind expression (A op B: mul add sub div pctOf). Legacy: tieredPricing, conditionalRules, packages, globalSurcharges"},"offerSettings":{"type":"object","description":"OTP, deal/sequence on accept/submit"},"customPlaceholders":{"type":"array","description":"→ {{custom.fieldId}} in templates. inputType text, date, richText. options turns the field into a dropdown"},"isActive":{"type":"boolean"},"workspaceId":{"type":"ObjectId"}}},"OfferProduct":{"description":"Product/service for offers. pricingType: fixed | advanced (tiered pricing).","fields":{"_id":{"type":"ObjectId"},"name":{"type":"string","required":true},"unitPrice":{"type":"number"},"unit":{"type":"string","default":"stk"},"pricingType":{"type":"string","enum":["fixed","advanced"]},"tieredPricing":{"type":"array"},"conditionalRules":{"type":"array"},"variants":{"type":"array"},"category":{"type":"string"},"isActive":{"type":"boolean"},"workspaceId":{"type":"ObjectId"}}}},"customFieldTypes":{"description":"Custom fields support these data types","types":[{"type":"text","description":"Free-text string"},{"type":"number","description":"Numeric value"},{"type":"date","description":"ISO date string"},{"type":"checkbox","description":"Boolean (true/false)"},{"type":"select","description":"Single-select from predefined options"},{"type":"multi_select","description":"Multi-select from predefined options"},{"type":"url","description":"URL string"},{"type":"email","description":"Email address"},{"type":"phone","description":"Phone number (stored as text)"},{"type":"textarea","description":"Multi-line text"}],"note":"Lead and Deal custom fields use Map storage (key → value). Company and People use array storage ([{ name, value }]). When creating via API, both formats are accepted and auto-converted."}},"webhooks":{"overview":{"title":"Webhooks","description":"Subscribe to CRM events and receive HTTP POST notifications at your URL. Each webhook delivery includes an HMAC signature for verification. Manage subscriptions via the /api/v1/webhooks/subscriptions endpoints."},"events":[{"event":"company.created","description":"A company was created"},{"event":"company.updated","description":"A company was updated"},{"event":"company.deleted","description":"A company was deleted"},{"event":"person.created","description":"A person was created"},{"event":"person.updated","description":"A person was updated"},{"event":"person.deleted","description":"A person was deleted"},{"event":"deal.created","description":"A deal was created"},{"event":"deal.updated","description":"A deal was updated"},{"event":"deal.deleted","description":"A deal was deleted"},{"event":"deal.stage_changed","description":"A deal moved to a different pipeline stage"},{"event":"deal.won","description":"A deal was marked as won"},{"event":"deal.lost","description":"A deal was marked as lost"},{"event":"lead.created","description":"A lead was created"},{"event":"lead.updated","description":"A lead was updated"},{"event":"lead.deleted","description":"A lead was deleted"},{"event":"call.created","description":"A call record was created"},{"event":"call.updated","description":"A call record was updated"},{"event":"note.created","description":"A note was created"},{"event":"note.updated","description":"A note was updated"},{"event":"task.created","description":"A task was created"},{"event":"task.updated","description":"A task was updated"},{"event":"task.completed","description":"A task was marked as completed"},{"event":"activity.created","description":"An activity was logged"},{"event":"custom_object.created","description":"A custom object record was created"},{"event":"custom_object.updated","description":"A custom object record was updated"},{"event":"custom_object.deleted","description":"A custom object record was deleted"},{"event":"offer.created","description":"An offer was created"},{"event":"offer.updated","description":"An offer was updated"},{"event":"offer.deleted","description":"An offer was deleted"},{"event":"offer.sent","description":"An offer was sent to recipient"},{"event":"contract.sent","description":"A contract was sent for signature"},{"event":"contract.signed","description":"A signer signed a contract"},{"event":"contract.completed","description":"All signers signed a contract"},{"event":"contract.declined","description":"A signer declined a contract"},{"event":"contract.voided","description":"A contract was voided by the sender"},{"event":"contract.expired","description":"A contract signing deadline expired"},{"event":"booking.created","description":"A booking was created"},{"event":"booking.approved","description":"A booking was approved"},{"event":"booking.rejected","description":"A booking was rejected"},{"event":"booking.cancelled","description":"A booking was cancelled"},{"event":"booking.rescheduled","description":"A booking was rescheduled"},{"event":"support_ticket.created","description":"A support ticket was created"},{"event":"support_ticket.updated","description":"A support ticket was updated"},{"event":"support_ticket.stage_changed","description":"A support ticket moved kanban stage"},{"event":"support_ticket.deleted","description":"A support ticket was deleted"}],"deliveryFormat":{"description":"Each webhook delivery sends a POST request to your URL with this JSON body:","schema":{"event":{"type":"string","description":"Event name (e.g. 'lead.created')"},"timestamp":{"type":"string","description":"ISO 8601 timestamp"},"data":{"type":"object","description":"The entity data that triggered the event"},"workspaceId":{"type":"string"}},"headers":{"Content-Type":"application/json","X-Webhook-Signature":"HMAC-SHA256 signature of the request body using your webhook secret"}},"signatureVerification":{"description":"Verify webhook authenticity by computing HMAC-SHA256 of the raw request body with your secret","example":"const crypto = require('crypto');\n\nfunction verifyWebhook(body, signature, secret) {\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(JSON.stringify(body))\n    .digest('hex');\n  return crypto.timingSafeEqual(\n    Buffer.from(signature),\n    Buffer.from(expected)\n  );\n}"},"endpoints":{"description":"See the Webhook Subscriptions section in Endpoints for CRUD operations","quickReference":[{"method":"GET","path":"/api/v1/webhooks/subscriptions","description":"List subscriptions"},{"method":"POST","path":"/api/v1/webhooks/subscriptions","description":"Create subscription (returns secret)"},{"method":"GET","path":"/api/v1/webhooks/subscriptions/:id","description":"Get subscription"},{"method":"PUT","path":"/api/v1/webhooks/subscriptions/:id","description":"Update subscription"},{"method":"DELETE","path":"/api/v1/webhooks/subscriptions/:id","description":"Delete subscription"}]},"inboundWebhooks":{"description":"Salesbase also accepts inbound webhooks to trigger automations","endpoints":[{"method":"POST","path":"/api/webhooks/automation/:automationId","description":"Trigger a webhook-type automation. The JSON body becomes available as webhookData.* placeholders in the automation flow.","authentication":"Header x-webhook-secret must match the secret on the automation webhook trigger node."},{"method":"POST","path":"/api/v1/webhooks/zapier","description":"Zapier inbound — creates person, lead, and deal. Bearer API key with inbound scope."}]}},"offerTemplates":{"title":"Offer & Email Templates (AI Guide)","description":"Build tilbudsskabeloner from native page-builder elements so the user can edit them afterwards. Read subsection=nativeElements before you write pages. type html is a last resort. An offer copies the template into templateSnapshot. Put prices on the offer as lineItems, rendered by line_items and totals. Email templates still use compiledHtml.","readFirst":["GET /api/v1/docs?section=offerTemplates&subsection=nativeElements","GET /api/v1/docs?section=offerTemplates&subsection=nativeElements&type=text"],"referenceDoc":"Archived offer AI prompt (MacData/Archives/salesbase-unused-2026-10-08/docs/OFFER-AI-PROMPT.md)","setupOrder":["1. PATCH /api/v1/workspace/offer-settings (companyInfo, bankInfo, defaults)","2. OfferProducts (pricingType: advanced for tiered pricing)","3. OfferTemplate (pages, headerElements, footerElements, branding)","4. OfferType / Prisregler (requiredInputFields, pricingRules, defaultTemplateId, offerSettings). Guide: ?section=pricingRules&subsection=workflow","5. CalculatorTemplate (optional, offerTypeId)","6. Offer (templateId, offerTypeId, quoteInput, calculatorResult)","7. POST /api/v1/offers/:id/send (creates snapshot, fires automations)"],"apiEndpoints":{"offerTemplates":["GET /api/v1/offer-templates","POST /api/v1/offer-templates (admin)","GET /api/v1/offer-templates/:id","PATCH /api/v1/offer-templates/:id (admin)","DELETE /api/v1/offer-templates/:id (admin)","POST /api/v1/offer-templates/:id/duplicate (admin)"],"emailTemplates":["GET /api/v1/email-templates","POST /api/v1/email-templates (admin)","GET /api/v1/email-templates/:id","PATCH /api/v1/email-templates/:id (admin)","DELETE /api/v1/email-templates/:id (admin)"],"offerStack":["GET/PATCH /api/v1/workspace/offer-settings (companyInfo, bankInfo, defaults)","GET/POST /api/v1/offer-types, /api/v1/products","GET /api/v1/offer-types/rule-reference | rule-templates, POST /api/v1/offer-types/validate | simulate","GET/POST /api/v1/offers, PUT draft fields, POST /api/v1/offers/:id/send"]},"nativeElements":{"description":"Native page-builder elements for offer templates (pages[].elements, headerElements, footerElements) and for the snapshot an offer copies from its template. Danish labels are what the user sees in the editor.","rule":"Prefer a native element whenever one fits. The user can then change the text, columns, image, table, or prices in the visual editor without opening HTML. Use type html only when no native element can express the block. Do not wrap headings, paragraphs, logos, key-value facts, price tables, spacers, or dividers in html.","readOneType":"GET /api/v1/docs?section=offerTemplates&subsection=nativeElements&type=<type>","sharedFields":{"id":"string. Stable id, e.g. element_intro. Required so later PATCH can target the block.","type":"One of the native types below.","order":"number. Sort order on the page.","visible":"boolean. Default true. false hides the block without deleting it.","visibilityCondition":"object|null. Optional show-if rule on a document variable. { variable: 'recipient.company' (no curly braces), operator: filled|empty|equals|not_equals|contains, value: string (only for equals/not_equals/contains) }. null or omit = no rule. Combined with visible: both must allow the block.","style":"object. See styleFields. Only set keys you need."},"styleFields":{"typography":"fontSize (px), fontWeight (normal|500|600|bold), color or textColor, textAlign (left|center|right), lineHeight","spacing":"marginTop, marginBottom, padding. Values like 16px or 12px 24px.","surface":"backgroundColor, borderWidth, borderColor, borderStyle (solid|dashed|dotted), borderRadius"},"placeholders":"{{offer.*}}, {{recipient.*}}, {{company.*}}, {{bank.*}}, {{calc.*}}, {{custom.*}} work in text, heading, table cells, and html. They do not work in button text.","fullWidthOnly":{"types":["line_items","totals","columns"],"note":"These cannot be nested inside a columns element."},"columnPresets":["50-50","33-33-33","70-30","30-70","25-50-25","25-25-25-25"],"whenBuildingATemplate":["Compose the page from native elements first: heading, text, image, columns, table, divider, spacer, line_items, totals, button.","Put the logo in an image element with url {{company.logo}}, not an html img tag.","Put recipient, offer number, dates, and bank details in a table (or in text with placeholders), not an html info box.","Put prices only in line_items and totals. They render calculatorResult / lineItems from the offer. Do not hardcode product rows in html or in a static table.","Rich copy (bold, lists, links) belongs on a text element with contentFormat html. That stays editable in the text editor. It is not a reason to use type html.","Brand colors go on branding.colors and on the native style object (heading color, divider borderColor, button backgroundColor)."],"whenBuildingAnOffer":["The visual document is the template. POST /api/v1/offers copies it into templateSnapshot. Do not send a second HTML layout on the offer.","Prices go on the offer as lineItems (name, quantity, unit, unitPrice) or calculatorResult.lineItems. The template line_items and totals elements render them.","Greeting, terms, and labels stay in the template as text and heading with placeholders. Override terms with customContent.termsOverride or paymentTerms when this one offer differs.","Do not fill customContent.headerHtml or footerHtml. Those fields are deprecated. Header and footer are headerElements and footerElements on the template.","If this offer needs a different layout, PATCH the template (or duplicate it) with native elements, then set templateId. Do not rebuild the page as html on the offer."],"htmlLastResort":{"type":"html","useWhen":"The block cannot be a heading, text, image, video, table, button, columns, divider, spacer, line_items, totals, or a signer field. Example: a one-off decorative mark that has no native control.","doNotUseFor":["Headlines and section titles (heading)","Body copy, terms, greetings, USP lines (text, with contentFormat html when you need bold or lists)","Logo and photos (image)","YouTube or Vimeo (video)","Key-value facts: offer number, date, address, bank (table)","Side-by-side layout (columns containing native children)","Gaps and rules (spacer, divider)","Price rows and VAT (line_items, totals)","Calls to action (button)","Signature lines (signature_field and the other signer fields)"]},"types":{"text":{"label":"Tekst","hint":"Brødtekst","use":"Paragraphs, greetings, terms, USP lines. First choice for any copy the user should edit later.","fields":{"content":"string. Plain text with \\n, or HTML when contentFormat is html.","contentFormat":"Omit for plain text. Set html for bold, lists, and links. Still the native text element.","style":"typography, spacing, backgroundColor"},"example":{"id":"el_intro","type":"text","visible":true,"content":"Hej {{recipient.firstName}}\n\nHer er jeres tilbud.","style":{"fontSize":"14px","lineHeight":"1.6","textAlign":"left"}}},"heading":{"label":"Overskrift","hint":"Titel / sektion","use":"Page title and section titles. Do not fake a heading with a styled html div.","fields":{"content":"string. Placeholders allowed.","level":"1-4. Sets the heading size (H1 32px, H2 24px, H3 20px, H4 18px).","style":"typography, spacing, backgroundColor. fontSize follows level unless you override it."},"example":{"id":"el_title","type":"heading","level":1,"content":"Tilbud til {{recipient.name}}","style":{"fontWeight":"bold","textAlign":"left"}}},"image":{"label":"Billede","hint":"Logo eller foto","use":"Logo, photos, case images. Use {{company.logo}} for the workspace logo.","fields":{"url":"https URL or {{company.logo}}. Required.","alt":"string","width":"e.g. 100% or 180px. Default 100%.","height":"e.g. auto or 120px. Default auto.","style":"textAlign (left|center|right), borderRadius, objectFit (contain|cover|fill), spacing"},"example":{"id":"el_logo","type":"image","url":"{{company.logo}}","alt":"{{company.name}}","width":"160px","height":"auto","style":{"objectFit":"contain","textAlign":"left"}}},"video":{"label":"Video","hint":"Indlejret video","use":"YouTube, Vimeo, or a direct file. Do not paste an iframe into html.","fields":{"url":"Required.","provider":"youtube | vimeo | direct. Default youtube.","thumbnail":"Optional image URL.","autoplay":"boolean. Default false.","controls":"boolean. Default true.","width":"Default 100%.","height":"Default 400px.","style":"borderRadius, backgroundColor, marginTop, marginBottom, padding"},"example":{"id":"el_video","type":"video","url":"https://www.youtube.com/watch?v=VIDEO_ID","provider":"youtube","controls":true,"autoplay":false,"width":"100%","height":"400px"}},"table":{"label":"Tabel","hint":"Statisk tabel","use":"Key-value facts and small static grids: offer number, date, address, sender, bank. Cells accept placeholders. Not for offer line items.","fields":{"hideHeader":"boolean. Default false (column labels are shown).","hideEmptyRows":"boolean. Default true. Rows whose cells are empty are hidden.","columns":"[{ id, label, width, align: left|center|right }]","rows":"[{ id, cells: { [columnId]: string | { content, contentFormat } } }]","style":"borderWidth, borderColor, headerBackground or headerBackgroundColor, headerTextColor, spacing"},"example":{"id":"el_meta","type":"table","hideEmptyRows":true,"columns":[{"id":"label","label":"","width":"40%","align":"left"},{"id":"value","label":"","width":"60%","align":"right"}],"rows":[{"id":"r1","cells":{"label":"Tilbud nr.","value":"{{offer.number}}"}},{"id":"r2","cells":{"label":"Dato","value":"{{offer.date}}"}},{"id":"r3","cells":{"label":"Gyldig til","value":"{{offer.validUntil}}"}}],"style":{"borderColor":"#e5e7eb","headerBackground":"#f9fafb"}}},"button":{"label":"Knap","hint":"CTA-knap","use":"A call to action. Accept is also available on the public offer page, so do not draw a fake accept button in html.","fields":{"text":"Visible label. Placeholders are not supported in button text.","action":"accept | download | link. accept opens accept. download gets the PDF. link needs url.","url":"Only when action is link.","style":"backgroundColor, textColor, fontSize, fontWeight, textAlign, padding, borderRadius, width, borderWidth, borderStyle, borderColor"},"example":{"id":"el_accept","type":"button","text":"Accepter tilbud","action":"accept","style":{"backgroundColor":"#111827","textColor":"#ffffff","borderRadius":"6px","padding":"12px 24px","fontWeight":"600"}}},"line_items":{"label":"Linjer","hint":"Prislinjer","use":"Renders the offer's line items. Always use this for the price table. Full width. Cannot sit inside columns.","fields":{"showDescription":"boolean. Default true.","showQuantity":"boolean. Default true.","showUnitPrice":"boolean. Default true.","showBillingFrequency":"boolean. Default true. Column label in the editor: Periode.","showDiscount":"boolean. Default true.","showTotal":"boolean. Default true.","style":"borderColor, fontSize, textColor, headerTextColor, headerBackgroundColor, fontWeight, totalFontWeight, marginTop, marginBottom"},"dataFrom":"offer.calculatorResult.lineItems or the offer lineItems payload. Do not duplicate those rows in the template.","example":{"id":"el_lines","type":"line_items","showDescription":true,"showQuantity":true,"showUnitPrice":true,"showBillingFrequency":true,"showDiscount":true,"showTotal":true}},"totals":{"label":"Totaler","hint":"Subtotal / moms","use":"Subtotal, discount, VAT, and total from the offer. Place it under line_items. Full width.","fields":{"showSubtotal":"boolean. Default true.","showDiscount":"boolean. Default true.","showVat":"boolean. Default true.","showTotal":"boolean. Default true.","style":"textAlign (default right), fontSize, textColor, fontWeight, discountColor, discountFontWeight, totalColor, totalFontWeight, totalBorderColor, marginTop, marginBottom"},"example":{"id":"el_totals","type":"totals","showSubtotal":true,"showDiscount":true,"showVat":true,"showTotal":true,"style":{"textAlign":"right","fontSize":"14px"}}},"divider":{"label":"Divider","hint":"Vandret streg","use":"A horizontal rule between sections. Do not draw the line as an html div.","fields":{"style":"borderWidth, borderStyle (solid|dashed|dotted), borderColor, width, align (left|center|right), marginTop, marginBottom"},"example":{"id":"el_rule","type":"divider","style":{"borderWidth":"1px","borderStyle":"solid","borderColor":"#e5e7eb","marginTop":"20px","marginBottom":"20px"}}},"spacer":{"label":"Mellemrum","hint":"Lodret mellemrum","use":"Vertical gap. Prefer this over empty html or margin hacks.","fields":{"height":"e.g. 24px, 40px.","style":"backgroundColor"},"example":{"id":"el_gap","type":"spacer","height":"32px"}},"columns":{"label":"Kolonner","hint":"Side om side","use":"Side-by-side native children, for example logo plus company text, or two case photos. Use it rarely. A single full-width stack is the default. Cannot be nested. Cannot contain line_items or totals.","fields":{"layout":"50-50 | 33-33-33 | 70-30 | 30-70 | 25-50-25 | 25-25-25-25 | custom","columns":"[{ id, width: '50%', elements: [ native elements ] }]. At least 2, at most 6.","style":"gap (default 20px), verticalAlign (top|center|stretch)"},"example":{"id":"el_cols","type":"columns","layout":"30-70","style":{"gap":"24px","verticalAlign":"top"},"columns":[{"id":"col_logo","width":"30%","elements":[{"id":"el_logo","type":"image","url":"{{company.logo}}","alt":"{{company.name}}","width":"140px","height":"auto"}]},{"id":"col_copy","width":"70%","elements":[{"id":"el_hello","type":"heading","level":2,"content":"Hej {{recipient.firstName}}"},{"id":"el_body","type":"text","content":"Tak for snakken. Her er tilbuddet."}]}]}},"signature_field":{"label":"Underskrift","hint":"Underskriver tegner her","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_signature_field","type":"signature_field","label":"Underskrift","assignee":{"slot":1},"required":true,"style":{"width":"100%","textAlign":"left"}}},"initials_field":{"label":"Initialer","hint":"Initialer pr. side","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_initials_field","type":"initials_field","label":"Initialer","assignee":{"slot":1},"required":true,"style":{"width":"100%","textAlign":"left"}}},"date_field":{"label":"Dato","hint":"Dato sættes til i dag","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_date_field","type":"date_field","label":"Dato","assignee":{"slot":1},"required":true,"style":{"width":"100%","textAlign":"left"}}},"text_field":{"label":"Tekstfelt","hint":"Fri tekst fra underskriver","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_text_field","type":"text_field","label":"Tekstfelt","assignee":{"slot":1},"required":true,"style":{"width":"100%","textAlign":"left"}}},"checkbox_field":{"label":"Tjekboks","hint":"Bekræftelse / vilkår","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_checkbox_field","type":"checkbox_field","label":"Tjekboks","assignee":{"slot":1},"required":true,"placeholderText":"Jeg accepterer vilkårene","style":{"width":"100%","textAlign":"left"}}},"dropdown_field":{"label":"Valgmulighed","hint":"Vælg én mulighed","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id.","fields":{"assignee":"{ slot: number }. Required, integer >= 1.","label":"string shown above the field.","placeholderText":"Helper text. On checkbox_field this is the label next to the box, e.g. Jeg accepterer vilkårene. Not used on signature_field.","required":"boolean. Default true.","options":"dropdown_field only. Array of choice strings, one option each. The signer picks one. The chosen value is stored and shown in the document and PDF.","style":"width: 100% | 75% | 50% | 33% | auto, or a custom px/% value. textAlign."},"example":{"id":"el_dropdown_field","type":"dropdown_field","label":"Valgmulighed","assignee":{"slot":1},"required":true,"options":["Mulighed 1","Mulighed 2"],"style":{"width":"100%","textAlign":"left"}}},"html":{"label":"HTML","hint":"Fri HTML","use":"Last resort. See htmlLastResort. The user cannot edit the structure in the visual controls, only the raw markup.","fields":{"content":"HTML string. Inline CSS. Placeholders work inside the markup.","style":"Optional typography around the block."}}}},"elementTypes":{"description":"Short index. Full fields, examples, and the prefer-native rule: subsection nativeElements. Do not default to html.","index":[{"type":"text","label":"Tekst","use":"Paragraphs, greetings, terms, USP lines. First choice for any copy the user should edit later."},{"type":"heading","label":"Overskrift","use":"Page title and section titles. Do not fake a heading with a styled html div."},{"type":"image","label":"Billede","use":"Logo, photos, case images. Use {{company.logo}} for the workspace logo."},{"type":"video","label":"Video","use":"YouTube, Vimeo, or a direct file. Do not paste an iframe into html."},{"type":"table","label":"Tabel","use":"Key-value facts and small static grids: offer number, date, address, sender, bank. Cells accept placeholders. Not for offer line items."},{"type":"button","label":"Knap","use":"A call to action. Accept is also available on the public offer page, so do not draw a fake accept button in html."},{"type":"line_items","label":"Linjer","use":"Renders the offer's line items. Always use this for the price table. Full width. Cannot sit inside columns."},{"type":"totals","label":"Totaler","use":"Subtotal, discount, VAT, and total from the offer. Place it under line_items. Full width."},{"type":"divider","label":"Divider","use":"A horizontal rule between sections. Do not draw the line as an html div."},{"type":"spacer","label":"Mellemrum","use":"Vertical gap. Prefer this over empty html or margin hacks."},{"type":"columns","label":"Kolonner","use":"Side-by-side native children, for example logo plus company text, or two case photos. Use it rarely. A single full-width stack is the default. Cannot be nested. Cannot contain line_items or totals."},{"type":"signature_field","label":"Underskrift","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"initials_field","label":"Initialer","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"date_field","label":"Dato","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"text_field","label":"Tekstfelt","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"checkbox_field","label":"Tjekboks","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"dropdown_field","label":"Valgmulighed","use":"A real signing field on the document. Use it when the offer or contract should be signed on the page. Do not draw a signature line with text or html. assignee.slot is the signer order (1-based), not a person id."},{"type":"html","label":"HTML","use":"Last resort. See htmlLastResort. The user cannot edit the structure in the visual controls, only the raw markup."}],"fullWidthOnly":["line_items","totals","columns"]},"placeholders":{"offer":["{{offer.number}}","{{offer.title}}","{{offer.date}}","{{offer.validUntil}}","{{offer.terms}}","{{offer.paymentTerms}}","{{offer.total}}","{{offer.subtotal}}","{{offer.vat}}"],"recipient":["{{recipient.name}}","{{recipient.firstName}}","{{recipient.email}}","{{recipient.phone}}","{{recipient.fullAddress}}","{{recipient.zip}}","{{recipient.city}}"],"company":["{{company.name}}","{{company.logo}}","{{company.phone}}","{{company.email}}","{{company.cvr}}","{{company.street}}","{{company.postalCode}}","{{company.city}}"],"bank":["{{bank.name}}","{{bank.reg}}","{{bank.account}}","{{bank.iban}}","{{bank.swift}}"],"calc":["{{calc.total}}","{{calc.subtotal}}","{{calc.vat}}","{{calc.discount}}"],"footer":["{{page}}","{{total}}","{{totalPages}}"],"email":["{{contact.firstName}}","{{contact.lastName}}","{{contact.email}}","{{company.name}}","{{offer.validUntil}}","{{sender.name}}"],"syntax":"Email templates also support {{field | default:\"fallback\"}} filter syntax."},"offerTemplateSchema":{"name":"string (required)","description":"string","branding":{"colors":{"primary":"#13143E","secondary":"#FBA212","accent":"#FBA212","background":"#ffffff","text":"#13143E","border":"#e0e0ea"},"typography":{"fontFamily":"Inter, sans-serif","headingFontFamily":"","fontSize":"base"},"logo":{"url":"","maxHeight":60,"position":"left|center|right"}},"headerElements":"Element[] shown on every page","footerElements":"Element[] shown on every page","headerSettings":{"height":80,"showOnAllPages":true,"showOnFirstPage":true},"footerSettings":{"height":40,"showPageNumbers":true,"pageNumberFormat":"Side {{page}} af {{total}}"},"pages":[{"id":"page_...","order":0,"title":"Side 1","elements":[],"settings":{"background":"#ffffff","minHeight":297,"margins":{"top":20,"right":15,"bottom":20,"left":15}}}],"defaultTerms":"string","defaultPaymentTerms":"string","defaultValidity":30,"isDefault":false,"isActive":true},"emailTemplateSchema":{"name":"string (required)","category":"marketing|transactional|reminder|follow-up|notification|quote|newsletter|other","bodyHtml":"string. Editable body content with {{placeholders}} (no <html>/<head>/<body>). Preferred input: Salesbase renders the full document from bodyHtml + bodyCss + layoutSettings.","bodyCss":"string (optional). CSS rules applied to bodyHtml when rendering.","compiledHtml":"string (legacy). Full HTML document with {{placeholders}}. Still accepted; re-rendered from its body on send so preheader and layout stay consistent.","plainText":"string (optional)","metadata":{"subject":"string with {{placeholders}}","preheader":"string. Inbox preview text, injected as a hidden block first in <body>"},"layoutSettings":{"width":"full|fixed. full = plain left-aligned mail (default), fixed = marketing container","align":"left|center|right (fixed only)","containerWidth":600,"textAlign":"left|center|right","paddingTop":0,"paddingRight":0,"paddingBottom":0,"paddingLeft":0,"backgroundColor":"#ffffff","outerBackground":"#f3f4f6","fullWidthBackground":false,"layoutMode":"DEPRECATED alias: minimal => width full, styled => width fixed","useTableWrapper":"DEPRECATED alias: true => width fixed","containerAlign":"DEPRECATED alias for align","outerBackgroundColor":"DEPRECATED alias for outerBackground"},"requiredVars":[{"key":"vars.offerLink","type":"string","description":"..."}],"tags":["string[]"]},"nativeComposition":{"description":"Default page, built so the user can edit each block in the editor. Do not copy older templates that stuffed the same layout into html.","page":["Header: columns with an image ({{company.logo}}) and a text element for company name, phone, and email.","Title: heading level 1.","Intro: text element. Use contentFormat html only for bold or a short list.","Facts: table with {{offer.number}}, {{offer.date}}, {{offer.validUntil}}, {{recipient.fullAddress}}.","Body: more text elements, split by spacer and divider.","Prices: line_items then totals. Never a handwritten price table.","CTA: button action accept.","Footer: text or columns for company and terms, plus footerSettings page numbers."]},"designRules":["Native elements first. html only when nativeElements.htmlLastResort says so.","One idea per section. spacer and divider between sections. columns only for a real side-by-side pair.","Brand colors via branding.colors and the native style object (heading, divider, button).","Prices live on the offer (lineItems). The template only places line_items and totals.","Signer fields (signature_field and the rest) are the signature UI. Do not draw a signature line in text or html.","Accept on the public offer page still works if you omit a button. A button uses action accept.","Email templates are separate: set compiledHtml directly; subject in metadata.subject with {{contact.firstName}}."],"createOfferExample":{"method":"POST","path":"/api/v1/offers","body":{"title":"Tilbud vinduespolering","offerTypeId":"<OfferType _id>","templateId":"<OfferTemplate _id>","personRef":"<Person _id>","quoteInput":{"contact":{"name":"Klaus Hansen","email":"klaus@example.dk","phone":"+4512345678"},"address":{"fullAddress":"Hovedgaden 1, 2100 København Ø"}},"calculatorResult":{"lineItems":[{"name":"Vinduespolering","quantity":12,"unit":"stk","unitPrice":45,"total":540}],"subtotal":540,"vatAmount":135,"vatRate":25,"total":675}}},"sendOffer":{"method":"POST","path":"/api/v1/offers/:id/send","description":"Marks offer sent, creates immutable OfferSnapshot, logs activity, runs onOfferSent automations, dispatches offer.sent webhook. Returns publicId for customer link /offer/{publicId}.","scope":"write"}},"pricingRules":{"overview":{"title":"Prisregler (OfferType) for AI clients","description":"An OfferType (UI: Prisregler) holds the questions the seller answers (requiredInputFields) and Hvis … så … rules (pricingRules.rules, schemaVersion 2) that add lines, adjust prices, add surcharges/discounts, warn or block sending. Computed fields chain simple A op B expressions (Bredde × Højde ÷ 10000 = m²). No free-form formulas.","uiPath":"/settings/tilbud-produkter?section=tilbudstyper (Tilbud & Produkter → Prisregler), editor tabs Regler, Felter, Generelt, Pladsholdere, Indstillinger","offerFieldValues":"On an offer, field answers live in quoteInput.calculatorInputs { fieldId: value }. Rules run when the offer is saved or Anvend regler is clicked.","safety":["All endpoints are scoped to the API key's workspace.","Create/update/delete need write scope and an admin/owner key owner. validate and simulate are read-only.","Never invent product ids. Use GET /api/v1/offer-types/rule-reference (products) or GET /api/v1/products."]},"workflow":["1. GET /api/v1/offer-types/rule-reference: products, subjects (BBR only when enabled), operators, actions.","2. Optional: GET /api/v1/offer-types/rule-templates?industry=Håndværk for starter rules.","3. Draft { name, requiredInputFields, pricingRules: { rules } }. fieldId: lowercase letters, digits, underscore (bredde, hoejde, model).","4. POST /api/v1/offer-types/validate with the draft. Fix every errors[] item; review warnings[].","5. POST /api/v1/offer-types/simulate { offerType: draft, fields: {...}, lines: [{ offerProductId, quantity }] } and check lines/subtotal.","6. Show the user the plan, then POST /api/v1/offer-types (create) or PATCH /api/v1/offer-types/:id. A PATCH with pricingRules merges into existing VAT/currency settings; rules[] is replaced as a whole.","7. Optional: set defaultTemplateId to an OfferTemplate _id (GET /api/v1/offer-templates) and isDefault=true to preselect on new offers.","8. GET /api/v1/offer-types/:id?describe=true returns ruleSummaries in Danish to confirm with the user."],"offerTypeSchema":{"name":"string (required)","description":"string","isActive":"boolean (default true)","isDefault":"boolean. Preselected on new offers","defaultTemplateId":"OfferTemplate _id or null","requiredInputFields":"[{ fieldId, label, type, unit?, options?: [{ value, label }], required?: boolean (default true), helpText?, defaultValue?, expression? (computed only) }]","pricingRules":{"schemaVersion":2,"currency":"DKK","vatRate":25,"vatDisplayMode":"incl | excl","rounding":"{ enabled, method: round|ceil|floor, precision }","rules":"[{ id?, name, isActive?, order?, phase?, stopProcessing?, when: { all: Condition[], any: Condition[] }, then: Action[] }] (id/order/phase are filled in when omitted)"},"bbrFieldMapping":"BBR module only: [{ bbrField, calculatorField: fieldId, editable }] prefills a field from the address"},"fieldTypes":{"description":"requiredInputFields[].type. Use select/radio with options for model, farve, binding.","types":{"number":{"example":{"fieldId":"bredde","label":"Bredde","type":"number","unit":"cm"}},"text":{"example":{"fieldId":"note","label":"Note","type":"text","required":false}},"select":{"example":{"fieldId":"model","label":"Model","type":"select","options":[{"value":"basic","label":"Basic"},{"value":"premium","label":"Premium"}]}},"checkbox":{"example":{"fieldId":"montage","label":"Montage","type":"checkbox"},"note":"Condition: equals true"},"radio":{"example":{"fieldId":"side","label":"Side","type":"radio","options":[{"value":"v","label":"Venstre"}]}},"slider":{"example":{"fieldId":"antal","label":"Antal","type":"slider"}},"computed":{"note":"Read-only. expression may only reference fields placed before it in the array.","example":{"fieldId":"areal","label":"Areal","type":"computed","unit":"m²","expression":{"left":{"kind":"field","key":"areal_cm2"},"op":"div","right":{"kind":"number","value":10000},"round":{"mode":"round","precision":2}}}}}},"phases":{"description":"Rules run phase by phase, in order within a phase: lines → linePrices → offerAdjust → control. Price rules in linePrices see lines added in lines.","types":{"lines":{"label":"Linjer"},"linePrices":{"label":"Linjepriser"},"offerAdjust":{"label":"Tilbudsrabat og tillæg"},"control":{"label":"Kontrol"}}},"subjects":{"description":"Condition subjects. field = this offer type's requiredInputFields (by fieldId). bbr is only valid when the workspace has the BBR module; check GET /api/v1/offer-types/rule-reference (bbrEnabled).","types":{"offer":{"label":"Tilbud","moduleKey":null,"keys":[{"key":"subtotal","label":"Subtotal","type":"number"},{"key":"subtotalMonthly","label":"Månedlig sum","type":"number"},{"key":"subtotalOneTime","label":"Engangssum","type":"number"},{"key":"manualDiscountPercent","label":"Manuel rabat %","type":"number"},{"key":"currency","label":"Valuta","type":"text"},{"key":"ownerId","label":"Sælger","type":"text"}]},"line":{"label":"Linjer","moduleKey":null,"keys":[{"key":"productIds","label":"Produkter på tilbuddet","type":"line"},{"key":"qtyOfProduct","label":"Antal af produkt","type":"number"},{"key":"qtyInCategory","label":"Antal i kategori","type":"number"},{"key":"sumInCategory","label":"Sum i kategori","type":"number"},{"key":"lineCount","label":"Antal linjer","type":"number"}]},"customer":{"label":"Kunde","moduleKey":null,"keys":[{"key":"industry","label":"Branche","type":"text"},{"key":"country","label":"Land","type":"text"},{"key":"postalCode","label":"Postnummer","type":"text"},{"key":"tags","label":"Tags","type":"text"},{"key":"isExisting","label":"Eksisterende kunde","type":"boolean"},{"key":"customField","label":"Eget felt","type":"text"}]},"deal":{"label":"Deal","moduleKey":null,"keys":[{"key":"value","label":"Værdi","type":"number"},{"key":"flowId","label":"Pipeline","type":"text"},{"key":"stageId","label":"Stadie","type":"text"},{"key":"source","label":"Kilde","type":"text"},{"key":"customField","label":"Eget felt","type":"text"}]},"field":{"label":"Felter","moduleKey":null,"keys":"requiredInputFields[].fieldId"},"date":{"label":"Dato","moduleKey":null,"keys":[{"key":"today","label":"I dag","type":"date"}]},"bbr":{"label":"BBR","moduleKey":"bbr","keys":[{"key":"buildingYear","label":"Byggeår","type":"number"},{"key":"buildingArea","label":"Samlet bygningsareal (m²)","type":"number"},{"key":"housingArea","label":"Samlet boligareal (m²)","type":"number"},{"key":"groundFloorArea","label":"Bebygget areal (m²)","type":"number"},{"key":"floors","label":"Antal etager","type":"number"},{"key":"roofMaterialCode","label":"Tagmateriale (kode)","type":"number"},{"key":"roofMaterial","label":"Tagmateriale","type":"text"},{"key":"wallMaterialCode","label":"Ydervægsmateriale (kode)","type":"number"},{"key":"wallMaterial","label":"Ydervægsmateriale","type":"text"},{"key":"heatingTypeCode","label":"Varmeinstallation (kode)","type":"number"},{"key":"heatingType","label":"Varmeinstallation","type":"text"},{"key":"buildingUsageCode","label":"Bygningsanvendelse (kode)","type":"number"},{"key":"buildingUsage","label":"Bygningsanvendelse","type":"text"},{"key":"buildingNumber","label":"Bygningsnummer","type":"number"},{"key":"buildingCoordinates","label":"Bygningens koordinater","type":"text"},{"key":"roofArea","label":"Tagareal (beregnet, m²)","type":"number"},{"key":"basementArea","label":"Kælderareal (m²)","type":"number"},{"key":"atticArea","label":"Tagetage areal (m²)","type":"number"},{"key":"hasBasement","label":"Har kælder","type":"boolean"},{"key":"isFlatRoof","label":"Er fladt tag","type":"boolean"},{"key":"roofPitch","label":"Taghældning (grader)","type":"number"},{"key":"heightToGutter","label":"Højde til tagfod (m)","type":"number"},{"key":"unitArea","label":"Enhedens areal (m²)","type":"number"},{"key":"livingArea","label":"Beboelsesareal (m²)","type":"number"},{"key":"rooms","label":"Antal værelser","type":"number"},{"key":"toilets","label":"Antal toiletter","type":"number"},{"key":"bathrooms","label":"Antal badeværelser","type":"number"},{"key":"hasKitchen","label":"Har køkken","type":"boolean"},{"key":"hasBathroom","label":"Har badeværelse","type":"boolean"},{"key":"unitUsageCode","label":"Enhedens anvendelse (kode)","type":"number"},{"key":"housingType","label":"Boligtype (kode)","type":"number"},{"key":"plotArea","label":"Grundareal (m²)","type":"number"},{"key":"roadArea","label":"Vejareal (m²)","type":"number"},{"key":"waterSupplyCode","label":"Vandforsyning (kode)","type":"number"},{"key":"waterSupply","label":"Vandforsyning","type":"text"},{"key":"drainageCode","label":"Afløbsforhold (kode)","type":"number"},{"key":"drainage","label":"Afløbsforhold","type":"text"},{"key":"propertyNumber","label":"Ejendomsnummer","type":"number"},{"key":"bfeNumber","label":"BFE nummer","type":"number"},{"key":"matrikelNumber","label":"Matrikelnummer","type":"text"},{"key":"ejerlav","label":"Ejerlav","type":"text"},{"key":"fælleslod","label":"Er fælleslod","type":"boolean"},{"key":"fullAddress","label":"Fuld adresse","type":"text"},{"key":"street","label":"Vejnavn","type":"text"},{"key":"houseNumber","label":"Husnummer","type":"text"},{"key":"floor","label":"Etage","type":"text"},{"key":"door","label":"Dør","type":"text"},{"key":"postalCode","label":"Postnummer","type":"text"},{"key":"city","label":"By","type":"text"},{"key":"municipality","label":"Kommune","type":"text"},{"key":"region","label":"Region","type":"text"},{"key":"latitude","label":"Breddegrad","type":"number"},{"key":"longitude","label":"Længdegrad","type":"number"},{"key":"isResidential","label":"Er bolig","type":"boolean"},{"key":"isCommercial","label":"Er erhverv","type":"boolean"},{"key":"isRecreational","label":"Er fritid/sommerhus","type":"boolean"},{"key":"isAuxiliary","label":"Er udhus/garage","type":"boolean"},{"key":"garageArea","label":"Garageareal (m²)","type":"number"},{"key":"carportArea","label":"Carportareal (m²)","type":"number"},{"key":"shedArea","label":"Udhusareal (m²)","type":"number"},{"key":"totalOutbuildingArea","label":"Samlet udhusareal (m²)","type":"number"},{"key":"outbuildingCount","label":"Antal udhuse/garager","type":"number"},{"key":"hasGarage","label":"Har garage","type":"boolean"},{"key":"hasCarport","label":"Har carport","type":"boolean"},{"key":"hasShed","label":"Har udhus","type":"boolean"},{"key":"totalBuildingCount","label":"Antal bygninger total","type":"number"},{"key":"mainBuildingCount","label":"Antal hovedbygninger","type":"number"},{"key":"totalBuildingArea","label":"Samlet bygningsareal (m²)","type":"number"},{"key":"totalGroundFloorArea","label":"Samlet bebygget areal (m²)","type":"number"}]}}},"operators":{"description":"Condition { subject, key, operator, value }.","valueShapes":{"default":"scalar (number, string or boolean)","between":"{ min, max }","in / notIn":"array or comma string","containsProduct / notContainsProduct":"{ productId }","containsAllProducts / containsAnyProduct":"{ productIds: [] }","line.qtyOfProduct":"{ productId, compare }","line.qtyInCategory / sumInCategory":"{ category, compare }","customer.customField / deal.customField":"{ fieldKey, compare }","isEmpty / isNotEmpty":"no value"},"types":{"equals":{"label":"er","types":["number","text","boolean","select"]},"notEquals":{"label":"er ikke","types":["number","text","boolean","select"]},"greaterThan":{"label":"er større end","types":["number"]},"lessThan":{"label":"er mindre end","types":["number"]},"greaterThanOrEqual":{"label":"er mindst","types":["number"]},"lessThanOrEqual":{"label":"er højst","types":["number"]},"between":{"label":"er mellem","types":["number","date"]},"contains":{"label":"indeholder","types":["text"]},"notContains":{"label":"indeholder ikke","types":["text"]},"in":{"label":"er én af","types":["text","select"]},"notIn":{"label":"er ikke én af","types":["text","select"]},"isEmpty":{"label":"er tom","types":["number","text","select","boolean"]},"isNotEmpty":{"label":"er ikke tom","types":["number","text","select","boolean"]},"containsProduct":{"label":"indeholder produkt","types":["line"]},"notContainsProduct":{"label":"indeholder ikke produkt","types":["line"]},"containsAllProducts":{"label":"indeholder alle produkter","types":["line"]},"containsAnyProduct":{"label":"indeholder mindst ét produkt","types":["line"]},"before":{"label":"er før","types":["date"]},"after":{"label":"er efter","types":["date"]}}},"actionTypes":{"description":"Action objects in rule.then[].","types":{"adjustLinePrice":{"label":"Justér linjepris","defaultPhase":"linePrices","use":"Change unit price on matching lines (model, colour, binding, campaign).","config":{"mode":"percent | fixed (+/- per unit) | setPrice | expression","value":"number. percent -10 = 10 % rabat, fixed 50 = +50 kr per unit","expression":"when mode=expression. May use operand linePrice (current price, so rules stack in order)","target":"{ kind: all | product | category | frequency, ids?, category?, frequencyId? }"},"example":{"type":"adjustLinePrice","mode":"expression","target":{"kind":"all"},"expression":{"left":{"kind":"linePrice"},"op":"mul","right":{"kind":"number","value":1.2}}}},"tieredPrice":{"label":"Trinpris (mængde)","defaultPhase":"linePrices","use":"Unit price from quantity tiers on one product.","config":{"offerProductId":"product _id","tiers":"[{ min, max|null, price, label? }]","quantity":"optional { kind: field|productQty|expression } to pick the tier by another value"},"example":{"type":"tieredPrice","offerProductId":"<OfferProduct _id>","tiers":[{"min":0,"max":24,"price":200},{"min":25,"max":null,"price":180}]}},"addLine":{"label":"Tilføj linje","defaultPhase":"lines","use":"Add a line (product or custom). Removed again automatically when the rule stops matching.","config":{"offerProductId":"product _id, or empty with lineName for a custom line","lineName":"string","unit":"string (m², stk, timer)","quantity":"{ kind: fixed, value } | { kind: field, value: fieldId } | { kind: productQty, value: productId } | { kind: expression, expression }","mode":"omit to use product price | setPrice with value | expression","billingFrequencyId":"one_time | monthly | …"},"example":{"type":"addLine","lineName":"Stof","unit":"m²","mode":"setPrice","value":450,"quantity":{"kind":"field","value":"areal"}}},"removeLine":{"label":"Fjern linje","defaultPhase":"lines","use":"Remove matching lines (e.g. no setup fee for existing customers).","config":{"target":"{ kind: product|category|frequency, … }"},"example":{"type":"removeLine","target":{"kind":"category","category":"setup"}}},"offerDiscount":{"label":"Rabat på tilbud","defaultPhase":"offerAdjust","use":"Discount on the whole offer.","config":{"mode":"percent | fixed | expression","value":"number (positive)"},"example":{"type":"offerDiscount","mode":"percent","value":10,"target":{"kind":"offer"}}},"surcharge":{"label":"Tillæg","defaultPhase":"offerAdjust","use":"Add a surcharge line (freight, montage, weekend).","config":{"mode":"percent (of subtotal) | fixed | expression","lineName":"line name shown on the offer"},"example":{"type":"surcharge","mode":"expression","lineName":"Fragt","expression":{"left":{"kind":"field","key":"km"},"op":"mul","right":{"kind":"number","value":8.5}}}},"minimumQuantity":{"label":"Minimumsantal","defaultPhase":"lines","use":"Raise quantity on matching lines to at least value.","config":{"value":"number","target":"line target"},"example":{"type":"minimumQuantity","value":5,"target":{"kind":"product","ids":["<id>"]}}},"warn":{"label":"Advarsel","defaultPhase":"control","use":"Show a warning to the seller.","config":{"message":"string"},"example":{"type":"warn","message":"Husk montage"}},"blockSend":{"label":"Blokér afsendelse","defaultPhase":"control","use":"Block sending until the condition is resolved.","config":{"message":"string"},"example":{"type":"blockSend","message":"Minimum 2.000 kr"}},"reverseCharge":{"label":"Omvendt betalingspligt","defaultPhase":"offerAdjust","use":"0 % VAT (omvendt betalingspligt).","config":{},"example":{"type":"reverseCharge"}}}},"expressions":{"description":"{ left: Operand, op, right: Operand, round?: { mode: none|round|ceil|floor, precision } }. Chain via computed fields for more steps.","ops":{"mul":"×","add":"+","sub":"-","div":"÷","pctOf":"% af"},"operands":{"number":"{ kind: number, value }","field":"{ kind: field, key: fieldId }","bbr":"{ kind: bbr, key } (BBR module only)","productQty":"{ kind: productQty, productId }","categoryQty":"{ kind: categoryQty, category }","offerSubtotal":"{ kind: offerSubtotal }","linePrice":"{ kind: linePrice } (adjustLinePrice only)"},"where":["requiredInputFields[].expression (type computed)","action.quantity = { kind: expression, expression } (addLine, tieredPrice)","action.mode = expression + action.expression (adjustLinePrice, addLine price, surcharge, offerDiscount)"]},"templates":{"description":"Starter rules (Start fra skabelon in the editor). Live list with fresh ids: GET /api/v1/offer-types/rule-templates. Add the template fields to requiredInputFields first, then the rule to pricingRules.rules. Replace empty productId/ids placeholders.","types":{"volume_discount":{"name":"Mængderabat","description":"Rabat når antal af et produkt når en grænse","industry":"IT","fields":[],"rule":{"id":"rule_volume_discount","name":"Mængderabat","description":"Rabat når antal af et produkt når en grænse","isActive":true,"order":0,"phase":"linePrices","stopProcessing":false,"when":{"all":[{"subject":"line","key":"qtyOfProduct","operator":"greaterThanOrEqual","value":{"productId":"","compare":25}}],"any":[]},"then":[{"type":"adjustLinePrice","mode":"percent","value":-10,"target":{"kind":"product","ids":[]}}]}},"binding_discount":{"name":"Bindingsrabat","description":"Rabat på månedlige linjer ved lang bindingsperiode","industry":"IT","fields":[{"fieldId":"binding","label":"Bindingsperiode","type":"select","options":[{"value":"12","label":"12 mdr"},{"value":"24","label":"24 mdr"},{"value":"36","label":"36 mdr"}]}],"rule":{"id":"rule_binding_discount","name":"Bindingsrabat 36 mdr","description":"Rabat på månedlige linjer ved lang bindingsperiode","isActive":true,"order":0,"phase":"linePrices","stopProcessing":false,"when":{"all":[{"subject":"field","key":"binding","operator":"equals","value":"36"}],"any":[]},"then":[{"type":"adjustLinePrice","mode":"percent","value":-12,"target":{"kind":"frequency","frequencyId":"monthly","ids":[]}}]}},"mandatory_onboarding":{"name":"Obligatorisk onboarding","description":"Tilføj onboarding-linje når der er brugere","industry":"IT","fields":[{"fieldId":"users","label":"Antal brugere","type":"number","unit":"stk"}],"rule":{"id":"rule_mandatory_onboarding","name":"Obligatorisk onboarding","description":"Tilføj onboarding-linje når der er brugere","isActive":true,"order":0,"phase":"lines","stopProcessing":false,"when":{"all":[{"subject":"field","key":"users","operator":"greaterThan","value":0}],"any":[]},"then":[{"type":"addLine","offerProductId":"","lineName":"Onboarding","quantity":{"kind":"field","value":"users"},"mode":"setPrice","value":0}]}},"bundle_discount":{"name":"Pakkerabat","description":"Rabat når flere specifikke produkter er på tilbuddet","industry":"IT","fields":[],"rule":{"id":"rule_bundle_discount","name":"Pakkerabat","description":"Rabat når flere specifikke produkter er på tilbuddet","isActive":true,"order":0,"phase":"offerAdjust","stopProcessing":false,"when":{"all":[{"subject":"line","key":"productIds","operator":"containsAllProducts","value":{"productIds":[]}}],"any":[]},"then":[{"type":"offerDiscount","mode":"percent","value":20,"target":{"kind":"offer","ids":[]}}]}},"dependency_warn":{"name":"Afhængighedsadvarsel","description":"Advar hvis et produkt mangler et andet","industry":"IT","fields":[],"rule":{"id":"rule_dependency_warn","name":"Husk tilhørende produkt","description":"Advar hvis et produkt mangler et andet","isActive":true,"order":0,"phase":"control","stopProcessing":false,"when":{"all":[{"subject":"line","key":"productIds","operator":"containsProduct","value":{"productId":""}},{"subject":"line","key":"productIds","operator":"notContainsProduct","value":{"productId":""}}],"any":[]},"then":[{"type":"warn","message":"Husk det tilhørende produkt"}]}},"sla_surcharge":{"name":"SLA-tillæg","description":"Tillæg ved højere supportniveau","industry":"IT","fields":[{"fieldId":"support","label":"Supportniveau","type":"select","options":[{"value":"office","label":"Kontortid"},{"value":"24_7","label":"24/7"}]}],"rule":{"id":"rule_sla_surcharge","name":"24/7 supporttillæg","description":"Tillæg ved højere supportniveau","isActive":true,"order":0,"phase":"linePrices","stopProcessing":false,"when":{"all":[{"subject":"field","key":"support","operator":"equals","value":"24_7"}],"any":[]},"then":[{"type":"adjustLinePrice","mode":"percent","value":35,"target":{"kind":"all","ids":[]}}]}},"min_order_block":{"name":"Minimumsordre","description":"Blokér afsendelse under beløbsgrænse","industry":"Generelt","fields":[],"rule":{"id":"rule_min_order_block","name":"Minimumsordre","description":"Blokér afsendelse under beløbsgrænse","isActive":true,"order":0,"phase":"control","stopProcessing":false,"when":{"all":[{"subject":"offer","key":"subtotalMonthly","operator":"lessThan","value":2000}],"any":[]},"then":[{"type":"blockSend","message":"Månedlig sum skal være mindst 2.000 kr"}]}},"discount_cap":{"name":"Rabatgrænse","description":"Blokér afsendelse ved for høj manuel rabat","industry":"Generelt","fields":[],"rule":{"id":"rule_discount_cap","name":"Rabatgrænse 20 %","description":"Blokér afsendelse ved for høj manuel rabat","isActive":true,"order":0,"phase":"control","stopProcessing":false,"when":{"all":[{"subject":"offer","key":"manualDiscountPercent","operator":"greaterThan","value":20}],"any":[]},"then":[{"type":"blockSend","message":"Manuel rabat over 20 % er ikke tilladt"}]}},"existing_customer":{"name":"Eksisterende kunde","description":"Fjern opstartsgebyr for eksisterende kunder","industry":"IT","fields":[],"rule":{"id":"rule_existing_customer","name":"Ingen opstart for eksisterende","description":"Fjern opstartsgebyr for eksisterende kunder","isActive":true,"order":0,"phase":"lines","stopProcessing":false,"when":{"all":[{"subject":"customer","key":"isExisting","operator":"equals","value":true}],"any":[]},"then":[{"type":"removeLine","target":{"kind":"category","category":"setup","ids":[]}}]}},"campaign":{"name":"Kampagneperiode","description":"Rabat i en dato-periode","industry":"Generelt","fields":[],"rule":{"id":"rule_campaign","name":"Kampagnerabat","description":"Rabat i en dato-periode","isActive":true,"order":0,"phase":"linePrices","stopProcessing":false,"when":{"all":[{"subject":"date","key":"today","operator":"between","value":{"min":"2026-11-01","max":"2026-12-31"}}],"any":[]},"then":[{"type":"adjustLinePrice","mode":"percent","value":-10,"target":{"kind":"all","ids":[]}}]}},"reverse_charge":{"name":"Omvendt betalingspligt","description":"0 % moms for EU-kunde uden for DK","industry":"Generelt","fields":[],"rule":{"id":"rule_reverse_charge","name":"Omvendt betalingspligt (EU)","description":"0 % moms for EU-kunde uden for DK","isActive":true,"order":0,"phase":"offerAdjust","stopProcessing":false,"when":{"all":[{"subject":"customer","key":"country","operator":"notEquals","value":"DK"},{"subject":"customer","key":"country","operator":"isNotEmpty","value":null}],"any":[]},"then":[{"type":"reverseCharge"}]}},"fabric_sqm":{"name":"Stof pr. m²","description":"Areal = Bredde × Højde, derefter ÷ 10000. Tilføj stof-linje","industry":"Håndværk","fields":[{"fieldId":"bredde","label":"Bredde","type":"number","unit":"cm"},{"fieldId":"hoejde","label":"Højde","type":"number","unit":"cm"},{"fieldId":"areal_cm2","label":"Areal (cm²)","type":"computed","unit":"cm²","expression":{"left":{"kind":"field","key":"bredde"},"op":"mul","right":{"kind":"field","key":"hoejde"}}},{"fieldId":"areal","label":"Areal","type":"computed","unit":"m²","expression":{"left":{"kind":"field","key":"areal_cm2"},"op":"div","right":{"kind":"number","value":10000},"round":{"mode":"round","precision":2}}}],"rule":{"id":"rule_fabric_sqm","name":"Stof efter areal","description":"Areal = Bredde × Højde, derefter ÷ 10000. Tilføj stof-linje","isActive":true,"order":0,"phase":"lines","stopProcessing":false,"when":{"all":[{"subject":"field","key":"areal","operator":"greaterThan","value":0}],"any":[]},"then":[{"type":"addLine","lineName":"Stof","unit":"m²","quantity":{"kind":"field","value":"areal"}}]}},"freight_per_km":{"name":"Fragt pr. km","description":"Tillæg = kilometer × sats","industry":"Håndværk","fields":[{"fieldId":"km","label":"Kilometer","type":"number","unit":"km"}],"rule":{"id":"rule_freight_per_km","name":"Fragt pr. km","description":"Tillæg = kilometer × sats","isActive":true,"order":0,"phase":"offerAdjust","stopProcessing":false,"when":{"all":[{"subject":"field","key":"km","operator":"greaterThan","value":0}],"any":[]},"then":[{"type":"surcharge","mode":"expression","lineName":"Fragt","expression":{"left":{"kind":"field","key":"km"},"op":"mul","right":{"kind":"number","value":8.5},"round":{"mode":"round","precision":2}}}]}},"annual_prepay":{"name":"Årlig forudbetaling","description":"Månedspris × 10 (to måneder gratis)","industry":"IT","fields":[],"rule":{"id":"rule_annual_prepay","name":"Årlig forudbetaling","description":"Månedspris × 10 (to måneder gratis)","isActive":true,"order":0,"phase":"linePrices","stopProcessing":false,"when":{"all":[],"any":[]},"then":[{"type":"adjustLinePrice","mode":"expression","target":{"kind":"frequency","frequencyId":"monthly"},"expression":{"left":{"kind":"linePrice"},"op":"mul","right":{"kind":"number","value":10}}}]}},"onboarding_per_25":{"name":"Onboarding pr. 25 brugere","description":"Antal dage = brugere ÷ 25, rundet op","industry":"IT","fields":[{"fieldId":"brugere","label":"Antal brugere","type":"number","unit":"stk"}],"rule":{"id":"rule_onboarding_per_25","name":"Onboarding pr. 25 brugere","description":"Antal dage = brugere ÷ 25, rundet op","isActive":true,"order":0,"phase":"lines","stopProcessing":false,"when":{"all":[{"subject":"field","key":"brugere","operator":"greaterThan","value":0}],"any":[]},"then":[{"type":"addLine","lineName":"Onboarding","unit":"dage","quantity":{"kind":"expression","expression":{"left":{"kind":"field","key":"brugere"},"op":"div","right":{"kind":"number","value":25},"round":{"mode":"ceil","precision":0}}}}]}}}},"examples":{"description":"Complete POST /api/v1/offer-types bodies.","markise":{"name":"Markiser efter mål","requiredInputFields":[{"fieldId":"bredde","label":"Bredde","type":"number","unit":"cm"},{"fieldId":"hoejde","label":"Højde","type":"number","unit":"cm"},{"fieldId":"model","label":"Model","type":"select","options":[{"value":"basic","label":"Basic"},{"value":"premium","label":"Premium"}]},{"fieldId":"montage","label":"Montage","type":"checkbox","required":false},{"fieldId":"areal_cm2","label":"Areal (cm²)","type":"computed","unit":"cm²","expression":{"left":{"kind":"field","key":"bredde"},"op":"mul","right":{"kind":"field","key":"hoejde"}}},{"fieldId":"areal","label":"Areal","type":"computed","unit":"m²","expression":{"left":{"kind":"field","key":"areal_cm2"},"op":"div","right":{"kind":"number","value":10000},"round":{"mode":"round","precision":2}}}],"pricingRules":{"rules":[{"name":"Stof efter areal","phase":"lines","when":{"all":[{"subject":"field","key":"areal","operator":"greaterThan","value":0}]},"then":[{"type":"addLine","lineName":"Stof","unit":"m²","mode":"setPrice","value":450,"quantity":{"kind":"field","value":"areal"}}]},{"name":"Premium +20 %","phase":"linePrices","when":{"all":[{"subject":"field","key":"model","operator":"equals","value":"premium"}]},"then":[{"type":"adjustLinePrice","mode":"percent","value":20,"target":{"kind":"all"}}]},{"name":"Montage","phase":"offerAdjust","when":{"all":[{"subject":"field","key":"montage","operator":"equals","value":true}]},"then":[{"type":"surcharge","mode":"fixed","value":1200,"lineName":"Montage"}]}]}},"simulate":{"method":"POST","path":"/api/v1/offer-types/simulate","body":{"offerTypeId":"<OfferType _id> (or offerType: draft)","fields":{"bredde":420,"hoejde":250,"model":"premium","montage":true},"lines":[]},"expect":"Stof 10.5 m² × 540 kr = 5670 kr plus Montage 1200 kr"}}},"publicBooking":{"overview":{"title":"Public Booking API","description":"Unauthenticated HTTP API for custom booking UIs on your website. Identify the booking page with its publicId (from Indstillinger → Workspace → Booking). Do not put a workspace API key in the browser. Creates, cancels and reschedules run the same pipeline as /book: slots, calendar, CRM, emails, automations and webhooks.","baseUrl":"/api/bookings","authentication":"None. Guest actions after booking use cancellationToken.","cors":"CORS is enabled. Empty embedSettings.allowedDomains allows all origins (*). If you set allowed domains on the booking config, only those origins are accepted."},"flow":{"title":"Typical custom UI flow","steps":["GET /api/bookings/config/:publicId — meeting types, fields, hours, optional agents","GET /api/bookings/:publicId/slots?start=&end=&meetingTypeId= — available slots (ISO start)","POST /api/bookings/:publicId/book — create; store publicId + cancellationToken","GET /api/bookings/booking/:publicId/details?token= — manage page","POST /api/bookings/booking/:publicId/cancel — action cancel or reschedule"]},"endpoints":[{"method":"GET","path":"/api/bookings/config/:publicId","description":"Public booking page config (no secrets).","response":"{ ok, data: { publicId, title, meetingTypes, agentSelectionMode, agents?, businessHours, branding, defaultFields, customFields, advanceBookingDays, minimumNoticeHours, slotInterval } }"},{"method":"GET","path":"/api/bookings/:publicId/slots","description":"Available time slots for a date range.","queryParams":{"start":{"type":"string","required":true,"description":"ISO date"},"end":{"type":"string","required":true,"description":"ISO date"},"meetingTypeId":{"type":"string","description":"Meeting type id or name"}}},{"method":"POST","path":"/api/bookings/:publicId/book","description":"Create a booking. Rate limited (~10/hour per IP+email).","body":{"slotStart":{"type":"string","required":true,"description":"ISO datetime from slots"},"attendeeInfo":{"type":"object","required":true,"description":"{ name, email, phone?, notes? }"},"meetingTypeId":{"type":"string"},"selectedAgentId":{"type":"string"},"customFieldsData":{"type":"object"},"invitedParticipants":{"type":"array","description":"[{ name, email }]"},"selectedCvrCompanyId":{"type":"string"},"groupBooking":{"type":"object"}},"response":"{ ok, data: { _id, publicId, status, startDateTime, endDateTime, meetingType, meetingChannel, assignedAgent, cancellationToken, manageUrl, googleMeetLink } }"},{"method":"GET","path":"/api/bookings/:publicId/cvr-search","description":"CVR lookup when the config has CVR enabled.","queryParams":{"q":{"type":"string","required":true,"description":"Min 3 characters"}}},{"method":"GET","path":"/api/bookings/booking/:publicId/details","description":"Guest booking details. Requires ?token=cancellationToken."},{"method":"POST","path":"/api/bookings/booking/:publicId/cancel","description":"Cancel or reschedule with cancellationToken.","body":{"cancellationToken":{"type":"string","required":true},"action":{"type":"string","description":"cancel | reschedule"},"newSlotStart":{"type":"string","description":"Required for reschedule"},"meetingTypeId":{"type":"string","description":"Required for reschedule"}}},{"method":"GET","path":"/api/bookings/reschedule-suggestions/:bookingId","description":"Suggested slots for reschedule. Requires ?token=."},{"method":"GET","path":"/api/bookings/group/:groupBookingId","description":"Group booking details. Requires ?token=."},{"method":"POST","path":"/api/bookings/group/:groupBookingId/join","description":"Join a group booking.","body":{"token":{"type":"string","required":true},"attendeeInfo":{"type":"object","required":true}}},{"method":"POST","path":"/api/bookings/feedback/:bookingPublicId","description":"Submit post-meeting feedback."},{"method":"POST","path":"/api/bookings/booking/:publicId/late","description":"Notify the host that the guest is late."}],"tokens":{"title":"cancellationToken","description":"Returned once on create. Store it securely (your backend) if you offer cancel/reschedule. Treat it like a password. manageUrl is /book/cancel/{publicId}?token=…"},"webhooks":{"description":"Public create/cancel/reschedule emit the same outbound events as the product: booking.created, booking.cancelled, booking.rescheduled. Subscribe via /api/v1/webhooks/subscriptions."},"authenticatedV1":{"description":"Workspace API keys use /api/v1/bookings (list, create, cancel, reschedule, available-slots) and /api/v1/booking-configs. Create/cancel/reschedule/slots share the same pipeline as this public API."}},"publicChat":{"overview":{"title":"Public Chat Widget API","description":"Unauthenticated HTTP API for a custom website chat UI. Identify the widget with its widgetId (from Indstillinger → Support & chat → Chat). Do not put a workspace API key in the browser. Messages land as support tickets (channel: chat) in the inbox — same pipeline as the official embed.","baseUrl":"/api/chat-widget","authentication":"None on bootstrap. After POST /session, send x-visitor-token on every request. Treat the token like a password.","cors":"CORS is enabled. Empty security.allowedDomains allows all origins (*). If you set allowed domains on the widget, only those hosts (and subdomains) are accepted. Send Origin (browser) or ?origin= for the iframe parent page."},"flow":{"title":"Typical custom UI flow","steps":["GET /api/chat-widget/:widgetId/bootstrap — branding, texts, pre-chat, isOnline, file upload flags","POST /api/chat-widget/:widgetId/session — create or resume; store visitorToken","POST /api/chat-widget/:widgetId/conversations — firstMessage + optional preChatFields","GET .../conversations/:id/messages — poll ~4s for agent replies, agentTyping, isClosed","POST .../conversations/:id/messages — { content, attachments? }","Optional: POST read, typing, csat; uploads via /api/uploads/public/* (purpose: chatWidgetVisitor)"]},"endpoints":[{"method":"GET","path":"/api/chat-widget/:widgetId/bootstrap","description":"Public widget config (no secrets). Inactive or blocked domain returns { active: false }.","queryParams":{"origin":{"type":"string","description":"Embedding page origin (iframe). Custom UIs can rely on the Origin header."}},"response":"{ ok, data: { active, widgetId, isOnline, branding, texts, preChatForm, notifications, fileUpload } }"},{"method":"POST","path":"/api/chat-widget/:widgetId/session","description":"Create or refresh a visitor session. Rate limited (~180/hour per IP).","body":{"pageUrl":{"type":"string","description":"Page the visitor is on"}},"response":"{ ok, data: { visitorToken, visitor: { name, email, phone }, conversations: [{ conversationId, title, status, ticketNumber, lastMessagePreview, lastMessageAt, unread }] } }"},{"method":"POST","path":"/api/chat-widget/:widgetId/conversations","description":"Start a conversation (creates a support ticket). Requires x-visitor-token. Rate limited (~5/hour per IP).","body":{"firstMessage":{"type":"string","required":true},"preChatFields":{"type":"object","description":"{ name?, email?, phone?, customFields? }"},"pageUrl":{"type":"string"}},"response":"{ ok, data: { conversationId, ticketNumber, message } }"},{"method":"GET","path":"/api/chat-widget/:widgetId/conversations/:conversationId/messages","description":"Public messages + thread state. Poll about every 4 seconds. Requires x-visitor-token.","response":"{ ok, data: { messages, status, isClosed, permanentlyClosed, customerSatisfaction, agentTyping, fileUploadEnabled } }"},{"method":"POST","path":"/api/chat-widget/:widgetId/conversations/:conversationId/messages","description":"Send a visitor message. Requires x-visitor-token.","body":{"content":{"type":"string","description":"Required unless attachments are present"},"attachments":{"type":"array","description":"[{ url, filename, size, contentType }] from /api/uploads/public"}}},{"method":"POST","path":"/api/chat-widget/:widgetId/conversations/:conversationId/read","description":"Mark the thread read for the visitor (clears unread)."},{"method":"POST","path":"/api/chat-widget/:widgetId/conversations/:conversationId/typing","description":"Visitor typing indicator for agents.","body":{"isTyping":{"type":"boolean"}}},{"method":"POST","path":"/api/chat-widget/:widgetId/conversations/:conversationId/csat","description":"Rate a resolved/closed conversation 1–5.","body":{"rating":{"type":"number","required":true,"description":"Integer 1–5"}}},{"method":"POST","path":"/api/uploads/public/presign","description":"Presign a visitor file. purpose: chatWidgetVisitor. Headers: x-visitor-token. Body meta.widgetId required."}],"tokens":{"title":"visitorToken","description":"Returned from POST /session. Persist it (localStorage is fine for a website widget). Send as header x-visitor-token. Conversations are scoped to this visitor — do not expose it to other users."},"polling":{"description":"There is no visitor WebSocket. Poll GET messages about every 4s while a thread is open, and POST /session about every 12s for the conversation list / unread badge."},"authenticatedV1":{"description":"Agent inbox uses /api/v1/support-tickets (API key). Website visitors must use this public API, not v1."}}},"integrationScope":"Workspace API key integrations. Mobile app, session, and platform endpoints are intentionally omitted."}