Contacts

The people inside a buying company — the contact row, the append-only activity timeline, and the registration approve/reject flow with the states it moves.

A contact is a person, and the unit that logs in: one login identity, one email, one role held inside its organization.

Two people at the same company are two contacts sharing an organization_id. A shared purchasing mailbox is one contact, not several. A contact with no organization is a standalone buyer, not an error.

Request
curl "https://api.revenexx.com/v1/customers/contacts?limit=50" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..."

The row

FieldMeaning
emailLogin identity and the unique key of a person within the tenant. Changing it changes the login with it.
organization_idThe company. Null is legitimate — a standalone buyer. Deleting the organization sets this null and keeps the person.
roleThe person's role inside their organization, and the only thing permissions derive from. One of the tenant's own roles.
statusinvited (created, has not accepted), active (works), blocked (cannot log in). A create through the API defaults to invited; a self-registration in an open store lands active.
registration_statuspending, approved, rejected. Only the approve/reject routes move it — it is ignored on an ordinary update.
is_primaryThe main contact of its organization. At most one per company is the intent; the tenant's primary_contact_required setting decides whether the last one may be demoted or deleted.
job_titleFree text on purpose. It is a title, not a grant — overloading it with authority silently un-grants everyone the day the ledger is enforced.
order_approval_limitAmount ceiling in the market's currency: with the orders.approve permission, the most this person may sign off. Null means no ceiling. An amount, never a grant.
localeBCP 47, one of the store's configured locales. Null falls back to the store default.
first_name, last_name, phoneOptional — an ERP import often has only a mailbox. phone is free text; send E.164.
external_user_idThe platform user this contact is mirrored as — the account that holds the password and the sessions. Written by the platform, ignored on any write you send.

Creating one

Request
curl -X POST https://api.revenexx.com/v1/customers/contacts \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{
        "email": "einkauf@example.com",
        "organization_id": "…",
        "role": "buyer",
        "first_name": "Anna",
        "last_name": "Berger"
      }'

email is the only field a create cannot omit. The create makes the person and their login together, so a contact that exists can always sign in. registration_status may only be set to pending or approved here — a rejection has to carry a reason, and that is the reject route's job.

Two rows of one tenant may not share an email.

The activity timeline

contact_events is one entry on a customer's timeline: an activity somebody logged, or a registration decision the app recorded itself. Append-only — there is no update and no delete, which is what makes it usable as evidence.

Request
curl -X POST "https://api.revenexx.com/v1/customers/contacts/{contact_id}/events" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{
        "kind": "call",
        "subject": "Discussed Q3 framework agreement",
        "occurred_at": "2026-08-07T09:15:00Z",
        "actor": "sales@merchant.example",
        "payload": { "note": "Wants a second delivery address for the Leipzig site." }
      }'
FieldMeaning
kindOne of the tenant's own activity types, seeded with note, call, email, meeting, visit, task.
subjectThe one line a person scans. Required for an activity.
occurred_atWhen it actually happened, which is not when it was written down. Defaults to now.
actorWho logged it — free text as the client supplied it. Null for a row the app wrote itself.
payloadMachine-readable body. activity.<kind> carries { note }.
nameSet by the app: activity.<kind>, or registration.submitted / .approved / .rejected.
organization_idDerived from the contact, never taken from the body — so an activity cannot be filed under a company the person does not belong to.
kind: "system" is reserved. Those rows are the app's own registration decision trail and no caller may file one.

Read the feed with the column the question needs:

Request
curl "https://api.revenexx.com/v1/customers/contact_events?organization_id=…&order=occurred_at.desc&limit=50" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..."

Filter by contact_id for one person, organization_id for a whole company, kind for one type. Newest first is order=occurred_at.desc.

Registration approval

The tenant's registration_mode setting decides what a self-registration is. When it is approval_required, POST /v1/customers/auth/register creates an application: registration_status: "pending", status: "invited", the login exists with the applicant's own password but is disabled, and a newly founded organization is parked as blocked.

Two routes decide it.

Approve

Request
curl -X POST "https://api.revenexx.com/v1/customers/contacts/{contact_id}/registration/approve" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"decided_by":"sales@merchant.example"}'

Enables the login first — the password the applicant chose at submit time works immediately, no new credential is issued — then sets registration_status: "approved" and status: "active", and un-blocks the organization the registration itself founded. Writes a registration.approved timeline row.

Reachable from pending or rejected: approving a rejection reinstates it. Approving an already-approved registration is a no-op, so a retry is safe.

Reject

Request
curl -X POST "https://api.revenexx.com/v1/customers/contacts/{contact_id}/registration/reject" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"reason":"No verifiable VAT ID supplied.","decided_by":"sales@merchant.example"}'

Only reachable from pending. Sets registration_status: "rejected" and status: "blocked", and keeps the login in place but disabled — the email must not fall free for a silent second identity, and the merchant keeps the record. Delete the contact to remove both.

reason is mandatory. It is stored on the contact and carried in the event payload. Whether the applicant is ever told it is the tenant's registration_reason_disclosed setting, because that is a legal decision and not a template one.

Rejecting an already-rejected registration is a no-op.

Where to go next

Was this page helpful?