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
| Field | Type | Description |
|---|---|---|
id | uuid | Contact ID |
tenant_id | string | Owning tenant |
phone | string | null | E.164 normalized. Unique per (tenant_id, phone) |
email | string | null | Email address |
name | string | null | Display name |
timezone | string | null | IANA timezone, used for quiet-hours calling windows |
language | string | null | Preferred language |
tags | string[] | Freeform tags |
custom_fields | object | Arbitrary key-value data, available as call template variables |
consent | object | Per-channel consent flags, e.g. { "voice": true, "sms": false, "whatsapp": true } |
source | string | null | csv_import, api, inbound_call, or crm |
created_at / updated_at | string | ISO 8601 timestamps |
Create Contact
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
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | No | E.164 phone number (e.g. +15551234567). Normalized server-side |
email | string | No | Email address |
name | string | No | Display name |
timezone | string | No | IANA timezone |
language | string | No | Preferred language |
tags | string[] | No | Freeform tags |
custom_fields | object | No | Arbitrary key-value data |
consent | object | No | Per-channel consent flags |
source | string | No | Origin label |
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"
}'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",
});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)
{
"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
| Status | Meaning |
|---|---|
400 | phone provided but not a valid E.164 number |
401 | Missing or invalid API key |
403 | Publishable keys cannot create contacts |
List Contacts
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max records (max 500) |
offset | integer | 0 | Records to skip |
ids | string | — | Comma-separated contact ids (max 100). When present, returns only those contacts (still tenant-scoped). |
curl "https://api.rymi.live/v1/contacts?limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"const { contacts } = await rymi.contacts.list({ limit: 50 });result = rymi.contacts.list(limit=50)Response 200
{
"contacts": [
{ "id": "c_1a2b3c", "phone": "+15551234567", "name": "Asha Patel", "tags": ["renewal-q3"] }
],
"total": 1,
"offset": 0,
"limit": 50
}Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
Get Contact
curl https://api.rymi.live/v1/contacts/c_1a2b3c \
-H "Authorization: Bearer YOUR_API_KEY"const { contact } = await rymi.contacts.get("c_1a2b3c");result = rymi.contacts.get("c_1a2b3c")Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
404 | Contact not found |
Update Contact
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.
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 } }'const { contact } = await rymi.contacts.update("c_1a2b3c", {
consent: { voice: true, sms: true, whatsapp: true },
});result = rymi.contacts.update("c_1a2b3c", consent={"voice": True, "sms": True, "whatsapp": True})Errors
| Status | Meaning |
|---|---|
400 | phone provided but not a valid E.164 number, or empty body |
401 | Missing or invalid API key |
403 | Publishable keys cannot update contacts |
404 | Contact not found |
Delete Contact
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.
curl -X DELETE https://api.rymi.live/v1/contacts/c_1a2b3c \
-H "Authorization: Bearer YOUR_API_KEY"const result = await rymi.contacts.delete("c_1a2b3c");result = rymi.contacts.delete("c_1a2b3c")Response 200
{ "status": "deleted", "id": "c_1a2b3c" }Errors
| Status | Meaning |
|---|---|
401 | Missing or invalid API key |
403 | Publishable keys cannot delete contacts |
Bulk Import Contacts
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:
| Field | Type | Description |
|---|---|---|
contacts | object[] | Inline rows — each may include phone, email, name, timezone, language, tags, custom_fields, consent, source |
csv | string | Raw CSV text. First row is the header; header names map directly to contact fields |
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" }
]
}'const result = await rymi.contacts.import({
contacts: [
{ phone: "+15551234567", name: "Asha Patel", tags: ["renewal-q3"] },
{ phone: "not-a-number", name: "Bad Row" },
],
});result = rymi.contacts.import_(
contacts=[
{"phone": "+15551234567", "name": "Asha Patel", "tags": ["renewal-q3"]},
{"phone": "not-a-number", "name": "Bad Row"},
]
)Example: CSV Import
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"
}'const result = await rymi.contacts.import({
csv: "phone,name,timezone\n+15551234567,Asha Patel,America/New_York\n+15557654321,Bob Lee,America/Chicago",
});result = rymi.contacts.import_(
csv="phone,name,timezone\n+15551234567,Asha Patel,America/New_York\n+15557654321,Bob Lee,America/Chicago"
)Response 200
{
"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
| Status | Meaning |
|---|---|
400 | Neither contacts nor csv provided, or no rows to import |
401 | Missing or invalid API key |
403 | Publishable 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.

