Sales reps
The sales-reps app keeps your field and inside sales team next to your customers: who the reps are, which organizations each of them looks after, and which rep is responsible for a customer. It lives under https://api.revenexx.com/v1/sales-reps, and its Cockpit screens sit under Sales reps.
The usual integration is a sync: your CRM or ERP owns the sales team and the territories, and a job mirrors them here so storefronts, quotes and reports can name the right person. This page is for the developer who builds that sync.
Before you start: the app installed on your tenant, and an API key for the tenant. Every example sends both as headers.
Sync order
- Seed the roles once:
POST /v1/sales-reps/assignment-roles/defaults, or create your own. An assignment can only name a role that exists. - Upsert the reps. Look a rep up by its code with
GET /v1/sales-reps/reps?code=<CODE>, thenPUTto itsidif it exists, orPOSTif not. - Upsert the assignments. Take the organization
idfrom the customers app. A409 already_assignedmeans the pair is already there. - Mark the responsible rep with
make-primary.
The shape of the domain
| Entity | Route | What it is |
|---|---|---|
| Rep | /v1/sales-reps/reps | One person on the sales team, keyed by a code you choose. |
| Assignment | /v1/sales-reps/assignments | One rep looks after one organization, in an optional role. |
| Assignment role | /v1/sales-reps/assignment-roles | The roles an assignment can carry, such as field sales or key account. |
| Impersonation | /v1/sales-reps/impersonations | A log entry: a rep acted for a buyer contact. |
Everything points at a rep by its code, not its id. An assignment stores rep_code, so the code is the key your sync should own. The organization is referenced by its id in the customers app, a UUID. In Cockpit, a customer is an organization.
Reps
curl -X POST https://api.revenexx.com/v1/sales-reps/reps \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"code":"R-0142","name":"Mara Keller","email":"mara.keller@example.com","territory":"South","external_id":"crm-8841"}'
| Field | Meaning |
|---|---|
code | Required, unique per tenant, and cannot change once set. Letters, digits, _, . and -, starting with a letter or digit. Case is kept and matched exactly: R-0142 and r-0142 are two reps. |
name | Required, non-empty. |
email, phone, territory | Optional text. territory is a single free-text label, and email need not be unique. |
active | Defaults to true. Set false to take a rep out of service. |
external_id | Your system's key for the rep. Unique per tenant when set; an empty string counts as set, so omit it or send null when you have none. |
external_refs, source_data, metadata | Free-form JSON objects for your sync. A filter on one matches only the whole JSON document, not a key inside it. |
source_synced_at | When your sync last wrote the row. |
platform_user_id | Reserved for a future rep login. Nothing reads it today (as of 2026-10). |
GET /v1/sales-reps/reps/{id} reads one rep and PUT /v1/sales-reps/reps/{id} edits it. Both take the row id, not the code. A PUT that sends a different code is refused with 400 code_immutable; sending the same code is fine.
A duplicate code returns 409 code_taken, and a duplicate external_id on create returns 409 source_key_taken.
Prefer deactivating to deleting
DELETE /v1/sales-reps/reps/{id} only works on a rep nothing names yet. A rep who still has assignments returns 409 rep_in_use, and one who appears in the impersonation log returns 409 rep_has_history. In both cases, set active: false instead: the rep's history stays readable, and Cockpit shows them as inactive.
Deactivating does not remove a rep's assignments or their responsible flag. If someone else takes over, move the customers explicitly.
Assignments
An assignment gives one organization to one rep.
curl -X POST https://api.revenexx.com/v1/sales-reps/assignments \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"rep_code":"R-0142","organization_id":"<ORGANIZATION_ID>","role":"key_account"}'
- One row per rep and organization. Assigning the same pair twice returns
409 already_assigned, and the error'sdetailscarry theidof the existing row, so a sync can treat it as "already there". roleis optional. When set, it must be thecodeof one of your assignment roles, and an unknown code returns400with the valid codes in the message.- The rep must exist (
400 unknown_rep), but an inactive rep is accepted. organization_idmust be a UUID (400 invalid_valueotherwise), and it is checked against the customers app (400 unknown_organization). If that check cannot be completed, the assignment is still written, so validate organization ids on your side as well.
PUT /v1/sales-reps/assignments/{id} changes the role only; send null or "" to clear it. To move a customer to another rep, delete the assignment and create a new one: changing rep_code or organization_id returns 400 pair_immutable. DELETE /v1/sales-reps/assignments/{id} takes one customer off one rep.
The responsible rep
A customer can have several reps but at most one responsible rep, shown in Cockpit as Responsible and stored as is_primary. A customer may also have none.
You cannot set is_primary on create or update; it is ignored on create and refused with 400 use_make_primary on update. Use the dedicated call:
curl -X POST "https://api.revenexx.com/v1/sales-reps/assignments/{id}/make-primary" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
{
"assignment": { "id": "…", "rep_code": "R-0142", "organization_id": "…", "role": "key_account", "is_primary": true, "created_at": "…" },
"demoted": ["R-0077"]
}
It clears the flag on every other assignment of that organization and lists their rep codes in demoted. Calling it again on the responsible rep changes nothing and returns an empty demoted. A 409 primary_taken means two calls raced. Read the organization's assignments and decide again.
To find a customer's responsible rep:
curl "https://api.revenexx.com/v1/sales-reps/assignments?organization_id={organization_id}&is_primary=true" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
Assignment roles
The roles an assignment can carry are your tenant's own list. Seed the three the app ships with:
curl -X POST https://api.revenexx.com/v1/sales-reps/assignment-roles/defaults \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
{ "assignment-roles": { "created": ["field_sales", "inside_sales", "key_account"], "existing": [] } }
This creates field_sales, inside_sales and key_account, and is safe to repeat: a second call lists them under existing. Call it once after installing the app, rather than relying on the install to seed them. If the role list is ever empty, the three are seeded again on the next role check or vocabulary read, so keep at least one role of your own before deleting the seeded ones.
| Field | Meaning |
|---|---|
code | Required, unique per tenant. Lowercase letters, digits, _ and -, starting with a letter. This is what an assignment's role names. |
title | Required display name. labels and descriptions hold per-locale versions. |
tone | Badge colour in Cockpit: neutral (default), info, success, warning or danger. |
position | Sort order, ascending. |
is_default | A hint for your importer: the role an unmapped source value should become. The API never applies it, so an assignment sent without role stays without one. False on all seeded roles. |
is_system | Marks the seeded roles. It protects nothing: you can still rename, reorder or delete them. |
GET, PUT and DELETE on /v1/sales-reps/assignment-roles/{id} work by row id. Keep a role's code unchanged once assignments use it.
The same list is also readable as a value list: GET /v1/sales-reps/vocabularies names the one vocabulary this app owns, and GET /v1/sales-reps/vocabularies/assignment-roles returns its values ordered by position, ready for a dropdown. See Value lists for the shape.
Acting for a buyer
/v1/sales-reps/impersonations is a log: an entry records that a rep acted for a buyer contact of an organization, with started_at, an optional ended_at and a reason.
curl -X POST https://api.revenexx.com/v1/sales-reps/impersonations \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"rep_code":"R-0142","organization_id":"<ORGANIZATION_ID>","contact_id":"<CONTACT_ID>","reason":"Phone order for the buyer"}'
rep_code,organization_idandcontact_idare required; both ids are UUIDs.started_atdefaults to now.- Entries cannot be edited. There is no
PUT, so sendended_atwhen you create the entry, or leave it null. - The rep must exist; the organization and contact are not checked, and the entry is not checked against the rep's assignments.
DELETE /v1/sales-reps/impersonations/{id}exists for erasure requests, because an entry names a person. It removes the entry for good and publishes no event.
Listing and filtering
Every list route takes limit (1 to 200, default 50; values outside are clamped), offset and order (column, column.asc or column.desc). The response carries items and a page object with total and hasMore.
Every column is an exact-match filter, for example ?rep_code=R-0142, ?territory=South or ?active=false. There are no ranges, no wildcards and no "is empty" filter, and a JSON field matches only as a whole document. An unknown filter returns 400 unknown_filter, and a value that starts with an operator and a dot, such as in. or not., returns 400 unsupported_filter_operator. Avoid rep codes of that form.
Errors use the platform's shape, { "error", "code", "message" }, plus details where there is more to say; switch on code.
Events
The app publishes rep.created, rep.updated, rep.deleted, assignment.created, assignment.updated, assignment.deleted and impersonation.created. Roles publish nothing. Copy the exact topic strings from the topic list in Cockpit's webhook endpoint form (Event types).
What it does not do yet
As of 2026-10:
- Reps cannot sign in, and the app defines no rep roles or permissions.
platform_user_idis reserved for that. - A rep cannot act as a buyer. The impersonation log records sessions; it does not start them.
- Answers are not narrowed to the caller. Every call sees the whole tenant, so a "my customers" view for a rep has to filter by
rep_codeitself. - No coverage inheritance. An assignment covers exactly one organization.
Where to go next
- Organizations: the customers an assignment points at.
- Contacts: the buyers an impersonation entry names.
- Webhooks: receive the events above in your CRM sync.