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.
Recording consent
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.