Watch

Watch an account for audience change

POST/data-api/v2/watch
Scope
directory:read
Freshness
catalog read
Group
Watch
Platforms
6 of 7

What this endpoint answers

Registers an account to be watched and says what counts as news. The handle is resolved to the catalog's own identifier at creation, so a later rename does not break the watch. Posting the same account to the same endpoint again updates its thresholds rather than creating a second watch.

This is a catalog read: it is served from the CRM Solid database in milliseconds, costs one budget unit, and reports how old the reading is in meta.cache_age_s. It needs the directory:read scope (Directory): Read the account and channel catalog across all seven platforms.

Good to know

  • The two thresholds combine with OR. Set both to catch either a big absolute move or a big relative one; set neither and any measured change fires.
  • Only accounts already in the catalog can be watched. An account nobody has ever asked us about has no daily series for a watch to read.
  • awaiting_first_measurement is true when the account is catalogued but has no daily rows yet. The watch is real and will start firing once the engine measures it.
  • A day whose change the engine could not measure never fires, on any threshold. A missing measurement is not a change of zero, and reporting it as one would be inventing a fact.
  • handle is a snapshot taken at creation and is not kept in step with renames. entity_id is the stable identifier and is what the event payload keys on.
  • Detection runs against the engine's end-of-day rollup, so events are daily, not intraday.

Parameters

This endpoint takes no path or query parameters.

Request body

  • endpoint_idstringrequired

    Which registered destination receives this account's events.

  • platformstringrequired

    Which catalog the account belongs to.

  • handlestringoptional

    The account's handle. Either this or entity_id is required; a handle is resolved to an entity_id and both are stored.

  • entity_idstringoptional

    The catalog identifier, if you already hold it. Skips handle resolution.

  • min_abs_changeintegeroptional

    Fire when the day's audience change is at least this many, in absolute terms.

  • min_pct_changenumberoptional

    Fire when the day's percentage change is at least this, as a percentage.

  • directionstringoptional

    Restrict to gains ('up') or losses ('down').

Example request body
{
  "endpoint_id": "string",
  "platform": "x",
  "handle": "elonmusk",
  "entity_id": "string",
  "min_abs_change": 50000,
  "min_pct_change": 1.5,
  "direction": "any"
}

Call it

Authenticate with a bearer token or the x-api-key header. Keys are server-to-server credentials. Never embed one in front-end code - call the API from your own backend and forward the result.

curl
curl -X POST "https://crmsolid.com/data-api/v2/watch" \
  -H "Authorization: Bearer psk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"endpoint_id":"string","platform":"x","handle":"elonmusk","entity_id":"string","min_abs_change":50000,"min_pct_change":1.5,"direction":"any"}'
JavaScript
const res = await fetch("https://crmsolid.com/data-api/v2/watch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CRM_SOLID_DATA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "endpoint_id": "string",
    "platform": "x",
    "handle": "elonmusk",
    "entity_id": "string",
    "min_abs_change": 50000,
    "min_pct_change": 1.5,
    "direction": "any"
  }),
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}

const { data, meta } = await res.json();
Python
import json
import os

import requests

body = json.loads("""
{
  "endpoint_id": "string",
  "platform": "x",
  "handle": "elonmusk",
  "entity_id": "string",
  "min_abs_change": 50000,
  "min_pct_change": 1.5,
  "direction": "any"
}
""")

res = requests.post(
    "https://crmsolid.com/data-api/v2/watch",
    headers={"Authorization": f"Bearer {os.environ['CRM_SOLID_DATA_API_KEY']}"},
    json=body,
    timeout=30,
)
res.raise_for_status()

payload = res.json()
data, meta = payload["data"], payload["meta"]

Keys look like psk_live_... for production keys, psk_test_... for test keys and are minted in the panel.

What comes back

Success is { data, meta }. Failure is { error: { code, message, request_id } }. The body below is the spec's own example: the values in it are illustrative readings, not live numbers.

Watch id
3c9e6f21-88a4-4b02-9f7d-11ab4c2e5d90
Watch endpoint id
9f1c2b7e-4d3a-4c88-9a10-2f6b5e0d7c31
Watch platform
x
Watch entity id
44196397
Watch handle
elonmusk
Watch display name
Elon Musk
Watch trigger min abs change
50,000
Watch trigger direction
any
Watch status
active
Watch cursor day
2026-08-30
Watch created at
8/31/2026, 9:20:00 AM
Awaiting first measurement
no
200 POST /watch
{
  "data": {
    "watch": {
      "id": "3c9e6f21-88a4-4b02-9f7d-11ab4c2e5d90",
      "endpoint_id": "9f1c2b7e-4d3a-4c88-9a10-2f6b5e0d7c31",
      "platform": "x",
      "entity_id": "44196397",
      "handle": "elonmusk",
      "display_name": "Elon Musk",
      "trigger": {
        "min_abs_change": 50000,
        "min_pct_change": null,
        "direction": "any"
      },
      "status": "active",
      "cursor_day": "2026-08-30",
      "last_event_at": null,
      "created_at": "2026-08-31T09:20:00.000Z"
    },
    "awaiting_first_measurement": false
  },
  "meta": {
    "request_id": "req_9f2c41a8b3d5",
    "generated_at": "2026-08-23T09:14:02.317Z",
    "took_ms": 42
  }
}

The meta block

  • request_idstringrequired

    Unique id for this request. Quote it in a support ticket.

  • generated_atstringrequired

    Server time the response was produced.

  • took_msintegerrequired

    Milliseconds spent server-side.

  • pagePageoptional
  • sourcestringoptional

    Which backend served the payload, for endpoints with more than one.

  • cache_age_sintegeroptional

    Age of the underlying data in seconds. 0 for live reads.

When it fails

POST /watch documents 7 failure statuses. Branch on error.code, which is stable and enumerated; message is prose and may change.

  • 401Unauthorizedunauthorized | invalid_key

    No key was presented, or the key is unknown, revoked or expired.

  • 402PaymentRequiredpayment_required | subscription_inactive

    The key is valid but the plan behind it cannot serve the call: the included requests are spent and overage is switched off, capped or unfunded (payment_required), or the billing period lapsed and was not renewed (subscription_inactive). Retrying does not help; paying does. The X-Plan-* headers on this response say how far past the line you are.

    Carries X-Plan, X-Plan-Limit, X-Plan-Overage, X-Plan-Period-End, X-Plan-Remaining.

  • 403Forbiddenforbidden_scope | forbidden_ip

    The key is valid but not allowed to make this call: it lacks the scope, or the request came from an address outside the key's allowlist.

  • 422InvalidRequestinvalid_request

    A parameter is malformed, out of range or mutually exclusive with another. `details` names the offending fields.

  • 429RateLimitedrate_limited | quota_exceeded

    Either the burst ceiling for the current minute or the daily quota is spent. Distinguish with the code: rate_limited clears within the minute, quota_exceeded does not clear until 00:00 UTC.

    Carries Retry-After, X-Quota-Limit, X-Quota-Remaining, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-Request-Id.

  • 500InternalErrorinternal_error

    Something failed on our side. Internals are never leaked; quote the request id.

  • 503Unavailableupstream_timeout | upstream_error | not_configured

    The request could not be served right now. BRANCH ON error.code, not on the status: 'upstream_timeout' means a source was too slow (this is what a catalog query hitting its 15-second statement timeout returns, so it is reachable from any endpoint that reads the corpus, not only the live-scrape ones) and the same call is worth retrying with backoff - narrowing it with a smaller limit, a filtered scope or a less popular account makes it far less likely; 'upstream_error' means a source was unreachable, so back off further; 'not_configured' means the capability has no backing service in this deployment, and retrying will never help.

Every response carries X-Request-Id and meta.request_id. Quote it in support requests.

Coverage and limits

This endpoint covers X, Instagram, TikTok, YouTube, Telegram, Bluesky. Rate limits come from the tier on your key.

XInstagramTikTokYouTubeTelegramBluesky
TierRequests a minuteRequests a dayLive reads a minute
free301,0005
standard12025,00020
pro600250,00060
unlimited6,00010,000,000600

This call only draws on the ordinary per-minute and per-day columns. Every response reports where you stand in X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and the X-Plan headers.

Start calling it

A key takes a minute to mint in the panel, no card. The reference covers authentication, the envelope, scopes, rate limits and every error code in one page.

Reference path: /data-api/reference/watch-create

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.