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:
| Field | Type | Notes |
|---|---|---|
id | string | The webhook configuration id. |
url | string | Full HTTPS endpoint. Must use https://; plain HTTP is rejected. |
domain | string | Hostname extracted from url, stored for the delivery-time check. |
events | string[] | Subscribed event types. Must be non-empty. |
status | string | ACTIVE or DISABLED. |
disabledReason | string | null | consecutive_failures, manual, or ssrf_blocked when disabled. |
consecutiveFailures | int | Running count of consecutive delivery failures; reset to 0 on any success or on re-enable. |
apiVersion | string | Payload format version. Defaults to 2024-01. |
tenantId | string | The tenant (live or test) the registration belongs to. |
createdAt | number | Creation timestamp (epoch millis). |
secret | string | Returned only in the create response. 64-char hex (32 random bytes). Never returned by GET/list. |
Operations
| Operation | Method / path | Notes |
|---|---|---|
| Register | POST /developer/webhooks | 201; returns the secret once. |
| Get one | GET /developer/webhooks/{id} | 200; secret omitted. |
| List | GET /developer/webhooks?tenantId=... | 200; tenantId query param required; secret omitted. |
| Update | PUT /developer/webhooks/{id} | Change url, events, apiVersion, or status. |
| Delete | DELETE /developer/webhooks/{id} | 204. There is no in-place secret rotation — delete and re-create. |
| List deliveries | GET /developer/webhooks/{id}/deliveries | 200; delivery records, payload bodies omitted. |
| Retry a delivery | POST /developer/webhooks/{id}/deliveries/{deliveryId}/retry | Re-queues a FAILED delivery. |
Registration validation rules
Enforced at POST (and re-run on PUT when the URL changes):
urlis required and must start withhttps://.eventsmust be present and non-empty.tenantIdis required and must be one of your own tenants (live or test) — otherwise403.- 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:
| Key | Type | Notes |
|---|---|---|
id | string | Unique delivery id; also sent as the X-Vectros-Delivery header. |
version | string | Envelope version, currently 2024-01. |
type | string | The event type (see below). |
created | number | Unix seconds at envelope build time. |
tenantId | string | The tenant the event belongs to. |
livemode | boolean | true for the live tenant, false for test. |
data | object | Event-type-specific fields. |
data for document.* events: id (document id), status, indexMode, and
optionally userId, orgId, clientId, 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, orgId, clientId when present.
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:
| Event | Fires when |
|---|---|
document.indexed | A document finishes indexing successfully. |
document.failed | A document fails indexing. |
record.indexed | A record finishes indexing successfully. |
record.failed | A record fails indexing. |
Adding new fields to
datais a non-breaking, no-version-bump change; removing or renaming a field would require a version bump. PinapiVersionon the registration if you need the envelope structure frozen.
Delivery headers
| Header | Value |
|---|---|
Content-Type | application/json |
X-Vectros-Delivery | The delivery id. |
X-Vectros-Timestamp | Unix seconds at signing time. |
X-Vectros-Signature | sha256=<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 theX-Vectros-Timestampheader. - 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
POSTuses a 5-second connect timeout and a 30-second response timeout. A2xxresponse marks the deliveryDELIVERED; 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
FAILEDand 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 withdisabledReason = consecutive_failures. Re-enable viaPUT {"status":"ACTIVE"}, which resets the counter. - Delivery records have a 7-day TTL while unresolved; the TTL is cleared once a delivery
reaches
DELIVEREDso 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
FAILEDstatus.
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.
| Field | Type | Notes |
|---|---|---|
period | string | YYYY-MM. |
credits.used | number | Credits consumed this period, rounded down to whole credits. |
credits.usedMilli | number | Exact consumption in milli-credits (1 credit = 1000 milli-credits; use this for reconciliation). |
credits.limit | number? | The period allowance, when applicable. |
search.queries.text.count | number | TEXT searches this period. |
search.queries.semantic.count | number | SEMANTIC searches this period. |
search.queries.hybrid.count | number | HYBRID searches this period. |
documents.ingest.text.count | number? | Inline-text document ingests. |
documents.ingest.file.count | number? | File-upload document ingests. |
records.writes.count | number? | Record writes this period. |
records.writes.lookupFieldCount | number? | Lookup-field writes (one per lookup field per write). |
inference.balanceCents | number | Pre-paid inference balance in cents. Never negative — the deduct path floors at 0. |
inference.endpoints.chat.calls | number | Chat calls this period. |
inference.endpoints.rag.calls | number | RAG calls this period. |
inference.endpoints.ask.calls | number | Document-ask calls this period. |
tenants.live | object | Same shape (credits / search / inference) scoped to the live tenant. |
tenants.test | object | Same 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.usedrounds the partner total, which can differ from the sum of the already-rounded per-tenant values by up to one cent — reconcile onusedMilli, notused. - 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
| Parameter | Type | Notes |
|---|---|---|
startTime | ISO-8601 string | Start of the query window. |
endTime | ISO-8601 string? | End of the window. An endTime earlier than startTime returns 400. |
limit | int? | Caps returned entries. |
errorsOnly | boolean? | When true, only entries with status >= 400. |
resource | string? | Allow-list-validated resource filter (e.g. search). |
method | string? | Allow-list-validated HTTP method filter (e.g. GET). |
keyId | string? | Filter to entries authored by one key. |
Response
| Field | Type | Notes |
|---|---|---|
entries | array | Log entries, newest first. |
truncated | boolean | True if the window held more than limit entries. |
queryDurationMs | number | Backend query latency. |
tenantId | string | The tenant the query ran against (credential-derived). |
Each entry: timestamp (ISO-8601), method, resource, status, and optionally keyId,
durationMs, path.
Notes & limits — activity log
- The
resourceandmethodfilters 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-requestsandGET /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 returns501 {"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 uniform403before 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 a separate reserved surface (its
read-side endpoint likewise returns
501today) — 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.