# Error Handling & Best Practices

Robust error handling prevents data loss and keeps your integration reliable. This guide covers rate limiting, error response formats, and practical patterns for handling failures across User.com API endpoints.

# Rate limiting

User.com enforces request throttling to protect platform stability. Your integration must be designed to stay within the rate limits. Exceeding limits not only wastes your resources on rejected requests, but also degrades performance for your own application.

Design for the limit, don't rely on retries

Rate limit retries are a safety net, not a strategy. If your integration regularly hits 429 errors, it is misconfigured. Throttle your request rate on your side before sending — don't use the API as a traffic regulator.

# Staying within limits

  1. Know your limit — the default rate limit is 10 requests per second.
  2. Throttle on your side — implement a request queue with a controlled drain rate that stays within your limit
  3. Spread requests evenly — avoid bursts; a single webhook that triggers 50 API calls at once will spike past any limit
  4. Use CSV imports for non-real-time data — if you're syncing historical data or large batches where real-time processing isn't needed, use the bulk import endpoints (available for deals, users, and products) instead of individual API calls

# Handling 429 Too Many Requests

Even with proper throttling, occasional 429 responses can occur during traffic spikes. When the API returns a 429 status code, it includes a retry-after header indicating how long to wait:

{"detail": "Request was throttled. Expected available in 1 second."}

Header casing

The API returns the header as lowercase retry-after, not the standard capitalized Retry-After. If your HTTP client parses headers case-sensitively (e.g., raw header access in JavaScript), response.headers["Retry-After"] will be undefined — use response.headers["retry-after"] instead. Libraries like Python requests handle this case-insensitively.

Your integration should:

  1. Pause — read the retry-after header and wait the specified number of seconds before retrying
  2. Queue — buffer requests and drain the queue at a sustainable rate
  3. Log — track 429 occurrences to identify if your integration is consistently too aggressive
import time
import requests

def api_request_with_retry(method, url, headers, data=None, max_retries=5):
    for attempt in range(max_retries):
        response = requests.request(method, url, headers=headers, json=data)

        if response.status_code != 429:
            return response

        retry_after = int(response.headers.get("retry-after", 2 ** attempt))
        time.sleep(retry_after)

    return response

# Monitoring

If you're seeing 429 errors regularly, treat it as a bug in your integration — not normal behavior. Track 429 rates and adjust your throttling accordingly.

# Error response formats

The API uses three different error response formats depending on the endpoint. Your error-handling code needs to account for each.

# Format 1: Flat field errors

Used by: Deals (POST /deals/, POST /deals/update_or_create/)

{
  "user_id": ["Invalid pk \"999999\" - object does not exist."],
  "products": ["Invalid pk \"999999\" - object does not exist."]
}

Errors are returned at the root level, keyed by field name. Multiple field errors are returned in a single response.

# Format 2: Nested under errors key

Used by: Events (POST /events/)

{
  "errors": {
    "user_id": ["Client user with ID '999999' does not exist"],
    "name": ["This field is required."]
  }
}

Field errors are nested inside an "errors" object.

# Format 3: Detail string

Used by: User-scoped endpoints (POST /users-by-id/:user_custom_id/events/)

{
  "detail": "No ClientUser matches the given query."
}

A single "detail" string with a 404 status code when the user is not found.

# Unified error parser

Handle all three formats in a single function:

def parse_api_error(response):
    data = response.json()

    if "detail" in data:
        return {"_general": [data["detail"]]}

    if "errors" in data:
        return data["errors"]

    return data

# Validation order

The API validates fields in a specific order, and earlier validation failures can mask later ones.

# Events: user is validated first

Both event endpoints (/events/ and /users-by-id/:id/events/) validate the user before anything else. If the user doesn't exist, you won't see errors for missing name, invalid event_type, or non-existent product_id.

Implication: if you get a user-not-found error, fix that first — there may be additional errors that only surface once the user exists.

# Deals: all fields validated at once

The deals endpoint validates all fields in parallel and returns every error in a single response. This makes debugging easier — you can fix all issues at once.

# Common error scenarios

# Contact must exist before events

Events require an existing contact. If you send an event for a user that hasn't been created yet, the request fails immediately.

Always create the contact first:

  1. POST /users/update_or_create/ — ensure contact exists
  2. POST /users-by-id/:user_custom_id/events/ — then send the event

# Non-existent references in deals

Deals validate all referenced entities — users, products, pipelines, and stages. Common errors:

Reference Error message pattern
user_id / user_custom_id "object does not exist" / "does not exist"
products / products_custom_id "object does not exist"
order_productsproduct_id "Could not find product with id '...'"
order_productsproduct_custom_id "Could not find product with custom_id '...'"
pipeline / stage "object does not exist"
status (invalid value) "is not a valid choice"

If a product reference fails, create the product first via POST /products/ and then retry the deal request.

# Product not found on product events

The recommended approach is to have your product catalog synced before sending product events. However, if you still encounter a product-not-found error, create the product via POST /products/ and retry the event.

Product is validated first on the /products-by-id/:custom_id/product_event/ endpoint. If the product doesn't exist, you always get a 404 regardless of anything else in the payload:

{"detail": "Couldn't find product with custom_id = 9999999"}

Once the product exists, user validation kicks in:

Scenario Status Error
Non-existent user_id 400 "Client user with ID '999999' does not exist"
Non-existent user_custom_id 404 "user for given user_id not found"

# Deals accept empty payloads

The deals endpoint creates a deal even with an empty {} payload, using defaults (name: "", value: 0.00, status: 1). Always validate required fields on your side before sending.

# Best practices

# 1. Validate before sending

Don't rely on the API for input validation. Check that:

  • Required fields (name, custom_id, segment) are present and non-empty
  • Referenced entities (users, products) exist or will be created in the correct order
  • Monetary values are positive numbers with the correct currency code

# 2. Order your API calls correctly

Entities must exist before they can be referenced:

  1. Products — sync product catalog first
  2. Contacts — create or update users before attaching events or deals
  3. Deals — create orders after contacts and products are synced
  4. Events — send after the associated contact exists

# 3. Use update_or_create for idempotency

The update_or_create endpoints for contacts and deals are idempotent — safe to retry on network failures. Prefer these over plain POST when possible.

# 4. Handle errors by status code

Status Meaning Action
200 / 201 Success Continue
400 Validation error Parse error body, fix the request, do not retry without changes
401 Unauthorized Check API key
404 Not found Referenced entity doesn't exist — create it first
429 Rate limited Retry with backoff
500 Server error Retry with backoff, contact support if persistent

# 5. Log API interactions

Log request/response pairs for debugging. Include:

  • Endpoint URL and method
  • Request payload (sanitize sensitive data)
  • Response status code and body
  • Timestamp and correlation ID (e.g., your order ID)