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 codemessage wording may change between releases. The object is present only on failure events; success deliveries omit it entirely.

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.

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; the TTL is cleared once a delivery reaches DELIVERED so it remains visible in the portal. A tenant teardown deletes delivery rows explicitly (they may carry event identifiers) rather than waiting on the TTL.

Delivery record fields (history view)

GET /developer/webhooks/{id}/deliveries returns, per delivery: id, webhookId, eventType, sourceId, sourceType, status (PENDING / DELIVERED / FAILED), attempts, nextRetryAt, createdAt. The limit query parameter caps the count (default 50, max 200). The envelope payload is never returned by this endpoint — it may carry identifiers.

Notes & limits — webhooks

  • Events are limited to the four indexing events above. 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 partner root key always passes). With no arguments returns the current calendar period; { year, month } selects a specific period.

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.
tenants.liveobjectSame shape (credits / search / inference) scoped to the live tenant.
tenants.testobjectSame shape scoped to the test tenant.

The two-axis model

  • Monthly credit allowance — covers data-plane work (record writes, document ingests, searches), 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 partner level and broken down per tenant; the partner total reconciles to tenants.live + tenants.test.
  • Reads do not draw down the credit allowance — only writes and searches count.
  • 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 partner-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 partner 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.

Activity log

client.auth.getAdminLogs(params) (GET /v1/admin/logs). Requires logs:r on a scoped token (a partner 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 (RATE_LIMITED, SUBSCRIPTION_LIMIT_EXCEEDED, INSUFFICIENT_BALANCE, RESOURCE_IN_USE, VERSION_CONFLICT, SESSION_REFRESH_REQUIRED, WRITE_FROZEN, or UNSUPPORTED_WIRE_VERSION — see Response versioning above for what triggers the last of those) — 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 partner 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.