POST /v1/actions/create-contact
Upsert a contact by phone number with optional name, email, tags, and metadata.
Creates a contact, or updates the existing one if a contact with the same phone already exists in your tenant. Returns 201 regardless of whether the row was inserted or updated.
POST /api/v1/actions/create-contact
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
phone |
string | yes | International format with +. Normalized server-side. |
name |
string | no | Display name |
email |
string | no | Must be a valid email if provided |
tags |
string | no | Comma-separated list, e.g. "vip, beta-testers, fr-FR". Tags are created on the fly if they don't exist. |
metadata |
object | no | Free-form JSON, max ~64 KB |
curl -X POST https://www.qyvo.io/api/v1/actions/create-contact \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"phone": "+14155550123",
"name": "Romain",
"email": "[email protected]",
"tags": "vip, fr-FR",
"metadata": { "shopify_id": "12345", "lifetime_value": 8742.00 }
}'
const contact = await fetch('https://www.qyvo.io/api/v1/actions/create-contact', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QYVO_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: '+14155550123',
name: 'Romain',
email: '[email protected]',
tags: 'vip, fr-FR',
metadata: { shopify_id: '12345', lifetime_value: 8742.0 },
}),
}).then((r) => r.json());
$contact = Http::withToken(env('QYVO_TOKEN'))
->post('https://www.qyvo.io/api/v1/actions/create-contact', [
'phone' => '+14155550123',
'name' => 'Romain',
'email' => '[email protected]',
'tags' => 'vip, fr-FR',
'metadata' => ['shopify_id' => '12345', 'lifetime_value' => 8742.0],
])
->json();
import os, httpx
contact = httpx.post(
'https://www.qyvo.io/api/v1/actions/create-contact',
headers={'Authorization': f"Bearer {os.environ['QYVO_TOKEN']}"},
json={
'phone': '+14155550123',
'name': 'Romain',
'email': '[email protected]',
'tags': 'vip, fr-FR',
'metadata': {'shopify_id': '12345', 'lifetime_value': 8742.0},
},
).json()
Response — 201 Created
{
"id": "01J1Y...",
"phone": "+14155550123",
"name": "Romain",
"email": "[email protected]",
"tags": ["vip", "fr-FR"],
"created_at": "2026-05-07T08:14:23+00:00"
}
Errors
| Status | Cause |
|---|---|
422 |
Validation (phone missing, invalid email, name too long) |
422 |
No workspace configured for this account. |
Idempotency
The endpoint upserts on (tenant_id, phone) — calling it twice with the same phone never creates two contacts. The second call updates name, email, metadata, and tags. To avoid wiping fields you didn't intend to change, use update-contact for partial updates.
