NeccessoryNeccessory
REST API

Usage log

Read your key's own call log over the API — every signed call with its method, latency, request id and a machine-readable errorCode.

Every signed call your key makes leaves a row in a log that belongs to that key, and GET /sdk/v1/logs hands it back. It is how you answer "did that request reach us, and what did we think of it" without opening the console — and it is the runtime that neccessory logs tail sits on.

GET /sdk/v1/logs

HMAC-signed like everything under /sdk/v1 — see Authentication. The signature covers the path without the query string and an empty body.

ParameterMeaning
limit1…200, default 50.
sinceTimeMillisecond timestamp. Returns entries at or after it.
cursornextCursor from the previous page. Returns entries strictly after it.

cursor and sinceTime are alternatives — when both are sent, cursor wins. A cursor that is not <time>.<id> is a 400 validation_error.

{
  "ok": true,
  "data": {
    "items": [
      {
        "id": "log_4f7c1b90a2d8e35617c4a0f2",
        "time": 1756400000123,
        "endpoint": "/sdk/v1/measurements",
        "method": "POST",
        "status": "success",
        "latencyMs": 12,
        "requestId": "req_9a1c4e77b0d2358fc6e4",
        "error": null,
        "errorCode": null,
        "qualityLevel": "good"
      }
    ],
    "nextCursor": "1756400000123.log_4f7c1b90a2d8e35617c4a0f2",
    "serverTime": 1756400000456
  }
}

Ordering is oldest first, keyset over (time, id) — the opposite of the console, and the right order for a tail. Paginate by feeding nextCursor back in; on an empty page it repeats the cursor you sent, so a poll loop needs no special case.

The row

FieldWhat it is
idLog row id, log_ + 24 hex characters. Only meaningful as the tiebreaker inside a cursor.
timeEpoch milliseconds, taken when the request arrived.
endpointPath without the query string, e.g. /sdk/v1/cloud/jobs.
methodHTTP method of the call.
statussuccess, unauthorized or rate_limited — those three and nothing else.
latencyMsServer-side handling time, or null.
requestIdThe request id of that call — see below.
errorFree-form English explanation on a failure, null on success. Written for a human; do not branch on it.
errorCodeStable machine code on a failure the server recognises, otherwise null.
qualityLevelgood / fair / poor when the call submitted a measurement, null otherwise.

errorCode

error is prose and may be reworded; errorCode is the value to branch on. It is not the same thing as the error.code of the failed response: the response that rejected a bad signature says error.code: "unauthorized", while the log row for it says errorCode: "invalid_signature" — the envelope tells the caller that it was refused, the log tells you why.

errorCodeWhat happened
missing_credentialsOne of x-key-id, x-timestamp, x-nonce, x-signature was absent.
invalid_nonceNonce outside 8…128 characters.
invalid_timestampx-timestamp is not a number.
timestamp_out_of_windowClock skew beyond ±300 s.
unknown_keyNo key with that x-key-id.
key_disabledThe key exists but is not active.
key_expiredThe key is past its expiry.
invalid_signatureThe signature did not match the canonical string.
nonce_reusedThat nonce was already spent within the window.
rate_limitedA per-key limit, or the polling limit on this endpoint.

Those ten are the whole vocabulary. Nine of them are credential problems and map one-to-one to the causes listed under Authentication — a tail of this log is the fastest way to tell clock skew apart from a wrong secret.

The log is not a record of every refusal

Only calls the signature layer ruled on are written: successes, rejected signatures, and rate limits. A request that authenticated and then failed validation — a malformed body, an unknown measurement id, an exhausted quota — returns its error to you and leaves no row here. Do not treat the log as a complete error feed; it is a record of access, not of outcomes.

Joining a call to what it produced

Every response from the API carries an X-Request-Id header, exposed to browser clients through CORS. The same value comes back three ways:

  • as requestId on the log row for that call;
  • as requestId on the measurement the call stored — POST /sdk/v1/measurements records it, and GET /sdk/v1/measurements/{id} returns it;
  • in the console, where ⌘K accepts a request id and opens the record.

So one id ties the HTTP call, its log row and the stored measurement together. Keep the header when a call fails and you have something to search for later.

const res = await fetch(url, { method: 'POST', headers, body });
const requestId = res.headers.get('x-request-id');
if (!res.ok) logger.warn('neccessory refused the call', { requestId });

Polling

The endpoint is limited to 60 requests per minute per key (NC_SDK_LOGS_RATE_LIMIT), and going over it answers 429 rate_limited — which is itself logged, so a runaway poller is visible in its own log. Successful reads of /sdk/v1/logs are not logged, so tailing never feeds itself.

From the CLI

neccessory logs tail follows this endpoint. The plain output prints time, status, endpoint, latency, request id and the error text; --json emits the rows above verbatim as NDJSON, method and errorCode included.

neccessory logs tail --since 1h --json | jq 'select(.errorCode)'