Skip to main content

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, contacts, 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 contact 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 contact before anything else. If the contact doesn't exist, you won't see errors for missing name, invalid event_type, or non-existent product_id.

Implication: if you get a contact-not-found error, fix that first — there may be additional errors that only surface once the contact 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 contact 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 — contacts, products, pipelines, and stages. Common errors:

ReferenceError message pattern
user_id / user_custom_id"object does not exist" / "does not exist"
products / products_custom_id"object does not exist"
order_products → product_id"Could not find product with id '...'"
order_products → product_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, contact validation kicks in:

ScenarioStatusError
Non-existent user_id400"Client user with ID '999999' does not exist"
Non-existent user_custom_id404"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 (contacts, 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 contacts 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​

StatusMeaningAction
200 / 201SuccessContinue
400Validation errorParse error body, fix the request, do not retry without changes
401UnauthorizedCheck API key
404Not foundReferenced entity doesn't exist — create it first
429Rate limitedRetry with backoff
500Server errorRetry 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)