Overview
Telltale is a retention brain. You push orders and customers; Telltale computes per-customer churn risk, lifecycle state (Stable, Slowing, Overdue, Lapsed), predicted next purchase and lifetime value, then builds campaign audiences (Sightings), sends them through your connected email or SMS provider, attributes the orders that follow, and measures lift against a ~5% universal holdout.
Base URL: https://api.telltale.pro. All request and response bodies are JSON. Timestamps are ISO‑8601; always include a UTC offset.
Using an AI coding tool? Add the Telltale MCP server and it can read these docs, validate and push your data, and run a sync for you.
What the minimum push unlocks
order_id, customer_id, order_date, order_amount, product_name plus email and/or phone on every order is enough for revenue, cohorts, customer profiles, churn scoring and attribution. A trained churn model (rather than heuristic scoring) needs roughly 100 customers with two or more orders, so push about 12 months of history.Three rules that keep billing honest
order_id; (2) the customer_id on orders must be the same identity Telltale messaged; (3) holdout customers must not be contacted through side channels.Getting credentials
One credential authorizes the ingest surface: an ingest key (tk_live_…), minted right here by an owner or admin. The key carries your account, so the account id header is optional. Keys are stored hashed, shown once, rotate with a grace window, and can be revoked instantly. Sandbox keys (tk_test_…) only open your sandbox workspace.
Your workspace
X-Telltale-Account-Id)…Ingest keys are generated, rotated and revoked in the console below. They carry the account, so this id is only needed with the legacy tile token.
Ingest keys · live
Owners and admins manage keys.Test a key
Paste a key to confirm it authenticates and which workspace it opens. The key is checked server-side and never stored.
Send a sample order
Dry run only on the live workspace: see exactly how a sample order is validated, nothing is written.
Sandbox workspace
A separate, zero-egress workspace for testing your integration end to end. Push data with tk_test_ keys, run sync, look at the dashboard, reset, repeat.
Ingest activity · live
No batches yet. Every /ingest call shows up here with counts and error codes (never payload data).
| Credential | Where | Notes |
|---|---|---|
| Ingest key | Signed in: the console above → Generate key (owner or admin). Public page: sign in first. | Sent as Authorization: Bearer tk_live_…. Hashed at rest, shown once, rotate/revoke here. Up to 10 live keys. |
| Account id | Shown above when signed in. | Optional with tk_ keys (must match if sent). Required with the legacy tile token. |
| Legacy tile token | Integrations → Custom API tile → Webhook panel. | Still accepted with X-Telltale-Account-Id. Prefer tk_ keys. |
Pixel key (pk_…) | Settings → On-site tracking. | Publishable browser key with an origin allow-list. Rotate from the same screen. |
Enterprise keys do not authorize ingest
sk_live_ keys minted there manage outbound webhook subscriptions only. Do not use them for the endpoints on this page.Auth, errors, limits
Authentication
| Surface | Credential | How to send it |
|---|---|---|
/ingest/* | Ingest key tk_live_… / tk_test_… | Authorization: Bearer <key>. The account comes from the key; X-Telltale-Account-Id is optional and must match if sent. Keys in the query string are rejected. Legacy tile tokens still work with the account id header (or ?account_id=&token=). |
POST /webhooks/custom-api | Same ingest key | Authorization: Bearer <key> (preferred). Legacy: ?account_id=<uuid>&token=<tile token>. |
POST /webhooks/zero-party | Zero-party tile token | Query string only. Separate key from the Zero-Party Data tile. |
POST /pixel/collect | Pixel key | In the JSON body as key, plus a browser Origin header on the allow-list. |
The account is derived from the authenticated key alone. Order and customer bodies never carry an account id, so one client can never write into another's data.
Auth failures with an ingest key, in the order they are checked:
| Status | Cause |
|---|---|
| 401 | Key sent in the query string, or key invalid, expired, revoked, or not for the account in X-Telltale-Account-Id |
| 400 | Legacy path only: account id missing or not a UUID |
| 404 | Legacy path only: no connected Custom API tile for the account |
| 429 | More than 30 failed authentications from one IP in a minute (returned instead of 401/404) |
Errors
Errors are JSON {"detail": "…"} unless noted. There is no path version today; unknown request fields on orders and customers are silently ignored (see FAQ).
| Status | When | Body |
|---|---|---|
| 400 | Empty batch; missing or invalid account id; webhook payload with no identity | JSON detail |
| 401 | Ingest key missing or mismatched; webhook token mismatch | JSON detail |
| 403 | Pixel: invalid or revoked key, origin not allowed, demo account | JSON detail |
| 404 | Custom API or Zero-Party tile not connected | JSON detail |
| 413 | Body over 32 MiB on orders or customers | Plain text payload too large, checked before the row count |
| 413 | More than 5,000 rows | JSON Batch too large (N > 5000). Page your upload. |
| 422 | Schema failure; bad subscription_status; non-IANA store_timezone; pixel unknown field or over 50 events | Validation body |
| 429 | Over 600 requests/min on one key (or one account for the legacy token); more than 30 failed auths/min from one IP; POST /ingest/sync more than once per 10 min; pixel per-IP/per-key limits | JSON detail Rate limit exceeded — try again in ~Ns. plus a Retry-After header |
| 500 | Orders: any failure rolls back the whole batch (nothing written). Customers: a mid-batch failure can leave earlier rows committed; re-POST the batch, it is idempotent. | JSON detail |
Limits
| Limit | Value | Applies to |
|---|---|---|
| Rows per batch | 5,000 | orders, customers |
| Body size | 32 MiB | orders, customers only |
| Rate limit | 600 requests/min per ingest key (per account for the legacy token); 30 failed auths/min per IP; POST /ingest/sync once per 10 min per account. Exceeding any returns 429 with Retry-After. | ingest, webhooks |
| Pixel | 600 req/min per IP, 6,000/min per key; 50 events per batch; 48 KB body; events older than 30 days or more than 1 h in the future are dropped | /pixel/collect |
| Timestamps | Naive order_date values are read as UTC and counted in naive_timestamps with a warning | orders |
| Currency | 3-letter ISO code or omit (USD). Longer strings fail the whole orders batch. | orders |
Integration walkthrough for a custom platform
Step 1 — Verify the key
Set TELLTALE_API_KEY in your environment (the account is derived from the key), then:
curl -s https://api.telltale.pro/ingest/health \
-H "Authorization: Bearer $TELLTALE_API_KEY"{"ok":true,"account_id":"…","connected_integrations":["custom-api"],"orders_ingested":0,"store_timezone":null,"hint":"…"}Then set your store timezone (IANA name only):
curl -s -X PATCH https://api.telltale.pro/ingest/account \
-H "Authorization: Bearer $TELLTALE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"store_timezone":"America/New_York"}'Step 2 — Historical backfill
- Dry-run a sample first.
POST /ingest/orders?dry_run=true(and/ingest/customers?dry_run=true) validates every row and writes nothing: per-row error codes and warnings come back so you fix the export before loading a year of history. See the Dry run reference below. - Customers first (consent and subscriptions):
POST /ingest/customersin pages of up to 5,000. Includeexternal_id(must equal thecustomer_idyou use on orders),email,phonein E.164,consent, andsubscription_statusif you run subscriptions. - Orders:
POST /ingest/ordersin pages of up to 5,000 rows and under 32 MiB, oldest first, at least 12 months. Overlapping pages are safe (idempotent onorder_id); if a run dies, restart from the last page you got a 200 for.GET /ingest/batchesis your audit trail of what each page did. - Size batches well under both caps. On a plain-text 413, split by bytes; on a JSON 413, split by rows. On a 500 from orders nothing was written, so fix and retry the same batch.
- Read the response counters. Treat
dropped_invalid > 0as a bug in your export: it means an unparseableorder_dateororder_amount, or a row whose customer id, email and phone are all blank. A missing required field is a 422 for the whole request instead (see FAQ).
Step 3 — Ongoing sync
- New or updated orders: POST a batch of one, or micro-batch every minute. Re-posting the same
order_idmerges lifecycle fields: refunds and cancels only ever move forward, andorder_dateandcustomer_idnever change. - Refunds: re-POST the whole order row (all required fields, unchanged
order_amount) with the new cumulativerefunded_amount.order_amountstays the gross total; Telltale nets refunds itself. This drives net revenue and any fee credits. - Cancellations: re-POST with
cancelled_at. - Consent changes (STOP, unsubscribe): re-POST the customer with
consent.sms=falseorconsent.suppressed=true, or send one event toPOST /webhooks/custom-api. Telltale otherwise learns suppressions only from connected email and SMS providers. - The nightly recompute at 02:00 UTC picks everything up. Call
POST /ingest/synconly after large loads.
Step 4 — Install the on-site pixel (optional)
- Settings → On-site tracking: add your storefront origins (
scheme://host, no path, up to 50) and copy the snippet. - Consent: add
data-telltale-consent="required"and callwindow.telltale('consent','granted')from your consent manager. Global Privacy Control is honoured. Events without granted consent are dropped silently. - Single-page apps: call
window.telltale('track', 'page_view' | 'add_to_cart' | 'checkout_started')on route changes. Setproduct_external_idto the same product id you send on orders.
<script async src="https://api.telltale.pro/pixel/v1/telltale-pixel.js"
data-telltale-key="pk_…" data-telltale-endpoint="https://api.telltale.pro/pixel/collect"></script>Known limitation for Custom API accounts
identify event yet; sessions are stitched when a landing URL carries a Telltale utm_content.Step 5 — Marketing consent
Consent is boolean today: consent.marketing_email, consent.sms, consent.suppressed, consent.bounced. SMS sends are gated on sms alone, so only set it true where you hold prior express written consent, and keep your own proof record. Consent provenance fields (when, how, source) are planned.
Step 6 — Coupons and attribution
Coupons today. Telltale never creates discount codes on any platform. Shopify, WooCommerce and BigCommerce libraries sync automatically; for a custom platform, enter codes by hand in Sightings → Templates & offers. Send discount_codes, discount_total and discount_pct on orders for discount-sensitivity analytics; codes are not used for attribution.
Attribution today. Every Telltale call-to-action links to your storefront with utm_source=telltale, utm_medium=<channel>, utm_campaign=<type> and utm_content=<tracking_id>. Orders are attributed either URL-precisely (when the order's source carries that utm_content) or by 7-day last-touch on the customer.
Keep source = acquisition channel for now
source is classified as the campaign channel and overrides the order's true acquisition channel. Until a separate landing_url field ships, send the acquisition channel in source and rely on last-touch attribution.Also make sure your storefront domain is set on the account (the team can set it for you today; a self-serve field is planned). Telltale builds every call-to-action on top of it.
Step 7 — Trigger sync and verify
curl -s -X POST https://api.telltale.pro/ingest/sync \
-H "Authorization: Bearer $TELLTALE_API_KEY"{"queued":true,"task_id":"…","status_url":"/ingest/sync/…","message":"Analytics recompute queued. Poll status_url; fresh numbers appear once it completes."}Poll the status_url (GET /ingest/sync/{task_id} with the same key) until status is done, failed or skipped; a few minutes is typical. Then check the app: the Nest Report shows metrics, Audience Builder lists scored customers, and Sightings shows opportunities. GET /ingest/health confirms the row count and GET /ingest/batches shows what happened to each page.
curl -s https://api.telltale.pro/ingest/sync/<task_id> \
-H "Authorization: Bearer $TELLTALE_API_KEY"Endpoint reference
All ingest routes take X-Telltale-Account-Id, Authorization: Bearer and Content-Type: application/json.
Health
Verifies the key and reports current state. connected_integrations lists every connected provider, orders_ingested is the total order count for the account.
{"ok": true, "account_id": "<uuid>", "connected_integrations": ["custom-api", "klaviyo"],
"orders_ingested": 12034, "store_timezone": "America/New_York",
"hint": "POST /ingest/orders and /ingest/customers to load data, then POST /ingest/sync to recompute analytics."}Orders
Batch upsert of 1 to 5,000 orders as {"orders": [ … ]}. The customer identity on each order is upserted too, so buyers do not need a separate customers call. Idempotent on order_id. The batch is one transaction.
| Field | Type | Required | Notes |
|---|---|---|---|
order_id | string | yes | Your order id. Idempotency key. |
customer_id | string | yes | Your stable customer id. Must match customers.external_id. |
order_date | string | yes | ISO‑8601 with offset. Unparseable → row dropped; naive → read as UTC with a warning. |
order_amount | number | yes | Gross order total in major units (send the pre-refund total; refunds go in refunded_amount). Must be a JSON number; a non-numeric value is a 422. |
product_name | string | yes | Primary product on the order (single line today; line items are planned). |
email | string | no | Buyer email. |
phone | string | no | Normalised to E.164 (default region US). |
customer_name | string | no | Buyer name. |
source | string | no | Acquisition channel (e.g. email, sms, paid_social, organic). See Step 6. |
order_channel | string | no | e.g. email / web / pos. Used only to flag email-attributed orders. |
currency | string | no | 3-letter ISO code. Defaults to USD. |
shipping_city / shipping_state / shipping_zip | string | no | Geo enrichment. |
refunded_amount | number | no | Cumulative refunded amount. Merges forward only. |
discount_total / discount_pct | number | no | Discount applied. |
discount_codes | string[] | no | Codes used. |
cancelled_at | string | no | ISO‑8601 when cancelled. |
Unknown fields are ignored, not rejected
line_items, fulfilled_at, delivered_at, shipping_country, financial_status and landing_url are accepted silently and discarded today. They are on the roadmap; do not rely on them yet.Behaviour: an in-batch duplicate order_id is last-write-wins; rows with no customer id, email or phone are dropped and counted in dropped_invalid.
{"received": 2, "distinct_order_ids": 2, "deduped_in_batch": 0, "accepted": 2, "dropped_invalid": 0,
"orders_upserted": 2, "customers_upserted": 1, "naive_timestamps": 0,
"next": "POST /ingest/sync to recompute analytics for this account.",
"warning": "<only when naive_timestamps > 0>"}curl -s -X POST https://api.telltale.pro/ingest/orders \
-H "Authorization: Bearer $TELLTALE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"orders":[{"order_id":"A1001","customer_id":"C42","order_date":"2026-09-20T14:03:00-04:00","order_amount":86.4,"currency":"USD","product_name":"Oat Milk 12pk","email":"jo@example.com","phone":"+12125551212","source":"email","shipping_city":"Brooklyn","shipping_state":"NY","shipping_zip":"11201","discount_codes":["WELCOME10"],"discount_total":8,"discount_pct":8.5,"refunded_amount":0}]}'Customers
Batch upsert of 1 to 5,000 customers as {"customers": [ … ]}. Identity resolves on external_id, then email, then phone, then creates a new customer. Use it for consent, addresses and subscription state; buyers are already created by orders.
| Field | Type | Required | Notes |
|---|---|---|---|
external_id | string | no | Same value as orders.customer_id. |
email / phone / name | string | no | At least one of external_id, email or phone is required, otherwise the row is counted in skipped_no_identity. |
address | object | no | {street, city, state, zip, country}. Written when street, city or zip is present; country defaults to US. |
consent | object | no | {marketing_email, sms, suppressed, bounced} booleans. Omitted keys keep their current value. |
source_label | string | no | Provenance label. Defaults to custom-api. |
subscription_status | enum | no | active | paused | cancelled. Anything else → 422. |
subscription_started_at | string | no | ISO‑8601. Write-once, earliest wins. |
{"received": 2, "customers_upserted": 2, "skipped_no_identity": 0, "subscriptions_applied": 1}curl -s -X POST https://api.telltale.pro/ingest/customers \
-H "Authorization: Bearer $TELLTALE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customers":[{"external_id":"C42","email":"jo@example.com","phone":"+12125551212","name":"Jo Park","address":{"street":"1 Main St","city":"Brooklyn","state":"NY","zip":"11201","country":"US"},"consent":{"marketing_email":true,"sms":false},"subscription_status":"active","subscription_started_at":"2026-02-01T00:00:00Z"}]}'Account settings
Body {"store_timezone": "America/New_York"}. IANA names only; abbreviations like CST return 422. The value is stored and echoed on /ingest/health. Note: the recompute pipeline does not consume it yet, so local-time analytics still assume UTC until that lands.
Sync
No body. Queues a full analytics recompute for the account. Throttled to one call per 10 minutes per account (429 with Retry-After otherwise); the nightly recompute runs regardless.
{"queued": true, "task_id": "…", "status_url": "/ingest/sync/<task_id>", "message": "Analytics recompute queued. Poll status_url; fresh numbers appear once it completes."}curl -s -X POST https://api.telltale.pro/ingest/sync \
-H "Authorization: Bearer $TELLTALE_API_KEY"Sync status
Status of a recompute this account queued. Task ids from other accounts (or unknown ids) return 404.
{"task_id": "…", "queued_at": "2026-09-22T20:00:00+00:00", "status": "queued | running | done | failed | skipped", "orders_added": 1200, "customers_added": 300, "duration_ms": 42000}skipped carries a reason (for example already_running). Counters appear only when done.
Dry run
Same body and caps as the real call; nothing is written. Each row is checked exactly the way the write path would treat it. The call is logged in GET /ingest/batches with dry_run: true.
{"dry_run": true, "received": 2, "would_accept": 1, "would_drop": 1,
"errors": [{"index": 1, "order_id": "A2", "codes": ["no_usable_identity"]}],
"warnings": [{"index": 0, "order_id": "A1", "code": "unknown_fields_ignored", "fields": ["line_items"]}],
"errors_truncated": 0, "next": "Fix any errors, then POST again without ?dry_run=true."}| Code | Kind | Meaning |
|---|---|---|
no_usable_identity | error (row dropped) | Customer id, email and phone are all blank. |
unparseable_order_date_or_amount | error (row dropped) | The date or amount cannot be parsed. |
invalid_currency_code_fails_whole_batch | error (whole batch fails) | Currency is not a 3-letter code; on a real call this aborts the entire batch. |
unknown_fields_ignored | warning | Fields the API does not store (listed in fields). |
duplicate_in_batch_last_wins | warning | The same order_id appears twice; the later row wins. |
naive_timestamp_read_as_utc | warning | order_date has no UTC offset. |
Up to 50 errors and 50 warnings are returned; errors_truncated counts the rest.
curl -s -X POST "https://api.telltale.pro/ingest/orders?dry_run=true" \
-H "Authorization: Bearer $TELLTALE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"orders":[{"order_id":"A1001","customer_id":"C42","order_date":"2026-09-20T14:03:00-04:00","order_amount":86.4,"currency":"USD","product_name":"Oat Milk 12pk","email":"jo@example.com","phone":"+12125551212","source":"email","shipping_city":"Brooklyn","shipping_state":"NY","shipping_zip":"11201","discount_codes":["WELCOME10"],"discount_total":8,"discount_pct":8.5,"refunded_amount":0}]}'Ingest activity
The last 1 to 200 batches for the account: endpoint, dry-run flag, status (accepted, rejected, error, queued), received/accepted/dropped counts, error and warning codes, duration, the sync task_id, and the key id used. Rows carry your own order and customer ids in error entries but never names, emails or phones. If you send an X-Request-Id header it is stored with the row so support can find it. Retained 90 days.
curl -s "https://api.telltale.pro/ingest/batches?limit=20" \
-H "Authorization: Bearer $TELLTALE_API_KEY"Customer event webhook
Single customer identity or consent event. Body is a free-form object with external_id (alias customer_id), email, phone, name, address, consent, event_type (default custom) and source_label. At least one of email, phone or external id is required.
Deduplicated per account on event_type + external_id (or a hash of the body). A duplicate returns {"received": true, "duplicate": true}; a failed attempt is re-processed on retry. This call also flips the Integrations tile from "Awaiting setup" to connected.
{"received": true, "customer_id": "<uuid>", "link_method": "provider_id|email|phone|fuzzy|created", "is_new": true}curl -s -X POST https://api.telltale.pro/webhooks/custom-api \
-H "Authorization: Bearer $TELLTALE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"external_id":"C42","phone":"+12125551212","event_type":"sms_stop","consent":{"sms":false,"suppressed":true}}'Zero-party data webhook
Quiz and preference submissions. Requires the separate token from the Zero-Party Data tile. Body: email, external_customer_id (aliases external_id, customer_id), interests[], attributes{}, source_label, quiz_version, completed_at. At least one of email or external id is required. Deduplicated on content, so a changed submission is a new event.
On-site events
Browser-only. Requires an Origin header on the pixel key's allow-list. Up to 50 events per call, 48 KB body. Unknown fields are rejected. Returns 204 on accept and also on a quiet drop (no consent, out of window).
{"key":"pk_…","events":[{"event_id":"<uuid>","event_type":"page_view|product_view|search|add_to_cart|checkout_started",
"occurred_at":"2026-09-22T10:00:00Z","anonymous_id":"<1..128>","session_id":"<≤128>","url_path":"/p/oat-milk",
"page_title":"…","referrer_host":"google.com","product_external_id":"p_88","collection":"…","search":"…",
"price_cents":4000,"dwell_ms":1200,"consent_state":"granted","raw_props":{"utm_content":"<tracking_id>","sku":"OM-12"}}]}The universal loader used by the snippet in Step 4. Cached for one hour.
Outbound webhooks (Telltale → you)
Available to workspaces with the API & webhooks settings page. Subscriptions are created in the app. Each delivery is a JSON POST {"event": "<type>", "data": { … }} with headers X-Telltale-Event, X-Telltale-Delivery (deduplicate on it) and X-Telltale-Signature, a hex HMAC‑SHA256 of the raw body with your whsec_… secret. Failed deliveries are retried up to three times with growing gaps (15, then 30, then 45 minutes).
Events today
campaign.sent is emitted, with {campaign_type, provider, tracking_id}. Customer-level events (customer.churned, recovery.attributed, lift.proven, sync.completed) are on the roadmap and will carry your customer_external_id.Roadmap
Planned additions, in priority order. Nothing here is callable yet. If one of these blocks your integration, tell us in the chat and we will prioritise it.
| Priority | Planned | Why |
|---|---|---|
| P1 | A versioned /v1/… path prefix for the ingest surface (today: /ingest/*) | Stable contract for generated SDKs. Keys, scopes, rotation, dry run, sync status and rate limits already ship. |
| P0 | Richer orders: line_items[], subtotal, financial_status, fulfilled_at, delivered_at, tracking, shipping.country, landing_url | Unlocks product-level insights, shipping signals and URL-precise attribution without overwriting the acquisition channel. |
| P0 | Customers: consent provenance (when, how, source), tags, richer subscription state | Compliance proof and payment-recovery plays. |
| P0 | Account: store_domain, primary_currency, and timezone-aware recompute | Every call-to-action needs a storefront to point at. |
| P0 | Send claim + confirm (POST /v1/sends) and audience exports for merchants sending from their own stack | Without it, self-sent campaigns cannot be attributed or measured against the holdout. |
| P0 | Pixel stitching and rollups for Custom API accounts | Makes on-site events useful for the accounts this API serves. |
| P1 | Coupon library (POST /v1/coupons), generic events (POST /v1/events), product catalog, pixel identify, key-authed reads (customers, campaigns, reports, metrics, proof), webhook management, more outbound events (customer.churned, recovery.attributed, lift.proven, sync.completed) | Parity with the native connectors. |
| P2 | Streaming backfill jobs, storefront offer lookup, audience push, GDPR redact | Scale and compliance conveniences. |
FAQ & gotchas
413 comes before the row count. The 32 MiB cap is checked while the body streams in and returns plain text. A JSON "Batch too large" is the 5,000-row cap. Only orders and customers are byte-capped.
Idempotency. Orders upsert on order_id; refunds and cancels only move forward; order_date and customer_id never change on re-POST. Customers resolve on external id, then email, then phone. The customer webhook deduplicates per event; the zero-party webhook deduplicates on content.
Timezones. Always send an offset. Naive values are read as UTC and reported in naive_timestamps.
Currency. Omit or send a 3-letter ISO code. Unknown codes convert at 1.0.
Unknown fields are ignored on orders and customers. Check the schema tables above rather than assuming a field is stored.
The Integrations tile says "Awaiting setup". The chip flips on real evidence. Ingest calls alone do not flip it; one call to POST /webhooks/custom-api does.
Attribution of campaigns you send yourself. Telltale attributes orders to Telltale-dispatched sends, or to orders whose source carries a Telltale utm_content. Campaigns sent from your own stack are not attributed or billable until the send claim endpoint ships.
Sync status. POST /ingest/sync returns a status_url; poll it until done. One trigger per 10 minutes per account; the nightly recompute runs at 02:00 UTC regardless.
422 on a whole request. A missing or mistyped required field (for example no product_name, or a string order_amount) fails the entire request with a validation body even in dry run; each entry's loc is ["body", "orders", <row index>, "<field>"]. Row-level content problems (identity, dates, currency) are reported per row by ?dry_run=true instead.
Ten live keys. The cap counts keys that still authenticate; revoked keys and keys past their rotation grace do not count. Revoke old ones from the console.
AI tools. Claude Code, Claude.ai, Cursor and any MCP client can drive this API through the hosted MCP server: MCP & AI tools. Machine-readable docs: https://api.telltale.pro/llms.txt, /llms-full.txt, /v1/openapi.json.
Questions? Use the chat bubble, OWL, or email owl@telltale.pro. Developers on your team can be invited under Team.