Assert a CRM Record (Upsert)
Upsert a CRM record into a Zeeg custom object: match on an attribute to update an existing record or create a new one. Ideal for sync and bulk imports.
curl --request PUT \
--url https://api.zeeg.me/v2/crm/{objectSlug} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
}
'import requests
url = "https://api.zeeg.me/v2/crm/{objectSlug}"
payload = {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sku: 'DRESS-001', price: 29, inventory_count: 150})
};
fetch('https://api.zeeg.me/v2/crm/{objectSlug}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zeeg.me/v2/crm/{objectSlug}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'sku' => 'DRESS-001',
'price' => 29,
'inventory_count' => 150
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zeeg.me/v2/crm/{objectSlug}"
payload := strings.NewReader("{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.zeeg.me/v2/crm/{objectSlug}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zeeg.me/v2/crm/{objectSlug}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"status": 200,
"record": {
"id": "c1d2e3f4-a5b6-7890-cdef-123456789012",
"objectSlug": "products",
"attributes": {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
},
"createdAt": "2025-06-01T10:00:00+00:00",
"updatedAt": "2025-06-01T12:00:00+00:00"
}
}{
"success": true,
"status": 201,
"record": {
"id": "c1d2e3f4-a5b6-7890-cdef-123456789012",
"objectSlug": "products",
"attributes": {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
},
"createdAt": "2025-06-01T10:00:00+00:00",
"updatedAt": "2025-06-01T12:00:00+00:00"
}
}{
"success": true,
"message": "<string>",
"status": 123
}{
"message": "Unauthenticated."
}{
"error": {
"type": "insufficient_scope",
"message": "Your token is missing the required scope: crm:write.",
"error_id": "b5237933-07ac-41e5-8a1a-33e7c48c995f",
"required_scopes": [
"crm:write"
]
}
}{
"message": "The given data was invalid.",
"errors": {
"field_name": [
"The field_name field is required."
]
}
}- Match found → updates the existing record and returns
200 OK. - No match → creates a new record and returns
201 Created.
Path parameter
| Parameter | Type | Description |
|---|---|---|
objectSlug | string | The slug of the custom CRM object to assert into (e.g. products, deals). |
Query parameter
| Parameter | Type | Required | Description |
|---|---|---|---|
matchingAttribute | string | Yes | The attribute slug used to look up an existing record. The API searches for a record where this attribute’s value exactly equals the value you send in the request body. |
Request body
A flat JSON object of attribute slug → value pairs. The value ofmatchingAttribute must be included in the body — it is both the lookup key and the new value to persist.
{
"sku": "DRESS-001",
"price": 35,
"inventory_count": 80
}
null on create, and left unchanged on update.
How matching works
The API performs exact equality matching on thematchingAttribute value. Only one record is ever matched. If multiple records somehow share the same value (e.g. before you marked the attribute as unique), the first match is used.
PUT /v2/crm/products?matchingAttribute=sku
sku = <value from body>.
Choosing the right matching attribute
Pick an attribute that uniquely identifies a record in your source system — an external ID, a product SKU, a deal number, an email address. Mark it asisUnique on the object schema to prevent collisions.
PUT /v2/crm/deals?matchingAttribute=external_id
PUT /v2/crm/products?matchingAttribute=sku
PUT /v2/crm/subscriptions?matchingAttribute=stripe_subscription_id
Uniqueness constraints
If another attribute on the record is markedisUnique, the assert will fail with 400 if you try to assign a value that already belongs to a different record. You can always re-assert the same unique value back onto the same record without error.
Error: matching value is empty
If the value ofmatchingAttribute in the request body is null or an empty string, the API returns 400 Bad Request. Always include a non-empty value for the matching attribute.
Example: bulk product sync
Your external catalog has 10,000 products. Run assert in a loop:PUT /v2/crm/products?matchingAttribute=sku
{
"sku": "JACKET-042",
"price": 89,
"inventory_count": 200,
"in_stock": true
}
Distinguishing create vs. update
Check the HTTP status code in the response:| Status | Meaning |
|---|---|
201 Created | No match was found; a new record was created. |
200 OK | A matching record was found and updated. |
When to use this endpoint
- Periodic sync jobs — push updated records from an external system on a schedule.
- Webhook-driven updates — handle incoming webhooks that may create or update a record depending on whether it exists.
- Bulk initial import — send all records from a legacy system without checking for duplicates first.
- Idempotent writes — retry failed requests safely; asserting the same data again results in an update (200) with no duplicate.
Authorizations
Path Parameters
Slug of the custom CRM object.
Query Parameters
Attribute slug to use when searching for an existing record. Uses exact equality matching.
Body
Flat object of attribute slug → value pairs.
Response
Updated — a record with the matching attribute value was found and updated
true
200
A record for a custom CRM object.
Hide child attributes
Hide child attributes
Unique identifier for the record.
"c1d2e3f4-a5b6-7890-cdef-123456789012"
Slug of the CRM object this record belongs to.
"products"
Key/value pairs for the custom attributes defined on the object. Keys are attribute slugs.
{
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
}
ISO 8601 timestamp when the record was created.
"2025-06-01T10:00:00+00:00"
ISO 8601 timestamp when the record was last updated.
"2025-06-01T12:00:00+00:00"
curl --request PUT \
--url https://api.zeeg.me/v2/crm/{objectSlug} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
}
'import requests
url = "https://api.zeeg.me/v2/crm/{objectSlug}"
payload = {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sku: 'DRESS-001', price: 29, inventory_count: 150})
};
fetch('https://api.zeeg.me/v2/crm/{objectSlug}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.zeeg.me/v2/crm/{objectSlug}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'sku' => 'DRESS-001',
'price' => 29,
'inventory_count' => 150
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.zeeg.me/v2/crm/{objectSlug}"
payload := strings.NewReader("{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}")
req, _ := http.NewRequest("PUT", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.put("https://api.zeeg.me/v2/crm/{objectSlug}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.zeeg.me/v2/crm/{objectSlug}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sku\": \"DRESS-001\",\n \"price\": 29,\n \"inventory_count\": 150\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"status": 200,
"record": {
"id": "c1d2e3f4-a5b6-7890-cdef-123456789012",
"objectSlug": "products",
"attributes": {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
},
"createdAt": "2025-06-01T10:00:00+00:00",
"updatedAt": "2025-06-01T12:00:00+00:00"
}
}{
"success": true,
"status": 201,
"record": {
"id": "c1d2e3f4-a5b6-7890-cdef-123456789012",
"objectSlug": "products",
"attributes": {
"sku": "DRESS-001",
"price": 29,
"inventory_count": 150
},
"createdAt": "2025-06-01T10:00:00+00:00",
"updatedAt": "2025-06-01T12:00:00+00:00"
}
}{
"success": true,
"message": "<string>",
"status": 123
}{
"message": "Unauthenticated."
}{
"error": {
"type": "insufficient_scope",
"message": "Your token is missing the required scope: crm:write.",
"error_id": "b5237933-07ac-41e5-8a1a-33e7c48c995f",
"required_scopes": [
"crm:write"
]
}
}{
"message": "The given data was invalid.",
"errors": {
"field_name": [
"The field_name field is required."
]
}
}