Sales reps

Field sales on revenexx — the rep roster, which rep looks after which customer, who is responsible, and the log of reps acting for a buyer.

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

  1. Seed the roles once: POST /v1/sales-reps/assignment-roles/defaults, or create your own. An assignment can only name a role that exists.
  2. Upsert the reps. Look a rep up by its code with GET /v1/sales-reps/reps?code=<CODE>, then PUT to its id if it exists, or POST if not.
  3. Upsert the assignments. Take the organization id from the customers app. A 409 already_assigned means the pair is already there.
  4. Mark the responsible rep with make-primary.

The shape of the domain

EntityRouteWhat it is
Rep/v1/sales-reps/repsOne person on the sales team, keyed by a code you choose.
Assignment/v1/sales-reps/assignmentsOne rep looks after one organization, in an optional role.
Assignment role/v1/sales-reps/assignment-rolesThe roles an assignment can carry, such as field sales or key account.
Impersonation/v1/sales-reps/impersonationsA 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

Request
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"}'
FieldMeaning
codeRequired, 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.
nameRequired, non-empty.
email, phone, territoryOptional text. territory is a single free-text label, and email need not be unique.
activeDefaults to true. Set false to take a rep out of service.
external_idYour 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, metadataFree-form JSON objects for your sync. A filter on one matches only the whole JSON document, not a key inside it.
source_synced_atWhen your sync last wrote the row.
platform_user_idReserved 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.

Request
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's details carry the id of the existing row, so a sync can treat it as "already there".
  • role is optional. When set, it must be the code of one of your assignment roles, and an unknown code returns 400 with the valid codes in the message.
  • The rep must exist (400 unknown_rep), but an inactive rep is accepted.
  • organization_id must be a UUID (400 invalid_value otherwise), 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.

Deleting an organization does not remove its assignments. Clean them up in the same job that deletes the organization.

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:

Request
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 '{}'
Response
{
  "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:

Request
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:

Request
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 '{}'
Response
{ "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.

FieldMeaning
codeRequired, unique per tenant. Lowercase letters, digits, _ and -, starting with a letter. This is what an assignment's role names.
titleRequired display name. labels and descriptions hold per-locale versions.
toneBadge colour in Cockpit: neutral (default), info, success, warning or danger.
positionSort order, ascending.
is_defaultA 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_systemMarks 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.

Deleting a role that assignments still use is not blocked. Those assignments keep the old code, and the next write that names it is refused. Move assignments off a role before you delete 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.

This log does not sign anyone in. Creating an entry returns the entry and nothing else, with no token and no session. The hand-over that lets a rep shop as a buyer is not available yet (as of 2026-10). Until it is, write entries only for sessions your own system ran.
Request
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_id and contact_id are required; both ids are UUIDs. started_at defaults to now.
  • Entries cannot be edited. There is no PUT, so send ended_at when 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_id is 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_code itself.
  • 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.
Was this page helpful?