Crm
Customer relationships: the deals of an entity's pipeline (opportunities with a customer or a prospect, their stages and probability), the timeline of calls, meetings, notes and tasks, the pipeline by stage, and the sales reports: the pipeline by owner and month, the win rate, each person's activity, quotas and the forecast; the people at customers and vendors (contacts, erased on request) and the organization's own fields on companies, contacts and deals. Nothing here posts.
| Operation | Summary | Scope | Idempotency-Key |
|---|---|---|---|
POST /forms/leads/{key} |
Send a lead from a website | ||
GET /v1/activities |
List activities | read |
|
POST /v1/activities |
Record an activity | draft |
required |
POST /v1/activities/{activity_id}/complete |
Mark a task done | draft |
|
GET /v1/contacts |
List contacts | read |
|
POST /v1/contacts |
Add a contact | draft |
required |
GET /v1/contacts/{contact_id} |
Get a contact | read |
|
PATCH /v1/contacts/{contact_id} |
Change a contact | draft |
|
POST /v1/contacts/{contact_id}/erase |
Erase a contact's personal data | draft |
|
POST /v1/contacts/{contact_id}/merge |
Merge a duplicate contact | draft |
|
GET /v1/custom-fields |
List the organization's own fields | read |
|
POST /v1/custom-fields |
Add a field of the organization's own | admin |
required |
PATCH /v1/custom-fields/{field_id} |
Change or retire a field of the organization's own | admin |
|
GET /v1/hubspot |
Get HubSpot | admin |
|
POST /v1/hubspot/authorize |
Start connecting HubSpot on its consent page | admin |
|
POST /v1/hubspot/connections |
Connect HubSpot with a private app | admin |
|
POST /v1/hubspot/connections/oauth |
Connect HubSpot with its consent page's answer | admin |
|
PATCH /v1/hubspot/connections/{connection_id} |
Change what HubSpot hands over | admin |
|
DELETE /v1/hubspot/connections/{connection_id} |
Disconnect HubSpot | admin |
|
POST /v1/hubspot/connections/{connection_id}/sync |
Read HubSpot now | admin |
|
GET /v1/lead-forms |
List website forms | read |
|
POST /v1/lead-forms |
Set up a website form | admin |
required |
PATCH /v1/lead-forms/{form_id} |
Change or switch off a website form | admin |
|
POST /v1/lead-forms/{form_id}/new-key |
Give a website form a new key | admin |
|
GET /v1/leads |
List leads | read |
|
POST /v1/leads |
Add a lead | draft |
required |
GET /v1/leads/summary |
Count leads by status | read |
|
GET /v1/leads/{lead_id} |
Get a lead | read |
|
PATCH /v1/leads/{lead_id} |
Change a lead | draft |
|
POST /v1/leads/{lead_id}/convert |
Convert a lead into a customer, contact and deal | draft |
required |
POST /v1/leads/{lead_id}/erase |
Erase a lead's personal data | draft |
|
GET /v1/me/mailbox |
Get your mailbox link | read |
|
DELETE /v1/me/mailbox |
Unlink your mailbox | read |
|
POST /v1/me/mailbox/google |
Link your Google account | draft |
|
POST /v1/me/mailbox/sync |
Read your mailbox now | draft |
|
GET /v1/me/record-access |
Which sales records you see | read |
|
GET /v1/opportunities |
List opportunities | read |
|
POST /v1/opportunities |
Create an opportunity | draft |
required |
GET /v1/opportunities/pipeline |
Get the pipeline of an entity | read |
|
GET /v1/opportunities/{opportunity_id} |
Get an opportunity | read |
|
PATCH /v1/opportunities/{opportunity_id} |
Change an opportunity | draft |
|
POST /v1/opportunities/{opportunity_id}/stage |
Move an opportunity to a stage | draft |
required |
PUT /v1/people/{user_id}/record-visibility |
Set which sales records a person sees | admin |
|
GET /v1/record-access |
List what each person sees | admin |
|
POST /v1/record-access/hand-over |
Hand one person's records to another | admin |
required |
GET /v1/sales-teams |
List sales teams | read |
|
POST /v1/sales-teams |
Set up a sales team | admin |
required |
PATCH /v1/sales-teams/{team_id} |
Rename a sales team | admin |
|
DELETE /v1/sales-teams/{team_id} |
Remove a sales team | admin |
|
PUT /v1/sales-teams/{team_id}/members/{user_id} |
Add a person to a sales team | admin |
|
DELETE /v1/sales-teams/{team_id}/members/{user_id} |
Take a person out of a sales team | admin |
|
GET /v1/sales/activity |
Get the sales activity per person | read |
|
GET /v1/sales/forecast |
Get the sales forecast | read |
|
GET /v1/sales/pipeline |
Get the pipeline report | read |
|
GET /v1/sales/quotas |
List quotas | read |
|
PUT /v1/sales/quotas |
Set quotas | approve |
|
GET /v1/sales/win-rate |
Get the win rate | read |
POST /forms/leads/{key}
Send a lead from a website
No credential: the key names the form. Whoever fills in the form becomes a lead of the
form's company (status new, source web_form, owned by the form's owner). Limited per client
address and per form (429 with Retry-After).
| Required scope | any valid credential |
| Idempotency-Key | not used |
| Operation id | submit_forms_leads__key__post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | key |
string | yes | The form's public key (GET /v1/lead-forms). |
| query | lang |
string or null | Language of an HTML answer. |
Request body
application/json (optional): object
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | ||
last_name |
string | ||
name |
string | ||
email |
string | yes | |
company |
string | ||
role |
string | ||
phone |
string | ||
country |
string | ||
message |
string | ||
campaign |
string | ||
website |
string |
application/x-www-form-urlencoded (optional): object
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | ||
last_name |
string | ||
name |
string | ||
email |
string | yes | |
company |
string | ||
role |
string | ||
phone |
string | ||
country |
string | ||
message |
string | ||
campaign |
string | ||
website |
string |
Example request
curl -X POST "$ERP_URL/forms/leads/lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Eva",
"last_name": "Beispiel",
"email": "eva@beispiel.example",
"company": "Beispiel GmbH",
"message": "Please send us a quote for 25 seats."
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Received (also when dropped as spam: a robot learns nothing). JSON to a script; the form's thank-you page to the form's own page. | object, text/html |
| 303 | Received from a website's own HTML form: to its redirect_url. |
|
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | lead_form_origin: not sent from one of the form's websites. |
|
| 404 | lead_form_unknown: no such form, or it is switched off. |
|
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
|
| 429 | rate_limited: too many submissions; see Retry-After. |
Example response (shape)
{
"received": true
}
GET /v1/activities
List activities
The timeline of a deal, a lead or a customer, or the open tasks: the newest first (open tasks: the earliest due first). Someone who sees only their own or their team's records gets the activities of those people, the tasks assigned to them and the timelines of the deals, leads and customers they see. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | list_activities_v1_activities_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only this entity's activities (code or id). | |
| query | opportunity_id |
string (uuid) or null | Only this deal's. | |
| query | party_id |
string (uuid) or null | Only this customer's. | |
| query | kind |
"call" | "email" | "meeting" | "note" | "task" or null |
Only this kind. | |
| query | open_tasks |
boolean | Only tasks not done yet. (default false) |
|
| query | assignee_user_id |
string (uuid) or null | Only tasks of this person. | |
| query | contact_id |
string (uuid) or null | Only those with this contact (GET /v1/contacts). |
|
| query | owner_user_id |
string (uuid) or null | Only those this person did or set (GET /v1/people). |
|
| query | lead_id |
string (uuid) or null | Only this lead's (GET /v1/leads). |
|
| query | limit |
integer | How many, the newest first. (min 1; max 1000; default 100) |
Example request
curl "$ERP_URL/v1/activities" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of ActivityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"body": "25 users, budget approved for Q1",
"created_at": "2026-10-04T08:10:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "4a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"kind": "call",
"occurred_on": "2026-10-04",
"opportunity_id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"opportunity_number": "OPP-000001",
"overdue": false,
"subject": "Discovery call"
}
]
POST /v1/activities
Record an activity
A note, call, meeting or e-mail about a deal (opportunity_id) or a customer (entity
and party), or a task with its due day and assignee (by default the deal's owner). An
activity is never edited or deleted; a task is completed once
(POST /v1/activities/{activity_id}/complete). Needs an Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_activity_v1_activities_post |
Request body
application/json (required): ActivityInput
| Field | Type | Required | Description |
|---|---|---|---|
opportunity_id |
string (uuid) or null | The deal it is about (GET /v1/opportunities). |
|
lead_id |
string (uuid) or null | The lead it is about, before they are a customer (GET /v1/leads); then name no deal, entity, customer or contact. |
|
entity |
string or null | Code (or id) of the entity, for an activity about a customer only (one about a deal is in the deal's entity). | |
party |
string or null | Code (or id) of the customer, for an activity about a customer only (one about a deal names the deal's customer). | |
kind |
"call" | "email" | "meeting" | "note" | "task" |
yes | note, call, meeting, email or task. |
subject |
string | yes | What it was or is. (min length 1; max length 200) |
body |
string or null | The details. (max length 5000) | |
occurred_on |
string (date) or null | When it happened (a task: when it was set); omitted: today. | |
due_on |
string (date) or null | A task: when it is due. | |
assignee_user_id |
string (uuid) or null | A task: who does it (GET /v1/people); omitted: the deal's or the lead's owner. |
|
owner_user_id |
string (uuid) or null | Who did it, or set the task (GET /v1/people); omitted: you (a key or an agent: the person behind it). It never changes. |
|
contact_id |
string (uuid) or null | The person at the customer it was with (GET /v1/contacts?party_id=); omitted for an activity of a deal: the deal's contact. |
Example request
curl -X POST "$ERP_URL/v1/activities" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"body": "25 users, budget approved for Q1",
"kind": "call",
"occurred_on": "2026-10-04",
"opportunity_id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"subject": "Discovery call"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | ActivityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"body": "25 users, budget approved for Q1",
"created_at": "2026-10-04T08:10:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "4a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"kind": "call",
"occurred_on": "2026-10-04",
"opportunity_id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"opportunity_number": "OPP-000001",
"overdue": false,
"subject": "Discovery call"
}
POST /v1/activities/{activity_id}/complete
Mark a task done
Record that a task is done, by whom and when. 409 for an activity that is not a task or a task already done; 404 when it does not exist.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | complete_activity_v1_activities__activity_id__complete_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | activity_id |
string (uuid) | yes | Id of the activity (GET /v1/activities). |
Example request
curl -X POST "$ERP_URL/v1/activities/4a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d/complete" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ActivityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"body": "25 users, budget approved for Q1",
"created_at": "2026-10-04T08:10:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "4a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"kind": "call",
"occurred_on": "2026-10-04",
"opportunity_id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"opportunity_number": "OPP-000001",
"overdue": false,
"subject": "Discovery call"
}
GET /v1/contacts
List contacts
The people at customers and vendors, by last and first name. Someone who sees only their own or their team's records gets the contacts of those people and of the customers they see (and every vendor's). Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | list_contacts_v1_contacts_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only this company's contacts (code or id). | |
| query | party_id |
string (uuid) or null | Only the people at this customer or vendor. | |
| query | q |
string or null | Text in the name, e-mail or role. (max length 200) | |
| query | email |
string or null | Only this e-mail address (any case). (max length 200) | |
| query | erased |
boolean | Also the erased contacts. (default false) |
|
| query | owner_user_id |
string (uuid) or null | Only those this person looks after (GET /v1/people). |
|
| query | limit |
integer | How many, by last and first name. (min 1; max 1000; default 200) |
Example request
curl "$ERP_URL/v1/contacts" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
]
POST /v1/contacts
Add a contact
A person at a customer or vendor (party): name, role, e-mail, phones, the language to
write to them in, whether they want sales e-mails, and the organization's own contact fields
(custom). An agent may add one. 404 for an unknown company or party; 422 for a field that
is not one of the organization's or a value of the wrong type. Needs an Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_contact_v1_contacts_post |
Request body
application/json (required): ContactInput
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string or null | First name. (min length 1; max length 100) | |
role |
string or null | Role or department, e.g. Head of purchasing. A role that says billing, accounting or Buchhaltung makes the contact one an invoice goes to by default. (max length 100) |
|
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
mobile |
string or null | Mobile number. (max length 50) | |
last_name |
string | yes | Last name. A name sent instead is split: its last word is the last name, the words before it the first name. (min length 1; max length 100) |
entity |
string | yes | Code (or id) of the company (entity). |
party |
string | yes | Code (or id) of the customer or vendor the person is at. |
kind |
"customer" | "vendor" or null |
Needed only when party is a code of a customer and a vendor. |
|
language |
"de" | "en" | "fr" | "it" | "rm" | "sv" or null |
Language to write to them in (de, fr, it, en, sv, rm). |
|
email_opt_out |
boolean | They asked not to get sales or marketing e-mails (invoices still go). (default false) |
|
custom |
map of string | number | integer | boolean or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
owner_user_id |
string (uuid) or null | Who looks after the contact (GET /v1/people); omitted: you (a key or an agent: the person behind it). |
Example request
curl -X POST "$ERP_URL/v1/contacts" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"email": "anna.muster@kunde.example",
"entity": "CH01",
"first_name": "Anna",
"language": "de",
"last_name": "Muster",
"party": "C-CH",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
GET /v1/contacts/{contact_id}
Get a contact
One contact; an erased one shows only that it existed.
| Required scope | read (admin implies every scope) |
| Operation id | get_contact_v1_contacts__contact_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | contact_id |
string (uuid) | yes | Id of the contact (GET /v1/contacts). |
Example request
curl "$ERP_URL/v1/contacts/5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
PATCH /v1/contacts/{contact_id}
Change a contact
Change what the request names ("" clears a text field but the last name; custom
changes only the keys sent). A contact never moves to another customer or vendor. The audit
trail records which fields changed, never their values. 409 contact_erased for an erased
one.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_contact_v1_contacts__contact_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | contact_id |
string (uuid) | yes | Id of the contact (GET /v1/contacts). |
Request body
application/json (required): ContactUpdate
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string or null | "" clears. (max length 100) |
|
role |
string or null | Role or department, e.g. Head of purchasing. A role that says billing, accounting or Buchhaltung makes the contact one an invoice goes to by default. (max length 100) |
|
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
mobile |
string or null | Mobile number. (max length 50) | |
last_name |
string or null | New. (min length 1; max length 100) | |
language |
"de" | "en" | "fr" | "it" | "rm" | "sv" | "" or null |
"" clears. |
|
email_opt_out |
boolean or null | New opt-out. | |
custom |
map of string | number | integer | boolean or null or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
owner_user_id |
string (uuid) or null | New owner (GET /v1/people). Someone who sees only their own or their team's records hands a contact only to one of those people. |
Example request
curl -X PATCH "$ERP_URL/v1/contacts/5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "anna@kunde.example",
"role": "CFO"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
POST /v1/contacts/{contact_id}/erase
Erase a contact's personal data
Empty the contact's name, role, e-mail, phones, and own fields for good (the
person asked to be forgotten, or the relationship ended). The row stays, so that the deals and
activities that named it still say someone was there; it cannot be undone. A person's
decision: 403 contact_erase_agent for an agent. 409 contact_erased if already erased.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | erase_contact_v1_contacts__contact_id__erase_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | contact_id |
string (uuid) | yes | Id of the contact (GET /v1/contacts). |
Example request
curl -X POST "$ERP_URL/v1/contacts/5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e/erase" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
POST /v1/contacts/{contact_id}/merge
Merge a duplicate contact
Merge this contact, entered twice, into into, another contact of the same customer or
vendor (ADR-0089): what the one that stays has no value for (first name, role, e-mail,
phones, language, own fields) comes from this one, a "no sales e-mails" wish stays, its deals
and activities move, and this one is erased (merged_into_id says where it went). Returns
the contact that stays. A person's decision: 403 merge_agent for an agent. 422
contact_merge_other_party for a contact of another customer or vendor, or itself; 409
contact_erased if either was erased.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | merge_contact_v1_contacts__contact_id__merge_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | contact_id |
string (uuid) | yes | Id of the contact (GET /v1/contacts). |
Request body
application/json (required): ContactMerge
| Field | Type | Required | Description |
|---|---|---|---|
into |
string (uuid) | yes | Id of the contact that stays (same customer or vendor). |
Example request
curl -X POST "$ERP_URL/v1/contacts/5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e/merge" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"into": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ContactOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"custom": {},
"email": "anna.muster@kunde.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Anna",
"id": "5b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e",
"language": "de",
"last_name": "Muster",
"name": "Anna Muster",
"party_code": "C-CH",
"party_id": "6c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
"party_kind": "customer",
"party_name": "Kunde AG",
"phone": "+41 44 123 45 67",
"role": "Head of purchasing"
}
GET /v1/custom-fields
List the organization's own fields
The fields the organization added to its companies (customers and vendors), contacts and
deals, in their order. A record's values are in its custom, by the field's key.
| Required scope | read (admin implies every scope) |
| Operation id | list_custom_fields_v1_custom_fields_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | record |
"company" | "contact" | "deal" or null |
Only the fields of company, contact or deal records. |
|
| query | retired |
boolean | Also the retired fields. (default false) |
Example request
curl "$ERP_URL/v1/custom-fields" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of CustomFieldOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"record": "company",
"key": "string",
"label": "string",
"type": "choice",
"choices": [
"string"
],
"position": 1,
"retired": true
}
]
POST /v1/custom-fields
Add a field of the organization's own
A field on every company, contact or deal: text, number, date, yes_no or choice
(with its choices). Its key and type never change. Up to 25 in use per kind of record (409
custom_fields_full); 409 custom_field_exists for a key in use. Needs an
Idempotency-Key.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_custom_field_v1_custom_fields_post |
Request body
application/json (required): CustomFieldInput
| Field | Type | Required | Description |
|---|---|---|---|
record |
"company" | "contact" | "deal" |
yes | company, contact or deal. |
key |
string | yes | How the API names it: lower case letters, digits and _, e.g. industry; never changes. (pattern ^[a-z][a-z0-9_]{0,39}$) |
label |
string | yes | What the screens call it. (min length 1; max length 80) |
type |
"choice" | "date" | "number" | "text" | "yes_no" |
yes | text, number, date, yes_no or choice; never changes. |
choices |
array of string or null | For choice: the choices, in order (1 to 50). |
|
position |
integer | Lower comes first. (min 0; max 1000; default 0) |
Example request
curl -X POST "$ERP_URL/v1/custom-fields" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"choices": [
"Retail",
"Manufacturing",
"Services"
],
"key": "industry",
"label": "Industry",
"record": "company",
"type": "choice"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | CustomFieldOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"record": "company",
"key": "string",
"label": "string",
"type": "choice",
"choices": [
"string"
],
"position": 1,
"retired": true
}
PATCH /v1/custom-fields/{field_id}
Change or retire a field of the organization's own
A new label, position or choices (a choice a record holds stays: 409
custom_field_choice_in_use), or retired: a retired field leaves the forms and takes no
new values; the values records hold stay readable. A field is never deleted.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_custom_field_v1_custom_fields__field_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | field_id |
string (uuid) | yes | Id of the field (GET /v1/custom-fields). |
Request body
application/json (required): CustomFieldUpdate
| Field | Type | Required | Description |
|---|---|---|---|
label |
string or null | New label. (min length 1; max length 80) | |
choices |
array of string or null | A choice field's new choices; a choice a record holds stays in the list. | |
position |
integer or null | New position. (min 0; max 1000) | |
retired |
boolean or null | true hides the field from forms (its values stay readable); false brings it back. |
Example request
curl -X PATCH "$ERP_URL/v1/custom-fields/7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Sector"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | CustomFieldOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"record": "company",
"key": "string",
"label": "string",
"type": "choice",
"choices": [
"string"
],
"position": 1,
"retired": true
}
GET /v1/hubspot
Get HubSpot
Whether HubSpot can be connected here, whether HubSpot's consent page can be used, the permissions a private app needs, and each company connected: the account, the stage at which a contact becomes a lead, the default owner, how far HubSpot was read and the last error.
| Required scope | admin (admin implies every scope) |
| Operation id | get_hubspot_v1_hubspot_get |
Example request
curl "$ERP_URL/v1/hubspot" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotSetup |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"available": true,
"oauth": true,
"client_id": "string",
"scopes": [
"string"
],
"connections": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity": "string",
"portal_id": 1,
"account_url": "string",
"method": "key",
"stage": "lead",
"owner": null,
"status": "active",
"read_to": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"leads": 1,
"linked_by": {},
"linked_at": "2026-09-21T09:30:00Z"
}
]
}
POST /v1/hubspot/authorize
Start connecting HubSpot on its consent page
HubSpot's consent page for the company: open url; HubSpot sends the browser back to
redirect_uri with a code and a state for POST /v1/hubspot/connections/oauth, within 15
minutes. 409 when this system has no HubSpot app (connect with a private app instead).
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | authorize_v1_hubspot_authorize_post |
Request body
application/json (required): HubSpotAuthorize
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company. |
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. (default "marketingqualifiedlead") |
|
owner_user_id |
string (uuid) or null | Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. |
Example request
curl -X POST "$ERP_URL/v1/hubspot/authorize" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"stage": "marketingqualifiedlead"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotAuthorizeOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"url": "string",
"redirect_uri": "string"
}
POST /v1/hubspot/connections
Connect HubSpot with a private app
Connect a company with the access token of a private app in its own HubSpot. HubSpot is asked for the account (proving the token) and Stabiso's properties are added to its contacts. From then on its contacts at the hand-over stage come in as leads every 5 minutes (the first round reads the last 90 days), and each contact is sent what became of its lead. Connecting again replaces the token. 409 in a test copy or when the company is connected to another account; 422 when HubSpot refuses the token or a permission is missing.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | connect_key_v1_hubspot_connections_post |
Request body
application/json (required): HubSpotKeyInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company. |
access_token |
string | yes | The access token of a private app in the company's HubSpot with the scopes of GET /v1/hubspot. Kept sealed, never shown again. (min length 10; max length 500) |
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. (default "marketingqualifiedlead") |
|
owner_user_id |
string (uuid) or null | Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. |
Example request
curl -X POST "$ERP_URL/v1/hubspot/connections" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"access_token": "pat-eu1-0b9c4d2e-7f1a-4c3b-9e8d-1a2b3c4d5e6f",
"entity": "CH01",
"stage": "marketingqualifiedlead"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotConnection |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity": "string",
"portal_id": 1,
"account_url": "string",
"method": "key",
"stage": "lead",
"owner": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
},
"status": "active",
"read_to": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"leads": 1,
"linked_by": {},
"linked_at": "2026-09-21T09:30:00Z"
}
POST /v1/hubspot/connections/oauth
Connect HubSpot with its consent page's answer
Finish connecting with the code and state HubSpot sent the browser back with (only
for whoever started it). 422 when the state is not theirs or too old; 409 as above.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | connect_oauth_v1_hubspot_connections_oauth_post |
Request body
application/json (required): HubSpotOAuthInput
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | HubSpot's one-time code. (min length 1; max length 2000) |
state |
string | yes | The state HubSpot returned. (min length 1; max length 2000) |
Example request
curl -X POST "$ERP_URL/v1/hubspot/connections/oauth" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "na1-6a2b-…",
"state": "eyJvcmciOiI….Xq3v…"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotConnection |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity": "string",
"portal_id": 1,
"account_url": "string",
"method": "key",
"stage": "lead",
"owner": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
},
"status": "active",
"read_to": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"leads": 1,
"linked_by": {},
"linked_at": "2026-09-21T09:30:00Z"
}
PATCH /v1/hubspot/connections/{connection_id}
Change what HubSpot hands over
Change the hand-over stage or the default owner (from the next round on).
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_connection_v1_hubspot_connections__connection_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | connection_id |
string (uuid) | yes | Id of the connection (GET /v1/hubspot). |
Request body
application/json (required): HubSpotUpdate
| Field | Type | Required | Description |
|---|---|---|---|
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" or null |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. |
|
owner_user_id |
string (uuid) | "" or null |
Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. "" clears. |
Example request
curl -X PATCH "$ERP_URL/v1/hubspot/connections/7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"owner_user_id": "",
"stage": "lead"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotConnection |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity": "string",
"portal_id": 1,
"account_url": "string",
"method": "key",
"stage": "lead",
"owner": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
},
"status": "active",
"read_to": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"leads": 1,
"linked_by": {},
"linked_at": "2026-09-21T09:30:00Z"
}
DELETE /v1/hubspot/connections/{connection_id}
Disconnect HubSpot
Disconnect: nothing more is read or sent, and a consent's token is taken back at HubSpot (a private app's token is deleted in HubSpot). The leads stay, and so does which contact each came from, so connecting the same account again brings no contact in twice.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | disconnect_v1_hubspot_connections__connection_id__delete |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | connection_id |
string (uuid) | yes | Id of the connection (GET /v1/hubspot). |
Example request
curl -X DELETE "$ERP_URL/v1/hubspot/connections/7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 204 | No content | |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
POST /v1/hubspot/connections/{connection_id}/sync
Read HubSpot now
A round now (they run every 5 minutes): read the contacts that changed, make leads of the new ones and send each contact what became of its lead. 409 when HubSpot stopped accepting the connection (connect again) or in a test copy.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | sync_connection_v1_hubspot_connections__connection_id__sync_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | connection_id |
string (uuid) | yes | Id of the connection (GET /v1/hubspot). |
Example request
curl -X POST "$ERP_URL/v1/hubspot/connections/7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a/sync" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HubSpotSyncOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"read": 1,
"new_leads": 1,
"updated_leads": 1,
"sent": 1,
"connection": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"entity": "string",
"portal_id": 1,
"account_url": "string",
"method": "key",
"stage": "lead",
"owner": {
"id": null,
"email": null,
"name": null
},
"status": "active",
"read_to": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"leads": 1,
"linked_by": {},
"linked_at": "2026-09-21T09:30:00Z"
}
}
GET /v1/lead-forms
List website forms
The forms on the organization's websites that create leads, by name, with their public key, their own page and how many leads each created.
| Required scope | read (admin implies every scope) |
| Operation id | list_lead_forms_v1_lead_forms_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only this company's forms (code or id). |
Example request
curl "$ERP_URL/v1/lead-forms" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of LeadFormOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"active": true,
"allowed_origins": [
"https://www.example.ch"
],
"created_at": "2026-10-20T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"key": "lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a",
"language": "de",
"last_lead_at": "2026-10-23T09:12:00Z",
"leads": 12,
"name": "Contact us",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"page_path": "/forms/leads/lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a"
}
]
POST /v1/lead-forms
Set up a website form
A form for a website: whoever fills it in becomes a lead of the company, owned by
owner_user_id, with the source web_form. Link to or embed its own page (page_path), or
send your website's own form to it (docs/erp/leads.md). A person's setting: 403
lead_form_agent for an agent. Needs an Idempotency-Key.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_lead_form_v1_lead_forms_post |
Request body
application/json (required): LeadFormInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company whose leads it creates. |
name |
string | yes | What the form is, e.g. Contact us. (min length 1; max length 100) |
owner_user_id |
string (uuid) or null | Who the leads go to (GET /v1/people); omitted: nobody yet. |
|
language |
"de" | "en" | "fr" | "it" | "sv" |
The language of its own page. (default "en") |
|
allowed_origins |
array of string | The websites it may be sent from, e.g. https://www.example.ch; empty: any. A browser names its page's site, so a copy of the form on another site is refused. (max items 20) |
|
thank_you |
string or null | What its page says after sending. (max length 500) | |
redirect_url |
string or null | A page of your own (https) to send people to after a form of your own website was sent; omitted: the thank-you page. (max length 500; pattern ^https://) |
Example request
curl -X POST "$ERP_URL/v1/lead-forms" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"allowed_origins": [
"https://www.example.ch"
],
"entity": "CH01",
"language": "de",
"name": "Contact us",
"owner_user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"thank_you": "Danke! Wir melden uns innert eines Arbeitstages."
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | LeadFormOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"active": true,
"allowed_origins": [
"https://www.example.ch"
],
"created_at": "2026-10-20T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"key": "lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a",
"language": "de",
"last_lead_at": "2026-10-23T09:12:00Z",
"leads": 12,
"name": "Contact us",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"page_path": "/forms/leads/lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a"
}
PATCH /v1/lead-forms/{form_id}
Change or switch off a website form
Its name, owner, language, websites, thank-you text or page to go to; active: false
switches it off (its key then answers 404). A form is never deleted: its leads name it.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_lead_form_v1_lead_forms__form_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | form_id |
string (uuid) | yes | Id of the form (GET /v1/lead-forms). |
Request body
application/json (required): LeadFormUpdate
| Field | Type | Required | Description |
|---|---|---|---|
name |
string or null | New name. (min length 1; max length 100) | |
active |
boolean or null | false switches it off. |
|
owner_user_id |
string (uuid) | "" or null |
Who the leads go to; "": nobody. |
|
language |
"de" | "en" | "fr" | "it" | "sv" or null |
Its page's language. | |
allowed_origins |
array of string or null | The websites it may be sent from; []: any. (max items 20) |
|
thank_you |
string or null | "" clears. (max length 500) |
|
redirect_url |
string or null | "" clears. (max length 500; pattern ^(https://.*)?$) |
Example request
curl -X PATCH "$ERP_URL/v1/lead-forms/9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"allowed_origins": [
"https://www.example.ch"
],
"language": "fr"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadFormOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"active": true,
"allowed_origins": [
"https://www.example.ch"
],
"created_at": "2026-10-20T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"key": "lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a",
"language": "de",
"last_lead_at": "2026-10-23T09:12:00Z",
"leads": 12,
"name": "Contact us",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"page_path": "/forms/leads/lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a"
}
POST /v1/lead-forms/{form_id}/new-key
Give a website form a new key
A new public key, for a form whose key is abused: the old key stops working at once, so the website must get the new one.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | new_lead_form_key_v1_lead_forms__form_id__new_key_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | form_id |
string (uuid) | yes | Id of the form (GET /v1/lead-forms). |
Example request
curl -X POST "$ERP_URL/v1/lead-forms/9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c/new-key" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadFormOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"active": true,
"allowed_origins": [
"https://www.example.ch"
],
"created_at": "2026-10-20T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"entity": "CH01",
"id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"key": "lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a",
"language": "de",
"last_lead_at": "2026-10-23T09:12:00Z",
"leads": 12,
"name": "Contact us",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"page_path": "/forms/leads/lf_0b9c4a2e7d1f4e3a9c5b6d7e8f9a0b1c_Qm3xV7pL2sK9tR4wY8zN1a"
}
GET /v1/leads
List leads
The people who may become customers, the newest first, with their status, source and owner. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | list_leads_v1_leads_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only this company's leads (code or id). | |
| query | status |
"converted" | "disqualified" | "new" | "working" or null |
Only leads at this status. | |
| query | open_only |
boolean | Only open leads (new and working). (default false) |
|
| query | source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" or null |
Only leads from this source. | |
| query | owner_user_id |
string (uuid) or null | Only this person's. | |
| query | unassigned |
boolean | Only the leads nobody owns. (default false) |
|
| query | q |
string or null | Text in the name, company, e-mail or number. (max length 200) | |
| query | limit |
integer | How many, the newest first. (min 1; max 1000; default 200) |
Example request
curl "$ERP_URL/v1/leads" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
]
POST /v1/leads
Add a lead
A person who may become a customer: name, company, e-mail, phone, where they came from
(source) and who works on them (owner_user_id); it starts new. An agent may add one.
Website forms add theirs themselves. Needs an Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_lead_v1_leads_post |
Request body
application/json (required): LeadInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company whose lead it is. |
first_name |
string or null | First name. (max length 100) | |
last_name |
string | yes | Last name. (min length 1; max length 100) |
company |
string or null | Their company. (max length 200) | |
role |
string or null | Their job title. (max length 100) | |
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
country |
string or null | Their country (ISO 3166-1). (pattern ^[A-Z]{2}$) |
|
message |
string or null | What they asked. (max length 5000) | |
source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" |
Where the lead came from: web_form, phone, email, referral, partner, event, advertising, import, hubspot, other. (default "other") |
|
source_detail |
string or null | The campaign, event or list, e.g. a fair. (max length 200) | |
owner_user_id |
string (uuid) or null | Who works on the lead (GET /v1/people); omitted: nobody yet. |
|
email_opt_out |
boolean | They asked not to get sales or marketing e-mails. (default false) |
Example request
curl -X POST "$ERP_URL/v1/leads" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"company": "Beispiel GmbH",
"country": "DE",
"email": "eva@beispiel.example",
"entity": "CH01",
"first_name": "Eva",
"last_name": "Beispiel",
"owner_user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"phone": "+49 30 1234567",
"role": "Head of Finance",
"source": "event",
"source_detail": "Swiss Finance Day 2026"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
GET /v1/leads/summary
Count leads by status
How many leads the company has at each status, where the open ones came from, how many nobody owns, and how many were converted in the last 30 days.
| Required scope | read (admin implies every scope) |
| Operation id | lead_summary_v1_leads_summary_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Code (or id) of the company. |
Example request
curl "$ERP_URL/v1/leads/summary?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadSummary |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"entity": "string",
"by_status": {},
"open_by_source": {},
"unassigned": 1,
"converted_last_30_days": 1
}
GET /v1/leads/{lead_id}
Get a lead
One lead; a converted one names its customer, contact and deal.
| Required scope | read (admin implies every scope) |
| Operation id | get_lead_v1_leads__lead_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | lead_id |
string (uuid) | yes | Id of the lead (GET /v1/leads). |
Example request
curl "$ERP_URL/v1/leads/8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
PATCH /v1/leads/{lead_id}
Change a lead
Change what the request names: its status (new, working, or disqualified with a
reason), owner, source or the person's details ("" clears a text field but the last name).
The audit trail records which fields changed, never their values. 409 lead_converted for a
converted lead, lead_erased for an erased one.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_lead_v1_leads__lead_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | lead_id |
string (uuid) | yes | Id of the lead (GET /v1/leads). |
Request body
application/json (required): LeadUpdate
| Field | Type | Required | Description |
|---|---|---|---|
status |
"disqualified" | "new" | "working" or null |
new, working or disqualified (with disqualified_reason); a disqualified lead moved back to new or working loses its reason. Converting is POST /v1/leads/{lead_id}/convert. |
|
disqualified_reason |
string or null | Why it is not a fit. (min length 3; max length 500) | |
first_name |
string or null | "" clears. (max length 100) |
|
last_name |
string or null | Last name. (min length 1; max length 100) | |
company |
string or null | "" clears. (max length 200) |
|
role |
string or null | "" clears. (max length 100) |
|
email |
string or null | "" clears. (max length 200) |
|
phone |
string or null | "" clears. (max length 50) |
|
country |
string or null | "" clears. (pattern ^([A-Z]{2})?$) |
|
message |
string or null | "" clears. (max length 5000) |
|
source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" or null |
Where the lead came from. | |
source_detail |
string or null | "" clears. (max length 200) |
|
owner_user_id |
string (uuid) | "" or null |
Who works on it; "" leaves it to nobody. |
|
email_opt_out |
boolean or null | No sales or marketing e-mails. |
Example request
curl -X PATCH "$ERP_URL/v1/leads/8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"owner_user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "working"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
POST /v1/leads/{lead_id}/convert
Convert a lead into a customer, contact and deal
In one step: the customer (an existing one, party, or a new one, customer, created with
every check a new customer has), the person as its contact (new, or an existing one,
contact_id) and, unless create_deal is false, a deal with them owned by the lead's owner,
at qualified by default, whose first note is what the person wrote. The lead then names all
three and is final. 409 lead_converted, lead_disqualified or lead_erased. A new customer
without a country takes the lead's, else the company's; its currency and language follow
the country (see creating a customer). Needs an Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | convert_lead_v1_leads__lead_id__convert_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | lead_id |
string (uuid) | yes | Id of the lead (GET /v1/leads). |
Request body
application/json (required): LeadConvert
| Field | Type | Required | Description |
|---|---|---|---|
party |
string or null | Code (or id) of the existing customer the person works for; or customer. |
|
customer |
LeadCustomer or null |
The new customer to create; or party. |
|
contact_id |
string (uuid) or null | An existing contact of party who is this person; omitted: the lead becomes a new contact of the customer. |
|
create_deal |
boolean | Open a deal with the customer. (default true) |
|
deal_title |
string or null | The deal; omitted: the company. (min length 1; max length 200) | |
amount |
decimal string | The deal's expected net value. (min 0; default "0") |
|
currency |
string or null | The deal's currency; omitted: the customer's. (pattern ^[A-Z]{3}$) |
|
stage |
"lead" | "negotiation" | "proposal" | "qualified" |
The open stage the deal starts at (a converted lead is qualified). (default "qualified") |
|
expected_close |
string (date) or null | When it should close. |
Example request
curl -X POST "$ERP_URL/v1/leads/8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b/convert" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": "15000.00",
"customer": {
"code": "C-BEISPIEL",
"country": "DE",
"currency": "EUR"
},
"deal_title": "Platform for 25 seats",
"expected_close": "2026-12-15"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
POST /v1/leads/{lead_id}/erase
Erase a lead's personal data
Empty the lead's name, company, role, e-mail, phone and message for good (the person asked
to be forgotten). The row stays with its number, status and source; it cannot be undone. A
converted lead's customer and contact are erased on their own. A person's decision: 403
lead_erase_agent for an agent. 409 lead_erased if already erased.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | erase_lead_v1_leads__lead_id__erase_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | lead_id |
string (uuid) | yes | Id of the lead (GET /v1/leads). |
Example request
curl -X POST "$ERP_URL/v1/leads/8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b/erase" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | LeadOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"company": "Beispiel GmbH",
"country": "DE",
"created_at": "2026-10-23T09:12:00Z",
"created_by": {
"id": "system:web_form",
"name": "Contact us",
"type": "system"
},
"email": "eva@beispiel.example",
"email_opt_out": false,
"entity": "CH01",
"erased": false,
"first_name": "Eva",
"form_id": "9f2a3b4c-5d6e-4f7a-8b8c-9d0e1f2a3b4c",
"form_name": "Contact us",
"id": "8e1f2a3b-4c5d-4e6f-9a7b-8c9d0e1f2a3b",
"last_name": "Beispiel",
"message": "Please send us a quote for 25 seats.",
"name": "Eva Beispiel",
"number": "LEAD-000042",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"role": "Head of Finance",
"source": "web_form",
"source_detail": "Contact us",
"status": "new"
}
GET /v1/me/mailbox
Get your mailbox link
Whether this system offers the Google link (available, with the OAuth client_id and
scopes the browser asks for consent with) and your own link: the account, whether Google
allowed mail and calendar, how far each was read and the last error. An API key has no
mailbox (linked false).
| Required scope | read (admin implies every scope) |
| Operation id | get_mailbox_v1_me_mailbox_get |
Example request
curl "$ERP_URL/v1/me/mailbox" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MailboxStatus |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"available": true,
"client_id": "string",
"scopes": [
"string"
],
"linked": true,
"provider": "google",
"account_email": "string",
"mail": true,
"calendar": true,
"status": "active",
"linked_at": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"mail_read_to": "2026-09-21T09:30:00Z",
"calendar_read_to": "2026-09-21T09:30:00Z"
}
DELETE /v1/me/mailbox
Unlink your mailbox
Unlink your Google account: Google takes the access back and nothing more is read. What was logged stays on the timelines (an activity is never deleted).
| Required scope | read (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | unlink_mailbox_v1_me_mailbox_delete |
Example request
curl -X DELETE "$ERP_URL/v1/me/mailbox" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MailboxStatus |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"available": true,
"client_id": "string",
"scopes": [
"string"
],
"linked": true,
"provider": "google",
"account_email": "string",
"mail": true,
"calendar": true,
"status": "active",
"linked_at": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"mail_read_to": "2026-09-21T09:30:00Z",
"calendar_read_to": "2026-09-21T09:30:00Z"
}
POST /v1/me/mailbox/google
Link your Google account
Link your Gmail and Google Calendar with the one-time code Google's consent popup gave
the browser. From then on your e-mails and meetings with the contact of an open deal, or with
a customer or one of its contacts, are logged on that deal's or customer's timeline (the last
30 days first). Read-only at Google: nothing is ever sent. Linking again replaces the link.
409 for an API key, in a test copy, when Google refuses the code, or when this system does not
offer the link; 422 when Google was allowed neither mail nor calendar.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | link_google_v1_me_mailbox_google_post |
Request body
application/json (required): LinkInput
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | The one-time code Google's consent popup gave the browser (Google Identity Services' code client, redirect postmessage). (min length 1; max length 2000) |
Example request
curl -X POST "$ERP_URL/v1/me/mailbox/google" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "4/0AeanS0b..."
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MailboxStatus |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"available": true,
"client_id": "string",
"scopes": [
"string"
],
"linked": true,
"provider": "google",
"account_email": "string",
"mail": true,
"calendar": true,
"status": "active",
"linked_at": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"mail_read_to": "2026-09-21T09:30:00Z",
"calendar_read_to": "2026-09-21T09:30:00Z"
}
POST /v1/me/mailbox/sync
Read your mailbox now
Read your linked mailbox and calendar now (a job reads every linked one every 15 minutes) and log what is about deals and customers. Returns how many e-mails and meetings were read and how many were newly logged. 409 when nothing is linked or Google stopped accepting the link (then link again).
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | sync_mailbox_v1_me_mailbox_sync_post |
Example request
curl -X POST "$ERP_URL/v1/me/mailbox/sync" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SyncOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
Example response (shape)
{
"emails": 1,
"meetings": 1,
"read_emails": 1,
"read_meetings": 1,
"cases": 0,
"mailbox": {
"available": true,
"client_id": "string",
"scopes": [
"string"
],
"linked": true,
"provider": "google",
"account_email": "string",
"mail": true,
"calendar": true,
"status": "active",
"linked_at": "2026-09-21T09:30:00Z",
"last_sync_at": "2026-09-21T09:30:00Z",
"last_error": "string",
"mail_read_to": "2026-09-21T09:30:00Z",
"calendar_read_to": "2026-09-21T09:30:00Z"
}
}
GET /v1/me/record-access
Which sales records you see
Whether you see every sales record, your team's or your own, and whose.
| Required scope | read (admin implies every scope) |
| Operation id | my_record_access_v1_me_record_access_get |
Example request
curl "$ERP_URL/v1/me/record-access" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MyRecordAccess |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"record_visibility": "all",
"people": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
}
]
}
GET /v1/opportunities
List opportunities
The deals of the pipeline, the newest first, each with its weighted amount. Someone who sees only their own or their team's records gets only those deals. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | list_opportunities_v1_opportunities_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string or null | Only this entity's deals (code or id). | |
| query | stage |
"lead" | "lost" | "negotiation" | "proposal" | "qualified" | "won" or null |
Only deals at this stage. | |
| query | open |
boolean | Only open deals (not won or lost). (default false) |
|
| query | party_id |
string (uuid) or null | Only this customer's deals. | |
| query | owner_user_id |
string (uuid) or null | Only this owner's deals. | |
| query | kind |
"expansion" | "new_business" | "renewal" or null |
Only deals of this kind (renewal: the renewal deals of subscriptions). |
|
| query | limit |
integer | How many deals, the newest first. (min 1; max 1000; default 100) |
Example request
curl "$ERP_URL/v1/opportunities" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of OpportunityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"created_at": "2026-10-04T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"currency": "CHF",
"custom": {},
"entity": "CH01",
"expected_close": "2026-12-15",
"forecast_category": "best_case",
"forecast_category_set": false,
"id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"kind": "new_business",
"next_step": "Demo on 20 October",
"number": "OPP-000001",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"probability": 50,
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"stage": "proposal",
"title": "Platform for 25 seats",
"weighted_amount": "7500.00"
}
]
POST /v1/opportunities
Create an opportunity
A deal in an entity's pipeline, with a customer (party) or with a prospect that is not a
customer yet (prospect_name). It gets the next number of the entity (OPP-000001) and the
stage's default probability unless probability is given. Nothing is posted. 404 for an
unknown entity or customer (a vendor is not one); 422 for an owner who is not a person of the
organization. Needs an Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_opportunity_v1_opportunities_post |
Request body
application/json (required): OpportunityInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the entity whose pipeline the deal is in. |
title |
string | yes | What the deal is. (min length 1; max length 200) |
party |
string or null | Code (or id) of the customer; omit it for a prospect that is not a customer yet (then prospect_name is required). |
|
prospect_name |
string or null | The prospect's company name. (min length 1; max length 200) | |
prospect_country |
string or null | The prospect's country (ISO 3166-1). (pattern ^[A-Z]{2}$) |
|
contact_name |
string or null | The person the deal is with. (max length 200) | |
contact_email |
string or null | Their e-mail. (max length 200) | |
contact_phone |
string or null | Their phone. (max length 50) | |
kind |
"expansion" | "new_business" | "renewal" |
new_business, expansion (more for an existing customer) or renewal. (default "new_business") |
|
stage |
"lead" | "negotiation" | "proposal" | "qualified" |
The open stage it starts at. (default "lead") |
|
probability |
integer or null | Percent; omitted: the stage's default (lead (10 %), qualified (25 %), proposal (50 %), negotiation (75 %), won (100 %) or lost (0 %)). (min 0; max 100) |
|
currency |
string or null | Currency of the amount; omitted: the customer's currency, else the entity's functional currency. (pattern ^[A-Z]{3}$) |
|
amount |
decimal string | Expected net value of the deal, e.g. 15000.00. (min 0; default "0") |
|
expected_close |
string (date) or null | When it should close. | |
owner_user_id |
string (uuid) or null | Who owns the deal (GET /v1/people); omitted: you (a person; a key or an agent: the person behind it). Someone who sees only their own or their team's records names one of those people. |
|
source |
string or null | Where the lead came from. (max length 100) | |
next_step |
string or null | What happens next. (max length 500) | |
forecast_category |
"best_case" | "commit" | "omitted" | "pipeline" or null |
Where the deal counts in the forecast: commit (expected to close in its month), best_case (could close), pipeline (still being worked) or omitted (left out of the forecast); null: as the stage says (negotiation commit, proposal best case, lead and qualified pipeline). |
|
contact_id |
string (uuid) or null | The person at the customer the deal is with (GET /v1/contacts?party_id=); a deal with a prospect names the person in contact_name instead. |
|
custom |
map of string | number | integer | boolean or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
Example request
curl -X POST "$ERP_URL/v1/opportunities" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"currency": "CHF",
"entity": "CH01",
"expected_close": "2026-12-15",
"owner_user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"title": "Platform for 25 seats"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | OpportunityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"created_at": "2026-10-04T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"currency": "CHF",
"custom": {},
"entity": "CH01",
"expected_close": "2026-12-15",
"forecast_category": "best_case",
"forecast_category_set": false,
"id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"kind": "new_business",
"next_step": "Demo on 20 October",
"number": "OPP-000001",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"probability": 50,
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"stage": "proposal",
"title": "Platform for 25 seats",
"weighted_amount": "7500.00"
}
GET /v1/opportunities/pipeline
Get the pipeline of an entity
The open deals by stage and currency (count, amount, amount weighted by probability), the deals won and lost in the range, and the entity's open and overdue tasks. Amounts are summed per currency and never converted. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | get_pipeline_v1_opportunities_pipeline_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | closed_from |
string (date) or null | First close day of the won and lost deals; default 1 January. | |
| query | closed_to |
string (date) or null | Last close day of the won and lost deals; default today. |
Example request
curl "$ERP_URL/v1/opportunities/pipeline?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | Pipeline |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"closed": [
{
"amount": "12000.00",
"count": 1,
"currency": "CHF",
"stage": "won",
"weighted_amount": "12000.00"
}
],
"closed_from": "2026-01-01",
"closed_to": "2026-10-04",
"entity": "CH01",
"open": [
{
"amount": "30000.00",
"count": 2,
"currency": "CHF",
"stage": "proposal",
"weighted_amount": "15000.00"
}
],
"open_tasks": 3,
"overdue_tasks": 1
}
GET /v1/opportunities/{opportunity_id}
Get an opportunity
One deal (its timeline: GET /v1/activities?opportunity_id=). 404 when it does not
exist.
| Required scope | read (admin implies every scope) |
| Operation id | get_opportunity_v1_opportunities__opportunity_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | opportunity_id |
string (uuid) | yes | Id of the opportunity (GET /v1/opportunities). |
Example request
curl "$ERP_URL/v1/opportunities/3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OpportunityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"created_at": "2026-10-04T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"currency": "CHF",
"custom": {},
"entity": "CH01",
"expected_close": "2026-12-15",
"forecast_category": "best_case",
"forecast_category_set": false,
"id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"kind": "new_business",
"next_step": "Demo on 20 October",
"number": "OPP-000001",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"probability": 50,
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"stage": "proposal",
"title": "Platform for 25 seats",
"weighted_amount": "7500.00"
}
PATCH /v1/opportunities/{opportunity_id}
Change an opportunity
Change what the body names (omitted fields keep their value, "" empties a text field),
for example the customer once the prospect is set up as one (party). The stage changes
through POST .../stage. 409 for a won deal (only owner_user_id and next_step change)
and for the probability of a won or lost one; the change is audited with the previous
values.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | update_opportunity_v1_opportunities__opportunity_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | opportunity_id |
string (uuid) | yes | Id of the opportunity (GET /v1/opportunities). |
Request body
application/json (required): OpportunityUpdate
| Field | Type | Required | Description |
|---|---|---|---|
title |
string or null | New title. (min length 1; max length 200) | |
party |
string or null | Code (or id) of the customer, e.g. once the prospect is set up as one. | |
prospect_name |
string or null | "" clears. (max length 200) |
|
prospect_country |
string or null | "" clears. (pattern ^([A-Z]{2})?$) |
|
contact_name |
string or null | "" clears. (max length 200) |
|
contact_email |
string or null | "" clears. (max length 200) |
|
contact_phone |
string or null | "" clears. (max length 50) |
|
kind |
"expansion" | "new_business" | "renewal" or null |
New kind. | |
probability |
integer or null | Percent (an open deal only). (min 0; max 100) | |
currency |
string or null | New currency. (pattern ^[A-Z]{3}$) |
|
amount |
decimal string or null | New expected value. (min 0) | |
expected_close |
string (date) or null | New close date. | |
owner_user_id |
string (uuid) or null | New owner; null clears. Someone who sees only their own or their team's records hands a deal only to one of those people. | |
source |
string or null | "" clears. (max length 100) |
|
next_step |
string or null | "" clears. (max length 500) |
|
forecast_category |
"best_case" | "commit" | "omitted" | "pipeline" or null |
An open deal: where it counts in the forecast, commit (expected to close in its month), best_case (could close), pipeline (still being worked) or omitted (left out of the forecast); null: as the stage says (negotiation commit, proposal best case, lead and qualified pipeline) (null clears the choice). |
|
contact_id |
string (uuid) | "" or null |
The person at the customer the deal is with; "" clears. |
|
custom |
map of string | number | integer | boolean or null or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
Example request
curl -X PATCH "$ERP_URL/v1/opportunities/3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "18000.00",
"expected_close": "2027-01-31"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OpportunityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"created_at": "2026-10-04T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"currency": "CHF",
"custom": {},
"entity": "CH01",
"expected_close": "2026-12-15",
"forecast_category": "best_case",
"forecast_category_set": false,
"id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"kind": "new_business",
"next_step": "Demo on 20 October",
"number": "OPP-000001",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"probability": 50,
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"stage": "proposal",
"title": "Platform for 25 seats",
"weighted_amount": "7500.00"
}
POST /v1/opportunities/{opportunity_id}/stage
Move an opportunity to a stage
Move a deal to an open stage, to won (it needs a customer: 409 for a prospect; final from
then on) or to lost (with lost_reason), or reopen a lost deal. won and lost set the
probability to 100 and 0 and the close day (on, default today). The deal's timeline gets a
stage_change activity with the note. 409 for a won deal or the stage it is at. Needs an
Idempotency-Key.
| Required scope | draft (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | set_opportunity_stage_v1_opportunities__opportunity_id__stage_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | opportunity_id |
string (uuid) | yes | Id of the opportunity (GET /v1/opportunities). |
Request body
application/json (required): StageInput
| Field | Type | Required | Description |
|---|---|---|---|
stage |
"lead" | "lost" | "negotiation" | "proposal" | "qualified" | "won" |
yes | The new stage: lead (10 %), qualified (25 %), proposal (50 %), negotiation (75 %), won (100 %) or lost (0 %). |
probability |
integer or null | For an open stage: percent; omitted: the stage's default. Ignored for won and lost. (min 0; max 100) |
|
lost_reason |
string or null | Why it was lost (for lost). (min length 3; max length 1000) |
|
on |
string (date) or null | For won and lost: the day it closed; omitted: today. |
|
note |
string or null | A note kept with the stage change. (max length 5000) |
Example request
curl -X POST "$ERP_URL/v1/opportunities/3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c/stage" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"note": "Pricing agreed in principle",
"stage": "negotiation"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OpportunityOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"amount": "15000.00",
"contact_email": "eva@beispiel.example",
"contact_name": "Eva Beispiel",
"created_at": "2026-10-04T08:00:00Z",
"created_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
},
"currency": "CHF",
"custom": {},
"entity": "CH01",
"expected_close": "2026-12-15",
"forecast_category": "best_case",
"forecast_category_set": false,
"id": "3f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
"kind": "new_business",
"next_step": "Demo on 20 October",
"number": "OPP-000001",
"owner": {
"email": "alice@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Alice"
},
"probability": 50,
"prospect_country": "DE",
"prospect_name": "Beispiel GmbH",
"source": "Trade fair",
"stage": "proposal",
"title": "Platform for 25 seats",
"weighted_amount": "7500.00"
}
PUT /v1/people/{user_id}/record-visibility
Set which sales records a person sees
all (every customer, contact, deal and activity), team (the records they or anyone in
a team of theirs own) or own (the records they own), from their next request on, on every
screen, the REST API and MCP alike; their keys and the agents they use see the same. Finance
documents and the ledger are not sales records: rights (scopes) govern them. Audited.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | set_record_visibility_v1_people__user_id__record_visibility_put |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | user_id |
string (uuid) | yes | Id of the person (GET /v1/people). |
Request body
application/json (required): VisibilityInput
| Field | Type | Required | Description |
|---|---|---|---|
record_visibility |
"all" | "own" | "team" |
yes | Which sales records the person sees: all (every customer, contact, deal and activity), team (the records they or anyone in a team of theirs own) or own (the records they own). |
Example request
curl -X PUT "$ERP_URL/v1/people/8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d/record-visibility" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"record_visibility": "team"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | PersonAccess |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"person": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
},
"status": "string",
"record_visibility": "all",
"teams": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string"
}
]
}
GET /v1/record-access
List what each person sees
Every person of the organization with which sales records they see (all, team or
own) and the teams they are in, by name.
| Required scope | admin (admin implies every scope) |
| Operation id | list_record_access_v1_record_access_get |
Example request
curl "$ERP_URL/v1/record-access" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of PersonAccess |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"person": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string"
},
"status": "string",
"record_visibility": "all",
"teams": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string"
}
]
}
]
POST /v1/record-access/hand-over
Hand one person's records to another
Give every customer, contact and deal (open, won and lost) one person owns to another, in
one company or all: when someone leaves or changes territory. Activities stay with who did
them; open tasks keep their assignee, and how many are left is returned. For someone who sees
all records (403 hand_over_needs_all_records); 422 owner_disabled for a disabled new
owner. Each record's history shows the change. Needs an Idempotency-Key.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | hand_over_records_v1_record_access_hand_over_post |
Request body
application/json (required): HandOverInput
| Field | Type | Required | Description |
|---|---|---|---|
from_user_id |
string (uuid) | yes | Whose records (GET /v1/people). |
to_user_id |
string (uuid) | yes | Who gets them. |
entity |
string or null | Only this company's records (code or id); omitted: every one. |
Example request
curl -X POST "$ERP_URL/v1/record-access/hand-over" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"from_user_id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"to_user_id": "9c2a3b4c-5d6e-4f7a-8b9c-0d1e2f3a4b5c"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | HandOverOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"customers": 1,
"contacts": 1,
"deals": 1,
"open_tasks_left": 1
}
GET /v1/sales-teams
List sales teams
The organization's sales teams, by name, with their people.
| Required scope | read (admin implies every scope) |
| Operation id | list_sales_teams_v1_sales_teams_get |
Example request
curl "$ERP_URL/v1/sales-teams" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of SalesTeamOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"created_at": "2026-10-23T08:00:00Z",
"id": "6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"members": [
{
"email": "lena@example.com",
"id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"name": "Lena"
}
],
"name": "Sales DACH"
}
]
POST /v1/sales-teams
Set up a sales team
A team of sales people; a person may be in several. People who see their team's records
(PUT /v1/people/{user_id}/record-visibility) see the records of everyone in their teams.
409 sales_team_exists for a name in use. Needs an Idempotency-Key.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | required |
| Operation id | create_sales_team_v1_sales_teams_post |
Request body
application/json (required): SalesTeamInput
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The team's name, unique. (min length 1; max length 80) |
member_ids |
array of string (uuid) | Its people (GET /v1/people); a person may be in several teams. |
Example request
curl -X POST "$ERP_URL/v1/sales-teams" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"member_ids": [
"8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d"
],
"name": "Sales DACH"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | SalesTeamOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"id": "6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"members": [
{
"email": "lena@example.com",
"id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"name": "Lena"
}
],
"name": "Sales DACH"
}
PATCH /v1/sales-teams/{team_id}
Rename a sales team
A new name for the team (409 sales_team_exists for one in use).
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | rename_sales_team_v1_sales_teams__team_id__patch |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | team_id |
string (uuid) | yes | Id of the team (GET /v1/sales-teams). |
Request body
application/json (required): SalesTeamUpdate
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The new name. (min length 1; max length 80) |
Example request
curl -X PATCH "$ERP_URL/v1/sales-teams/6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sales Switzerland"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SalesTeamOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"id": "6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"members": [
{
"email": "lena@example.com",
"id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"name": "Lena"
}
],
"name": "Sales DACH"
}
DELETE /v1/sales-teams/{team_id}
Remove a sales team
Remove the team. Its people and their records stay; who sees their team's records sees from now on what their other teams give them. The audit trail keeps the team's name and people.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | delete_sales_team_v1_sales_teams__team_id__delete |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | team_id |
string (uuid) | yes | Id of the team (GET /v1/sales-teams). |
Example request
curl -X DELETE "$ERP_URL/v1/sales-teams/6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 204 | No content | |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
PUT /v1/sales-teams/{team_id}/members/{user_id}
Add a person to a sales team
Add the person to the team (again: nothing changes): anyone of the organization, also someone invited who has not signed in yet. 404 for someone who is not.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | add_sales_team_member_v1_sales_teams__team_id__members__user_id__put |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | team_id |
string (uuid) | yes | Id of the team (GET /v1/sales-teams). |
| path | user_id |
string (uuid) | yes | Id of the person (GET /v1/people). |
Example request
curl -X PUT "$ERP_URL/v1/sales-teams/6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/members/8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SalesTeamOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"id": "6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"members": [
{
"email": "lena@example.com",
"id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"name": "Lena"
}
],
"name": "Sales DACH"
}
DELETE /v1/sales-teams/{team_id}/members/{user_id}
Take a person out of a sales team
Take the person out of the team; their records stay theirs. 404 not_team_member when they
are not in it.
| Required scope | admin (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | remove_sales_team_member_v1_sales_teams__team_id__members__user_id__delete |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | team_id |
string (uuid) | yes | Id of the team (GET /v1/sales-teams). |
| path | user_id |
string (uuid) | yes | Id of the person (GET /v1/people). |
Example request
curl -X DELETE "$ERP_URL/v1/sales-teams/6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f/members/8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SalesTeamOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"created_at": "2026-10-23T08:00:00Z",
"id": "6c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
"members": [
{
"email": "lena@example.com",
"id": "8b1f2c3d-4e5f-4a6b-9c7d-8e9f0a1b2c3d",
"name": "Lena"
}
],
"name": "Sales DACH"
}
GET /v1/sales/activity
Get the sales activity per person
Per person (or agent), the calls, meetings, e-mails and notes they logged, the tasks they set and did, the deals they moved to another stage, and the tasks assigned to them that are open and overdue today. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | get_sales_activity_v1_sales_activity_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | date_from |
string (date) or null | First day counted; default the 1st of this month. | |
| query | date_to |
string (date) or null | Last day counted; default today. |
Example request
curl "$ERP_URL/v1/sales/activity?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | SalesActivityReport |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"date_from": "2026-10-01",
"date_to": "2026-10-23",
"entity": "CH01",
"rows": [],
"total": {
"agent": false,
"calls": 14,
"emails": 22,
"key": "total",
"meetings": 5,
"name": "",
"notes": 3,
"open_tasks": 6,
"overdue_tasks": 2,
"stage_changes": 11,
"tasks_done": 7,
"tasks_set": 9
}
}
GET /v1/sales/forecast
Get the sales forecast
By owner and by month: the quota, what is won (closed), and the open deals expected in the
months by forecast category (commit, best case, pipeline; omitted ones left out), the
forecast (closed plus commit), the best case, attainment and the gap to the quota. Deals
expected before the months (late) or without a close date (undated) are counted, not
summed. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | get_forecast_v1_sales_forecast_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | from_month |
string or null | First month, YYYY-MM; default the first month of this quarter. (pattern `^\d{4}-(0[1-9] |
|
| query | to_month |
string or null | Last month, YYYY-MM; default the last month of this quarter. (pattern `^\d{4}-(0[1-9] |
|
| query | rate_day |
string (date) or null | The day whose ECB fixing converts other currencies into the entity's functional currency (the latest fixing on or before it); default today. |
Example request
curl "$ERP_URL/v1/sales/forecast?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ForecastReport |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"currency": "CHF",
"entity": "CH01",
"fixing_date": "2026-10-23",
"from_month": "2026-10",
"months": [],
"rate_day": "2026-10-23",
"rows": [],
"to_month": "2026-12",
"total": {
"attainment": "32.0",
"best": "118000.00",
"best_case": {
"amount": "30000.00",
"by_currency": [
{
"amount": "30000.00",
"currency": "CHF"
}
]
},
"closed": {
"amount": "48000.00",
"by_currency": [
{
"amount": "48000.00",
"currency": "CHF"
}
]
},
"commit": {
"amount": "40000.00",
"by_currency": [
{
"amount": "40000.00",
"currency": "CHF"
}
]
},
"deals": 9,
"forecast": "88000.00",
"gap": "102000.00",
"key": "total",
"late": 1,
"pipeline": {
"amount": "25000.00",
"by_currency": [
{
"amount": "25000.00",
"currency": "CHF"
}
]
},
"quota": "150000.00",
"undated": 2,
"weighted": {
"amount": "53750.00",
"by_currency": [
{
"amount": "53750.00",
"currency": "CHF"
}
]
}
},
"unconverted": []
}
GET /v1/sales/pipeline
Get the pipeline report
The open deals by owner, by stage and by the month they are expected to close: how many,
their value and their value weighted by probability, in the entity's functional currency
(each deal converted at one ECB fixing) and in each deal's own currency. late counts the
open deals whose expected close day has passed. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | get_pipeline_report_v1_sales_pipeline_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | owner_user_id |
string (uuid) or null | Only this owner's deals. | |
| query | rate_day |
string (date) or null | The day whose ECB fixing converts other currencies into the entity's functional currency (the latest fixing on or before it); default today. |
Example request
curl "$ERP_URL/v1/sales/pipeline?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | PipelineReport |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"by_month": [],
"by_owner": [
{
"amount": {
"amount": "42870.00",
"by_currency": [
{
"amount": "30000.00",
"currency": "CHF"
},
{
"amount": "13700.00",
"currency": "EUR"
}
]
},
"count": 3,
"key": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"owner": {
"email": "lena@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Lena Meier"
},
"weighted": {
"amount": "19040.00",
"by_currency": [
{
"amount": "13000.00",
"currency": "CHF"
},
{
"amount": "6165.00",
"currency": "EUR"
}
]
}
}
],
"by_stage": [],
"currency": "CHF",
"entity": "CH01",
"fixing_date": "2026-10-23",
"late": 1,
"rate_day": "2026-10-23",
"today": "2026-10-23",
"total": {
"amount": {
"amount": "42870.00",
"by_currency": [
{
"amount": "30000.00",
"currency": "CHF"
},
{
"amount": "13700.00",
"currency": "EUR"
}
]
},
"count": 3,
"key": "total",
"weighted": {
"amount": "19040.00",
"by_currency": [
{
"amount": "13000.00",
"currency": "CHF"
},
{
"amount": "6165.00",
"currency": "EUR"
}
]
}
},
"unconverted": []
}
GET /v1/sales/quotas
List quotas
Each person's quota per month of the year, in the entity's functional currency. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | list_quotas_v1_sales_quotas_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | year |
integer or null | Calendar year; default this year. (min 2000; max 2100) |
Example request
curl "$ERP_URL/v1/sales/quotas?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of QuotaOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"amount": "50000.00",
"currency": "CHF",
"entity": "CH01",
"month": "2026-10",
"person": {
"email": "lena@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Lena Meier"
},
"updated_at": "2026-10-01T08:00:00Z",
"updated_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
}
}
]
PUT /v1/sales/quotas
Set quotas
Set what each person is to win in a month, in the entity's functional currency; a quota already set takes the new amount (audited with the previous one) and 0 takes it back. Needs the approve scope: targets are set by a manager, never by an agent. 422 for a person who is not of the organization or a person and month named twice. Repeating the request changes nothing.
| Required scope | approve (admin implies every scope) |
| Idempotency-Key | not used |
| Operation id | set_quotas_v1_sales_quotas_put |
Request body
application/json (required): QuotaInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the entity; quotas are in its currency. |
quotas |
array of QuotaLine |
yes | Each person's quota for a month. (min items 1; max items 600) |
Example request
curl -X PUT "$ERP_URL/v1/sales/quotas" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"quotas": [
{
"amount": "50000.00",
"month": "2026-10",
"user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
},
{
"amount": "50000.00",
"month": "2026-11",
"user_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}
]
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of QuotaOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"amount": "50000.00",
"currency": "CHF",
"entity": "CH01",
"month": "2026-10",
"person": {
"email": "lena@example.com",
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Lena Meier"
},
"updated_at": "2026-10-01T08:00:00Z",
"updated_by": {
"id": "user:alice",
"name": "Alice",
"type": "human"
}
}
]
GET /v1/sales/win-rate
Get the win rate
The deals won and lost in the range by owner: how many and their value, the win rate by count and by value, the average deal won and the days it took from creation to close. Read-only.
| Required scope | read (admin implies every scope) |
| Operation id | get_win_rate_v1_sales_win_rate_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | entity |
string | yes | Entity code (or id). |
| query | closed_from |
string (date) or null | First close day counted; default 1 January. | |
| query | closed_to |
string (date) or null | Last close day counted; default today. | |
| query | rate_day |
string (date) or null | The day whose ECB fixing converts other currencies into the entity's functional currency (the latest fixing on or before it); default today. |
Example request
curl "$ERP_URL/v1/sales/win-rate?entity=CH01" \
-H "Authorization: Bearer $ERP_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | WinRateReport |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"closed_from": "2026-01-01",
"closed_to": "2026-10-23",
"currency": "CHF",
"entity": "CH01",
"fixing_date": "2026-10-23",
"rate_day": "2026-10-23",
"rows": [],
"total": {
"average_days_to_win": "41.5",
"average_won": "16000.00",
"key": "total",
"lost": 2,
"lost_amount": {
"amount": "24000.00",
"by_currency": [
{
"amount": "24000.00",
"currency": "CHF"
}
]
},
"value_win_rate": "80.0",
"win_rate": "75.0",
"won": 6,
"won_amount": {
"amount": "96000.00",
"by_currency": [
{
"amount": "96000.00",
"currency": "CHF"
}
]
}
},
"unconverted": []
}
Schemas
ActivityInput
| Field | Type | Required | Description |
|---|---|---|---|
opportunity_id |
string (uuid) or null | The deal it is about (GET /v1/opportunities). |
|
lead_id |
string (uuid) or null | The lead it is about, before they are a customer (GET /v1/leads); then name no deal, entity, customer or contact. |
|
entity |
string or null | Code (or id) of the entity, for an activity about a customer only (one about a deal is in the deal's entity). | |
party |
string or null | Code (or id) of the customer, for an activity about a customer only (one about a deal names the deal's customer). | |
kind |
"call" | "email" | "meeting" | "note" | "task" |
yes | note, call, meeting, email or task. |
subject |
string | yes | What it was or is. (min length 1; max length 200) |
body |
string or null | The details. (max length 5000) | |
occurred_on |
string (date) or null | When it happened (a task: when it was set); omitted: today. | |
due_on |
string (date) or null | A task: when it is due. | |
assignee_user_id |
string (uuid) or null | A task: who does it (GET /v1/people); omitted: the deal's or the lead's owner. |
|
owner_user_id |
string (uuid) or null | Who did it, or set the task (GET /v1/people); omitted: you (a key or an agent: the person behind it). It never changes. |
|
contact_id |
string (uuid) or null | The person at the customer it was with (GET /v1/contacts?party_id=); omitted for an activity of a deal: the deal's contact. |
ActivityOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Activity id. |
entity |
string | yes | Entity code. |
opportunity_id |
string (uuid) or null | yes | The deal it is about. |
opportunity_number |
string or null | yes | The deal's number. |
party_id |
string (uuid) or null | yes | The customer it is about. |
party_code |
string or null | yes | The customer's code. |
lead_id |
string (uuid) or null | The lead it is about. | |
lead_number |
string or null | The lead's number. | |
contact_id |
string (uuid) or null | The person it was with. | |
contact_name |
string or null | Their name; "" once the contact is erased. |
|
kind |
string | yes | note, call, meeting, email, task or stage_change (written by a stage change of the deal). |
subject |
string | yes | What it was or is. |
body |
string or null | yes | The details. |
occurred_on |
string (date) | yes | When it happened. |
due_on |
string (date) or null | yes | A task: when it is due. |
assignee |
Person or null |
yes | A task: who does it. |
owner |
Person or null |
Who did it, or set the task; null for what an agent, a key or the system recorded without a person behind it. | |
done_at |
string (date-time) or null | yes | A task: when it was done (UTC). |
done_by |
object or null | yes | A task: who did it. |
overdue |
boolean | yes | A task not done whose due day has passed (today where the entity is). |
source |
"google_calendar" | "google_mail" or null |
Where an e-mail or meeting logged from a linked mailbox came from (GET /v1/me/mailbox); null for what a person or the system recorded. |
|
external_url |
string or null | Where to open that e-mail or meeting (Gmail, Google Calendar). | |
created_by |
object | yes | Who recorded it (whose mailbox, if captured). |
created_at |
string (date-time) | yes | When (UTC). |
ActivityRow
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | Who: the actor id that recorded it (user:<id> for a person, an agent's or a key's id otherwise), total. |
name |
string | yes | Their name as recorded. |
person |
Person or null |
yes | The person, when it is a user of the org. |
agent |
boolean | yes | True for an AI agent. |
calls |
integer | yes | Calls logged. |
meetings |
integer | yes | Meetings logged. |
emails |
integer | yes | E-mails logged. |
notes |
integer | yes | Notes logged. |
tasks_set |
integer | yes | Tasks set. |
tasks_done |
integer | yes | Tasks they marked done in the range. |
stage_changes |
integer | yes | Deals they moved to another stage. |
open_tasks |
integer | yes | Tasks assigned to them and not done, today. |
overdue_tasks |
integer | yes | Of them, past their due day. |
ContactInput
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string or null | First name. (min length 1; max length 100) | |
role |
string or null | Role or department, e.g. Head of purchasing. A role that says billing, accounting or Buchhaltung makes the contact one an invoice goes to by default. (max length 100) |
|
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
mobile |
string or null | Mobile number. (max length 50) | |
last_name |
string | yes | Last name. A name sent instead is split: its last word is the last name, the words before it the first name. (min length 1; max length 100) |
entity |
string | yes | Code (or id) of the company (entity). |
party |
string | yes | Code (or id) of the customer or vendor the person is at. |
kind |
"customer" | "vendor" or null |
Needed only when party is a code of a customer and a vendor. |
|
language |
"de" | "en" | "fr" | "it" | "rm" | "sv" or null |
Language to write to them in (de, fr, it, en, sv, rm). |
|
email_opt_out |
boolean | They asked not to get sales or marketing e-mails (invoices still go). (default false) |
|
custom |
map of string | number | integer | boolean or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
owner_user_id |
string (uuid) or null | Who looks after the contact (GET /v1/people); omitted: you (a key or an agent: the person behind it). |
ContactMerge
| Field | Type | Required | Description |
|---|---|---|---|
into |
string (uuid) | yes | Id of the contact that stays (same customer or vendor). |
ContactOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the contact. |
entity |
string | yes | Code of the company. |
party_id |
string (uuid) | yes | Id of the customer or vendor. |
party_code |
string | yes | Its code. |
party_name |
string | yes | Its name. |
party_kind |
"customer" | "vendor" |
yes | customer or vendor. |
name |
string | yes | First and last name; "" once erased. |
first_name |
string or null | First name. | |
last_name |
string or null | Last name; null only once erased. | |
role |
string or null | Role or department. | |
email |
string or null | E-mail address. | |
phone |
string or null | Phone number. | |
mobile |
string or null | Mobile number. | |
language |
string or null | Language to write to them in (de, fr, it, en, sv, rm). |
|
email_opt_out |
boolean | They asked not to get sales or marketing e-mails. (default false) |
|
custom |
object | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
owner |
Person or null |
Who looks after the contact. | |
erased |
boolean | Its personal data was erased (it stays as a placeholder). (default false) |
|
erased_at |
string (date-time) or null | When it was erased (UTC). | |
merged_into_id |
string (uuid) or null | The contact it was merged into (POST .../merge), if any. |
|
created_by |
object | yes | Who added it (actor object). |
created_at |
string (date-time) | yes | When (UTC). |
updated_by |
object or null | Who changed it last. | |
updated_at |
string (date-time) or null | When (UTC). |
ContactUpdate
What changes (null or omitted: as is; "" clears a text field but the last name).
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string or null | "" clears. (max length 100) |
|
role |
string or null | Role or department, e.g. Head of purchasing. A role that says billing, accounting or Buchhaltung makes the contact one an invoice goes to by default. (max length 100) |
|
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
mobile |
string or null | Mobile number. (max length 50) | |
last_name |
string or null | New. (min length 1; max length 100) | |
language |
"de" | "en" | "fr" | "it" | "rm" | "sv" | "" or null |
"" clears. |
|
email_opt_out |
boolean or null | New opt-out. | |
custom |
map of string | number | integer | boolean or null or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
owner_user_id |
string (uuid) or null | New owner (GET /v1/people). Someone who sees only their own or their team's records hands a contact only to one of those people. |
CustomFieldInput
| Field | Type | Required | Description |
|---|---|---|---|
record |
"company" | "contact" | "deal" |
yes | company, contact or deal. |
key |
string | yes | How the API names it: lower case letters, digits and _, e.g. industry; never changes. (pattern ^[a-z][a-z0-9_]{0,39}$) |
label |
string | yes | What the screens call it. (min length 1; max length 80) |
type |
"choice" | "date" | "number" | "text" | "yes_no" |
yes | text, number, date, yes_no or choice; never changes. |
choices |
array of string or null | For choice: the choices, in order (1 to 50). |
|
position |
integer | Lower comes first. (min 0; max 1000; default 0) |
CustomFieldOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the field. |
record |
"company" | "contact" | "deal" |
yes | company, contact or deal. |
key |
string | yes | How the API names it. |
label |
string | yes | What the screens call it. |
type |
"choice" | "date" | "number" | "text" | "yes_no" |
yes | text, number, date, yes_no or choice. |
choices |
array of string or null | A choice field's choices. | |
position |
integer | yes | Lower comes first. |
retired |
boolean | yes | Hidden from forms; its values stay readable. |
CustomFieldUpdate
| Field | Type | Required | Description |
|---|---|---|---|
label |
string or null | New label. (min length 1; max length 80) | |
choices |
array of string or null | A choice field's new choices; a choice a record holds stays in the list. | |
position |
integer or null | New position. (min 0; max 1000) | |
retired |
boolean or null | true hides the field from forms (its values stay readable); false brings it back. |
ForecastMonth
| Field | Type | Required | Description |
|---|---|---|---|
month |
string | yes | YYYY-MM. |
quota |
string | yes | Everyone's quota. |
closed |
string | yes | Won in the month. |
commit |
string | yes | Commit expected in the month. |
best_case |
string | yes | Best case expected in the month. |
pipeline |
string | yes | Pipeline expected in the month. |
ForecastReport
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
currency |
string | yes | The report's currency (quotas are in it). |
rate_day |
string (date) | yes | The day whose ECB fixing converts the amounts. |
fixing_date |
string (date) or null | yes | The fixing used; null when there is none near the day (then every other currency is in unconverted). |
unconverted |
array of string | yes | Currencies left out of the converted amounts. |
from_month |
string | yes | First month, YYYY-MM. |
to_month |
string | yes | Last month, YYYY-MM. |
rows |
array of ForecastRow |
yes | Per owner (and everyone with a quota), the highest quota first. |
total |
ForecastRow |
yes | Everyone. |
months |
array of ForecastMonth |
yes | Every month of the range, in order. |
ForecastRow
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | The owner's user id, none for no owner, total. |
owner |
Person or null |
yes | The owner. |
quota |
string | yes | Their quota for the months (0: none set). |
closed |
Figure |
yes | Won in the months (by close day). |
commit |
Figure |
yes | Open, expected in the months, category commit. |
best_case |
Figure |
yes | Open, expected in the months, category best case. |
pipeline |
Figure |
yes | Open, expected in the months, category pipeline. |
weighted |
Figure |
yes | Open, expected in the months and not omitted, weighted by probability. |
forecast |
string | yes | Closed plus commit, converted: the call. |
best |
string | yes | Closed plus commit plus best case, converted. |
attainment |
string or null | yes | Closed of quota, in percent (one decimal); null without a quota. (pattern ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$) |
gap |
string | yes | Quota minus closed (negative: above quota). |
deals |
integer | yes | Open deals expected in the months (not omitted). |
late |
integer | yes | Open deals whose expected close is before the months (not in the forecast). |
undated |
integer | yes | Open deals without an expected close. |
HandOverInput
| Field | Type | Required | Description |
|---|---|---|---|
from_user_id |
string (uuid) | yes | Whose records (GET /v1/people). |
to_user_id |
string (uuid) | yes | Who gets them. |
entity |
string or null | Only this company's records (code or id); omitted: every one. |
HandOverOut
| Field | Type | Required | Description |
|---|---|---|---|
customers |
integer | yes | How many customers changed owner. |
contacts |
integer | yes | How many contacts. |
deals |
integer | yes | How many deals (open, won and lost). |
open_tasks_left |
integer | yes | Open tasks still assigned to the person handing over: a task keeps its assignee (an activity never changes); the new owner sees them on the deals and customers. |
HubSpotAuthorize
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company. |
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. (default "marketingqualifiedlead") |
|
owner_user_id |
string (uuid) or null | Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. |
HubSpotAuthorizeOut
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | yes | HubSpot's consent page: open it in the browser. |
redirect_uri |
string | yes | Where HubSpot sends the browser back. |
HubSpotConnection
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the connection. |
entity |
string | yes | The company whose leads the contacts become. |
portal_id |
integer | yes | HubSpot's id of the account (the Hub ID). |
account_url |
string | yes | The account's contacts in HubSpot. |
method |
"key" | "oauth" |
yes | oauth: connected on HubSpot's consent page; key: with a private app's access token. |
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" |
yes | The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. |
owner |
Person or null |
yes | Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. |
status |
"active" | "reconnect" |
yes | reconnect: HubSpot stopped accepting the token; connect again. |
read_to |
string (date-time) | yes | HubSpot's contacts are read up to here (UTC). |
last_sync_at |
string (date-time) or null | yes | When HubSpot was last read (UTC). |
last_error |
string or null | yes | Why the last round failed, in plain words. |
leads |
integer | yes | Leads that came from this account. |
linked_by |
object | yes | Who connected it. |
linked_at |
string (date-time) | yes | When it was connected (UTC). |
HubSpotKeyInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company. |
access_token |
string | yes | The access token of a private app in the company's HubSpot with the scopes of GET /v1/hubspot. Kept sealed, never shown again. (min length 10; max length 500) |
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. (default "marketingqualifiedlead") |
|
owner_user_id |
string (uuid) or null | Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. |
HubSpotOAuthInput
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | HubSpot's one-time code. (min length 1; max length 2000) |
state |
string | yes | The state HubSpot returned. (min length 1; max length 2000) |
HubSpotSetup
| Field | Type | Required | Description |
|---|---|---|---|
available |
boolean | yes | Whether HubSpot can be connected on this system (false: tokens could not be kept sealed). |
oauth |
boolean | yes | Whether HubSpot's consent page can be used (this system has its HubSpot app); else connect with a private app's access token. |
client_id |
string or null | yes | Stabiso's HubSpot app, when there is one. |
scopes |
array of string | yes | What Stabiso needs: tick these on a private app. |
connections |
array of HubSpotConnection |
yes | The companies connected. |
HubSpotSyncOut
| Field | Type | Required | Description |
|---|---|---|---|
read |
integer | yes | Contacts read from HubSpot. |
new_leads |
integer | yes | Leads made of them. |
updated_leads |
integer | yes | New leads that HubSpot's changes updated. |
sent |
integer | yes | Contacts that Stabiso's news of their lead was sent to. |
connection |
HubSpotConnection |
yes | The connection after the round. |
HubSpotUpdate
| Field | Type | Required | Description |
|---|---|---|---|
stage |
"lead" | "marketingqualifiedlead" | "salesqualifiedlead" or null |
The lifecycle stage at which a HubSpot contact becomes a lead here: lead, marketingqualifiedlead (the default) or salesqualifiedlead. Contacts at that stage or a later one before opportunity come in. |
|
owner_user_id |
string (uuid) | "" or null |
Who owns a lead whose HubSpot owner is not a person here (GET /v1/people); none: Autopilot or a person gives it one. "" clears. |
LeadConvert
| Field | Type | Required | Description |
|---|---|---|---|
party |
string or null | Code (or id) of the existing customer the person works for; or customer. |
|
customer |
LeadCustomer or null |
The new customer to create; or party. |
|
contact_id |
string (uuid) or null | An existing contact of party who is this person; omitted: the lead becomes a new contact of the customer. |
|
create_deal |
boolean | Open a deal with the customer. (default true) |
|
deal_title |
string or null | The deal; omitted: the company. (min length 1; max length 200) | |
amount |
decimal string | The deal's expected net value. (min 0; default "0") |
|
currency |
string or null | The deal's currency; omitted: the customer's. (pattern ^[A-Z]{3}$) |
|
stage |
"lead" | "negotiation" | "proposal" | "qualified" |
The open stage the deal starts at (a converted lead is qualified). (default "qualified") |
|
expected_close |
string (date) or null | When it should close. |
LeadCustomer
The customer converting the lead creates (master data, with the customer controls).
| Field | Type | Required | Description |
|---|---|---|---|
code |
string or null | Its code; left out, the company's next customer number (C-00001). (min length 1; max length 40) |
|
name |
string or null | Legal name; omitted: the lead's company. (min length 1; max length 200) | |
country |
string or null | Country; omitted: the lead's, else the company's. (pattern ^[A-Z]{2}$) |
|
currency |
string or null | Currency of its documents; omitted: its country's when the company has exchange rates for it, else the company's functional currency. (pattern ^[A-Z]{3}$) |
|
business |
boolean | false for a private person (B2C). (default true) |
LeadFormInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company whose leads it creates. |
name |
string | yes | What the form is, e.g. Contact us. (min length 1; max length 100) |
owner_user_id |
string (uuid) or null | Who the leads go to (GET /v1/people); omitted: nobody yet. |
|
language |
"de" | "en" | "fr" | "it" | "sv" |
The language of its own page. (default "en") |
|
allowed_origins |
array of string | The websites it may be sent from, e.g. https://www.example.ch; empty: any. A browser names its page's site, so a copy of the form on another site is refused. (max items 20) |
|
thank_you |
string or null | What its page says after sending. (max length 500) | |
redirect_url |
string or null | A page of your own (https) to send people to after a form of your own website was sent; omitted: the thank-you page. (max length 500; pattern ^https://) |
LeadFormOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Form id. |
entity |
string | yes | Company code. |
name |
string | yes | What the form is. |
key |
string | yes | Its public key: it goes into your website. A new key (POST /v1/lead-forms/{form_id}/new-key) stops the old one. |
page_path |
string | yes | Its own page on this server (to link to or embed in an iframe); POST to the same path sends a lead. |
active |
boolean | yes | Whether it takes leads. |
owner |
Person or null |
yes | Who its leads go to. |
language |
"de" | "en" | "fr" | "it" | "sv" |
yes | Its page's language. |
allowed_origins |
array of string | yes | The websites it may be sent from; empty: any. |
thank_you |
string or null | yes | What its page says after sending. |
redirect_url |
string or null | yes | Where people go after sending your own form. |
leads |
integer | yes | How many leads it created. |
last_lead_at |
string (date-time) or null | yes | When it created the last one (UTC). |
created_by |
object | yes | Who set it up. |
created_at |
string (date-time) | yes | When (UTC). |
updated_by |
object or null | yes | Who changed it last. |
updated_at |
string (date-time) or null | yes | When (UTC). |
LeadFormUpdate
| Field | Type | Required | Description |
|---|---|---|---|
name |
string or null | New name. (min length 1; max length 100) | |
active |
boolean or null | false switches it off. |
|
owner_user_id |
string (uuid) | "" or null |
Who the leads go to; "": nobody. |
|
language |
"de" | "en" | "fr" | "it" | "sv" or null |
Its page's language. | |
allowed_origins |
array of string or null | The websites it may be sent from; []: any. (max items 20) |
|
thank_you |
string or null | "" clears. (max length 500) |
|
redirect_url |
string or null | "" clears. (max length 500; pattern ^(https://.*)?$) |
LeadInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the company whose lead it is. |
first_name |
string or null | First name. (max length 100) | |
last_name |
string | yes | Last name. (min length 1; max length 100) |
company |
string or null | Their company. (max length 200) | |
role |
string or null | Their job title. (max length 100) | |
email |
string or null | E-mail address. (max length 200) | |
phone |
string or null | Phone number. (max length 50) | |
country |
string or null | Their country (ISO 3166-1). (pattern ^[A-Z]{2}$) |
|
message |
string or null | What they asked. (max length 5000) | |
source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" |
Where the lead came from: web_form, phone, email, referral, partner, event, advertising, import, hubspot, other. (default "other") |
|
source_detail |
string or null | The campaign, event or list, e.g. a fair. (max length 200) | |
owner_user_id |
string (uuid) or null | Who works on the lead (GET /v1/people); omitted: nobody yet. |
|
email_opt_out |
boolean | They asked not to get sales or marketing e-mails. (default false) |
LeadOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Lead id. |
entity |
string | yes | Company code. |
number |
string | yes | LEAD-000001, per company. |
status |
"converted" | "disqualified" | "new" | "working" |
yes | new (nobody has talked to them yet), working (someone is on it), disqualified (not a fit, with a reason) or converted (now a customer, a contact and a deal). |
name |
string | yes | First and last name; "" once erased. |
first_name |
string or null | yes | First name. |
last_name |
string or null | yes | Last name. |
company |
string or null | yes | Their company. |
role |
string or null | yes | Their job title. |
email |
string or null | yes | E-mail address. |
phone |
string or null | yes | Phone number. |
country |
string or null | yes | Their country. |
message |
string or null | yes | What they asked. |
source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" |
yes | Where the lead came from. |
source_detail |
string or null | yes | The campaign, event, list or form. |
form_id |
string (uuid) or null | yes | The website form that created it. |
form_name |
string or null | yes | That form's name. |
hubspot_url |
string or null | The HubSpot contact the lead came from (GET /v1/hubspot). |
|
owner |
Person or null |
yes | Who works on it. |
email_opt_out |
boolean | yes | No sales or marketing e-mails. |
disqualified_reason |
string or null | yes | Why it is not a fit. |
party_id |
string (uuid) or null | yes | Converted: the customer. |
party_code |
string or null | yes | The customer's code. |
party_name |
string or null | yes | The customer's name. |
contact_id |
string (uuid) or null | yes | Converted: the person as its contact. |
opportunity_id |
string (uuid) or null | yes | Converted: the deal, if one was opened. |
opportunity_number |
string or null | yes | The deal's number. |
converted_at |
string (date-time) or null | yes | When it was converted (UTC). |
converted_by |
object or null | yes | Who converted it. |
erased |
boolean | yes | Its personal data was erased. |
erased_at |
string (date-time) or null | yes | When (UTC). |
created_by |
object | yes | Who recorded it (a form: the form's name). |
created_at |
string (date-time) | yes | When (UTC). |
updated_by |
object or null | yes | Who changed it last. |
updated_at |
string (date-time) or null | yes | When (UTC). |
LeadSummary
How many leads a company has at each status, and where the open ones came from.
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Company code. |
by_status |
map of integer | yes | Leads per status (every status, zeros too). |
open_by_source |
map of integer | yes | Open leads (new, working) per source. |
unassigned |
integer | yes | Open leads nobody owns. |
converted_last_30_days |
integer | yes | Leads converted in the last 30 days. |
LeadUpdate
| Field | Type | Required | Description |
|---|---|---|---|
status |
"disqualified" | "new" | "working" or null |
new, working or disqualified (with disqualified_reason); a disqualified lead moved back to new or working loses its reason. Converting is POST /v1/leads/{lead_id}/convert. |
|
disqualified_reason |
string or null | Why it is not a fit. (min length 3; max length 500) | |
first_name |
string or null | "" clears. (max length 100) |
|
last_name |
string or null | Last name. (min length 1; max length 100) | |
company |
string or null | "" clears. (max length 200) |
|
role |
string or null | "" clears. (max length 100) |
|
email |
string or null | "" clears. (max length 200) |
|
phone |
string or null | "" clears. (max length 50) |
|
country |
string or null | "" clears. (pattern ^([A-Z]{2})?$) |
|
message |
string or null | "" clears. (max length 5000) |
|
source |
"advertising" | "email" | "event" | "hubspot" | "import" | "other" | "partner" | "phone" | "referral" | "web_form" or null |
Where the lead came from. | |
source_detail |
string or null | "" clears. (max length 200) |
|
owner_user_id |
string (uuid) | "" or null |
Who works on it; "" leaves it to nobody. |
|
email_opt_out |
boolean or null | No sales or marketing e-mails. |
LinkInput
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | The one-time code Google's consent popup gave the browser (Google Identity Services' code client, redirect postmessage). (min length 1; max length 2000) |
MailboxStatus
| Field | Type | Required | Description |
|---|---|---|---|
available |
boolean | yes | Whether this system offers the Google link (its OAuth client is set up). False: nothing can be linked yet. |
client_id |
string or null | yes | The Google OAuth client the browser asks for consent with (the sign-in one). |
scopes |
array of string | yes | What the consent asks for. |
linked |
boolean | yes | Whether your Google account is linked. |
provider |
"google" or null |
yes | The linked provider. |
account_email |
string or null | yes | The linked Google account. |
mail |
boolean | yes | Google allowed reading Gmail. |
calendar |
boolean | yes | Google allowed reading the calendar. |
status |
"active" | "relink" or null |
yes | relink: Google stopped accepting the link; link again. |
linked_at |
string (date-time) or null | yes | When it was linked (UTC). |
last_sync_at |
string (date-time) or null | yes | When it was last read (UTC). |
last_error |
string or null | yes | Why the last reading failed, in plain words. |
mail_read_to |
string (date-time) or null | yes | E-mails are read up to here (UTC). |
calendar_read_to |
string (date-time) or null | yes | Meetings are read up to here (UTC). |
MyRecordAccess
| Field | Type | Required | Description |
|---|---|---|---|
record_visibility |
"all" | "own" | "team" |
yes | Which sales records the person sees: all (every customer, contact, deal and activity), team (the records they or anyone in a team of theirs own) or own (the records they own). A key or an agent sees what the person behind it sees; a key the system issued sees every record. |
people |
array of Person |
yes | With team or own: the people whose records you see (you first); empty with all. |
OpportunityInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the entity whose pipeline the deal is in. |
title |
string | yes | What the deal is. (min length 1; max length 200) |
party |
string or null | Code (or id) of the customer; omit it for a prospect that is not a customer yet (then prospect_name is required). |
|
prospect_name |
string or null | The prospect's company name. (min length 1; max length 200) | |
prospect_country |
string or null | The prospect's country (ISO 3166-1). (pattern ^[A-Z]{2}$) |
|
contact_name |
string or null | The person the deal is with. (max length 200) | |
contact_email |
string or null | Their e-mail. (max length 200) | |
contact_phone |
string or null | Their phone. (max length 50) | |
kind |
"expansion" | "new_business" | "renewal" |
new_business, expansion (more for an existing customer) or renewal. (default "new_business") |
|
stage |
"lead" | "negotiation" | "proposal" | "qualified" |
The open stage it starts at. (default "lead") |
|
probability |
integer or null | Percent; omitted: the stage's default (lead (10 %), qualified (25 %), proposal (50 %), negotiation (75 %), won (100 %) or lost (0 %)). (min 0; max 100) |
|
currency |
string or null | Currency of the amount; omitted: the customer's currency, else the entity's functional currency. (pattern ^[A-Z]{3}$) |
|
amount |
decimal string | Expected net value of the deal, e.g. 15000.00. (min 0; default "0") |
|
expected_close |
string (date) or null | When it should close. | |
owner_user_id |
string (uuid) or null | Who owns the deal (GET /v1/people); omitted: you (a person; a key or an agent: the person behind it). Someone who sees only their own or their team's records names one of those people. |
|
source |
string or null | Where the lead came from. (max length 100) | |
next_step |
string or null | What happens next. (max length 500) | |
forecast_category |
"best_case" | "commit" | "omitted" | "pipeline" or null |
Where the deal counts in the forecast: commit (expected to close in its month), best_case (could close), pipeline (still being worked) or omitted (left out of the forecast); null: as the stage says (negotiation commit, proposal best case, lead and qualified pipeline). |
|
contact_id |
string (uuid) or null | The person at the customer the deal is with (GET /v1/contacts?party_id=); a deal with a prospect names the person in contact_name instead. |
|
custom |
map of string | number | integer | boolean or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
OpportunityOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Opportunity id. |
entity |
string | yes | Entity code. |
number |
string | yes | OPP-000001, per entity. |
title |
string | yes | What the deal is. |
party_id |
string (uuid) or null | yes | The customer's id; null for a prospect. |
party_code |
string or null | yes | The customer's code. |
party_name |
string or null | yes | The customer's name. |
prospect_name |
string or null | yes | The prospect's company name. |
prospect_country |
string or null | yes | The prospect's country. |
contact_name |
string or null | yes | The person the deal is with. |
contact_email |
string or null | yes | Their e-mail. |
contact_phone |
string or null | yes | Their phone. |
contact_id |
string (uuid) or null | The contact at the customer the deal is with; contact_name, contact_email and contact_phone are then the contact's (empty once it is erased). |
|
kind |
"expansion" | "new_business" | "renewal" |
yes | new_business, expansion or renewal. |
stage |
"lead" | "lost" | "negotiation" | "proposal" | "qualified" | "won" |
yes | The stage. |
probability |
integer | yes | Percent. |
currency |
string | yes | Currency of the amounts. |
amount |
string | yes | Expected net value. |
weighted_amount |
string | yes | amount x probability, rounded to the cent. |
expected_close |
string (date) or null | yes | When it should close. |
owner |
Person or null |
yes | Who owns the deal. |
source |
string or null | yes | Where the lead came from. |
next_step |
string or null | yes | What happens next. |
lost_reason |
string or null | yes | Why it was lost. |
closed_on |
string (date) or null | yes | The day it was won or lost. |
forecast_category |
"best_case" | "closed" | "commit" | "omitted" | "pipeline" |
yes | Where it counts in the forecast: closed (won), omitted (lost or left out), commit, best_case or pipeline. |
forecast_category_set |
boolean | yes | True when its owner chose the category; false when it follows the stage. |
custom |
object | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
|
subscription_id |
string (uuid) or null | A renewal deal's subscription: the deal was opened when its renewal fell due, and is won or lost with the renewal (docs/erp/recurring-revenue.md). | |
subscription_number |
string or null | Its number. | |
renewal_term_start |
string (date) or null | The first day of the term the deal renews. | |
created_by |
object | yes | Who created it (actor object). |
created_at |
string (date-time) | yes | When (UTC). |
updated_by |
object or null | yes | Who changed it last. |
updated_at |
string (date-time) or null | yes | When (UTC). |
OpportunityUpdate
| Field | Type | Required | Description |
|---|---|---|---|
title |
string or null | New title. (min length 1; max length 200) | |
party |
string or null | Code (or id) of the customer, e.g. once the prospect is set up as one. | |
prospect_name |
string or null | "" clears. (max length 200) |
|
prospect_country |
string or null | "" clears. (pattern ^([A-Z]{2})?$) |
|
contact_name |
string or null | "" clears. (max length 200) |
|
contact_email |
string or null | "" clears. (max length 200) |
|
contact_phone |
string or null | "" clears. (max length 50) |
|
kind |
"expansion" | "new_business" | "renewal" or null |
New kind. | |
probability |
integer or null | Percent (an open deal only). (min 0; max 100) | |
currency |
string or null | New currency. (pattern ^[A-Z]{3}$) |
|
amount |
decimal string or null | New expected value. (min 0) | |
expected_close |
string (date) or null | New close date. | |
owner_user_id |
string (uuid) or null | New owner; null clears. Someone who sees only their own or their team's records hands a deal only to one of those people. | |
source |
string or null | "" clears. (max length 100) |
|
next_step |
string or null | "" clears. (max length 500) |
|
forecast_category |
"best_case" | "commit" | "omitted" | "pipeline" or null |
An open deal: where it counts in the forecast, commit (expected to close in its month), best_case (could close), pipeline (still being worked) or omitted (left out of the forecast); null: as the stage says (negotiation commit, proposal best case, lead and qualified pipeline) (null clears the choice). |
|
contact_id |
string (uuid) | "" or null |
The person at the customer the deal is with; "" clears. |
|
custom |
map of string | number | integer | boolean or null or null | Values of the organization's own fields (GET /v1/custom-fields) by key, e.g. {"industry": "Retail"}: text, a number, a date (2026-10-23), true or false, or one of a choice field's choices. On a change only the keys sent change; null removes a value. |
PersonAccess
| Field | Type | Required | Description |
|---|---|---|---|
person |
Person |
yes | The person. |
status |
string | yes | invited, active or disabled. |
record_visibility |
"all" | "own" | "team" |
yes | Which sales records the person sees: all (every customer, contact, deal and activity), team (the records they or anyone in a team of theirs own) or own (the records they own). |
teams |
array of SalesTeamRef |
yes | The sales teams they are in. |
Pipeline
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
closed_from |
string (date) | yes | First day of the range of closed. |
closed_to |
string (date) | yes | Last day of the range of closed. |
open |
array of StageTotal |
yes | The open opportunities by stage (in pipeline order) and currency. Amounts are summed per currency and never converted. |
closed |
array of StageTotal |
yes | The opportunities won and lost in the range, by stage and currency. |
open_tasks |
integer | yes | Tasks of the entity not done yet. |
overdue_tasks |
integer | yes | Of them, those whose due day has passed. |
PipelineReport
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
currency |
string | yes | The report's currency (the entity's functional one). |
rate_day |
string (date) | yes | The day whose ECB fixing converts the amounts. |
fixing_date |
string (date) or null | yes | The fixing used; null when there is none near the day (then every other currency is in unconverted). |
unconverted |
array of string | yes | Currencies left out of the converted amounts. |
today |
string (date) | yes | Today where the entity is. |
by_owner |
array of PipelineRow |
yes | Open deals by owner, the largest first. |
by_stage |
array of PipelineRow |
yes | Open deals by stage, in pipeline order. |
by_month |
array of PipelineRow |
yes | Open deals by the month they are expected to close, the earliest first; none last. |
total |
PipelineRow |
yes | All open deals. |
late |
integer | yes | Open deals whose expected close day has passed. |
PipelineRow
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | What the row groups: an owner's user id (none for no owner), a stage, or a month YYYY-MM of the expected close (none for no date). |
owner |
Person or null |
The owner (rows by owner). | |
count |
integer | yes | How many open deals. |
amount |
Figure |
yes | Their expected value. |
weighted |
Figure |
yes | Their value weighted by probability. |
QuotaInput
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Code (or id) of the entity; quotas are in its currency. |
quotas |
array of QuotaLine |
yes | Each person's quota for a month. (min items 1; max items 600) |
QuotaLine
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string (uuid) | yes | The person (GET /v1/people). |
month |
string | yes | YYYY-MM. (pattern `^\d{4}-(0[1-9] |
amount |
decimal string | yes | What they are to win in the month; 0 takes it back. (min 0) |
QuotaOut
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
person |
Person |
yes | Whose quota. |
month |
string | yes | YYYY-MM. |
currency |
string | yes | The entity's functional currency. |
amount |
string | yes | The quota. |
updated_by |
object | yes | Who set it last (actor object). |
updated_at |
string (date-time) | yes | When (UTC). |
SalesActivityReport
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
date_from |
string (date) | yes | First day counted. |
date_to |
string (date) | yes | Last day counted. |
rows |
array of ActivityRow |
yes | Per person (or agent), the busiest first. |
total |
ActivityRow |
yes | Everyone. |
SalesTeamInput
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The team's name, unique. (min length 1; max length 80) |
member_ids |
array of string (uuid) | Its people (GET /v1/people); a person may be in several teams. |
SalesTeamOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the team. |
name |
string | yes | Its name. |
members |
array of Person |
yes | Its people, by name. |
created_at |
string (date-time) | yes | When it was set up (UTC). |
SalesTeamRef
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id of the team. |
name |
string | yes | Its name. |
SalesTeamUpdate
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The new name. (min length 1; max length 80) |
StageInput
| Field | Type | Required | Description |
|---|---|---|---|
stage |
"lead" | "lost" | "negotiation" | "proposal" | "qualified" | "won" |
yes | The new stage: lead (10 %), qualified (25 %), proposal (50 %), negotiation (75 %), won (100 %) or lost (0 %). |
probability |
integer or null | For an open stage: percent; omitted: the stage's default. Ignored for won and lost. (min 0; max 100) |
|
lost_reason |
string or null | Why it was lost (for lost). (min length 3; max length 1000) |
|
on |
string (date) or null | For won and lost: the day it closed; omitted: today. |
|
note |
string or null | A note kept with the stage change. (max length 5000) |
StageTotal
| Field | Type | Required | Description |
|---|---|---|---|
stage |
"lead" | "lost" | "negotiation" | "proposal" | "qualified" | "won" |
yes | The stage. |
currency |
string | yes | The currency of the amounts. |
count |
integer | yes | How many opportunities. |
amount |
string | yes | Their expected value. |
weighted_amount |
string | yes | Their value weighted by probability. |
SyncOut
| Field | Type | Required | Description |
|---|---|---|---|
emails |
integer | yes | E-mails newly logged on deals and customers. |
meetings |
integer | yes | Meetings newly logged. |
read_emails |
integer | yes | E-mails read from the mailbox (matched or not). |
read_meetings |
integer | yes | Meetings read from the calendar (matched or not). |
cases |
integer | E-mails to or from a support queue's inbox newly put on a case. (default 0) |
|
mailbox |
MailboxStatus |
yes | The link after reading. |
VisibilityInput
| Field | Type | Required | Description |
|---|---|---|---|
record_visibility |
"all" | "own" | "team" |
yes | Which sales records the person sees: all (every customer, contact, deal and activity), team (the records they or anyone in a team of theirs own) or own (the records they own). |
WinRateReport
| Field | Type | Required | Description |
|---|---|---|---|
entity |
string | yes | Entity code. |
currency |
string | yes | The report's currency. |
rate_day |
string (date) | yes | The day whose ECB fixing converts the amounts. |
fixing_date |
string (date) or null | yes | The fixing used; null when there is none near the day (then every other currency is in unconverted). |
unconverted |
array of string | yes | Currencies left out of the converted amounts. |
closed_from |
string (date) | yes | First close day counted. |
closed_to |
string (date) | yes | Last close day counted. |
rows |
array of WinRateRow |
yes | Per owner, the most won value first. |
total |
WinRateRow |
yes | Every deal closed in the range. |
WinRateRow
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | The owner's user id, none for no owner, total. |
owner |
Person or null |
yes | The owner. |
won |
integer | yes | Deals won in the range. |
lost |
integer | yes | Deals lost in the range. |
won_amount |
Figure |
yes | Value of the deals won. |
lost_amount |
Figure |
yes | Value of the deals lost. |
win_rate |
string or null | yes | Won of won and lost, by count, in percent (one decimal); null when none closed. (pattern ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$) |
value_win_rate |
string or null | yes | Won of won and lost, by converted value, in percent; null when nothing of value closed. (pattern ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$) |
average_won |
string or null | yes | The average deal won, converted. |
average_days_to_win |
string or null | yes | Days from a won deal's creation to its close, on average (one decimal). (pattern ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$) |