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.
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
- Define the field once (an admin/settings operation) —
POST /api/v1/custom-fields. This returns a field definition with anid. - Set values per contact — either inline on contact create/update (
custom_fieldsobject, many fields at once, strict typed validation) or one field at a time withPOST /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.
| Scope | Purpose |
|---|---|
custom_fields:read | List / view custom field definitions (GET /custom-fields, GET /custom-fields/{id}) |
custom_fields:create | Create custom field definitions (POST /custom-fields) |
custom_fields:update | Update custom field definitions (PUT /custom-fields/{id}) |
custom_fields:create or custom_fields:update | Check where a key is still used (GET /custom-fields/key-references) |
custom_fields:read or custom_fields:delete | How many contacts hold a value (GET /custom-fields/{id}/usage) |
custom_fields:delete | Delete 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:
| Scope | Purpose |
|---|---|
contacts:read | Read custom field values on a contact |
contacts:update | Set custom field values on a contact (also via PUT/PATCH /contacts/{id}) |
contacts:create | Set 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_type | Description |
|---|---|
text | Free-form text |
number | Numeric values (integers and decimals) |
boolean | True / false |
date | Date only (YYYY-MM-DD) |
datetime | Date and time (ISO 8601) |
select | Single selection from options |
multiselect | Multiple selections from options |
email | Email address |
phone | Phone number |
url | Website URL |
location | Latitude / longitude |
collection | Array 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
| Field | Required | Notes |
|---|---|---|
name | yes | Display name (1–100 chars) |
key | yes | Internal key, snake_case, must match ^[a-z][a-z0-9_]*$ |
field_type | yes | One of the types above |
description | no | Help text (≤ 500 chars) |
options | for select/multiselect | Array of allowed string values |
required | no | Defaults to false |
default_value | no | Default for new contacts |
agent_visible | no | Visible to agents (default true) |
agent_editable | no | Editable by agents (default true) |
user_editable | no | Editable by the contact via self-service (default false) |
display_order | no | Lower numbers sort first (default 0) |
ai_access | no | What AI agents may do with the field: none (default), read, read_write or fill_if_empty. See AI access |
ai_hint | no | What the field means and how an AI agent should use or set it (max 1000 characters) |
option_descriptions | no | select / 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 field | Type | Description |
|---|---|---|
field_type | string | The new type. Must differ from the current type. |
options | string[] | 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_descriptions | object | null | select / multiselect targets: omit to keep the descriptions of options that still exist, send an object to replace, null to clear. |
incompatible_values | string | fail (default), remove or keep: what happens to stored values that do not fit the new type. |
dry_run | boolean | true 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) orchecked(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) becomesreadon alocation/collectiontarget.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.codeisoperator_incompatible,type_mismatch,options_changedorreview.
Conversions:
| From | To | Kind | Result |
|---|---|---|---|
| any | text | lossless | Rendered as text: lists joined with , , location as lat, lng |
select | multiselect, collection | lossless | One-item list |
multiselect | collection | lossless | Same list |
text | collection | lossless | One-item list |
date | datetime | lossless | YYYY-MM-DDT00:00:00Z |
boolean | number | lossless | 1 / 0 |
text and others | number | checked | Canonical number; 00123 becomes 123 (lossy) |
datetime | date | checked | The UTC date (lossy when the time is not midnight) |
number | boolean | checked | Only 1 and 0 |
| any | select / multiselect | checked | Must 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 / collection | any single-value type | checked | Only one-item lists |
| everything else | checked | Validated 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 type | Allowed keys |
|---|---|
number | min, max |
text, email, phone, url | min_length, max_length, pattern |
date, datetime | date_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
nullto remove the rules, or leavevalidationout to keep them. patternmust 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,
mingreater thanmax, or an unsafe pattern return422:
{
"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 fieldkey. 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
422and 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 fullcustom_fieldsmap. - Up to 200 fields per request.
requiredanddefault_valueon a definition are not enforced by these endpoints. - With duplicate handling
auto_merge, whenPOST /contactsmatches an existing contact: if your API key / app also has thecontacts:updatescope, the values you send overwrite that contact's values. Withoutcontacts:update, they only fill fields that are empty — fields that already have a value are left unchanged (no error). Changed fields trigger acontact.updatedevent.
Accepted Input and Stored Format
field_type | Accepted input | Stored / returned as |
|---|---|---|
text | string (numbers/booleans are converted), max 10,000 chars | the string |
number | JSON number or numeric string ("1e3" ok); not booleans, NaN or Infinity; max 38 digits | plain decimal, no exponent or trailing zeros: "1000", "7.5", "-0.25" |
boolean | true/false, 1/0, "true"/"false", "yes"/"no", "1"/"0" (any case) | "true" / "false" |
date | ISO 8601 date YYYY-MM-DD (a full date-time is accepted; its date part is kept) | "2026-09-01" |
datetime | ISO 8601 date-time; no offset means UTC | UTC "2026-09-01T13:45:00Z" |
select | one option (matched case-insensitively) | the option's exact spelling |
multiselect | array of options, or a comma-separated string; every item must be an option; duplicates removed | JSON array string '["Shoes", "Bags"]' |
email | valid email address | the address |
phone | international number +<country code><number> (spaces/dashes ok) | E.164 "+491701234567" |
url | absolute http:// or https:// URL, max 2,048 chars | the 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 |
collection | array of strings/numbers, a JSON array string, or a comma-separated string | JSON 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
}
]
}
code | Meaning |
|---|---|
unknown_field | No active field with this id or key (never existed, deleted, or belongs to another workspace) |
duplicate_field | The same field was sent twice (once by id, once by key) |
invalid_type | Wrong JSON type (e.g. an object for a text field) |
invalid_format | Right type, but unparseable (e.g. "abc" for a number, "31.02.2026" for a date) |
not_an_option | Not one of the field's options |
out_of_range | Number too large or not finite, coordinates out of range |
too_long | Text or URL too long, or too many list items |
too_many_fields | More than 200 entries in one request |
validation_failed | The 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"
}
}
}
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_access | Label in the app | Agents see the value | Agents may write |
|---|---|---|---|
none (default) | None | No | No |
read | Read | Yes | No |
read_write | Read and write | Yes | Yes, and may replace any existing value |
fill_if_empty | Fill if empty | Yes | Only while the field is empty |
noneis the default. Existing fields and fields created withoutai_accessare invisible to AI until you change them.read_writereplaces 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_emptynever changes an existing value. An empty text, an empty selection and an unset value all count as empty.locationandcollectionfields accept onlynoneorread. A writable level on them returns422withinvalid_ai_access.
Explain the field to the agent
Two optional settings tell the agent what a field means. Both are only used for AI.
| Field | Applies to | Limit | Purpose |
|---|---|---|---|
ai_hint | All field types | 1000 characters | What the field means and when to set it |
option_descriptions | select, multiselect | 300 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
validationrules 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_emptyfield 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.updatedevent with these additional fields indata:{
"type": "contact.updated",
"data": {
"contact": { "id": "ct_456", "...": "..." },
"origin": "ai",
"changed_by": "ai_agent",
"changed_fields": ["custom.contact_type"]
}
}Use
data.originto 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
key | field_type | Use Case |
|---|---|---|
company | text | CRM sync |
source | select | Lead tracking |
plan | select | Subscription tier |
preferred_language | text | Localization |
external_id | text | Third-party system mapping |
lifetime_value | number | Revenue tracking |
last_order_date | date | E-commerce integration |
is_reseller | boolean | Segmentation flag |
Next Steps
- Manage Contacts -- full contact CRUD operations
- Tags & Lists -- organize contacts with tags and lists
- Messaging Campaigns -- target contacts with campaigns