CRM API

Manage the people you talk to. Contacts live in lists, carry standard details and any custom fields you define, and can be archived rather than deleted outright.

Success is rarely 200 here

Creating something answers 201. Updating, deleting and archiving answer 202. Validation failures answer 406, not 400. Code that treats anything other than 200 as a failure will report successful writes as errors — see What the status codes mean.

A list is the unit of organisation

Almost everything hangs off a list: contacts belong to one, custom fields are defined per list, and segments are read per list. Create a list first.

Download for AI review

Copy or download this guide as Markdown to paste into an AI assistant for help integrating against it.

Environment

You are reading the guide for the host that served this page. The badge in the header and every example below already point at it — nothing to substitute by hand, and no other environment's addresses appear on this page.

PropertyValue
Environment
Base URLhttps://crm-dev.jirafix.net
Contact pathsare under /v1/contacts
Channel countsare under /v1/{channel}

Authentication

Every endpoint needs a bearer token, and this host does not issue one. Exchange your credentials at the Authenticate API — on this environment that is https://authenticate-dev.jirafix.net — then send what it returns as Authorization: Bearer <access_token> on each request here.

This API holds personal data

Contacts are real people's names, phone numbers and email addresses. Credentials embedded in a browser page or a mobile app are published credentials — request the token from your own backend and never ship it to a client.

POST https://authenticate-dev.jirafix.net/v1/token

Exchanges your credentials for an access token.

Tokens last 3600 seconds by default. Request a new one when it expires — there is no separate refresh call, though the response does include a refresh_token. Full detail is on the Authenticate guide at https://authenticate-dev.jirafix.net/docs.

Parameters

NameTypeRequiredDescription
usernamestringYesThe account's username, usually an email address.
passwordstringYesThe account's password. Server-side only.
privatetokenstringYesYour account's private token, from the portal's configuration section. Note the spelling — one word, all lower case.
validityintegerNoHow long the token should last, in seconds. Defaults to 3600.

Responses

StatusMeaning
200Returns access_token, refresh_token, token_type and expires_in.
400The body was missing or a required field was absent.
401The username, password or private token was not accepted.
# 1. get a token from the Authenticate host
ACCESS_TOKEN=$(curl -s -X POST https://authenticate-dev.jirafix.net/v1/token \
  -H "Content-Type: application/json" \
  -d '{"username":"you@yourcompany.com","password":"'"$OLANZO_PASSWORD"'","privatetoken":"'"$OLANZO_PRIVATE_TOKEN"'"}' \
  | jq -r .access_token)

# 2. spend it here
curl -X GET "https://crm-dev.jirafix.net/v1/contacts/lists" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
using var auth = new HttpClient { BaseAddress = new Uri("https://authenticate-dev.jirafix.net") };

var tokenResponse = await auth.PostAsJsonAsync("/v1/token", new
{
    username = "you@yourcompany.com",
    password = Environment.GetEnvironmentVariable("OLANZO_PASSWORD"),
    privatetoken = Environment.GetEnvironmentVariable("OLANZO_PRIVATE_TOKEN"),
});

// the property is access_token, not accessToken
var payload = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>();
var token = payload.GetProperty("access_token").GetString();

using var api = new HttpClient { BaseAddress = new Uri("https://crm-dev.jirafix.net") };
api.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

var response = await api.SendAsync(
    new HttpRequestMessage(HttpMethod.Get, "/v1/contacts/lists"));
// 1. get a token from the Authenticate host
const tokenResponse = await fetch("https://authenticate-dev.jirafix.net/v1/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    username: "you@yourcompany.com",
    password: process.env.OLANZO_PASSWORD,
    privatetoken: process.env.OLANZO_PRIVATE_TOKEN,
  }),
});

// note the underscore — accessToken is undefined
const { access_token } = await tokenResponse.json();

// 2. spend it here
const response = await fetch("https://crm-dev.jirafix.net/v1/contacts/lists", {
  method: "GET",
  headers: { Authorization: `Bearer ${access_token}` },
});
import os, requests

# 1. get a token from the Authenticate host
token_response = requests.post(
    "https://authenticate-dev.jirafix.net/v1/token",
    json={
        "username": "you@yourcompany.com",
        "password": os.environ["OLANZO_PASSWORD"],
        "privatetoken": os.environ["OLANZO_PRIVATE_TOKEN"],
    },
)

# note the underscore — "accessToken" raises KeyError
access_token = token_response.json()["access_token"]

# 2. spend it here
response = requests.get(
    "https://crm-dev.jirafix.net/v1/contacts/lists",
    headers={"Authorization": f"Bearer {access_token}"},
)
200 OK

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}
The response is snake_case

The fields are access_token, refresh_token, token_type and expires_in — not accessToken or expiresIn. Reading the camelCase spelling gives you nothing, with no error to explain it.

Use the Authenticate host for this same environment

A token carries the API domains it is allowed to reach. One issued by a different environment's Authenticate host is rejected here with a 401 that reads like bad credentials, so check the pair before you check your password: this page is https://crm-dev.jirafix.net and its Authenticate host is https://authenticate-dev.jirafix.net.

What the status codes mean

Worth reading before you write any error handling — this API's codes do not follow the pattern most do.

CodeWhenNote
200A read succeeded.Reads only.
201Something was created.Creating a list, a custom field, or a contact that did not exist.
202An update, delete or archive was accepted.The common success code for writes — not 200.
400The request body was missing or unreadable.Structural, not business rules.
401The token is missing, expired or invalid.
406A business rule rejected the request.Where most APIs would answer 400.
Treat 2xx as success, not 200

The single most common integration mistake here is if (status == 200). Creating a contact returns 201, updating one returns 202, and both are successes.

Add your first contact

Create a list, then put someone in it. The list id from step one is what step two needs.

curl -X POST https://crm-dev.jirafix.net/v1/contacts/list \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "Name": "Newsletter", "Description": "Monthly mailing" }'

# 201 Created
curl -X POST https://crm-dev.jirafix.net/v1/contacts/insertupdate \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "ListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "Email": "customer@example.com",
        "MobileCountryCode": 506,
        "MobileNumber": 88887777,
        "EmailOptIn": true
      }'

# 201 Created if new, 202 Accepted if it matched an existing contact

Contact lists

Lists are where contacts live. Create one before anything else.

GET /v1/contacts/lists

Lists every contact list on the account.

Parameters

NameTypeRequiredDescription
limitintegerNoRows to return. Defaults to 100.
offsetintegerNoRows to skip. Defaults to 0.

Responses

StatusMeaning
200The lists.
401Missing, expired or invalid bearer token.
curl -X GET "https://crm-dev.jirafix.net/v1/contacts/lists?limit=50&offset=0" \
  -H "Authorization: Bearer <your-token>"
GET /v1/contacts/list/{listId}

Reads one contact list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to read.

Responses

StatusMeaning
200The list.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "Authorization: Bearer <your-token>"
POST /v1/contacts/list

Creates a contact list.

Parameters

NameTypeRequiredDescription
NamestringYesWhat the list is called.
DescriptionstringNoFree text for your own reference.

Responses

StatusMeaning
201Created. The response carries the new list's id.
401Missing, expired or invalid bearer token.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/list \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "Name": "Newsletter", "Description": "Monthly mailing" }'
PUT /v1/contacts/list

Renames or re-describes an existing list.

The list is identified by listId in the body, not in the path.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to update — in the body.
NamestringNoNew name.
DescriptionstringNoNew description.

Responses

StatusMeaning
202Accepted. Not 200.
401Missing, expired or invalid bearer token.
curl -X PUT https://crm-dev.jirafix.net/v1/contacts/list \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "listId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "Name": "Newsletter 2026" }'
DELETE /v1/contacts/list/{listId}

Deletes a contact list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to delete.

Responses

StatusMeaning
202Accepted.
401Missing, expired or invalid bearer token.
curl -X DELETE https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "Authorization: Bearer <your-token>"

Adding and updating contacts

Insert and update are the same call

There is no separate update endpoint for a contact. Post the contact and we match it against what already exists — 201 means we created a new one, 202 means we updated a match.

POST /v1/contacts/insertupdate

Adds one contact, or updates it if it already exists.

Parameters

NameTypeRequiredDescription
ListIdguidYesThe list to add the contact to.
EmailstringNoEmail address. Supply this, a mobile number, or both.
MobileCountryCodeintegerNoDialling code as a number, e.g. 506 for Costa Rica.
MobileNumbernumberNoMobile number as a number, without the country code.
EmailOptInbooleanNoWhether they have agreed to email.
MobileOptInbooleanNoWhether they have agreed to SMS.
PropertyFieldsarrayNoYour custom fields for this contact — see Custom fields.
ContactIdnumberNoTarget a specific existing contact instead of matching on email or mobile.

Responses

StatusMeaning
201A new contact was created.
202An existing contact was updated.
401Missing, expired or invalid bearer token.
406A business rule rejected it — for example neither an email nor a mobile number.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/insertupdate \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "ListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "Email": "customer@example.com",
        "MobileCountryCode": 506,
        "MobileNumber": 88887777,
        "EmailOptIn": true
      }'
POST /v1/contacts/batch

Adds or updates many contacts in one call.

Runs in the background. The response carries an id you poll with the import status endpoint — the response itself does not tell you the outcome.

Parameters

NameTypeRequiredDescription
ListIdguidNoThe list to import into.
CreateUpdateContactsarrayYesThe contacts, each shaped like a single insert.
WebHookUrlstringNoWe post the import result here when it finishes, so you need not poll.

Responses

StatusMeaning
202Accepted for import. Returns the id to poll.
401Missing, expired or invalid bearer token.
406A business rule rejected the batch.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/batch \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "ListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "CreateUpdateContacts": [
          { "Email": "a@example.com", "EmailOptIn": true },
          { "Email": "b@example.com", "EmailOptIn": true }
        ]
      }'

Check an import

GET /v1/contacts/{id}/Status

Reads the progress of a batch import.

Parameters

NameTypeRequiredDescription
idguidYesThe id the batch call returned.

Responses

StatusMeaning
200Current import status.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f/Status \
  -H "Authorization: Bearer <your-token>"

Reading contacts

Two reads use the OPTIONS method

Searching contacts and listing archived contacts are served over HTTP OPTIONS with a JSON body, not POST or GET. That is deliberate and is what the API expects — but some HTTP clients, proxies and browser fetch wrappers will not send a body on OPTIONS, or will intercept it as a CORS preflight. If your client silently drops the body, that is why. Call these from a server-side client that lets you control the method.

GET /v1/contacts/{contactId}

Reads one contact by its id.

Parameters

NameTypeRequiredDescription
contactIdnumberYesThe contact's numeric id.

Responses

StatusMeaning
200The contact.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/104213 \
  -H "Authorization: Bearer <your-token>"
OPTIONS /v1/contacts

Searches contacts by list or by contact id.

Note the method: OPTIONS, with a JSON body. Supply ListIds, ContactIds, or both.

Parameters

NameTypeRequiredDescription
ListIdsarrayNoLists to read from.
ContactIdsarrayNoSpecific contacts to read.
LimitintegerNoRows to return. Defaults to 100.
OffsetintegerNoRows to skip. Defaults to 0.

Responses

StatusMeaning
200The matching contacts.
400The request body was missing entirely.
401Missing, expired or invalid bearer token.
curl -X OPTIONS https://crm-dev.jirafix.net/v1/contacts \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "ListIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], "Limit": 100, "Offset": 0 }'

Archiving contacts

Archiving takes a contact out of active use without destroying it. Archived contacts can be listed, and separately deleted for good.

PATCH /v1/contacts/archive

Archives contacts by id or by list.

Parameters

NameTypeRequiredDescription
ContactIdsarrayNoContacts to archive.
ListIdsarrayNoArchive everyone in these lists.

Responses

StatusMeaning
202Accepted.
400The request body was missing.
401Missing, expired or invalid bearer token.
curl -X PATCH https://crm-dev.jirafix.net/v1/contacts/archive \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "ContactIds": [104213, 104214] }'
OPTIONS /v1/contacts/archived

Lists archived contacts.

Also OPTIONS with a JSON body — see the note above.

Parameters

NameTypeRequiredDescription
ListIdsarrayNoLists to read archived contacts from.
LimitintegerNoRows to return. Defaults to 100.
OffsetintegerNoRows to skip. Defaults to 0.

Responses

StatusMeaning
200The archived contacts.
401Missing, expired or invalid bearer token.
curl -X OPTIONS https://crm-dev.jirafix.net/v1/contacts/archived \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "ListIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], "Limit": 100 }'
POST /v1/contacts/archived/delete

Permanently deletes archived contacts.

This one does not come back. Use isDeleteAll only when you genuinely mean every archived contact on the account.

Parameters

NameTypeRequiredDescription
ContactIdsarrayNoArchived contacts to delete.
ListIdsarrayNoDelete archived contacts from these lists.
isDeleteAllbooleanNoQuery parameter. true deletes every archived contact.

Responses

StatusMeaning
200Deleted.
202Accepted for deletion.
400The request body was missing.
401Missing, expired or invalid bearer token.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/archived/delete \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "ContactIds": [104213] }'

Custom fields

Fields you define yourself, per list. Once defined, set them on a contact through PropertyFields when adding or updating.

GET /v1/contacts/list/{listId}/propertyfield_datatype

Lists the data types a custom field can have.

Call this first — DataType must be one of these.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.

Responses

StatusMeaning
200The available data types.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfield_datatype \
  -H "Authorization: Bearer <your-token>"
GET /v1/contacts/list/{listId}/propertyfields

Lists the custom fields defined on a list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.

Responses

StatusMeaning
200The custom fields.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfields \
  -H "Authorization: Bearer <your-token>"
GET /v1/contacts/list/{listId}/propertyfield/{propertyfieldId}

Reads one custom field.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.
propertyfieldIdintegerYesThe field.

Responses

StatusMeaning
200The field.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfield/12 \
  -H "Authorization: Bearer <your-token>"
POST /v1/contacts/list/{listId}/propertyfield

Defines a new custom field on a list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.
NamestringYesInternal name for the field.
DisplayNamestringNoLabel shown in the portal.
DataTypestringYesOne of the values from the data-type list above.
DefaultValuestringNoValue used when a contact has none.

Responses

StatusMeaning
201Created.
401Missing, expired or invalid bearer token.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfield \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "Name": "loyalty_tier", "DisplayName": "Loyalty tier", "DataType": "Text" }'
PUT /v1/contacts/list/{listId}/propertyfield

Updates a custom field definition.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.
NamestringNoNew internal name.
DisplayNamestringNoNew label.

Responses

StatusMeaning
202Accepted.
401Missing, expired or invalid bearer token.
curl -X PUT https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfield \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "DisplayName": "Loyalty level" }'
DELETE /v1/contacts/list/{listId}/propertyfield/{propertyfieldId}

Deletes one custom field.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.
propertyfieldIdintegerYesThe field to delete.

Responses

StatusMeaning
202Accepted.
401Missing, expired or invalid bearer token.
curl -X DELETE https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfield/12 \
  -H "Authorization: Bearer <your-token>"
PUT /v1/contacts/list/{listId}/propertyfields/delete

Deletes several custom fields at once.

A PUT that deletes — the method does not match the intent, but it is what the API expects.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.

Responses

StatusMeaning
202Accepted.
400The request body was missing.
401Missing, expired or invalid bearer token.
curl -X PUT https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/propertyfields/delete \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '[12, 13]'

Segments

GET /v1/contacts/list/{listId}/segmentations

Lists the segments defined on a list.

Segments are built in the portal; this reads them back so you can target one.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list.

Responses

StatusMeaning
200The segments.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/contacts/list/3fa85f64-5717-4562-b3fc-2c963f66afa6/segmentations \
  -H "Authorization: Bearer <your-token>"

Add a contact and message them

POST /v1/contacts/send/predefinedTrigger

Adds or updates a contact and fires a saved template at them in one call.

Useful for sign-up flows: one request creates the contact and sends the welcome message.

Parameters

NameTypeRequiredDescription
ListIdguidYesThe list to add the contact to.
EmailstringNoEmail address. Supply this, a mobile number, or both.
MobileNumbernumberNoMobile number as a number.

Responses

StatusMeaning
201Contact created and the trigger fired.
202Existing contact updated and the trigger fired.
401Missing, expired or invalid bearer token.
406A business rule rejected it — this endpoint is where 406 is most often seen.
curl -X POST https://crm-dev.jirafix.net/v1/contacts/send/predefinedTrigger \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "ListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "Email": "customer@example.com"
      }'

Reachable contacts per channel

How many contacts you can actually reach on each channel — which is not the same as how many contacts exist, because it accounts for opt-in and for having the right contact detail.

The channel is in the path

email, sms and whatsapp each have their own pair of paths, taking the same parameters and returning the same shape.

GET /v1/email/lists

Lists every list with its email-reachable contact count.

Parameters

NameTypeRequiredDescription
excludeCountbooleanNoSet to true to skip counting, which returns faster on large lists.

Responses

StatusMeaning
200The lists, with counts unless excluded.
401Missing, expired or invalid bearer token.
curl -X GET "https://crm-dev.jirafix.net/v1/email/lists?excludeCount=false" \
  -H "Authorization: Bearer <your-token>"
GET /v1/email/{listId}/contacts/count

Email-reachable contact count for one list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to count.

Responses

StatusMeaning
200The count and segment breakdown.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/email/3fa85f64-5717-4562-b3fc-2c963f66afa6/contacts/count \
  -H "Authorization: Bearer <your-token>"
GET /v1/sms/lists

Lists every list with its sms-reachable contact count.

Parameters

NameTypeRequiredDescription
excludeCountbooleanNoSet to true to skip counting, which returns faster on large lists.

Responses

StatusMeaning
200The lists, with counts unless excluded.
401Missing, expired or invalid bearer token.
curl -X GET "https://crm-dev.jirafix.net/v1/sms/lists?excludeCount=false" \
  -H "Authorization: Bearer <your-token>"
GET /v1/sms/{listId}/contacts/count

Sms-reachable contact count for one list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to count.

Responses

StatusMeaning
200The count and segment breakdown.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/sms/3fa85f64-5717-4562-b3fc-2c963f66afa6/contacts/count \
  -H "Authorization: Bearer <your-token>"
GET /v1/whatsapp/lists

Lists every list with its whatsapp-reachable contact count.

Parameters

NameTypeRequiredDescription
excludeCountbooleanNoSet to true to skip counting, which returns faster on large lists.

Responses

StatusMeaning
200The lists, with counts unless excluded.
401Missing, expired or invalid bearer token.
curl -X GET "https://crm-dev.jirafix.net/v1/whatsapp/lists?excludeCount=false" \
  -H "Authorization: Bearer <your-token>"
GET /v1/whatsapp/{listId}/contacts/count

Whatsapp-reachable contact count for one list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe list to count.

Responses

StatusMeaning
200The count and segment breakdown.
401Missing, expired or invalid bearer token.
curl -X GET https://crm-dev.jirafix.net/v1/whatsapp/3fa85f64-5717-4562-b3fc-2c963f66afa6/contacts/count \
  -H "Authorization: Bearer <your-token>"

Errors

See What the status codes mean for the full picture. The short version: 2xx is success, and a rejected request is 406 rather than 400.

StatusWhat it meansWhat to do
200A read succeeded, or a delete completed.Use the payload.
201Something was created.Treat as success. Read the new id from the body.
202An update, delete or archive was accepted.Treat as success. For a batch import, poll the status endpoint.
400The request body was missing or unreadable.Check you sent a body and that it parses.
401The token is missing, expired or invalid.Fetch a new token and retry once.
406A business rule rejected the request.Read the message. Retrying unchanged will fail again.