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
idsstring-Comma-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.