Everything you need to connect Nexoro to another system — authentication, error handling, and every endpoint, with examples you can copy.
Base URLhttps://<mandant>.nexoro.net/rest/v1
Introduction
The Nexoro REST API v1 gives you the same access to contacts, orders, partners, products and commercial documents the interface itself uses — as JSON, over HTTPS, authenticated with a bearer token.
Every tenant has its own base URL. The subdomain selects the tenant; a key issued for one tenant does not work against another.
Base URL
http
https://<mandant>.nexoro.net/rest/v1
Principle
Description
JSON
Requests and responses are application/json; charset=utf-8.
snake_case
Every API field name is English snake_case — regardless of which language the interface runs in. The translation happens at the API boundary, in both directions.
ISO 8601
Dates and timestamps are expected as 2026-05-07T14:30:00Z. Other common notations are accepted too, but ISO 8601 is the reliable one.
data
Successful responses always sit under data, with lists carrying a pagination object alongside.
The secret half of a key is shown only once, at creation. Treat it like a password — server-side only, never in frontend code, never in a repository. A key that has been exposed belongs revoked and reissued in the admin center.
Authentication errors
Status
When
401
The Authorization header is missing or malformed, or the token does not belong to a valid key — because it was revoked, or belongs to a different tenant.
403
The key is valid but does not carry the scope this endpoint requires. See Scopes & permissions.
Scopes & permissions
Every key carries a set of scopes. Always issue a key with the narrowest set the integration actually needs — a key that only reads has no business holding orders:write.
Scope
Description
contacts:read / contacts:write
Read / write contacts.
orders:read / orders:write
Read / write orders, including status, communications and attached files.
partners:read / partners:write
Read / write partners.
catalog:read
The status and task catalogs, plus products, categories and price lists.
documents:read / documents:write
Commercial documents (offer, invoice, …) read / write, including confirm and PDF rendering.
documents:send
Send a commercial document to a customer.
documents:send is deliberately not implied by documents:write. It is the only action that leaves the building and reaches a customer — so a key allowed to create documents is not automatically allowed to send them.
When the required scope is missing, the API answers 403 and names exactly what was missing — rather than quietly returning less:
A key can optionally be bound to a CRM user. Writes then run with that user's rights and appear in the audit log under their name (rest:<username>) instead of the key label.
If that user is later deactivated, the key stops working and returns 403. That is deliberate: a departing employee's integration token must not outlive their account. Keys without a bound user behave as before — there, the scope alone decides.
Errors
Errors come back as RFC 7807 problem details, with the content type application/problem+json.
Some errors carry extra fields naming exactly what was missing — for example required_scope and granted_scopes on a 403. Always branch on the status code, and use detail for logs rather than for program logic.
Status
When
200
Success, with a body.
201
Created. The Location header points at the new resource.
204
Success, no body.
400
A parameter was unusable, typically an unparseable date.
401
Not authenticated — see Authentication.
403
Authenticated, but the required scope is missing — see Scopes & permissions.
404
The resource does not exist, or is soft-deleted.
405
The HTTP method is not defined for this path.
409
Conflict with the current state — a document already sent, a delete blocked by a relationship, or a request with the same idempotency key still in flight.
422
The body was syntactically fine but failed validation.
500
Unexpected server error. Retry with exponential backoff.
Request volume
Throttle client-side to a rate that matches your use case rather than polling endpoints in a tight loop. For recurring syncs use updated_since and fetch only what changed since the last run. Retry 5xx responses with exponential backoff and a ceiling, not immediately.
Pagination
Every list endpoint paginates the same way: limit and offset in, a pagination object out.
Name
Type
Default
Description
limit
int
50
Page size. Capped at 200 on most endpoints, at 500 on the communications timeline.
total is the overall number of matches for the filters you asked for, not the length of the current page. As long as offset + limit is less than total, there is another page.
Idempotency
Send an Idempotency-Key header on POST, PUT or PATCH to make a retry safe.
This matters most for anything that consumes a number range: without it, a retried POST /v1/commercial_documents creates a second document and burns a number.
Case
Description
Erster Aufruf
Processed normally. The response is stored for 30 days.
Wiederholung, gleicher Body
The stored response comes back verbatim, with an Idempotent-Replay: true header. Nothing is created twice.
Wiederholung, während der erste Aufruf noch läuft
409 Request In Progress. Retry later — a first call that never finished is released after five minutes.
Gleicher Key, anderer Body
422 Idempotency-Key Mismatch. Keys are scoped per API key, so two integrations cannot collide.
Serverfehler (5xx)
Not stored. So a retry after a crash really does retry.
Keys are free-form strings up to 255 characters. A UUID per logical operation is the usual choice.
A contact can carry any number of roles — the function this person has in your processes: site manager, foreman, structural engineer, purchasing. That is distinct from position, the single free-text job title inside their own company.
Roles hang on the contact, not on the order. A contact keeps its roles across every order it appears in.
The catalogue is per tenant and fills itself. Posting a role name that does not exist yet creates it — there is no separate call to create a role, and therefore no ordering dependency between two requests.
Roles are returned on every contact representation (list, single, create, update) and on the contacts embedded in an order — always in the same shape:
To write them, send roles to POST or PATCH /v1/contacts. The list replaces the contact's roles entirely:
json
{ "roles": ["Bauleiter", "Polier"] }
Objects are accepted too, so you can send back what you read: {"roles": [{"name": "Bauleiter"}]}. An empty array removes all roles; omitting the field leaves them untouched. Names are capped at 80 characters and matched case-insensitively — polier and Polier are the same role.
GET/v1/contact_rolescontacts:read
List the role catalogue
Every role name this tenant uses, so a client does not have to guess them. Inactive roles are not listed.
endpoints is an object keyed by channel type (email, phone, mobil, xmpp, …). Each value is an array, because a contact can have multiple endpoints of the same type. Types with zero entries are omitted from the object entirely.
Status
When
404
The contact does not exist.
POST/v1/contactscontacts:write
Create a contact
At least one of email, phone or mobile must be present.
Writable fields
first_name
last_name
email
phone
mobile
company
salutation
title
position
street
door
postal_code
city
country
note
website
vat_id
birthday
group_id
lat
lng
roles
endpoints
source is read-only. It records the contact's integration origin (graph, google, reonic, imap) and is set internally — it is not a marketing-attribution field. For order or lead attribution use source on POST/PATCH /v1/orders instead.
email, phone and mobile on the contact are the primary values, denormalized for convenience. The full set of channels — multiple emails, phones, mobiles — lives in the endpoints object, keyed by channel type.
Optionally include endpoints as an array of { "type": "email|phone|mobil|...", "subtype": "primary|...", "value": "..." }. The type values are free-form domain strings; legacy data uses mobil for mobile numbers — write new entries using whatever value matches your existing records.
Response — 200 with the full updated contact including endpoints.
Status
When
404
The contact does not exist or is soft-deleted (active = 0).
422
No writable field in the body. roles counts as one, so a patch that only sets roles is accepted.
DELETE/v1/contacts/{id}contacts:write
Delete a contact
The default behavior is a soft delete: active is set to 0. The record remains and disappears from the default list — it is still visible with ?include_inactive=1.
With ?hard=1 (or ?hard=true) the contact is removed permanently instead: the contact itself, its endpoints, its links to orders, its change history and its role assignments. This is irreversible. The role catalogue itself is untouched — a role outlives the last contact carrying it.
Response — 204, no body.
Status
When
404
The contact does not exist.
409
The hard delete is blocked by an existing relationship in the database.
Orders
An order is the central work unit in the CRM. It can represent a data-recovery case, a photovoltaic project or a construction project — the same structure, shaped differently per tenant.
Contacts and customers are two things
The CRM keeps two parallel records for people and companies, and the API surfaces both. They serve different roles.
Field
Description
contacts
The master record. Long-lived, the source of truth for who someone is. Managed through /v1/contacts.
customers
An order-time billing snapshot of the contact. Deliberately decoupled from the master record so historical invoices stay stable when contact data later changes. The order endpoints manage these snapshots automatically; there is intentionally no separate CRUD surface for them.
Every order has its own snapshot. Creating an order creates a fresh one; updating an order's customer block updates only that order's snapshot and never the master contact.
The response model
Most fields on an order are tenant- and industry-specific. The API hides them behind a stable, generic envelope:
Directly readable pointers to the current status and current task, kept consistent by the API.
sections
The tenant-defined custom fields, with this order's values overlaid.
contacts is built like this: it starts from every contact person linked to the order's customer — each carrying its link_type, often "primary". If the order names a primary contact who is not in that list yet, they are added. That contact is marked is_primary: true and sorted first; the rest follow by ascending id. Each entry carries its roles, read-only here — roles belong to the person, not to this one order, and are written through /v1/contacts. If the order has neither a customer nor a primary contact, contacts is empty.
The site block — the execution address
Where the work happens, as opposed to where the invoice goes. The address prints on offers, order confirmations, invoices, credit notes, delivery notes and service reports. The block is always present on an order.
same_as_billing: true means the order has no separate execution address and the address in customer applies. That is the default for every order.
To write it, send a site object to POST or PATCH /v1/orders. Two rules matter here:
Writing any non-empty address field automatically sets same_as_billing to false. Without that the address would be stored and then ignored everywhere — the CRM and the document generators skip the execution address unless the order is explicitly flagged as having one. So you do not need to send the flag yourself.
{"site": {"same_as_billing": true}} clears the address again and returns the order to using the billing address.
Fields you omit stay untouched; sending an empty string clears a single field. Unknown keys inside site are rejected with 422 rather than silently dropped.
The address only appears on generated PDFs if the tenant has Durchführungsadresse anzeigen switched on in the document settings. That is a CRM setting, not something the API controls.
Tenant-specific fields
The fields defined per tenant live in sections[]. Each section has an ordered list of items shaped { id, name, type, value, options? }. The types are text, textarea, select, checkbox and termin. value is this order's entry and is null until something is written; options is only present on select.
To write them, send a custom_fields object on POST or PATCH. The keys are item ids, which you find under sections[*].items[*].id. null clears a field, and unknown ids are ignored.
A section can be marked repeatable in the admin center. It then holds a list of entries per order instead of one set of values — three forklifts, ten laptops, several metering points.
items[].value keeps the value of the oldest entry, so a client that only reads the flat shape keeps working. Sections that are not repeatable are unchanged and carry no entries.
Per section this replaces rather than appends: the list you send becomes that section's entries for this order, in exactly that order. Entry ids are assigned by the server; you do not send them. custom_fields cannot address a repeatable section — an item id belonging to one returns 422, naming the ids.
422 is also returned for an unknown section_id, a section that is not repeatable, an item id that does not belong to the named section, a malformed entry, or more than 200 entries in one section. Validation runs to completion before anything is replaced, so a rejected request leaves the existing entries intact.
The status model
Every order has a current status. Status changes are append-only, never overwritten — the history stays complete, and the API keeps the directly readable pointers on the order in sync automatically.
A status change also writes the matching task entry so the CRM's task pipeline stays consistent. This mirrors exactly what the status button in the interface does.
GET/v1/ordersorders:read
List orders
Query parameters
Name
Type
Default
Description
limit
int
50
Page size, capped at 200.
offset
int
0
Pagination offset.
customer_id
int
—
Exact-match filter.
customer_email
string
—
Exact match on the email address of the billing snapshot.
contact_email
string
—
Exact match on the email address of any linked contact. Use this to find all orders belonging to a person across individual snapshots.
partner_id
int
—
Exact-match filter.
order_number
string
—
Exact-match filter.
status_id
int
—
Only orders whose current status equals this id.
updated_since
ISO date
—
Only orders whose updated_at is greater than or equal to this timestamp.
include_inactive
1 / 0
0
Also returns soft-deleted orders.
include
csv
—
Comma-separated extras to embed. Currently: sections.
The list does not include the per-order sections block by default — that would be N extra rows per order. Ask for it with ?include=sections when you need it.
Resolve a contact — find or create. If primary_contact_id is supplied it is used as is. Otherwise an active contact with the same email address as customer.email is reused. Otherwise a new contact is created from the customer data, provided at least one of email, phone or mobile is present. Otherwise no contact is linked — the rare case of a pure address block.
Materialize the billing snapshot. Name, company, address, email, phone and language are copied from the resolved contact first; an inline customer block then overrides that copy.
Link the contact to the customer and set the resolved contact as the order's primary contact.
Insert the order pointing at the snapshot. With no number supplied, order_number is set to EXT-<id>.
Write the initial status and task entries.
Persist any custom_fields that were sent.
You must supply at least one of primary_contact_id or customer — otherwise the snapshot would have nothing to record, and the call is rejected with 422.
Writable fields on the order
title
order_number
partner_id
primary_contact_id
inquiry_contact_id
company
website
source
responsible
received_at
customer_id is not writable directly — the server sets it when it creates the snapshot.
Writable fields in the customer block
first_name
last_name
email
phone
mobile
company
salutation
title
street
door
postal_code
city
country
vat_id
website
discount
discount_code
send_sms
send_email
vat_exempt
language
note
partner_id
Writable fields in the site block
company
contact_person
street
postal_code
city
email
phone
same_as_billing
Request — minimum, snapshot from an existing contact
Response — 201 with the created order including the full customer block, contacts, current_status and sections, plus a Location: /rest/v1/orders/{id} header.
PATCH/v1/orders/{id}orders:write
Update an order
Partial update, in any combination: core fields such as title, responsible or source on the order itself, custom_fields for tenant-specific values, customer for this order's billing snapshot, and site for the execution address. Every change is recorded in the order's change history.
Clearing a custom field: send null as the value — the underlying entry is removed.
Updating billing data: only the fields you supply in customer are written, the rest of the snapshot is untouched. The linked master contact does not change in any way — that is exactly what the separate snapshot is for.
Status
When
404
The order does not exist or is soft-deleted.
422
custom_fields or customer is not an object, custom_field_entries is not an array or was rejected, or there was nothing to update. A body carrying only custom_field_entries counts as an update.
DELETE/v1/orders/{id}orders:write
Delete an order
The default behavior is a soft delete: the order is set inactive, and the status and task history are preserved.
With ?hard=1 (or ?hard=true) the order is removed permanently, along with its status, task and change history; the tenant-specific field values go with it. This is irreversible.
Status
When
404
The order does not exist.
409
The hard delete is blocked by an existing relationship in the database.
POST/v1/orders/{id}/statusorders:write
Change status
Appends a new entry to the status history, writes the matching task entry and updates the directly readable pointers on the order.
Request
json
{ "status_id": 4 }
Response — 200 with the order and its new current_status.
If no task in the catalog maps to this status, the task entry is still written — with task 0, exactly how the CRM itself behaves in that case.
Status
When
404
The order does not exist.
422
status_id is missing or unknown.
GET/v1/orders/{id}/status_logorders:read
Full status history
Returns the last 200 entries, newest first, each with the name of the status.
Returns every communication attached to this order across all channels — emails, WhatsApp messages and incoming and outgoing calls — merged into one chronological feed, newest first.
Every entry is normalized into the same envelope, no matter which channel it came from:
json
{
"type": "email | whatsapp | call",
"source": "<Quellbezeichner des Kanals>",
"direction": "inbound | outbound",
"at": "2026-05-07 14:30:00",
"id": 12345,
"subject": "...",
"preview": "Kurzer Textauszug, HTML entfernt, max. ~240 Zeichen",
"from": "Name <adresse>",
"to": "Name <adresse>",
"user": "CRM-Benutzer, der es bearbeitet hat, falls vorhanden",
"meta": { "...kanalspezifische Zusätze..." }
}
Pagination is applied after the merge across all channels — limit and offset therefore operate on the unified timeline, not on any single channel.
Query parameters
Name
Type
Default
Description
type
csv
alle
Limit the channels. Any subset of email, whatsapp, call — e.g. type=email,call.
direction
string
beide
inbound or outbound.
since
ISO date
—
Only entries at or after this timestamp.
limit
int
50
Page size, capped at 500.
offset
int
0
Offset within the merged timeline.
meta per channel
Channel
Description
email
body, html, folder, kontoid, contact_id, unread, answered, forwarded, read_date, plus attachments when the message was sent through the CRM.
whatsapp
message, whatsapp_number_id, endpoint_id, plus unread on inbound messages.
call
duration_seconds, ring_seconds, caller_type, answer_time, hangup_time, notes, contact_id, partner_id, herkunft, plus caller_email or callee_email.
Status
When
404
The order does not exist.
400
since could not be parsed as a date.
GET/v1/orders/{id}/documentsorders:read
List the order's files
Not to be confused with commercial documents. This is about the files in the order's folder; the documents themselves, with positions and totals, live under /v1/commercial_documents.
An order's files live in a directory tree whose root is resolved per order. The whole tree is flattened, one entry per file. Read-only — no upload, no delete.
A file has no numeric id. Its stable identifier is its path relative to the order root, returned both readably as path and as an opaque token id used in the download URL.
The path's token. Pass it to the download endpoint.
filename
The file's basename.
path
Path relative to the order root, with folders preserved.
mime_type
Detected from the file content, falling back to application/octet-stream.
size
Size in bytes.
created_at
ISO 8601 of the file's modification time.
download_url
Absolute URL of the download endpoint for this file.
An order with no storage or no files returns { "data": [] }.
Status
When
404
The order does not exist.
GET/v1/orders/{id}/documents/{docId}orders:read
Download one file
Streams the file itself, not a JSON envelope. docId is the id token from the list. Content-Type is the detected MIME type, Content-Disposition is attachment with the filename, Content-Length is set, and X-Content-Type-Options: nosniff is sent.
The file is always resolved inside that order's own root. A forged token returns 404 — never a file from another order or another tenant.
Status
When
404
The order does not exist, has no storage, or the file is not inside its root.
Embedding files in the order
GET /v1/orders/{id}?include=documents returns the same array under a documents key on the order object. Deliberately opt-in, so an ordinary order read stays lean.
The tenant-defined catalogs the order workflow uses. External integrations typically fetch these once at startup and work with the ids from then on. Read-only, scope catalog:read.
GET/v1/staticatalog:read
List status definitions
The tenant's status catalog, as used by orders.
Query parameters
Name
Type
Default
Description
aktiv
1 / 0
1
Filter by the active flag. aktiv=0 shows archived statuses.
ablauf
int
—
Filter by workflow: 1 is the primary one, 2 the secondary.
The tenant's task catalog. Each task is linked to a follow-up status through next_status_id — that is how POST /v1/orders/{id}/status resolves which task entry to write.
The same fields as the list, plus attributes and technical_specs as decoded JSON — both null when empty or unparseable. 404 if the product is unknown.
GET/v1/products/{id}/pricecatalog:read
Resolve the selling price
The price a customer actually gets. The price list is resolved in this order: an explicitly supplied price_list_id, then the contact's list, then the order's list, then the default list. If the product is not in the winning list, the selling price on the product itself applies.
Query parameters
Name
Type
Default
Description
order_id
int
—
Resolve in the context of this order, i.e. through its customer's list.
contact_id
int
—
Resolve for this customer directly.
price_list_id
int
—
Force a specific list. Ignored if it is inactive or unknown.
source says where the number came from: price_list when a list matched, product when it fell back to the price on the product, and none when there is no price at all. Always show or log it — a wrong price on an offer is otherwise impossible to explain without a debugger. price_list is null on tenants with no price lists.
GET/v1/product_categoriescatalog:read
List categories
A flat list, parents before children, with id, parent_id, name, description, external_code and product_count.
GET/v1/price_listscatalog:read
List active price lists
Per list: id, name, currency, vat_rate and is_default. On tenants without price lists the answer is { "data": [] } — a valid state, not an error.
Commercial documents
Offers, order confirmations, invoices, delivery notes and service reports — with positions and totals.
Not the same as /v1/orders/{id}/documents. Those are the files in an order's folder; these are the documents themselves.
{type} is one of offer, order_confirmation, invoice, delivery_note or service_report. The German names angebot, auftragsbestaetigung, rechnung, lieferschein and servicebericht are accepted too. The type is part of the key, not decoration: ids are only unique per type.
Recurring contracts are not available here and are rejected with 422. They carry their own interval and billing configuration and are managed in the interface. Also deliberately not available through the API: position groups, markup calculation, partial and final invoices with deductions, dunning, and text templates. Those are interface features — the API does not silently ignore the fields, it does not offer them.
The draft rule
A new document is a draft: status is entwurf, number is null. The number is drawn from the tenant's number range only on confirm. This is deliberate — a discarded draft must not leave a gap in a sequential invoice number.
Send an Idempotency-Key on create and on confirm. A retried create otherwise means a second document, and a retried confirm means a burnt number.
GET/v1/commercial_documentsdocuments:read
List documents
Query parameters: order_id, type, status, include_inactive, limit (capped at 200) and offset. Without type, all five types are merged, newest first.
Fields per entry
id
type
order_id
number
status
is_draft
document_title
net
gross
currency
sent
created_by
created_at
updated_at
POST/v1/commercial_documentsdocuments:write
Create a document
Fields
Name
Type
Default
Description
typeRequired
string
—
See above.
order_idRequired
int
—
Must be an active order.
positions[]Required
array
—
At least one position, see below.
confirm
bool
false
true draws the number immediately. The default is a draft.
date
date
heute
Document date.
valid_until
date
—
Default: the document date plus the tenant's configured validity period, usually 14 days.
document_title
string
—
Optional per-document title.
currency
string
—
Default: the currency of the company behind the order.
tax_rate
number
—
Default: the company's tax rate, usually 20.
tax_regulation
string
—
One of mwst_ausweisen, kleinunternehmer, reverse_charge, innergemeinschaftlich, nicht_steuerbar. Default: the customer's setting.
price_list_id
int
—
Forces a price list when resolving position prices.
text_above / text_below
string
—
Free text above and below the positions.
letterhead
string
—
Address block. Not available for invoice.
work_description
string
—
service_report only.
source_document_id
int
—
The originating document, e.g. the offer behind an order confirmation.
Positions
Name
Type
Default
Description
product_id
int
—
Optional. If set, name, unit, description and the price are filled from the catalog.
name
string
—
Required unless product_id is given.
description
string
—
Long text under the position.
quantity
number
1
Must be greater than 0.
unit
string
ST
Or the product's unit.
unit_price
number
—
Net unit price. If omitted it is resolved via the price list, which requires a product_id.
tax_rate
number
—
Per-position tax rate. Defaults to the document's.
is_optional
bool
—
Shown on the document but not counted in the totals.
quantity_planned
number
—
service_report only: the planned quantity.
A negative unit_price is how a discount is expressed.
?dry_run=1 returns the fully validated and calculated result under data.preview and writes nothing. It uses the same arithmetic the real create uses — exactly what a confirmation screen should render.
Response — 201 with the document as a GET would return it: including positions, totals with net, gross and by_rate, and the customer snapshot.
Status
When
422
Any validation problem: unknown type, inactive order, empty positions, unknown product, quantity less than or equal to 0, or a position without a resolvable price.
This call requires documents:send, deliberately separate from documents:write: it is the only call that reaches a customer.
Fields
Name
Type
Default
Description
to
string / array
—
Recipient. Defaults to the address on the order.
cc
string / array
—
Optional.
subject
string
—
Default: type and number. The order number is appended in square brackets so replies can be matched back.
body
string
—
HTML. Defaults to a plain cover text.
from_account_id
int
—
The sending account. Defaults to the first configured one.
attach_pdf
bool
true
Renders the PDF and attaches it.
allow_foreign_recipient
bool
false
Required to send to an address that is not on the order.
Recipients are resolved from the order by default, and an explicitly named address is checked against the addresses known for that order unless allow_foreign_recipient is set. That guard matters precisely when the caller is an assistant acting on text it did not author. Set the flag only where the recipient comes from a source you trust.
Only SMTP accounts are supported. A mailbox configured for Microsoft Graph is rejected with a clear message rather than sent from the wrong identity.
A draft cannot be sent (409) — confirm it first. On success the document is flagged as sent and the message is logged, so it appears in the order's communications timeline.
Status
When
409
The document is still a draft.
502
The mail server refused the message.
Partners
A partner is a company's B2B agreement record: commercial terms, partner tier, lifecycle dates.
The partner does not carry the company's name, email or phone itself — those live on the linked contacts. One partner can have many: typically one record representing the company itself, plus several contact persons.
To create or update the people at a partner, use the /v1/contacts endpoints with partner_id set. The partner endpoints only touch the agreement record.
Every contact the partner endpoints return carries its roles — the function that person has in your processes. Because a contact person is an ordinary contact, this needs no partner-specific model: read them here, write them through PATCH /v1/contacts/{id}.
GET/v1/partnerspartners:read
List partners
Query parameters
Name
Type
Default
Description
limit
int
50
Page size, capped at 200.
offset
int
0
Pagination offset.
partner_number
string
—
Exact-match filter.
partner_type
string
—
Exact-match filter, e.g. Vertriebspartner, Servicepartner or Lieferant.
updated_since
ISO date
—
Only partners whose updated_at is greater than or equal to this timestamp.
include_inactive
1 / 0
0
Also returns soft-deleted partners.
Each row carries a denormalized display_company — the first non-empty company name from its linked contacts — plus contact_count, so a typical list view does not need a second call per row.
Creates the agreement record. Optionally it also creates one or more linked contacts in the same transaction, by passing a contacts array. Each item is a full contact object — the same shape and the same validation as POST /v1/contacts. The new partner's id is forced onto each nested contact; any partner_id sent there is ignored.
If any nested contact fails validation, the whole call is rolled back — neither the partner nor any contact is persisted.
Writable partner fields
partner_number
accounting_number
partner_type
partner_tier
registration_number
website
note
title
discount
discount_code
acquired_by
partnership_started_at
partnership_ended_at
webinar_date
webinar_type
send_sms
send_email
territorial_protection
exclusivity
If partner_type is omitted, Vertriebspartner applies.
Response — 201 with the created partner and a Location header. When nested contacts were sent, the response also carries a created_contact_ids array and the full contacts list, in the same shape as GET /v1/partners/{id}.
Status
When
422
contacts is not an array, or a nested contact failed validation — for example because none of email, phone or mobile was present.
PATCH/v1/partners/{id}partners:write
Update a partner
Partial update. Every change is recorded in the partner's change history, and updated_at is set explicitly.
DELETE/v1/partners/{id}partners:write
Delete a partner
The default behavior is a soft delete: the partner is set inactive and an entry is written to the change history. Linked contacts are untouched — soft-delete them separately through /v1/contacts/{id} if needed.
With ?hard=1 (or ?hard=true) the partner and its change history are removed permanently. Linked contacts are not deleted, and their reference to the partner is left as it is. This is irreversible.
Status
When
404
The partner does not exist.
409
The hard delete is blocked by an existing relationship in the database.
GET/v1/partners/{id}/contactspartners:read
A partner's contacts
Exactly the contacts array from the single-partner response, without the rest of the partner data. Each entry carries its roles. Combined with GET /v1/contacts?role=Bauleiter you search across all partners instead of within one.
We only use technically necessary cookies plus, with your consent, Google Analytics and Microsoft Clarity to help us understand site usage. You can change your choice anytime. Cookie policy