touchmαrkdocstouchmark.ai

Errors & recovery

Every failure the SDK surfaces is a TouchmarkError with a .code from a closed set of seven. Branch on .code, never on the message.

import { TouchmarkError } from "@touchmark/sdk";

try {
  await tm.session.emit(/* … */);
} catch (err) {
  if (err instanceof TouchmarkError && err.code === "session_closed") {
    /* recover — see below */
  }
}

The seven codes at a glance

CodeWhat happenedWhat you do
session_closedThe session lapsed (≈1 week with no emits) and the server closed it.Re-open with session.start, then re-emit with the same event_id. Wrap this once.
session_not_foundThe session_id isn't one the server knows.Re-open the scope with session.start and use the fresh id.
authThe api_key is missing, wrong, or lacks permission.Fix the credential/config. Not retryable — fail loud.
invalid_payloadThe payload, event_idx, or base_price_usd failed validation.Fix the event data. It's a caller bug; it never reached the wire (for client-side checks).
timeoutA network attempt exceeded its deadline.Transient — retry later (e.g. via your outbox).
unreachableTouchmark couldn't be reached at all.Handled for you by default (degraded mode). See below.
rate_limitedYou exceeded your quota.The SDK already retried with backoff; if you still see it, back off and retry later.

The rest of this page covers the ones with real recovery logic. The others (auth, invalid_payload, session_not_found, timeout) are summarized above and revisited briefly at the end.

session_closed

This is the one error every integration must handle, and the only recovery the SDK deliberately leaves to you. A session is server-closed after about a week with no emits (consuming the valuation stream doesn't reset the clock); the next emit (or end) on it throws session_closed.

The recovery is always the same: re-open the scope, swap in the new session_id, and re-emit with the same event_id (the server de-duplicates, so re-sending is safe even if the first attempt secretly landed).

try {
  await tm.session.emit({ session_id, event_id, event_idx, event_type, payload, base_price_usd });
} catch (err) {
  if (err instanceof TouchmarkError && err.code === "session_closed") {
    // start is idempotent on scope_id, so this returns a fresh, open session. Keep its
    // session_id as your current one, then re-emit with the SAME event_id.
    const reopened = await tm.session.start({ scope_id, user_id });
    // New session → the per-session counter restarts, so re-emit this event as event_idx 0.
    await tm.session.emit({ session_id: reopened.session_id, event_id, event_idx: 0, event_type, payload, base_price_usd });
  }
}

The throw is deliberate and impossible to ignore — a stale id can't be used silently — but you own the retry decision, and you write the wrapper once. The copy-pasteable version, with an outbox for anything that still fails, is the tested reference example.

unreachable & degraded mode

When Touchmark can't be reached, the SDK is fail-safe by default so an outage on our side never takes down your hot path. emit and end swallow the outage, return normally, and fire your onDegrade hook.

A degraded emit is not delivered — the event never reached Touchmark, and the SDK does not buffer or replay it. So there is no later valuation for it: the base_price_usd you supplied is the price that stands, unless you re-emit the event (with the same event_id, so the server de-duplicates if it secretly landed). The natural place to do that is straight from onDegrade — enqueue the event into your own outbox and retry it on reconnect. (onDegrade carries the identifiers, not the payload, so the replay reads from your event store.) Emits that did land are unaffected — their valuations arrive on the stream as usual.

MethodDefault (strict: false)strict: true
startthrows unreachable (no safe id to invent)throws
emitdegrades — returns, fires onDegradethrows
enddegrades — returns, fires onDegradethrows

Two things to know:

  • Every non-unreachable error always throws, even in non-strict mode. Degraded mode only ever swallows a genuine outage — never a caller bug or a server-side error.
  • streamValuations is separate. It isn't part of degraded mode: it reconnects from its held cursor on a transient drop and surfaces only fatal errors.

Wire onDegrade so a degrade is never silent:

const tm = new Touchmark({
  api_key: process.env.TOUCHMARK_API_KEY!,
  onDegrade: ({ method, session_id, event_id }) => {
    // metrics and logger are your own telemetry — wire these to whatever you already use.
    metrics.increment("touchmark.degraded", { method });
    logger.warn("Touchmark unreachable; base price stands", { method, session_id, event_id });
  },
});

Use strict: true only if you'd rather fail loud than let the base price stand — it opts out of the safety net everywhere.

rate_limited

The SDK retries rate_limited for you, internally, with full-jitter exponential backoff, all inside the call's timeout_ms budget. So in normal operation you won't see this code at all — a brief quota spike is absorbed transparently.

If a rate_limited error does reach you, it means the retries were exhausted within the budget. Treat it as transient: back off and retry later (your outbox is the natural place), and if it's persistent, you're over quota — talk to us about limits.

The rest, briefly

auth

A missing/invalid api_key or insufficient permission. Configuration, not a transient fault: it always throws (never degrades), and retrying won't help — fix the key. Startup smoke-tests are the cheapest place to catch it.

invalid_payload

Your payload isn't JSON-serializable, or event_idx/base_price_usd is out of range, or the payload doesn't match the schema registered for your application's event_type. Client-side checks throw before anything is sent, with a path to the offending value (e.g. payload.items[3].when: Date is not JSON-serializable). A caller bug — fix the data; don't retry.

If the data is JSON-clean but the server returns invalid_payload, the payload didn't match the schema we registered for that event_type on your application — a missing required field, a wrong type, or an unknown event_type. The error message names the offending field. Compare against the shape we set up at onboarding (see Payload → what schema?); if it still looks right, send your Touchmark contact the event_type and payload and we'll reconcile the registered schema. No SDK change is ever needed — schemas live server-side.

session_not_found

The session_id isn't known to the server (usually a stale or hand-built id). Re-open the scope with session.start and use the id it returns. If you only ever use ids from start, you'll see session_closed far more often than this.

timeout

A transport/per-attempt deadline elapsed. This is a network deadline, not an eval-completion SLA (scoring is asynchronous and unbounded — valuations are never awaited). Transient: retry later.


See also: the API reference for full signatures, and the reference example for session_closed recovery + an outbox in one tested file.