Skip to content

Contacts

Tenant-level people and leads. A contact's identity, consent, and history live once and can belong to many campaigns, each with independent per-campaign state (a campaign_member).

The Contact Object

FieldTypeDescription
iduuidContact ID
tenant_idstringOwning tenant
phonestring | nullE.164 normalized. Unique per (tenant_id, phone)
emailstring | nullEmail address
namestring | nullDisplay name
timezonestring | nullIANA timezone, used for quiet-hours calling windows
languagestring | nullPreferred language
tagsstring[]Freeform tags
custom_fieldsobjectArbitrary key-value data, available as call template variables
consentobjectPer-channel consent flags, e.g. { "voice": true, "sms": false, "whatsapp": true }
sourcestring | nullcsv_import, api, inbound_call, or crm
created_at / updated_atstringISO 8601 timestamps

Create Contact

POST/v1/contacts

Creates a tenant-level contact. Dedup on (tenant_id, phone): if a contact with the same phone already exists for the tenant, that existing row is returned instead of creating a duplicate — check the deduped flag in the response.

Request Body

FieldTypeRequiredDescription
phonestringNoE.164 phone number (e.g. +15551234567). Normalized server-side
emailstringNoEmail address
namestringNoDisplay name
timezonestringNoIANA timezone
languagestringNoPreferred language
tagsstring[]NoFreeform tags
custom_fieldsobjectNoArbitrary key-value data
consentobjectNoPer-channel consent flags
sourcestringNoOrigin label
bash
curl -X POST https://api.rymi.live/v1/contacts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+15551234567",
    "name": "Asha Patel",
    "timezone": "America/New_York",
    "tags": ["renewal-q3"],
    "custom_fields": { "plan": "Pro", "company": "Northstar Dental" },
    "consent": { "voice": true, "sms": true, "whatsapp": false },
    "source": "crm"
  }'
ts
const { contact, deduped } = await rymi.contacts.create({
  phone: "+15551234567",
  name: "Asha Patel",
  timezone: "America/New_York",
  tags: ["renewal-q3"],
  custom_fields: { plan: "Pro", company: "Northstar Dental" },
  consent: { voice: true, sms: true, whatsapp: false },
  source: "crm",
});
python
result = rymi.contacts.create(
    phone="+15551234567",
    name="Asha Patel",
    timezone="America/New_York",
    tags=["renewal-q3"],
    custom_fields={"plan": "Pro", "company": "Northstar Dental"},
    consent={"voice": True, "sms": True, "whatsapp": False},
    source="crm",
)

Response 201 (new contact)

json
{
  "contact": {
    "id": "c_1a2b3c",
    "tenant_id": "ten_456",
    "phone": "+15551234567",
    "email": null,
    "name": "Asha Patel",
    "timezone": "America/New_York",
    "language": null,
    "tags": ["renewal-q3"],
    "custom_fields": { "plan": "Pro", "company": "Northstar Dental" },
    "consent": { "voice": true, "sms": true, "whatsapp": false },
    "source": "crm",
    "created_at": "2026-07-03T10:00:00Z",
    "updated_at": "2026-07-03T10:00:00Z"
  },
  "deduped": false
}

Response 200 (existing contact returned)

Same shape with "deduped": true when a contact with that (tenant_id, phone) already existed.

Errors

StatusMeaning
400phone provided but not a valid E.164 number
401Missing or invalid API key
403Publishable keys cannot create contacts

List Contacts

GET/v1/contacts

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max records (max 500)
offsetinteger0Records to skip
idsstringComma-separated contact ids (max 100). When present, returns only those contacts (still tenant-scoped).
bash
curl "https://api.rymi.live/v1/contacts?limit=50" \
  -H "Authorization: Bearer YOUR_API_KEY"
ts
const { contacts } = await rymi.contacts.list({ limit: 50 });
python
result = rymi.contacts.list(limit=50)

Response 200

json
{
  "contacts": [
    { "id": "c_1a2b3c", "phone": "+15551234567", "name": "Asha Patel", "tags": ["renewal-q3"] }
  ],
  "total": 1,
  "offset": 0,
  "limit": 50
}

Errors

StatusMeaning
401Missing or invalid API key

Get Contact

GET/v1/contacts/:id
bash
curl https://api.rymi.live/v1/contacts/c_1a2b3c \
  -H "Authorization: Bearer YOUR_API_KEY"
ts
const { contact } = await rymi.contacts.get("c_1a2b3c");
python
result = rymi.contacts.get("c_1a2b3c")

Errors

StatusMeaning
401Missing or invalid API key
404Contact not found

Update Contact

PATCH/v1/contacts/:id

Only provided fields change. Passing null for email, name, timezone, language, or source clears the field.

Request Body

At least one field required: phone, email, name, timezone, language, tags, custom_fields, consent, source.

bash
curl -X PATCH https://api.rymi.live/v1/contacts/c_1a2b3c \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "consent": { "voice": true, "sms": true, "whatsapp": true } }'
ts
const { contact } = await rymi.contacts.update("c_1a2b3c", {
  consent: { voice: true, sms: true, whatsapp: true },
});
python
result = rymi.contacts.update("c_1a2b3c", consent={"voice": True, "sms": True, "whatsapp": True})

Errors

StatusMeaning
400phone provided but not a valid E.164 number, or empty body
401Missing or invalid API key
403Publishable keys cannot update contacts
404Contact not found

Delete Contact

DELETE/v1/contacts/:id

Removes a tenant-scoped contact. Deleting a contact does not cascade to campaign membership history the way deleting a campaign cascades to its members — campaign_members referencing a deleted contact retain their row with a null-safe reference.

bash
curl -X DELETE https://api.rymi.live/v1/contacts/c_1a2b3c \
  -H "Authorization: Bearer YOUR_API_KEY"
ts
const result = await rymi.contacts.delete("c_1a2b3c");
python
result = rymi.contacts.delete("c_1a2b3c")

Response 200

json
{ "status": "deleted", "id": "c_1a2b3c" }

Errors

StatusMeaning
401Missing or invalid API key
403Publishable keys cannot delete contacts

Bulk Import Contacts

POST/v1/contacts/import

Imports contacts from JSON rows or raw CSV text. Each row's phone is normalized to E.164 and upserted on (tenant_id, phone) — existing contacts are merged (updated), new phones are created. Rows with an unparseable phone are skipped and reported in invalid; every other field is optional. Within a single import, later rows with the same normalized phone win (last write wins) before the upsert runs.

Request Body

Provide exactly one of:

FieldTypeDescription
contactsobject[]Inline rows — each may include phone, email, name, timezone, language, tags, custom_fields, consent, source
csvstringRaw CSV text. First row is the header; header names map directly to contact fields
bash
curl -X POST https://api.rymi.live/v1/contacts/import \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone": "+15551234567", "name": "Asha Patel", "tags": ["renewal-q3"] },
      { "phone": "not-a-number", "name": "Bad Row" }
    ]
  }'
ts
const result = await rymi.contacts.import({
  contacts: [
    { phone: "+15551234567", name: "Asha Patel", tags: ["renewal-q3"] },
    { phone: "not-a-number", name: "Bad Row" },
  ],
});
python
result = rymi.contacts.import_(
    contacts=[
        {"phone": "+15551234567", "name": "Asha Patel", "tags": ["renewal-q3"]},
        {"phone": "not-a-number", "name": "Bad Row"},
    ]
)

Example: CSV Import

bash
curl -X POST https://api.rymi.live/v1/contacts/import \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csv": "phone,name,timezone\n+15551234567,Asha Patel,America/New_York\n+15557654321,Bob Lee,America/Chicago"
  }'
ts
const result = await rymi.contacts.import({
  csv: "phone,name,timezone\n+15551234567,Asha Patel,America/New_York\n+15557654321,Bob Lee,America/Chicago",
});
python
result = rymi.contacts.import_(
    csv="phone,name,timezone\n+15551234567,Asha Patel,America/New_York\n+15557654321,Bob Lee,America/Chicago"
)

Response 200

json
{
  "created": 1,
  "merged": 0,
  "invalid": [
    { "row": 1, "reason": "Unparseable phone number" }
  ]
}

row is the 0-based index into the input array (or CSV data rows, excluding the header).

Errors

StatusMeaning
400Neither contacts nor csv provided, or no rows to import
401Missing or invalid API key
403Publishable keys cannot import contacts

TIP

To import contacts and attach them to a campaign in one call, use POST /v1/campaigns/:id/members/import instead — it runs the same normalization/dedup logic and also creates the campaign_members rows.


Publishable Key Rules

Publishable keys cannot create, update, delete, or import contacts. Reads (list/get) require a secret key.