Operations and trust — reference

The exhaustive catalog for the operational surface: webhook configuration and delivery, the event envelope, the usage report, the activity log, and teardown. For the conceptual model see explanation.md; for runnable recipes see how-to.md. For the raw endpoint/parameter listing, see the generated API reference (the OpenAPI/Scalar spec) — this page documents behavior, fields, limits, and honest limitations, not the wire schema.


Webhooks

Configuration object

A webhook registration is per environment (live and test tenants are registered separately). Fields, as returned by the developer-portal webhook endpoints:

FieldTypeNotes
idstringThe webhook configuration id.
urlstringFull HTTPS endpoint. Must use https://; plain HTTP is rejected.
domainstringHostname extracted from url, stored for the delivery-time check.
eventsstring[]Subscribed event types. Must be non-empty.
statusstringACTIVE or DISABLED.
disabledReasonstring | nullconsecutive_failures, manual, or ssrf_blocked when disabled.
consecutiveFailuresintRunning count of consecutive delivery failures; reset to 0 on any success or on re-enable.
apiVersionstringPayload format version. Defaults to 2024-01.
tenantIdstringThe tenant (live or test) the registration belongs to.
createdAtnumberCreation timestamp (epoch millis).
secretstringReturned only in the create response. 64-char hex (32 random bytes). Never returned by GET/list.

Operations

OperationMethod / pathNotes
RegisterPOST /developer/webhooks201; returns the secret once.
Get oneGET /developer/webhooks/{id}200; secret omitted.
ListGET /developer/webhooks?tenantId=...200; tenantId query param required; secret omitted.
UpdatePUT /developer/webhooks/{id}Change url, events, apiVersion, or status.
DeleteDELETE /developer/webhooks/{id}204. There is no in-place secret rotation — delete and re-create.
List deliveriesGET /developer/webhooks/{id}/deliveries200; delivery records, payload bodies omitted.
Retry a deliveryPOST /developer/webhooks/{id}/deliveries/{deliveryId}/retryRe-queues a FAILED delivery.

Registration validation rules

Enforced at POST (and re-run on PUT when the URL changes):

  • url is required and must start with https://.
  • events must be present and non-empty.
  • tenantId is required and must be one of your own tenants (live or test) — otherwise 403.
  • The URL hostname must resolve, and every resolved IP must be publicly routable. A hostname resolving to any private, loopback, link-local, any-local, multicast, CGNAT, or IPv6 unique-local-address range is rejected with 400. This includes the cloud metadata address.
  • The hostname's domain must be verified for your account, or the registration is rejected with 403.

The delivery-time SSRF gate

The registration-time DNS check is defense-in-depth and fast feedback; the real security boundary is re-validation at delivery. Immediately before each POST, the destination hostname is re-resolved and every resolved IP must pass the same public-routability predicate. The predicate rejects:

  • Loopback (127.0.0.0/8, ::1)
  • IPv4 private ranges (RFC 1918 site-local)
  • Link-local (169.254.0.0/16, fe80::/10) — including the cloud metadata IP
  • Any-local and the 0.0.0.0/8 "this network" block
  • Multicast
  • CGNAT 100.64.0.0/10 (RFC 6598)
  • IPv6 unique-local fc00::/7 (RFC 4193)
  • IPv4-mapped/compatible IPv6 forms are unwrapped to their IPv4 address first, so a private IPv4 cannot hide inside an IPv6 wrapper.

If delivery-time resolution returns a non-public IP, the delivery is failed and the webhook is auto-disabled with disabledReason = ssrf_blocked — a DNS-rebinding attempt takes the registration offline rather than merely dropping one delivery. A null or unresolvable address fails closed (treated as non-public).

The event envelope

The body delivered to your endpoint:

KeyTypeNotes
idstringUnique delivery id; also sent as the X-Vectros-Delivery header.
versionstringEnvelope version, currently 2024-01.
typestringThe event type (see below).
creatednumberUnix seconds at envelope build time.
tenantIdstringThe tenant the event belongs to.
livemodebooleantrue for the live tenant, false for test.
dataobjectEvent-type-specific fields.

data for document.* events: id (document id), status, indexMode, and optionally userId, scopes, folderId when present. The document title is intentionally excluded — it is the filename, a top-level (non-typed) value that the field-masking machinery does not cover, so it is never egressed in an envelope. Retrieve the title via GET /v1/documents/{id}, where reveal-scope and tenant/scope enforcement apply.

data for record.* events: id (record id), typeName, indexStatus, and optionally userId, scopes when present (scopes is the same <namespace>:<value> array as the REST response — org:.../client:... or a namespace you registered).

indexFailure on document.failed / record.failed: both event types carry an indexFailure object explaining the failure — whenever the platform recorded a classification — so you can act on the event itself rather than calling back to find out why. It is omitted when no reason was recorded, so treat it as optional. It has a stable code and a human-readable message:

codeMeaning
SOURCE_UNAVAILABLEThe underlying item could not be loaded and may have been deleted.
TEXT_INDEX_FAILEDKeyword indexing failed; the content may still be findable by semantic search.
EMBEDDING_FAILEDSemantic indexing failed; the content may still be findable by keyword search.
INDEXING_FAILEDNo index leg is serving this content, so it is not findable by search at all.
VECTOR_LIMIT_EXCEEDEDYour vector storage limit was reached, so semantic indexing was skipped; keyword search is still serving this content.
INTERNALAn error on our side; retry, and contact support if it persists.

Branch on code — message wording may change between releases. The object is present only on failure events; success deliveries omit it entirely.

The per-tenant semantic (vector) capacity limit is 5,000,000 vectors by default — well above what the overwhelming majority of use cases need. It is a per-tenant limit, not shared across customers, and it can be raised for larger use cases (contact us). Past it, new content still gets keyword-indexed and stays findable via TEXT/HYBRID search; only its semantic indexing is skipped (VECTOR_LIMIT_EXCEEDED above).

The webhook data field names match the public REST API surface exactly — typeName and userId are the same keys the REST request/response uses, so a consumer can share models across both surfaces without remapping.

Event types:

EventFires when
document.indexedA document finishes indexing successfully.
document.failedA document fails indexing.
record.indexedA record finishes indexing successfully.
record.failedA record fails indexing.
trigger.failedAn async trigger execution fails terminally — the platform has stopped retrying it. Not per attempt: a retryable failure that later succeeds sends nothing. The payload mirrors a GET /v1/trigger-failures record.

Adding new fields to data is a non-breaking, no-version-bump change; removing or renaming a field would require a version bump. Pin apiVersion on the registration if you need the envelope structure frozen.

Delivery headers

HeaderValue
Content-Typeapplication/json
X-Vectros-DeliveryThe delivery id.
X-Vectros-TimestampUnix seconds at signing time.
X-Vectros-Signaturesha256=<hex> — HMAC-SHA256 of "<timestamp>.<body>" keyed by the hex-decoded secret.

Signing and verification contract

  • The signature is computed over the literal string <timestamp>.<body>, where <body> is the exact bytes of the JSON payload and <timestamp> is the value in the X-Vectros-Timestamp header.
  • The secret is hex; decode it to its 32 raw bytes before using it as the HMAC key.
  • The signature is recomputed fresh on every attempt (including retries) so the timestamp is always current. Receivers should reject a delivery whose timestamp is more than 300 seconds from now — this is the replay window.
  • Use a constant-time comparison when checking the signature.

Delivery, retry, and auto-disable

  • Delivery is at-least-once. Your receiver must be idempotent — dedupe on the delivery id.
  • The HTTP POST uses a 5-second connect timeout and a 30-second response timeout. A 2xx response marks the delivery DELIVERED; anything else (non-2xx, timeout, connection error) is a failure.
  • Retry backoff after the first attempt fails: 30s → 5m → 30m → 2h → 8h. After the fifth retry delay is exhausted the delivery is marked FAILED and not retried automatically (you can re-drive it manually).
  • Each consecutive failure increments the webhook's consecutiveFailures; a success resets it to 0. At 10 consecutive failures the webhook is auto-disabled with disabledReason = consecutive_failures. Re-enable via PUT {"status":"ACTIVE"}, which resets the counter.
  • Delivery records have a 7-day TTL while unresolved. Once a delivery reaches DELIVERED it switches to a 30-day retention window instead — long enough to review in the portal, but no longer retained indefinitely. A tenant teardown deletes delivery rows explicitly (they may carry event identifiers) rather than waiting on either TTL.

Delivery record fields (history view)

GET /developer/webhooks/{id}/deliveries returns a paginated envelope: {"data": [...], "nextCursor": "..."}. Each entry in data carries: id, webhookId, eventType, sourceId, sourceType, status (PENDING / DELIVERED / FAILED), attempts, nextRetryAt, createdAt, newest first. limit caps the page size (default 50, max 200); pass a previous page's nextCursor back as the startFrom query parameter to continue — treat the cursor as opaque, and null means there is no further page. The envelope payload is never returned by this endpoint — it may carry identifiers.

Notes & limits — webhooks

  • Events are limited to the five listed above — the four indexing events, plus trigger.failed for a terminally failed async trigger execution. There is no webhook for synchronous CRUD operations, deletes, search, inference, identity, or billing.
  • Registration is per environment; there is no account-wide registration spanning live and test.
  • No in-place secret rotation — delete and re-create to roll the secret.
  • No payload customization, header injection, or per-event endpoint routing — one registration receives all of its subscribed event types at one URL.
  • Manual retry only applies to deliveries in FAILED status.

Response versioning (Vectros-Version)

Any request to /v1/* may carry a Vectros-Version request header that pins the shape of the response you get back — field names, the envelope, pagination shape, enum values, and error-body structure. It never changes behavior: authorization, tenant isolation, quota enforcement, and security fixes are identical no matter which version you send.

  • Sending nothing changes nothing. An unversioned request is served exactly as it is today. Adopting the header is entirely opt-in.
  • Responses echo Vectros-Version, so you can confirm which shape you were actually served.
  • 2026-08-01 is the only published version today — it describes the API exactly as it behaves right now. A version remains supported for 12 months after a successor is published; once deprecated, responses served under it carry Deprecation (RFC 9745) and Sunset (RFC 8594) headers ahead of its retirement.
  • An unpublished or expired version is a 400, with errorCode: "UNSUPPORTED_WIRE_VERSION" (see Activity log below — this is the code you'll see there if a client sends a bad value) and a message naming the versions currently supported.
  • The SDKs do not send this header yet. With no header, resolution falls back to the calling credential's own wire_version_pin — stamped onto it at mint time from your tenant's setting at that moment, not read fresh from your account on every call — and only then to the platform floor. A call made through the Node SDK therefore resolves through that fallback rather than pinning a version explicitly; SDK support lands in a later release. If you need to pin a shape today, set the header yourself on a direct HTTP call.
  • Two surfaces don't honor it yet. The streaming POST /v1/rag and POST /v1/chat endpoints, and the POST /v1/documents/{id}/ask sub-resource, currently ignore Vectros-Version and always serve the current shape — note that this is narrower than "all of documents": ingesting and managing documents through the rest of /v1/documents does honor the header, it's specifically the ask sub-resource that doesn't yet. This gap closes before a second version is published.
  • This is a different axis from two other things this page mentions "version" about: the webhook envelope's own apiVersion (above, under Configuration object) versions the webhook payload shape on a per-registration basis, and your SDK's package version moves on any API-surface change — including purely additive ones. Upgrading your SDK does not change the response shape you're served; Vectros-Version does, and only when an existing response shape changes in a breaking way.

Usage and billing

getUsage — the report

client.auth.getUsage() (GET /v1/usage). Not enveloped — returns the report object directly. Requires billing:r on a scoped token (a root key always passes). With no arguments returns the current calendar period; { year, month } selects a specific period.

A token confined to a single app context sees only that context's usage. The top-level totals — credits, search, documents, records, identity, inference, readAccess, and tenants — narrow to your context alone, and the environment (tenants.live/tenants.test) your context is not bound to reads null rather than the other environment's real totals. Only a token with cross-context reach (a root key, or a scoped credential with the cross-context wildcard) sees your full account-wide totals; for it, contextId continues to work as a display-only filter on the contexts breakdown, and your top-level totals are unaffected by it. If your integration reads top-level usage totals from a context-confined token, those numbers now reflect only that context — read contexts[] for the full picture, or use a credential with cross-context reach.

execution narrows like the other per-context charge fields — a token confined to a single app context sees only that context's executions. Like every other top-level section it is not filtered by the contextId query parameter, which stays a display-only filter on contexts. One caveat specific to it: its credits carry sub-credit remainders forward across your whole account for the period, so for a confined credential compare execution.creditsMilli against your account-wide figure rather than recomputing it from that one context's billableMillis.

Two narrower exceptions — do not read this as "everything narrows": reads.calls.used and reads.dataOut.bytes (the raw call-count and egress-byte counters) have no per-context breakdown to narrow to at all — they're metered per account, not per context — so a context-confined token sees 0 for both rather than a narrowed figure. Don't read reads.calls.used == 0 as "this credential made no calls this period"; it means "this dimension isn't visible at context granularity." The corresponding charge fields (reads.calls.overageCredits, reads.dataOut.overageCredits) do narrow correctly, since overage is charged per context. Separately, credits.limit continues to reflect your whole plan's ceiling regardless of confinement — only credits.used/credits.remaining narrow, so credits.remaining may overstate a context-confined credential's true remaining room.

FieldTypeNotes
periodstringYYYY-MM.
credits.usednumberCredits consumed this period, rounded down to whole credits.
credits.usedMillinumberExact consumption in milli-credits (1 credit = 1000 milli-credits; use this for reconciliation).
credits.limitnumber?The period allowance, when applicable.
search.queries.text.countnumberTEXT searches this period.
search.queries.semantic.countnumberSEMANTIC searches this period.
search.queries.hybrid.countnumberHYBRID searches this period.
documents.ingest.text.countnumber?Inline-text document ingests.
documents.ingest.file.countnumber?File-upload document ingests.
records.writes.countnumber?Record writes this period.
records.writes.indexCountnumber?Logical index credits maintained (ownership + externalId + schema lookups; an equality index counts 1, a range-enabled index 3).
inference.balanceCentsnumberPre-paid inference balance in cents. Never negative — the deduct path floors at 0.
inference.endpoints.chat.callsnumberChat calls this period.
inference.endpoints.rag.callsnumberRAG calls this period.
inference.endpoints.ask.callsnumberDocument-ask calls this period.
identity.users / identity.entitiesobjectWrite + storage activity for the two identity surfaces (users, and identity entities across every namespace). Reported as an account-level rollup, not narrowed per-namespace.
readAccess.storageBytes / readAccess.rowsobject / number?PHI read-access-log storage and logged-read count. Present only when read-access logging is enabled on a schema or context.
reads.calls.used / reads.dataOut.bytesnumberThe per-call and data-out (egress) read-metering axes — see the context-narrowing note above for the two exceptions.
credits.breakdown.scriptExecutionnumberCredits consumed by script execution time (trigger firings and POST /v1/scripts/execute alike) beyond the allowance your billable operations earned. Present alongside the other credits.breakdown categories; scriptExecutionMilli is the exact figure.
execution.executionsnumberTrigger-script executions metered this period. Counts every execution that ran your script, including those that ended in an error and those that owed no charge; excludes firings refused before your script ran, which are never metered.
execution.totalMillisnumberTotal measured wall-clock time across those executions.
execution.includedMillisnumberExecution time included at no charge and actually applied this period. The allowance CONSUMED, not the allowance earned — an execution that finished well inside its included time contributes only what it used, and an execution we do not charge for at all (an internal platform error, say) has its whole duration absorbed here. Both are what make the arithmetic below exact.
execution.billableMillisnumberThe portion actually charged, after the per-execution platform baseline and the included time above.
execution.credits / execution.creditsMillinumberCredits charged for execution time. The milli figure is exact; a small charge usually rounds to 0 whole credits.
tenants.liveobjectSame shape (credits / search / inference) scoped to the live tenant. null if your token is confined to a context in the test environment.
tenants.testobjectSame shape scoped to the test tenant. null if your token is confined to a context in the live environment.
contextsarrayPer-app-context breakdown, one entry per context with activity. Filtered to a single context when ?contextId= is supplied on a cross-context-reach token.

The two-axis model

  • Monthly credit allowance — covers data-plane work (record writes, document ingests, searches, read and data-out overage, and script execution time — trigger and synchronous), resets each calendar month, reported by credits.
  • Pre-paid inference balance — covers chat / RAG / document-ask, denominated in cents, drawn down per inference call and topped up out of band, reported by inference.balanceCents.

Metering semantics

  • Counts are tracked at the account level and broken down per tenant; the account total reconciles to tenants.live + tenants.test.

  • Reads do not draw down the credit allowance within your plan's included read and data-out allowances; past those, overage does, and it is reported under reads and in the credits.breakdown reads / dataOut categories.

  • Trigger-script execution time draws the same credit allowance, reported by the execution section and the credits.breakdown.scriptExecution category — trigger firings and synchronous POST /v1/scripts/execute calls are summed together. It is charged on the measured span of each execution — the whole execution, not only your script's own run time: resolving the rule's identity, preparing the sandbox, running your code, and recording the outcome are all inside it, so totalMillis is legitimately larger than what your own instrumentation would report. From that span it subtracts the execution time the execution's billable operations earned, and adds a fixed per-execution platform baseline. Verify a charge like this:

    billableMillis = totalMillis + (baseline × executions) − includedMillis
    creditsMilli   = billableMillis ÷ (milliseconds per milli-credit)
    

    The report gives you every term but two: the per-execution baseline and the rate. Those are published as numbers on the Vectros pricing page and in the execution schema of the API reference — deliberately not here, since this page does not pin numeric prices and a stale one must never ship from it. Sub-credit remainders carry forward within the period rather than being rounded away, so compare creditsMilli against your period-to-date total, not against a single execution.

  • A firing refused before your script ran is never metered at all — a rate limit, a credit ceiling, a concurrency limit, a rule whose principal or grant does not resolve, a deleted rule, an unresolvable script. None of them appears in execution.executions or execution.totalMillis; there is no duration to report, because nothing of yours ran.

  • An execution that did run your script is metered, and separately decided to be chargeable or not. A failure we attribute to ourselves — an internal platform error — is metered but not charged, and its whole duration is absorbed into includedMillis, so the identity above stays exact rather than being thrown off by the executions least likely to be your fault.

  • ⚠️ An execution that started and then failed is charged, including for reasons that look like refusals. Your script throwing, a timeout, a resource or write-buffer limit, an optimistic-lock conflict on a record another writer touched — and, the two most easily mistaken for pre-flight refusals, a call outside the rule's declared manifest and a permission denial. Those last two are decided while your script runs, and a script that probes, recovers, and then runs for minutes is reported under them: the wall clock was spent either way. Do not read the never-charged list above as covering permission problems generally — it covers a rule that could not be started, not a call your running script was refused.

  • Counters tick as operations dispatch (e.g. a document ingest ticks at dispatch, before indexing completes; a chat call ticks after the stream finalizes), so the report is near-real-time and eventually consistent with in-flight settlement.

  • The inference endpoint section always carries all three keys (chat, rag, ask) even at zero — defaulting is per-endpoint.

  • The all-three inference keys are present on both the account-level and per-tenant sections.

Notes & limits — usage

  • The report is read-only and observability-oriented; it is not an invoice or a line-item transaction export.
  • Rounding: credits.used rounds the account total, which can differ from the sum of the already-rounded per-tenant values by up to one cent — reconcile on usedMilli, not used.
  • Pricing rates, plan allowances, and overage policy are not part of this surface; this documentation does not state numeric prices.
  • Execution time is not attributed per trigger rule or per script. The execution section is an account rollup over the period, and there is no per-rule figure and no execution block inside the per-context contexts[] entries — so if you run several trigger rules, the report cannot tell you which script's execution time you are looking at. If you need that separation today, run the script in its own app context and read that context's credits.usedMilli (which includes the charge), or query with a credential confined to it. If a charge looks wrong and you cannot account for it, raise it with support promptly — the finer-grained evidence behind it is retained for a limited window.

Activity log

client.auth.getAdminLogs(params) (GET /v1/admin/logs). Requires logs:r on a scoped token (a root key always passes). The tenant is derived from the credential — there is no request channel to query another tenant.

Parameters

ParameterTypeNotes
startTimeISO-8601 stringStart of the query window.
endTimeISO-8601 string?End of the window. An endTime earlier than startTime returns 400.
limitint?Caps returned entries.
errorsOnlyboolean?When true, only entries with status >= 400.
resourcestring?Allow-list-validated resource filter. Accepted values are the data-plane resources (documents, records, search, schemas, folders, usage, models, rag, chat, ask, export, erasure-requests, auth, ping) and the identity surfaces entities, namespaces and users; clients and orgs also validate, matching rows written before those surfaces were folded into entities. An unlisted value is rejected with 400.
methodstring?Allow-list-validated HTTP method filter (e.g. GET).
keyIdstring?Filter to entries authored by one key.

Response

FieldTypeNotes
entriesarrayLog entries, newest first.
truncatedbooleanTrue if the window held more than limit entries.
queryDurationMsnumberBackend query latency.
tenantIdstringThe tenant the query ran against (credential-derived).

Each entry: timestamp (ISO-8601), method, resource, status, and optionally keyId, durationMs, path, requestId, errorCode.

requestId is the call's correlation id — the same value returned as requestId in an error response body. Quote it when contacting support to have a specific call traced. It is recorded for successful calls too.

For a failed call, the entry also carries errorCode when the failure had a typed code — an uppercase token such as RATE_LIMITED, VERSION_CONFLICT or SCRIPT_ERROR.

The set is open and grows as the platform gains new typed failures, so treat any list you see as examples rather than an enumeration: branch on the specific codes you handle and fall back to generic handling for anything else. The codes a given call can return are documented on that endpoint's own error responses; POST /v1/scripts/execute additionally reports the name of its failure category, listed in the data-model reference. See Response versioning above for what raises UNSUPPORTED_WIRE_VERSION specifically.

Branch on the errorCode, not the message text. It is null on successful calls, on failures that carry only a message, and on calls recorded before the field existed that are still inside the retention window — so read an absent errorCode as "not recorded", not as "no error". Request and response bodies are never logged, so no further failure detail is available here by design.

Notes & limits — activity log

  • The resource and method filters are allow-list validated at the boundary; an out-of-allow-list value is rejected, not silently dropped.
  • There is a short ingestion lag (seconds) between a request completing and its entry becoming queryable.
  • This is an operational API call log, not the compliance audit/version history (which is a separate, retained data-layer mechanism — see compliance.md).

Teardown and erasure

Per-customer / per-context hard-delete — implemented

  • Context delete runs an owner-filtered cascade that removes the records, documents, folders, and schemas under a context (the isolation boundary). It is the mechanism for removing one customer's or one application's footprint.
  • Tenant teardown decommissions an entire live or test tenant, cascading across its contexts and tenant-level config (including webhook registrations and delivery rows, which are deleted explicitly because they may carry identifiers).
  • These operations are irreversible and are control-plane actions.

End-subject right-to-erasure — RESERVED (not implemented)

  • POST /v1/erasure-requests and GET /v1/erasure-requests/{id} exist as a frozen contract stub. The request/response shapes are stable so SDK integrations will not break when the engine ships, but the endpoint returns 501 {"error":"not_implemented"} today — there is no erasure engine behind it yet.
  • The endpoint requires the root API key; a scoped credential (ssk_* / st_*) is rejected with a uniform 403 before the stub runs.
  • This is distinct from context/tenant hard-delete. Per-customer deletion works today; per-individual ("erase everything about this one subject everywhere") does not.

Notes & limits — teardown

  • Right-to-erasure is reserved, not turnkey.
  • Read-access logging / accounting-of-disclosures is available but off by default (opt-in per context); its admin accounting endpoint is live — see compliance.md.
  • Data-retention periods are platform constants today; they are not configurable per controller.

Where to go next

  • explanation.md — the concepts behind everything cataloged here.
  • how-to.md — runnable recipes for webhooks, usage, and the activity log.
  • compliance.md — the trust posture, the three sensitive-data mechanisms, retention, and the full reserved list.