> ## Documentation Index
> Fetch the complete documentation index at: https://developer.zeeg.me/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

Creates or updates a record depending on whether a matching record already exists. This is the recommended endpoint for sync pipelines and bulk imports.

* **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 of `matchingAttribute` must be included in the body — it is both the lookup key and the new value to persist.

```json theme={null}
{
  "sku": "DRESS-001",
  "price": 35,
  "inventory_count": 80
}
```

The entire body is applied to the record (create or update). Attributes you omit are set to `null` on create, and left unchanged on update.

## How matching works

The API performs exact equality matching on the `matchingAttribute` 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
```

The above searches for a record where `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 as `isUnique` 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 marked `isUnique`, 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 of `matchingAttribute` 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:

```json theme={null}
PUT /v2/crm/products?matchingAttribute=sku
{
  "sku": "JACKET-042",
  "price": 89,
  "inventory_count": 200,
  "in_stock": true
}
```

First run: all products are created (201). Subsequent runs: existing products are updated with latest prices (200). No pre-check needed — the API handles it.

## 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.


## OpenAPI

````yaml PUT /crm/{objectSlug}
openapi: 3.0.0
info:
  title: Zeeg Public API
  description: >-
    Zeeg public API documentation.


    ## Authentication

    All endpoints require a Bearer token. You can generate an API token from
    [your Zeeg dashboard](https://app.zeeg.me/account/settings/api-access).


    Each token is scoped to specific permissions (e.g. `events:read`,
    `webhooks:write`). Make sure your token has the required scopes for the
    endpoints you want to use.


    ## Recommended Headers

    We recommend including the `Accept: application/json` header in all API
    requests to ensure you receive JSON responses.
  version: 2.0.0
  x-logo:
    url: https://app.zeeg.me/img/logo-dark.2ca83593.svg
    backgroundColor: '#f7f7f9'
    altText: zeeg
  contact:
    name: Zeeg Support
    email: support@zeeg.me
    url: https://zeeg.me/en/contact
  license:
    name: Proprietary
    url: https://zeeg.me/en/legal/terms
  termsOfService: https://zeeg.me/en/legal/terms
servers:
  - url: https://api.zeeg.me/v2
    description: Production
security:
  - bearer: []
tags:
  - name: Scheduled Events
    description: Management of events scheduled via Zeeg
  - name: Scheduling Pages
    description: Scheduling pages information and management
  - name: Availability Schedule
    description: Read and change availability for users
  - name: Webhooks
    description: Webhooks management
  - name: Notes
    description: Notes for scheduled events
  - name: Workspaces & Teams
    description: Workspace users and team member management
  - name: AI Agent
    description: AI Agent integration endpoints
  - name: Payloads
    description: Webhook payload schemas
  - name: CRM - Objects
    description: >-
      Discover the schema of CRM objects (standard and custom) including all
      attribute definitions
  - name: CRM - Companies
    description: Create, read, update, and delete CRM company records
  - name: CRM - People
    description: Create, read, update, and delete CRM person records
paths:
  /crm/{objectSlug}:
    put:
      tags:
        - CRM - Records
      summary: Assert a CRM record (upsert)
      description: >-
        Creates or updates a record using a unique attribute to find an existing
        match.


        - **Match found** → the record is updated and `200` is returned.

        - **No match** → a new record is created and `201` is returned.


        This is the recommended endpoint for bulk imports and sync jobs.


        **Required scope:** `crm:write`
      operationId: put-crm-record
      parameters:
        - name: objectSlug
          in: path
          required: true
          schema:
            type: string
          description: Slug of the custom CRM object.
          example: products
        - name: matchingAttribute
          in: query
          required: true
          schema:
            type: string
          description: >-
            Attribute slug to use when searching for an existing record. Uses
            exact equality matching.
          example: sku
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: Flat object of attribute slug → value pairs.
              example:
                sku: DRESS-001
                price: 29
                inventory_count: 150
      responses:
        '200':
          description: >-
            Updated — a record with the matching attribute value was found and
            updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  status:
                    type: integer
                    example: 200
                  record:
                    $ref: '#/components/schemas/CrmRecord'
        '201':
          description: >-
            Created — no record with the matching attribute value was found; a
            new one was created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  status:
                    type: integer
                    example: 201
                  record:
                    $ref: '#/components/schemas/CrmRecord'
        '400':
          description: >-
            Bad Request — matching attribute value is empty or attribute
            validation failed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  status:
                    type: integer
        '401':
          $ref: '#/components/responses/401'
        '403':
          description: Forbidden — missing scope or CRM not enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  status:
                    type: integer
        '422':
          $ref: '#/components/responses/422'
      security:
        - bearer: []
components:
  schemas:
    CrmRecord:
      type: object
      description: A record for a custom CRM object.
      required:
        - id
        - objectSlug
        - attributes
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the record.
          example: c1d2e3f4-a5b6-7890-cdef-123456789012
        objectSlug:
          type: string
          description: Slug of the CRM object this record belongs to.
          example: products
        attributes:
          type: object
          description: >-
            Key/value pairs for the custom attributes defined on the object.
            Keys are attribute slugs.
          additionalProperties: true
          example:
            sku: DRESS-001
            price: 29
            inventory_count: 150
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the record was created.
          example: '2025-06-01T10:00:00+00:00'
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the record was last updated.
          example: '2025-06-01T12:00:00+00:00'
  responses:
    '401':
      description: Unauthorized
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: Unauthenticated.
          examples:
            Unauthenticated:
              value:
                message: Unauthenticated.
    '422':
      description: Unprocessable Entity
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: The given data was invalid.
              errors:
                type: object
                additionalProperties:
                  type: array
                  items:
                    type: string
                example:
                  field_name:
                    - The field_name field is required.
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: ''

````