Skip to main content

Custom Fields

Custom fields let you store business-specific data on contacts — company name, subscription plan, lead source, contract renewal date, or anything else relevant to your workflow.

Unlike a free-form JSON blob, SendSeven custom fields are schema-defined and typed. You first create a field definition (with a key, a type, and optional validation rules), then set values for that field on individual contacts. This gives you type validation, dropdown options, segmentation support, and personalization variables.

Each field also has an AI access level that decides whether your Conversational Agents can read it, and whether they can fill it in during a chat.

Set values in the same call

You can set typed custom field values directly on POST /contacts, PUT /contacts/{id} and PATCH /contacts/{id} with a custom_fields object — see Set values on contact create / update. The fields must be defined first; unknown keys are rejected with a 422.

The Two-Step Model​

  1. Define the field once (an admin/settings operation) — POST /api/v1/custom-fields. This returns a field definition with an id.
  2. Set values per contact — either inline on contact create/update (custom_fields object, many fields at once, strict typed validation) or one field at a time with POST /api/v1/contacts/{contact_id}/fields/{field_id}.

You only define a field once; afterwards you set/read its value on as many contacts as you like.

Required Scopes​

Field definitions have their own scopes. The contacts:* and settings:* scopes do not give access to definitions. Tokens and roles that used them for custom fields before these scopes existed were given the matching custom_fields:* scope automatically.

ScopePurpose
custom_fields:readList / view custom field definitions (GET /custom-fields, GET /custom-fields/{id})
custom_fields:createCreate custom field definitions (POST /custom-fields)
custom_fields:updateUpdate custom field definitions (PUT /custom-fields/{id})
custom_fields:create or custom_fields:updateCheck where a key is still used (GET /custom-fields/key-references)
custom_fields:read or custom_fields:deleteHow many contacts hold a value (GET /custom-fields/{id}/usage)
custom_fields:deleteDelete a custom field definition (and all its values); change a field's type (POST /custom-fields/{id}/change-type)

Values on a contact use the contact scopes:

ScopePurpose
contacts:readRead custom field values on a contact
contacts:updateSet custom field values on a contact (also via PUT/PATCH /contacts/{id})
contacts:createSet custom field values while creating a contact (POST /contacts)

Custom field keys, names and options are field definitions. Endpoints that run under another scope leave them out unless your key also has custom_fields:read (contacts:read alone is not enough): the variable and field catalogues (GET /flows/variables, GET /variables/available, GET /variables/condition-fields, GET /variables/writable-contact-fields, GET /campaigns/variables, GET /email-contents/variables, GET /whatsapp-templates/fields), the custom columns of GET /email-contacts/import/template, the Agent Builder's view of your fields, the AI field-reference check (POST /ai-field-references/check then returns no issues), and custom-field targets in the HubSpot field mapping (GET /integrations/hubspot/status, PATCH /integrations/hubspot/settings). Dynamic-list segment_conditions in GET /lists still need contacts:read. In the HubSpot mapping, without custom_fields:read custom targets are left out, sending one returns 403 custom_fields_read_required, and a settings update that sends a property mapped to a custom field you can't see returns 409 hubspot_property_has_hidden_mapping. Previewing a dynamic list's matches (POST /lists/evaluate-count, POST /lists/{list_id}/evaluate) and GET /lists/{list_id}/contacts need lists:read and contacts:read.

Field Types​

field_typeDescription
textFree-form text
numberNumeric values (integers and decimals)
booleanTrue / false
dateDate only (YYYY-MM-DD)
datetimeDate and time (ISO 8601)
selectSingle selection from options
multiselectMultiple selections from options
emailEmail address
phonePhone number
urlWebsite URL
locationLatitude / longitude
collectionArray of values

select and multiselect require an options array. After a field is created, its key and field_type are immutable to preserve data integrity.


Step 1 — Define a Custom Field​

curl -X POST "https://api.sendseven.com/api/v1/custom-fields" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Company Size",
"key": "company_size",
"field_type": "select",
"description": "Number of employees",
"options": ["1-10", "11-50", "51-200", "201-500", "500+"],
"required": false,
"display_order": 0
}'

Request fields

FieldRequiredNotes
nameyesDisplay name (1–100 chars)
keyyesInternal key, snake_case, must match ^[a-z][a-z0-9_]*$
field_typeyesOne of the types above
descriptionnoHelp text (≤ 500 chars)
optionsfor select/multiselectArray of allowed string values
requirednoDefaults to false
default_valuenoDefault for new contacts
agent_visiblenoVisible to agents (default true)
agent_editablenoEditable by agents (default true)
user_editablenoEditable by the contact via self-service (default false)
display_ordernoLower numbers sort first (default 0)
ai_accessnoWhat AI agents may do with the field: none (default), read, read_write or fill_if_empty. See AI access
ai_hintnoWhat the field means and how an AI agent should use or set it (max 1000 characters)
option_descriptionsnoselect / multiselect only: {option: description} for AI agents (max 300 characters each)

Response (201 Created) — note the id, which you use to set values:

{
"id": "cf-12345678-abcd-efgh-ijkl-mnopqrstuvwx",
"tenant_id": "tn-12345678-abcd-efgh-ijkl-mnopqrstuvwx",
"name": "Company Size",
"key": "company_size",
"field_type": "select",
"description": "Number of employees",
"options": ["1-10", "11-50", "51-200", "201-500", "500+"],
"required": false,
"default_value": null,
"agent_visible": true,
"agent_editable": true,
"user_editable": false,
"display_order": 0,
"is_active": true,
"created_at": "2026-01-15T10:30:00Z"
}

List, Get, Update, Delete Definitions​

# List active definitions (paginated, page_size max 100; add active_only=false for inactive too)
curl "https://api.sendseven.com/api/v1/custom-fields?page=1&page_size=100" \
-H "Authorization: Bearer s7_api_..."

# Get one definition
curl "https://api.sendseven.com/api/v1/custom-fields/cf-12345678" \
-H "Authorization: Bearer s7_api_..."

# Update a definition (key cannot change; field_type changes via /change-type, below)
curl -X PUT "https://api.sendseven.com/api/v1/custom-fields/cf-12345678" \
-H "Authorization: Bearer s7_api_..." \
-H "Content-Type: application/json" \
-d '{"options": ["1-10", "11-50", "51-200", "201-1000", "1000+"]}'

# How many contacts hold a value (shown before deleting)
curl "https://api.sendseven.com/api/v1/custom-fields/cf-12345678/usage" \
-H "Authorization: Bearer s7_api_..."
# -> {"field_id": "cf-12345678", "contacts_with_value": 42}

# Delete a definition and all its values (permanent)
curl -X DELETE "https://api.sendseven.com/api/v1/custom-fields/cf-12345678" \
-H "Authorization: Bearer s7_api_..."

Deleting a field definition is permanent: the definition and every contact's value for it are deleted (204). The key becomes free, so a new field with the same key can be created; it starts empty and gets a new id. References by id (for example a connector step) then return unknown_field; references by key ({{contact.custom.<key>}}, flow conditions) use the new field. GET /custom-fields/{id}/usage (custom_fields:read, custom_fields:delete, contacts:read or settings:admin) returns how many contacts would lose a value. Returns 409 CUSTOM_FIELD_IN_USE_BY_INBOX while a Custom Inbox filter rule uses the field.

Change a Field's Type​

PUT cannot change field_type. POST /custom-fields/{id}/change-type (custom_fields:delete) changes it in place and converts every stored value. The field keeps its id, key, name, display order and every reference, so you do not need to delete and re-create it (a re-created field starts empty and with ai_access: "none").

Preview first with dry_run: true (nothing changes):

curl -X POST "https://api.sendseven.com/api/v1/custom-fields/cf-12345678/change-type" \
-H "Authorization: Bearer s7_api_..." \
-H "Content-Type: application/json" \
-d '{"field_type": "number", "dry_run": true}'
Request fieldTypeDescription
field_typestringThe new type. Must differ from the current type.
optionsstring[]Options for a select / multiselect target. Required unless the field already has options. Trimmed, de-duplicated (case-insensitive), at most 1,000, each at most 500 characters. Ignored for other targets.
option_descriptionsobject | nullselect / multiselect targets: omit to keep the descriptions of options that still exist, send an object to replace, null to clear.
incompatible_valuesstringfail (default), remove or keep: what happens to stored values that do not fit the new type.
dry_runbooleantrue returns the preview only.

Response (200; the same shape is in detail of the 409 below):

{
"dry_run": true,
"applied": false,
"from_type": "text",
"to_type": "number",
"conversion": "checked",
"total_values": 120,
"unchanged_count": 80,
"converted_count": 37,
"incompatible_count": 3,
"removed_count": 0,
"kept_incompatible_count": 0,
"converted_samples": [{"from": "00123", "to": "123", "count": 12, "lossy": true}],
"incompatible_values": [{"value": "n/a", "count": 3}],
"incompatible_truncated": false,
"ai_access": {"before": "read_write", "after": "read_write", "changed": false},
"dropped_validation_keys": ["max_length"],
"options_cleared": false,
"options_changed": false,
"values_limit": 200000,
"values_limit_exceeded": false,
"field": {"id": "cf-12345678", "key": "customer_number", "field_type": "text", "...": "..."},
"reference_warnings": [
{"kind": "flow_condition", "code": "operator_incompatible", "message": "...",
"count": 1, "items": [{"id": "flow_abc", "name": "Welcome"}], "truncated": false}
]
}
  • conversion: lossless (no value can fail) or checked (each value is validated against the new type).
  • converted_samples: up to 10, lossy ones first (lossy: true = detail was lost, such as leading zeros or a time of day). incompatible_values: up to 20 distinct values with counts; values are cut at 200 characters. Empty values are not counted.
  • field: the definition after the call (unchanged on a dry run or a 409).
  • ai_access: a writable mode (read_write / fill_if_empty) becomes read on a location / collection target.
  • dropped_validation_keys: validation limits that do not apply to the new type and are removed. options_cleared: the field had options and the new type has none.
  • reference_warnings: places that refer to the field and may need a review because of the new type (flow conditions, set-field and collect-input steps, AI goal outputs, list and campaign conditions, campaign segments, AI agent context, WhatsApp Flow mappings, the HubSpot mapping, Custom Inbox rules). Advisory: it never blocks the change. code is operator_incompatible, type_mismatch, options_changed or review.

Conversions:

FromToKindResult
anytextlosslessRendered as text: lists joined with , , location as lat, lng
selectmultiselect, collectionlosslessOne-item list
multiselectcollectionlosslessSame list
textcollectionlosslessOne-item list
datedatetimelosslessYYYY-MM-DDT00:00:00Z
booleannumberlossless1 / 0
text and othersnumbercheckedCanonical number; 00123 becomes 123 (lossy)
datetimedatecheckedThe UTC date (lossy when the time is not midnight)
numberbooleancheckedOnly 1 and 0
anyselect / multiselectcheckedMust match an option (case-insensitive, stored in the option's spelling). For multiselect, text is matched whole first, then split at , / ; and every part must match
multiselect / collectionany single-value typecheckedOnly one-item lists
everything elsecheckedValidated like a value written through the API

If values do not fit and incompatible_values is fail, the call returns 409 and nothing changes:

{"detail": {"code": "CUSTOM_FIELD_TYPE_INCOMPATIBLE_VALUES", "message": "3 stored values do not fit the new type. Choose to remove them or keep them as they are.", "...": "the full preview"}}

Send the call again with remove (those values are deleted) or keep (they stay stored as they are; they may read as null for number, list and location types and will not match typed filters until corrected). The change runs in one transaction. It does not emit contact.updated webhooks or start flows.

422 detail.code: same_type, invalid_field_type, options_required, invalid_options, invalid_option_descriptions, invalid_incompatible_values, too_many_values. 404 for an unknown field.

One type change converts at most 200,000 non-empty values (values_limit). Above that, applying returns 422 and nothing changes:

{"detail": {"code": "too_many_values", "message": "...", "total_values": 250000, "limit": 200000}}

A dry run still works and sets values_limit_exceeded: true; its counts and samples then cover only the first values_limit values. If another request is changing this field's values at the same moment, the call returns 409 {"detail": {"code": "custom_field_busy", "message": "..."}} after a few seconds and nothing changes; retry it.

Check Where a Key Is Used​

Flows, dynamic lists, campaign segments, campaigns, message templates, AI agent context settings and the HubSpot field mapping refer to custom fields by key. After a field is deleted, those references stay; a new field with the same key takes them over. GET /custom-fields/key-references?key= (custom_fields:create, custom_fields:update or settings:update) lists them, so you can warn before creating the field:

curl "https://api.sendseven.com/api/v1/custom-fields/key-references?key=customer_number" \
-H "Authorization: Bearer s7_api_..."
{
"key": "customer_number",
"field_exists": false,
"total": 2,
"references": [
{"kind": "flow_condition", "code": "references_key", "message": "...", "count": 1,
"items": [{"id": "flow_abc", "name": "Welcome"}], "truncated": false},
{"kind": "message_template_merge_tag", "code": "references_key", "message": "...", "count": 1,
"items": [{"id": "cr_123", "name": "Greeting"}], "truncated": false}
]
}

field_exists is true when an active field already has this key (creating it returns 409). Each group lists up to 10 items; count is the number found. kind is one of flow_condition, flow_set_field, flow_collect_input, flow_merge_tag, flow_trigger, flow_reference, list_condition, segment, campaign, message_template_merge_tag, agent_context, hubspot_mapping. Archived flows and finished campaigns are not listed. key must match ^[a-z][a-z0-9_]*$ (422 otherwise). Read-only.

Validation Rules (optional)​

A definition can carry validation constraints that every value written through the API or the app must meet. Only keys that fit the field type are accepted:

Field typeAllowed keys
numbermin, max
text, email, phone, urlmin_length, max_length, pattern
date, datetimedate_min, date_max (YYYY-MM-DD or today)
curl -X PUT "https://api.sendseven.com/api/v1/custom-fields/cf-12345678" \
-H "Authorization: Bearer s7_api_..." \
-H "Content-Type: application/json" \
-d '{"validation": {"min": 1, "max": 100000}}'
  • Send it on create or update. On update, send null to remove the rules, or leave validation out to keep them.
  • pattern must match the whole value. A safe regex subset is supported: no backreferences, lookarounds or nested repetition, max 200 characters.
  • Other keys, keys that do not fit the type, min greater than max, or an unsafe pattern return 422:
{
"detail": {
"code": "invalid_validation",
"message": "The validation rules are invalid for this field type",
"errors": [{"path": "validation.min", "code": "not_applicable", "message": "min does not apply to select fields"}]
}
}

The rules are checked whenever a value is set through custom_fields on contact create/update or POST /contacts/{contact_id}/fields/{field_id}. Clearing a value is always allowed. Contact imports and integration syncs do not check them yet.


Step 2 — Set a Field Value on a Contact​

Send the value to POST /api/v1/contacts/{contact_id}/fields/{field_id}. The body is a single value whose type must match the field's field_type (and, for select/multiselect, must be one of the defined options).

curl -X POST "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7/fields/cf-12345678" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{"value": "51-200"}'

Response

{ "success": true, "message": "Field value updated" }

Setting the value again on the same field overwrites the previous value. A 404 is returned if the contact or the field definition does not exist.


Set Values on Contact Create / Update​

POST /api/v1/contacts, PUT /api/v1/contacts/{contact_id} and PATCH /api/v1/contacts/{contact_id} accept an optional custom_fields object:

{
"name": "Sophie Martin",
"email": "[email protected]",
"custom_fields": {
"cf-12345678-abcd-efgh-ijkl-mnopqrstuvwx": "51-200",
"lifetime_value": 1249.5,
"is_reseller": true,
"last_order_date": "2026-09-01",
"interests": ["Shoes", "Bags"]
}
}
  • Keys are a field definition id (recommended, since it never changes) or the field key. Only active fields of your workspace can be used.
  • Update merges: fields you don't send are left as they are. null, "" or [] clears a field. On create, empty values are ignored.
  • All or nothing: every entry is validated before anything is saved. If any entry is invalid, the request fails with 422 and neither the contact nor any custom field is changed.
  • Values are normalized to a canonical format (table below), so "pro" for a select with option "Pro" is stored as "Pro", and "7.50" for a number is stored as "7.5".
  • When the request contains custom_fields, the response includes the contact's full custom_fields map.
  • Up to 200 fields per request. required and default_value on a definition are not enforced by these endpoints.
  • With duplicate handling auto_merge, when POST /contacts matches an existing contact: if your API key / app also has the contacts:update scope, the values you send overwrite that contact's values. Without contacts:update, they only fill fields that are empty — fields that already have a value are left unchanged (no error). Changed fields trigger a contact.updated event.

Accepted Input and Stored Format​

field_typeAccepted inputStored / returned as
textstring (numbers/booleans are converted), max 10,000 charsthe string
numberJSON number or numeric string ("1e3" ok); not booleans, NaN or Infinity; max 38 digitsplain decimal, no exponent or trailing zeros: "1000", "7.5", "-0.25"
booleantrue/false, 1/0, "true"/"false", "yes"/"no", "1"/"0" (any case)"true" / "false"
dateISO 8601 date YYYY-MM-DD (a full date-time is accepted; its date part is kept)"2026-09-01"
datetimeISO 8601 date-time; no offset means UTCUTC "2026-09-01T13:45:00Z"
selectone option (matched case-insensitively)the option's exact spelling
multiselectarray of options, or a comma-separated string; every item must be an option; duplicates removedJSON array string '["Shoes", "Bags"]'
emailvalid email addressthe address
phoneinternational number +<country code><number> (spaces/dashes ok)E.164 "+491701234567"
urlabsolute http:// or https:// URL, max 2,048 charsthe URL
location{"lat": 52.52, "lng": 13.40} (latitude/longitude/lon also accepted) or a text string'{"lat": 52.52, "lng": 13.4}' or the JSON-encoded string
collectionarray of strings/numbers, a JSON array string, or a comma-separated stringJSON array string '["a", "b"]'

Validation Errors​

A 422 response lists one entry per invalid field. The submitted value is never included in the error.

{
"detail": [
{
"loc": ["body", "custom_fields", "plan"],
"msg": "Value is not one of the custom field's defined options",
"type": "custom_field.not_an_option",
"code": "not_an_option",
"field": "plan",
"field_id": "cf-12345678-abcd-efgh-ijkl-mnopqrstuvwx",
"field_key": "plan",
"field_type": "select"
},
{
"loc": ["body", "custom_fields", "old_field"],
"msg": "Unknown custom field: no active custom field with this id or key exists in this workspace",
"type": "custom_field.unknown_field",
"code": "unknown_field",
"field": "old_field",
"field_id": null,
"field_key": null,
"field_type": null
}
]
}
codeMeaning
unknown_fieldNo active field with this id or key (never existed, deleted, or belongs to another workspace)
duplicate_fieldThe same field was sent twice (once by id, once by key)
invalid_typeWrong JSON type (e.g. an object for a text field)
invalid_formatRight type, but unparseable (e.g. "abc" for a number, "31.02.2026" for a date)
not_an_optionNot one of the field's options
out_of_rangeNumber too large or not finite, coordinates out of range
too_longText or URL too long, or too many list items
too_many_fieldsMore than 200 entries in one request
validation_failedThe value breaks the field's validation rules. validation_code says which rule (too_small, too_large, too_short, too_long, pattern_mismatch, date_too_early, date_too_late), and msg names the limit

Read Custom Field Values​

All values for a contact​

curl "https://api.sendseven.com/api/v1/contacts/contact_d4e5f6a7/fields" \
-H "Authorization: Bearer s7_api_..."
{
"contact_id": "contact_d4e5f6a7",
"fields": [
{
"field_id": "cf-12345678",
"field_key": "company_size",
"field_name": "Company Size",
"field_type": "select",
"value": "51-200"
}
]
}

Embedded in the contact​

GET /api/v1/contacts/{contact_id} also returns custom field values under a custom_fields object, keyed by field key:

{
"id": "contact_d4e5f6a7",
"name": "Sophie Martin",
"email": "[email protected]",
"custom_fields": {
"company_size": {
"field_definition_id": "cf-12345678",
"name": "Company Size",
"field_type": "select",
"value": "51-200"
}
}
}
info

The embedded custom_fields object in a contact response is keyed by field key and each entry is an object. To write values, send a plain {identifier: value} map instead — see below.

Filter contacts by a field value​

GET /api/v1/contacts (and the contact export and bulk actions) filter by one field with custom_field_id, custom_field_value and custom_field_op. Text, email, phone and URL fields support contains (the default) and not_contains; number fields support eq, gt, lt, gte, lte and between; select fields match an option exactly. not_contains also returns contacts that have no value for the field. See Manage Contacts for the full parameter list.

# Contacts whose "notes" field does not mention "complaint" (or have no notes)
curl "https://api.sendseven.com/api/v1/contacts?custom_field_id=cf-12345678&custom_field_op=not_contains&custom_field_value=complaint" \
-H "Authorization: Bearer YOUR_API_TOKEN"

AI access​

Your Conversational Agents can work with your custom fields. They can read what you already know about a customer and record new details the customer states in a chat, so the next agent, campaign or Flow starts from a complete record. Every field decides for itself what AI may do with it, and nothing is visible to AI until you allow it.

Pro use case: a select field "Contact type" (prospect, trial, customer) set to fill_if_empty. A new lead writes on WhatsApp: "We are trialling you for our support team." The agent records trial on the contact. A value your team or your CRM sync already set stays untouched, and a Flow that listens for contact.updated can route the lead from there.

Access levels​

Set ai_access on the field definition. In the app you find the same setting in the custom field editor under Settings → Custom Fields.

ai_accessLabel in the appAgents see the valueAgents may write
none (default)NoneNoNo
readReadYesNo
read_writeRead and writeYesYes, and may replace any existing value
fill_if_emptyFill if emptyYesOnly while the field is empty
  • none is the default. Existing fields and fields created without ai_access are invisible to AI until you change them.
  • read_write replaces any value, including values set by your team, the API, an import, a Flow or a contact token. Use it for fields the customer is the best source for, for example a preferred contact time.
  • fill_if_empty never changes an existing value. An empty text, an empty selection and an unset value all count as empty.
  • location and collection fields accept only none or read. A writable level on them returns 422 with invalid_ai_access.

Explain the field to the agent​

Two optional settings tell the agent what a field means. Both are only used for AI.

FieldApplies toLimitPurpose
ai_hintAll field types1000 charactersWhat the field means and when to set it
option_descriptionsselect, multiselect300 characters per option{option: description} for each option

Keys in option_descriptions must be existing options of the field, otherwise the request returns 422 with invalid_option_descriptions. On update, option_descriptions replaces the whole map, null clears it and omitting it keeps it. The same applies to ai_hint. Sending "ai_access": null on update resets the level to none.

curl -X POST "https://api.sendseven.com/api/v1/custom-fields" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Contact type",
"key": "contact_type",
"field_type": "select",
"options": ["prospect", "trial", "customer"],
"ai_access": "fill_if_empty",
"ai_hint": "Where the contact stands with us. Set it only when the customer says it themselves.",
"option_descriptions": {
"prospect": "Interested, has not started a trial",
"trial": "Currently testing the product",
"customer": "Has a paid subscription"
}
}'

To change the level of an existing field:

curl -X PUT "https://api.sendseven.com/api/v1/custom-fields/cf-12345678" \
-H "Authorization: Bearer s7_api_..." \
-H "Content-Type: application/json" \
-d '{"ai_access": "read"}'

The definition response returns ai_access, ai_hint and option_descriptions.

How agents write fields during a chat​

When at least one field allows writing, the agent gets a tool to save contact details. It is instructed to save only details the customer clearly stated about themselves in the conversation, never guesses. Each write is checked again on the server:

  • the value is validated like any other write: the field type, the validation rules and, for select fields, the defined options. An invalid value is not saved;
  • the access level is checked against the stored value, so a fill_if_empty field that already has a value is skipped;
  • the value is always written to the contact of the current conversation.

Writes in the agent test console are a dry run: the turn shows what would be saved, nothing is written.

How AI writes are attributed​

  • Contact panel: in a conversation, a value written by an agent is labelled Set by AI and is treated as unverified, next to values marked On record, From token or Visitor-provided.

  • Webhooks: a saved value sends one contact.updated event with these additional fields in data:

    {
    "type": "contact.updated",
    "data": {
    "contact": { "id": "ct_456", "...": "..." },
    "origin": "ai",
    "changed_by": "ai_agent",
    "changed_fields": ["custom.contact_type"]
    }
    }

    Use data.origin to tell AI writes apart from writes by your team (ui), the API (api), an integration (integration:<name>) or a Flow (flow:<flow_id>/run:<run_id>).

  • Flows: the same event starts Flows with a contact updated trigger.

The agent's Context tab​

The field level is the upper limit for all agents. Each agent's Context tab decides which of the allowed fields that agent reads and writes. By default an agent uses every field your levels allow; you can limit an agent to a subset there. A field that is none cannot be added to an agent. See Context sources.


Python Example​

import requests

BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}

# 1. Define the field once (custom_fields:create)
field = requests.post(
f"{BASE_URL}/custom-fields",
headers=HEADERS,
json={
"name": "Plan",
"key": "plan",
"field_type": "select",
"options": ["basic", "professional", "scale", "enterprise"],
},
).json()
field_id = field["id"]

# 2. Set the value on a contact (contacts:update)
contact_id = "contact_d4e5f6a7"
requests.post(
f"{BASE_URL}/contacts/{contact_id}/fields/{field_id}",
headers=HEADERS,
json={"value": "enterprise"},
)

# 3. Read all custom field values for the contact (contacts:read)
values = requests.get(
f"{BASE_URL}/contacts/{contact_id}/fields",
headers=HEADERS,
).json()
print(values["fields"])

Common Custom Field Patterns​

keyfield_typeUse Case
companytextCRM sync
sourceselectLead tracking
planselectSubscription tier
preferred_languagetextLocalization
external_idtextThird-party system mapping
lifetime_valuenumberRevenue tracking
last_order_datedateE-commerce integration
is_resellerbooleanSegmentation flag

Next Steps​