Skip to content
Norbelys
Esc
↑↓navigate↵open⌘Jpreview
On this page

People, imports and campaign audiences

Import a CSV once, combine groups and live segments, and enroll each contact once.

All examples use https://api.norbelys.com/v1 and an Authorization: Bearer $NORBELYS_API_KEY header. IDs belong to the authenticated workspace. A foreign or missing audience ID returns 404; it never silently broadens the audience. JSON field names are snake_case and enum values keep their Rust spelling, such as Csv, All and Completed.

People have a read-only status derived from the workspace suppression list: Active (unsuppressed), Bounced, Unsubscribed, Complained or Suppressed (manual). Active does not verify that an inbox exists. Use suppressed=false to exclude blocked addresses from an audience. Keep bounced contacts for their history; deleting or reimporting one never clears its address-level suppression. Suppression stops active enrollments as Failed and queued messages as Suppressed. Explicitly removing it allows new work but does not restart those terminated messages or sequences.

Import a CSV and use its ID

Create an import from a URL on an operator-approved file host. A signed URL stays encrypted and is not returned by the API. CSV limits are 20 MiB and 50,000 records; inline JSON accepts 1–1,000 contacts. The server downloads and processes the source asynchronously.

curl -X POST https://api.norbelys.com/v1/people/imports \
  -H "Authorization: Bearer $NORBELYS_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: conference-csv-2026-09' \
  -d '{"source":{"type":"Csv","url":"https://files.example.com/leads.csv","mapping":{"email":"Email","given_name":"First name"}}}'

The 202 response contains an imp_… ID and a Location header. Poll that URL until status is Completed and people_available is true. This ID identifies the import cohort; there is no separate public file resource to create first.

Existing emails, compared without ASCII letter case, are reused without overwriting names or custom fields. Repeated rows create no duplicate contact. Every valid existing or new contact belongs to the import cohort once. Optional group_id also adds those contacts to a static group. imported counts new contacts; skipped counts existing/repeated valid rows; invalid counts rejected rows. For a completed import, their sum equals total.

An import’s cohort is independent of later group changes. Deleting a person removes that person from the cohort. Older imports created before cohort storage report people_available: false; import the source again to record the cohort safely. The API does not guess historical membership from a mutable group.

Add contacts to a campaign

Configure the campaign’s initial sequence and sending accounts first. Then send one PATCH /campaigns/{id} with an Idempotency-Key:

{
  "audience": {
    "import_ids": ["imp_0195f30ed98470008000000000000001"],
    "group_ids": ["grp_0195f30ed98470008000000000000001"],
    "segment_ids": ["seg_0195f30ed98470008000000000000001"],
    "person_ids": ["per_0195f30ed98470008000000000000001"]
  }
}

Use only the arrays you need. The result is the union of all sources, with each contact once. A one-item person_ids array adds one contact. An empty audience is rejected; it never means “everyone.” Limits are 20 groups, 20 segments, 20 imports and 1,000 explicit people per selection. The same audience object is accepted when creating a campaign with its sequence definitions. Optional start_at sets the admission’s initial time.

Read campaign detail to follow audience.status, matched, enrolled, skipped and status_detail. Admission processes at most 500 contacts per transaction, retaining a cursor across worker restarts. People already enrolled in this campaign are skipped, including completed or cancelled enrollments. A new audience does not reactivate them. Use the explicit single-contact enrollment operation when you intentionally need a new enrollment after an earlier one ended. Active duplicates remain disallowed.

Repeating a PATCH with the same key and body resumes/replays the same admission. A changed body with the same key returns 409; a different admission must wait until the active one finishes. Pending, failed or legacy imports without a recorded cohort also return 409. Admission does not activate a paused campaign. Suppressions still apply before delivery.

Groups and segments are evaluated while the worker walks the audience; this is a one-time admission, not an ongoing subscription or a transaction-wide snapshot of a changing dataset. If membership changes during that walk, submit another selection afterward to admit new matches. Imports provide a stable cohort once completed, except for contact deletions.

List and combine people

Use the people collection for segment membership; a separate segment-members endpoint is unnecessary. Direct filters combine with AND:

curl --get https://api.norbelys.com/v1/people \
  -H "Authorization: Bearer $NORBELYS_API_KEY" \
  --data-urlencode "segment_id=$SEGMENT_ID" \
  --data-urlencode "group_id=$GROUP_ID" \
  --data-urlencode 'suppressed=false' \
  --data-urlencode 'page_size=100'

To select members of either of two groups who also match a saved segment:

curl --get https://api.norbelys.com/v1/people \
  -H "Authorization: Bearer $NORBELYS_API_KEY" \
  --data-urlencode "audience={\"group_ids\":[\"$GROUP_A\",\"$GROUP_B\"]}" \
  --data-urlencode "segment_id=$SEGMENT_ID"

The audience query uses the same PeopleSelection as campaigns, encoded as JSON with a maximum of 8,192 bytes. It supports imports and explicit people too. Its union is intersected with all other parameters. segment_id=A plus audience={"segment_ids":[B,C]} means A AND (B OR C).

For an unsaved filter, pass filter as URL-encoded JSON. This is also how clients preview a segment before saving it:

{
  "match": "All",
  "rules": [
    { "field": "CustomField", "key": "score", "operator": "Gte", "value": 50 },
    { "field": "Email", "operator": "EndsWith", "value": "@example.com" }
  ]
}

All requires every rule; Any requires one. Filters support 1–20 rules. The OpenAPI extension x-norbelys-people-filter lists supported fields and operators. Saved segments use this same grammar. Rules compile to bound SQL predicates; clients never send raw SQL.

Other filters include literal substring q, exact email, suppressed, created/updated timestamp bounds, and min_group_count/max_group_count. max_group_count=0 finds ungrouped contacts. Timestamp bounds are exclusive. Field values, contact data and counts stay scoped to the workspace; the same email can independently exist in another workspace.

The response contains data and meta: {total, has_more, next_cursor}. total is the exact matching count before pagination, evaluated with the page in one database snapshot. Pages are newest first. Keep the filters unchanged and pass cursor=meta.next_cursor until has_more is false. Separate requests see current data, so edits between pages can change the total. Complex substring/custom-field searches may scan their candidate audience.

Was this page helpful?