Contacts

Manage the people you send to, create, update, segment, and export contacts.

contacts are the people you send to. Store them once and address a message by contactId instead of a raw email, or group them with contact groups for segmented sends.

What you can do

  • Manage contacts, create, retrieve, list, update, and remove.
  • Addresses and tags, add or remove one identity or a set of tags without touching the rest.
  • Consent, record whether a contact agreed to hear from you at an address.
  • Bulk operations, update status or tags for multiple contacts.
  • Engagement, read a contact's engagement history.
  • Export your contacts.

Addressing a contact

Once a contact exists, reference it from the send body instead of an email address:

{
  "to": [{ "contactId": "contact_k9mvt4znw8rh7q2x" }],
  "channel": "email",
  "email": { "subject": "Hello", "html": "<p>Hi!</p>" }
}

Reading and updating a contact

A list returns contact summaries: name, status, first email address, the channels the contact can be reached on, tags, and lists. Get a contact returns the full record, with each identity's id, the profile, preferences, and metadata.

Find or create with attributes

Choose exactly one lookup. Attributes under create apply only when the contact is new:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const { contact, created } = await samva.contacts.findOrCreate({
  lookup: { type: "email", value: "ada@example.com" },
  create: {
    name: "Ada Lovelace",
    externalId: "user_123",
    profile: { firstName: "Ada", customFields: { plan: "creator" } },
  },
});

Define custom fields before setting them; unknown keys or values of the wrong type are rejected. An existing contact returns unchanged with created: false, even when create names different attributes. Use update to change it. To look up by your own user id and create an email address, use lookup: { type: "externalId", value: "user_123" } and create.identities: [{ type: "email", identifier: "ada@example.com" }]. A lookup accepts email, phone, or externalId.

Identity types and phone format

Each identity has a type, email or phone, and an identifier stored in normalized form: emails are lowercased and phone numbers are E.164 (+14155550123). A contact holds one identity per address, unique within the organization. A phone number without a leading + is read in the organization's default region, an ISO 3166-1 alpha-2 code such as US. Set it with PATCH /v1/organizations/current as defaultRegion. With no default region set, a phone number without + is rejected with a 422 validation error.

Inspect selected custom fields

Keep list summaries lean by default, or select defined custom field keys and filter by one value:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const page = await samva.contacts.list({
  fields: "plan,signup_at",
  customField: "plan",
  customFieldValue: JSON.stringify("creator"),
});
// page.items contain customFields with only the requested keys that are present.

customFieldValue is a JSON scalar: a quoted string, number, boolean, or null. Null matches a stored null value, not an absent key. Unknown keys, malformed values, and incomplete filters fail with ValidationError. Pagination uses the same filters on every page.

From the CLI, use samva contacts list --fields plan,signup_at --json or samva contacts get <contact-id> --json.

Update a contact changes only the fields you name. Metadata merges by key, and a null value deletes that key:

{ "metadata": { "plan": "pro", "trialEndsAt": null } }

Identities and tags are never replaced by an update. Add an address with POST /v1/contacts/{id}/identities and remove one by its id with DELETE /v1/contacts/{id}/identities/{identityId}. Tags change through POST /v1/contacts/{id}/tags and DELETE /v1/contacts/{id}/tags.

Consent is what a contact agreed to at one address on one channel. Each identity lists its channels, and each channel carries consent: null when none is recorded, otherwise the status (opted_in or opted_out), the source you gave, when the contact decided (capturedAt), and any proof you kept. No record means unknown, and email sends to an address with unknown consent.

PUT /v1/contacts/{id}/identities/{identityId}/consent/{channel} records a decision and returns the contact:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

await samva.contacts.setConsent({
  id: "contact_k9mvt4znw8rh7q2x",
  identityId: "cident_x3pq8wnr2tz5v7k1",
  channel: "email",
  status: "opted_out",
  source: "Support request",
  capturedAt: new Date().toISOString(),
});

source says where the decision was captured, such as a form URL or a checkout step, and proof keeps your evidence as a JSON object. A decision whose capturedAt is earlier than the recorded one is ignored. A confirmed double opt-in records opted_in with the source double_opt_in.

An opt-out stops every send to that address on that channel. Samva writes an unsubscribe suppression for it, so the address is skipped wherever suppressions apply, and an opt-in lifts that suppression. Bounce and complaint suppressions stay. A change publishes contact.consent_changed.

Shared conventions (auth, status codes, errors, pagination) are in the REST API reference. Per-operation request and response detail is in the sidebar.

Related documentation

On this page