https://api.valtrix.com/v1
Mint an API key in the console under Developers, then install the SDK and generate a typed client from your published tables. Your first table read is a findMany call; the same read works over plain REST with the key in the Authorization header.
$ export VALTRIX_API_KEY=vlt_...$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "limit=5"{ "rows": [...], "next_cursor": null, "schema_version": "v1" }
Valtrix turns the raw data living in the third-party systems your organizations use into clean tables you query like a database. Ingestion and modeling happen upstream, so by the time data reaches this API it is already in your schema, the same shape for every organization and every connector.
Organizations connect their third-party systems through Connect. Valtrix syncs their data continuously and stores it as Records.
Your Transformation rules clean, map, and validate those Records into a data model you design once. Steps run separately within each organization's data; rows never mix across organizations.
Publishing a Transformation produces typed, versioned Tables where data from every organization shares one shape.
Read rows from tables with filters, query records directly for ad hoc reads, follow change feeds to stay in sync, and write records back through the endpoints below.
Authenticate every request with an API key in the Authorization header; requests without a valid key return 401. Keys come in two kinds. Platform-wide keys, minted in the console under Developers, reach every organization granted to you. Organization-scoped keys, minted through POST /v1/keys, are pinned to one organization: reads and writes default to it, the org parameter becomes optional, and naming any other organization returns 403 org_not_scoped. To mint your first key, sign in to the console, open Developers, and click Create API key. Name the key, leave the scope on Entire platform (all organizations), and pick an access level. The full key is shown once, on creation, so copy it there; a lost key cannot be recovered, only revoked and replaced. Every key also carries an access level: read, write, or manage. Read-only keys can call every read endpoint except key management (GET /v1/keys needs manage) but cannot write or delete records or create connect sessions (those return 403 key_access_denied); write is the default and covers records and connect sessions; manage exists only on platform-wide keys and additionally governs transformation deploys, key management, and webhook endpoints. A key's power is visible in its prefix: scoped keys start with vlt_org_ instead of vlt_, and read-only keys with vlt_read_ or vlt_org_read_, so a key found in a log or a leak is immediately recognizable. Hand a scoped key to each app or environment that acts for a single organization, a read-only key to anything that only consumes data, and keep platform-wide keys on your own servers. The SDK and the valtrix CLI read the key from VALTRIX_API_KEY and the API base URL from VALTRIX_API_URL (the production API when unset), from the shell or from a .env.local or .env file in the project directory, so neither value needs to appear in code.
$ export VALTRIX_API_KEY=vlt_...$ curl https://api.valtrix.com/v1/tables \-H "Authorization: Bearer $VALTRIX_API_KEY"
There are two ways to build with AI on Valtrix, split by when the model is involved. Through the MCP server, Claude and ChatGPT query and write the data live during a session, with the model in the request path at runtime. In Embedded coding agents, a coding agent writes an app against the SDK and the generated client, and the model's involvement ends when the code ships.
The MCP server gives Claude, ChatGPT, and any other MCP client live access to the same tables and records as the REST API, for writes as well as reads. It speaks the Model Context Protocol over streamable HTTP at /mcp and follows the same semantics as the REST endpoints: organization-scoped keys pin every call to their organization, platform-wide keys accept an org argument, and write access follows the key. Fifteen tools mirror the REST surface: get_schema, list_tables, query_table, query_records, upsert_record, get_record, delete_record, list_write_intents, get_write_intent, retry_write_intent, create_connect_link, request_connector (add a connector that is not in the catalog yet by provider name and homepage URL, mirroring POST /v1/connector-requests; write access), list_connector_requests (mirroring GET /v1/connector-requests), list_orgs (platform-wide keys only; resolve a connected organization by name and pass its id as the org argument), and list_connections. Organization-scoped keys do not get list_orgs; list_connections mirrors GET /v1/connections on both key kinds, narrowed to the organization on an organization-scoped key and taking an optional org on a platform-wide one, and carries each connection's writes and write_guidance. get_schema and list_tables take an optional org on platform-wide keys to narrow to what one organization granted, upsert_record and delete_record take an org argument on platform-wide keys (omitting it returns org_required) and the same propagate, connection, and idempotency_key arguments as POST /v1/records/:entityType, and delete_record removes only records written through the API, never connector-synced ones, and the write intent tools mirror GET /v1/write-intents, GET /v1/write-intents/:id, and POST /v1/write-intents/:id/retry. In claude.ai or chatgpt.com, add the /mcp URL as a custom MCP server, click Connect, and paste your API key on the Valtrix authorization page; Claude Code, Cursor, VS Code, and Codex authenticate through the same OAuth flow, and clients without OAuth support can send the key as a bearer header instead. The MCP setup guide has step-by-step instructions for every client. Where the Embedded coding agents path produces an app that queries the API on its own, with no model at runtime, the MCP server keeps the model itself in the conversation, reading live data and writing records back.
Server URLhttps://api.valtrix.com/mcpAuthorization headerAuthorization: Bearer YOUR_API_KEY
For embedded apps written by a coding agent (i.e. vibe-coded tools your users spin up inside your product). These apps run on organization-scoped keys and the SDK. For example, a builder on your platform asks the agent for a tool that flags unpaid vendor bills, and the app it gets back queries the bills table with a key pinned to that builder's organization and nothing else. Provision the key and the generated client in the app template, and the agent opens a project with the schema already in its editor and the ground rules already in the repository. The rules ship as the AGENTS.md file inside @valtrix/sdk: query tables live instead of mirroring them, go through the generated client, never handle keys.
Mint a key per appyour backendWhen a user creates an app, mint an organization-scoped key named after it with POST /v1/keys and inject it into the app's environment as VALTRIX_API_KEY. The key never appears in source or in the agent's conversation. Revoke it when the app is deleted.
Generate the clientyour app templateAdd @valtrix/sdk as a dependency and run npx valtrix generate while scaffolding, so valtrix/client.ts is already in the tree with the full schema when the agent first opens the project.
Copy in AGENTS.mdyour app templateThe instructions file ships at node_modules/@valtrix/sdk/AGENTS.md and versions with the SDK. Copy it to the project root so every coding agent that opens the repository reads the same rules.
Guard the schema in CIyour app templateRun npx valtrix generate --check next to the typecheck, so a published schema change surfaces as a failing build the agent knows how to fix.
$ curl -X POST https://api.valtrix.com/v1/keys \-H "Authorization: Bearer $VALTRIX_API_KEY" \-d '{ "org": "org_2d9x4v", "name": "snag-tracker" }'$ npm install @valtrix/sdk$ npx valtrix generate$ cp node_modules/@valtrix/sdk/AGENTS.md .
Failed requests return a human readable error and a stable code. Branch on the code, not the message. The SDK throws the same shape as a ValtrixError, with code and status on the error.
invalid_source_fields400source_fields must be an object and the connection the write propagates to must take native SDK fields; sending it without propagate: true is invalid_propagation.
connection_not_found404No connection with that id belongs to an organization that has connected to you, or the key is scoped to a different organization.
run_not_found404No sync run with that id exists on the connection.
invalid_history_from400history_from is not an ISO 8601 date, is in the future, or would narrow the connection's existing history depth. History only widens.
invalid_until400until is not an ISO 8601 timestamp in the future, or is more than a year away. Omit it to pause with no expiry.
invalid_mode400mode is not "full". Incremental syncs run on the connection's own schedule and cannot be requested.
sync_in_progress409A sync is already queued or running for the connection. Wait for it to finish, then request another.
connection_paused409The connection is paused. Resume it before requesting a sync.
connection_not_syncable409The connection's connector is still being built or does not sync, so no run can be queued.
connection_shared409Another platform also reads this connection, so only the organization can pause it. The message names the other readers.
connection_not_pausable409The connection is pending, disconnected, or already paused, so there is nothing to pause.
connection_not_paused409The connection is not paused, so there is nothing to resume.
invalid_filter400A row filter is not in <op>.<value> form, or its value does not match the column type.
invalid_order400order is not in <column>.<asc|desc> form.
invalid_limit400limit is not a positive integer.
invalid_cursor400The cursor is malformed. Always pass next_cursor back verbatim.
unknown_column400A filter, select, or order names a column that is not in the table or entity schema.
unknown_connector400No connector with that slug. GET /v1/connectors lists the valid ones.
connector_required400An organization-scoped key mints a sign-in link for one system, so POST /v1/connect/sessions and create_connect_link need a connector on such keys.
org_required400Record endpoints need an org parameter, POST /v1/keys needs an org in the body, and the MCP upsert_record and delete_record tools need an org argument on platform-wide keys.
invalid_name400An API key name is empty or longer than 100 characters.
invalid_external_id400external_id is missing or longer than 255 characters.
invalid_idempotency_key400The Idempotency-Key header is longer than 255 characters.
invalid_fields400Record data failed validation. The response includes a fields array naming each issue.
invalid_attachments400The attachments on a record write failed validation: not an array, more than 10 entries, a missing filename, a media type outside the supported set, content that is not base64, or more than 25 MB together. The response includes a fields array naming each issue.
invalid_scope400scope is not a valid list of entity types.
invalid_credential_sections400credential_sections must be "login" or "api".
invalid_status400status must be active, revoked, or expired on GET /v1/orgs; pending, applied, or failed on GET /v1/write-intents; or pending, delivered, or dead on GET /v1/webhooks/:id/events.
invalid_url400The webhook endpoint url is missing or not an https URL on a public host.
invalid_subscriptions400The event_types or tables on a webhook endpoint are not valid: an unknown event type, no event types at all, a table that is not published, a column the table does not have, or table.row_changed without any table.
invalid_payload_template400The payload_template on a webhook endpoint is not usable: not a JSON string, not valid JSON, over 16 KB, a malformed placeholder, or a placeholder naming an event field or table column the endpoint's subscriptions do not carry. Also returned when a subscription change would orphan a placeholder in the stored template.
invalid_access400access must be read or write. Keys minted through POST /v1/keys never carry manage.
invalid_disposition400disposition on GET /v1/attachments/:attachmentId/url must be inline or attachment.
invalid_definitions400The definitions payload on POST /v1/transformations/plan or /v1/transformations/apply failed validation. The response includes the failing items.
scope_not_granted403The grant for this organization does not cover that entity type.
org_not_scoped403The request names an organization other than the one this organization-scoped key is pinned to.
key_access_denied403The key's access level does not allow this action. Read-only keys cannot write or delete records, create connect sessions, or request connectors; the response names the missing capability.
manage_required403This endpoint needs a platform-wide key with manage access. Manage governs applying transformation deploys, minting, listing, or revoking organization-scoped API keys, managing webhook endpoints, and requesting connectors from a platform-wide key.
platform_not_granted403The platform named on POST /v1/connector-requests does not have an active grant to the organization the key is scoped to. Omit platform to request through the organization's own Valtrix access.
invalid_request400The request body is missing a required field or a field has the wrong shape. The error message names the problem.
product_choice_required409The provider sells more than one product, so the request is ambiguous. Repeat it with product_choice set to one of the returned candidates.
table_not_found404No published table with that name.
unknown_entity_type404No entity type with that slug. GET /v1/schema lists the valid ones.
record_not_found404No record matches. For external_id lookups, nothing with that id was written through this API. For record ID lookups, the id is unknown or is a syn_ prefixed synthetic row id, which has no single record behind it.
org_not_granted404 or 403No active grant for that organization: 404 when the organization has never granted you access, 403 when the grant exists but is revoked or expired.
org_not_found404No organization with that id has connected to you.
key_not_found404No API key with that id exists.
webhook_not_found404No webhook endpoint with that id exists, or it was deleted.
attachment_not_found404No attachment with that id exists, or its record is outside your granted organizations.
transformation_not_found404No transformation publishes a table with that name.
external_id_conflict409The external_id on a connect session already identifies a different organization. Omit org to reuse it, or send the matching org id.
cursor_expired410The changes cursor is older than the 30 day retention window. Restart from a full rows read.
payload_too_large413Record data must be under 100KB, and a request body with attachments under 40 MB.
poll_too_frequent429This change feed read found no changes, and the same read with the same cursor found none less than 60 seconds ago; the feed only moves when a sync finishes or a record is written. Treat it as caught up and retry after the seconds in the Retry-After header, or read the feed when a webhook says it moved instead of on a timer.
rate_limited429Over 300 requests per minute on this API key. Retry after the seconds in the Retry-After header.
internal_error500Something went wrong handling the request. Safe to retry after a short wait. Valtrix is alerted to persistent failures automatically.
storage_unavailable503Attachment storage is temporarily unavailable. Safe to retry after a short wait.
invalid_propagation400connection was passed without propagate: true. The connection field only addresses a propagated write.
invalid_op400A write intent op filter is not one of create, update, or delete.
unknown_connection404The connection id passed for propagation does not belong to this organization.
write_not_supported409propagate: true was requested but no connected system for the organization declares that create, update, or delete on that entity type.
write_ambiguous409More than one connection can take the write. Pass connection to choose which one propagates.
write_fields_unsupported409propagate: true was requested but the connection sends none of the fields in data on that operation (and the write carries no lines or document it takes), so nothing would reach the source. The message names the fields the operation sends; nothing was written.
write_intent_not_found404No write intent with that id exists.
write_intent_not_retryable409Only failed write intents can be retried.
$ curl "https://api.valtrix.com/v1/records/customer/customer_8f2?org=org_2d9x4v" \-H "Authorization: Bearer $VALTRIX_API_KEY"HTTP/2 404{"error": "No active grant for that organization.","code": "org_not_granted"}
Each API key can make 300 requests per minute, counted across all endpoints; a request that carries no valid key is limited the same way per client address. Past the limit, requests return 429 with code rate_limited and a Retry-After header giving the seconds until the window resets; wait that long before retrying rather than retrying immediately. The SDK handles this for you: it waits out Retry-After and retries automatically, and throws a ValtrixRateLimitError, with retryAfterSeconds on the error, only once its retries are exhausted. If you hit the ceiling while reading tables, request more per call instead of calling more often: raise limit to 200 and page with cursors rather than issuing many small reads. The change feed has one more limit of its own. A change feed read that finds no changes, repeated with the same cursor less than 60 seconds after the first one that found none, returns 429 with code poll_too_frequent and a Retry-After header instead of an empty list, because the feed only moves when a sync finishes or a record is written. A read that finds changes is always served, and the SDK's changes() treats this 429 as caught up and ends without an error. Read the feed when a table.changed webhook fires rather than on a timer.
$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY"HTTP/2 429Retry-After: 12{"error": "Rate limit exceeded.","code": "rate_limited"}
Filter rows server side with query parameters of the form <column>=<op>.<value>. Repeat parameters to combine conditions; a row must match all of them. Values are validated against the column type from your schema, so number columns compare numerically, and filtering a column that is not in the schema returns unknown_column. The same grammar filters canonical records on GET /v1/records/:entityType, against the entity's fields instead of table columns. The SDK expresses the same grammar as a where object. Store ids rather than copies of rows: keep your own external_id for records you write and the _record_id from rows you read, and either one fetches the current state again later. One id appears under three names: a row's _record_id, a record object's id, and a change event's record_id are the same rec_ prefixed record ID, and reference columns hold that same value.
eq, neqoperatorsEquals and not equals.
gt, gte, lt, lteoperatorsRange comparisons, typed per column: number columns compare numerically, date columns compare in ISO order.
in.(a,b)operatorMatches any value in a comma separated list.
is.null, not.nulloperatorsWhether the column has a value.
_record_idmeta columnPoint lookup by row id with eq.<id> or in.(a,b); other operators are rejected. Every row carries its id in _record_id, and within a table an id matches at most one row, so store it to fetch the same row again. rec_ prefixed ids resolve to a record through GET /v1/records/:recordId; syn_ prefixed ids from aggregating or reshaping steps filter the same way but resolve to no record.
_external_idmeta columnOn GET /v1/records/:entityType only: point lookup by the record's id in its source system with eq.<id> or in.(a,b); other operators are rejected. Every record carries it as _external_id: a connected system's own key for the row (an invoice's CFDI UUID, a vendor code) or the external_id an API write supplied. Rows do not carry _external_id; the records endpoint resolves a source id to its _record_id.
reference columnstypedColumns typed reference hold record ids and filter like ids: eq, neq, in.(a,b), is.null, not.null. Range operators are rejected. To cross-reference, collect the reference values from one read and pass them to the target table as _record_id=in.(a,b). A reference that can point at more than one entity type pairs with a sibling type column naming the linked entity, so a cost row carries payee_id plus payee_type set to vendor or employee.
$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "email=not.null" -d "order=created_at.desc"$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "_record_id=eq.rec_9d4f2c81a7b3e650d21c4f8a"
List endpoints return up to limit items and a next_cursor. Pass it back as cursor for the next page. On row and record pages, a null next_cursor means you have everything. On the change feed, a null next_cursor means you are caught up for now: keep the last non-null cursor you received and poll with it again later, never store the null. A cursor is a bookmark minted by the server, and it works the same everywhere in the API, on row pages and on the change feed alike: send it back exactly as you received it, never build or edit one, and never treat it as an id. In the SDK, page() exposes the same cursor round-trip when you want explicit control, and iterate() and changes() thread it for you until the feed is drained.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from the previous page, sent back exactly as you received it. Omit on the first call.
next_cursorresponseSend back as cursor to fetch the next page. null means the last page.
$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "limit=200" -d "cursor=eyJpIjoiMDFo..."
Every row a table returns is your columns plus five meta fields. The shape is identical for every organization and every connector; GET /v1/schema is the contract for the column part.
<column>per schemaOne property per column in the table schema, named by the column key and typed string, number, boolean, date, json, or reference. A reference column holds the _record_id of a related record, so it joins without name matching: filter the target table with _record_id=eq.<value>, or resolve it directly with GET /v1/records/:recordId. Nullable columns can be null.
_record_idstringThe id of the row. Within a table an id matches at most one row, so store it to fetch the same row again with a _record_id filter. rec_ prefixed ids resolve to a record through GET /v1/records/:recordId; ids starting with syn_ belong to derived rows from aggregating or reshaping steps and resolve to no record. A syn_ id survives a republish of the transformation only when the SQL step that generates the row declares a key; otherwise the id is a hash of every value in the row, and a republish that changes the query replaces every row under a new id.
_org_idstringThe organization the row belongs to. Reads span every organization you hold an active grant for, so use it to attribute rows when you serve more than one.
_connectorstring or nullThe slug of the connector the row's record was synced from, so you can attribute the row to its origin system. GET /v1/connectors resolves slugs to display names. Rows derived from records you wrote through this API carry the API source. null on syn_ rows, which aggregate or reshape many records and have no single origin.
_synced_atstringWhen the row's record was last re-read from its source, as an ISO 8601 timestamp. It moves on every sync whether or not anything changed, so it says how fresh the row is, not when it changed. Orderable, so order=_synced_at.desc reads the most recently confirmed rows first, and filterable by range, so _synced_at=lt.<timestamp> finds rows not confirmed since then.
_changed_atstringWhen the row's record last actually changed, as an ISO 8601 timestamp: the last time a sync or a write found different data, not the last time it was looked at. A row whose _synced_at keeps moving while _changed_at stays put has been stable since then. Orderable, so order=_changed_at.desc reads the most recently changed rows first, and filterable by range, so _changed_at=gte.<timestamp> reads the rows that changed since a cutoff. Rows that aggregate many records carry the newest of their records' values. Counts from when Valtrix first synced the record, so it never predates the organization's connection.
_frozenbooleantrue when the row's record is outside the connector's sync window and no longer refreshed. The row keeps the values last fetched inside the window and stays in the table. false on rows whose record still syncs.
{"email": "...","created_at": "2026-07-01","_record_id": "rec_9d4f2c81a7b3e650d21c4f8a","_org_id": "org_7f3k2m","_connector": "procore","_synced_at": "2026-07-08T09:30:00Z","_changed_at": "2026-07-06T15:12:00Z","_frozen": false}
One entry in a table's change feed, describing a row that was created, updated, or removed.
typestringupsert when the row was created or updated, delete when it was removed.
cursorstringA bookmark for this change's position in the feed. Store the last one you processed and resume from it.
occurred_atstringWhen the change happened, as an ISO 8601 timestamp.
record_idstringThe id of the affected row, the same value the row carries as _record_id. For deletes it identifies which row to drop.
org_idstringThe organization the row belongs to.
changed_columnsarray or nullThe columns whose values differ from the row's previous version, for an update. null when the row is new to the table, and null for deletes. Check it before reacting, so an edit to an unrelated column does not look like the change you care about.
previousobject or nullThe values the changed columns held before this change, keyed by column, and only those columns. An empty object for an update whose changed_columns is empty, null whenever changed_columns is null.
rowobject or nullThe full row after the change for upserts, in the row object shape. null for deletes.
{"type": "upsert","cursor": "eyJjIjoiODIzMiJ9","occurred_at": "2026-07-08T09:30:00Z","record_id": "rec_9d4f2c81a7b3e650d21c4f8a","org_id": "org_7f3k2m","changed_columns": ["status"],"previous": { "status": "under_review" },"row": { ... }}
/v1/schemaYour full API contract: every published table with its columns, and every writable entity type. Compare schema_version between reads to detect published changes.
orgstringNarrow to the tables and entity types one organization granted, by id. Omit to see everything published for the platform.
tablesarrayOne entry per published table, with its columns.
recordsarrayOne entry per entity type, with its fields and its own schema_version. Entity schemas only gain fields, so a version bump never removes or retypes one.
$ curl https://api.valtrix.com/v1/schema \-H "Authorization: Bearer $VALTRIX_API_KEY"{"tables": [{ "name": "customers_clean", "entity_type": "customer", "status": "live", "row_count": 1204, "row_identity": "record", "last_built_at": "2026-07-08T09:30:00Z", "schema_version": "v1", "columns": [{ "key": "email", "type": "string", "nullable": true, "writable": true, "description": "..." }] }],"records": [{ "entity_type": "customer", "label": "Customer", "description": "...", "title_key": "name", "schema_version": "v1", "columns": [...] }]}
/v1/tablesLists your published tables in the same shape as /v1/schema. GET /v1/tables/:name returns a single table, or 404 table_not_found. Each table reports row_identity: "record" when every row keeps its source record id, "synthetic" when the transformation aggregates or reshapes so row ids are generated, or "mixed" when both occur; null until the first build. A synthetic id is stable across republishes of the transformation only when its SQL step declares a key; without one the id is a hash of every value in the row, so a republish that adds a column replaces every row, and the change feed reports each as a delete of the old id followed by an insert of the new one.
orgstringNarrow to the tables and entity types one organization granted, by id. Omit to see everything published for the platform.
tablesarrayTable objects in the same shape as /v1/schema.
The single table form returns one table object, or 404 table_not_found.
$ curl https://api.valtrix.com/v1/tables \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "tables": [{ "name": "customers_clean", "entity_type": "customer", "status": "live", "row_count": 1204, "row_identity": "record", "last_built_at": "2026-07-08T09:30:00Z", "schema_version": "v1", "columns": [...] }] }
/v1/tables/:name/rowsReads rows across every organization you hold an active grant for. Filtering, ordering, projection, and pagination run server side.
namepathThe table name, as listed by /v1/tables.
<column>string, repeatableFilter as <op>.<value> with eq, neq, gt, gte, lt, lte, plus in.(a,b), is.null, and not.null. Repeat with different columns; all must match.
_record_idstringPoint lookup by row id, as eq.<id> or in.(a,b). Every row carries its id in _record_id, so store it to fetch the same row again. Ids come in two forms, and both filter the same way: rec_ prefixed ids are record ids that resolve through GET /v1/records/:recordId, while syn_ prefixed ids belong to rows generated by aggregating or reshaping steps and resolve to no record. The table's row_identity field tells you which form to expect. Supports eq and in only.
_changed_atstring, repeatableRange filter on when the row last actually changed, as gt., gte., lt., or lte. followed by an ISO 8601 timestamp. Combine two for a window. A cutoff, not a change feed: rows deleted since the cutoff never appear here, so use the table's change feed to stay in sync and this filter for one-off reads such as a backfill.
_synced_atstring, repeatableRange filter on when the row's record was last re-read from its source, with the same gt., gte., lt., and lte. forms as _changed_at.
orderstringOne column as <column>.<asc|desc>. _synced_at and _changed_at are also orderable, and filterable by range.
selectstringComma separated column keys to return. The row's underscore fields (_record_id, _org_id, _connector, _synced_at, _changed_at, _frozen) are always returned whatever you select, and naming one of them here is allowed.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from the previous page, sent back exactly as you received it.
orgstringNarrow to one organization by id.
rowsarrayRow objects matching the query.
next_cursorstring or nullThe bookmark for the next page. null means the last page.
schema_versionstringThe table's current schema version.
$ curl https://api.valtrix.com/v1/tables/customers_clean/rows \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "email=not.null" -d "order=created_at.desc" -d "limit=50"{"rows": [{ "email": "...", "created_at": "2026-07-01", "_record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "_org_id": "org_7f3k2m", "_connector": "procore", "_synced_at": "2026-07-08T09:30:00Z", "_changed_at": "2026-07-06T15:12:00Z", "_frozen": false }],"next_cursor": "eyJpIjoiMDFo...","schema_version": "v1"}
/v1/tables/:name/changesAn ordered, resumable feed of row changes, deletes included. Each change carries a cursor, a bookmark for its position in the feed; it is not the record id. Read from the last cursor you processed until the list comes back empty, which means you are caught up. The feed only moves when a sync of the organization's connections finishes or a record is written, so read it when something has changed rather than on a tight timer. Subscribe a webhook endpoint to table.changed and drain the feed from your stored cursor each time it fires; then reconcile from the same cursor on a slow schedule, every 15 to 60 minutes, to cover a delivery you missed. Without webhooks, poll no more often than every 15 minutes. A read that finds no changes, repeated with the same cursor less than 60 seconds after the first one that found none, returns 429 poll_too_frequent with a Retry-After header instead of an empty list. A read that finds changes is always served, so a webhook that fires right after an empty read, or a backlog being drained, is never held back. A change appears in the feed only once its write has committed, so a cursor you store never skips a change that was still being written when you read. Changes are retained 30 days, after which a stale cursor returns 410 cursor_expired.
namepathThe table name, as listed by /v1/tables.
cursorstringThe cursor of the last change you processed, or the last next_cursor. Omit on the first call to read from the start.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
orgstringNarrow to one organization by id.
changesarrayChange objects in feed order. Empty means you are caught up.
next_cursorstring or nullThe bookmark to resume from on your next poll. Null when this poll returned no changes: keep the last non-null cursor you received and poll with it again later, never store the null.
schema_versionstringThe table's current schema version.
$ curl https://api.valtrix.com/v1/tables/customers_clean/changes \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "cursor=eyJjIjoiODIzMSJ9" -d "limit=200"{"changes": [{ "type": "upsert", "cursor": "eyJjIjoiODIzMiJ9", "occurred_at": "2026-07-08T09:30:00Z", "record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "org_id": "org_7f3k2m", "changed_columns": ["status"], "previous": { "status": "under_review" }, "row": { ... } }],"next_cursor": "eyJjIjoiODIzMiJ9","schema_version": "v1"}
Records are the canonical store: every entity Valtrix syncs from an organization's connected systems, in the same shape for every connector, plus the records you write back through this API. Query them directly with GET /v1/records/:entityType when you want the canonical data as-is; your tables remain the curated layer on top, with your own column shapes, quality gates, and change feeds. Writes create records, not rows: each write flows through your published transformation, and the response reports the outcome per table as applied, removed, quarantined, filtered, or pending. Fields typed reference in the schema hold record ids and link entities without name matching: a project's customer_id is the id of a customer record, so resolve it with GET /v1/records/:recordId, filter the entity list with customer_id=eq.<id>, or batch the values into a _record_id=in.(a,b) filter on a customer table.
A record as Valtrix stores it, before transformation. Records synced from connected systems and records you write through this API share this shape.
idstringThe rec_ prefixed record ID. Table rows derived from this record carry it as their _record_id, and reference fields on other records hold it as their value, so either resolves back to this record through GET /v1/records/:recordId.
entity_typestringThe entity type slug. GET /v1/schema lists them.
external_idstringThe identifier the record was written under: yours for records written through this API, the source system's for synced records.
org_idstringThe organization the record belongs to.
connectorstringThe slug of the connector the record was synced from, so you can attribute it to its origin system. GET /v1/connectors resolves slugs to display names. Records you wrote through this API carry the API source.
dataobjectThe record's fields as written or synced, before your transformation runs. The fields each entity type carries are listed under Record entity types below.
connection_idstring or nullThe id of the connection the record lives in, matching an entry in the organization object's connections. Pass it as connection on a propagated write to address that system explicitly. null for records that exist only in Valtrix.
source_idstring or nullThe id the source system assigned to the record. For synced records it is the source's own id; for records you propagated it is filled once the write intent is applied, and null before then.
last_write_intentobject or nullThe most recent write intent on this record, as { id, status }, or null when nothing has been propagated. Fetch it with GET /v1/write-intents/:id to see the outcome and any source error.
synced_atstringWhen the record was last written or re-read from its source, as an ISO 8601 timestamp. It moves on every sync whether or not the data changed.
changed_atstringWhen the record's data last actually changed, as an ISO 8601 timestamp: the last sync or write that found different values. Stays put while synced_at keeps moving on unchanged re-reads. Counts from when Valtrix first synced the record.
frozenbooleantrue when the record is outside the connector's sync window and no longer refreshed. The data holds the values last fetched inside the window, and the record stays readable here and in your tables.
{"id": "rec_9d4f2c81a7b3e650d21c4f8a","entity_type": "customer","external_id": "customer_8f2","org_id": "org_7f3k2m","connector": "procore","connection_id": "conn_7f3k2m","source_id": "CR-88213","last_write_intent": null,"data": { "name": "...", "email": "..." },"synced_at": "2026-07-08T09:30:00Z","changed_at": "2026-07-06T15:12:00Z","frozen": false}
A source document Valtrix captured alongside a record while syncing, such as the scanned vendor bill behind a cost record. Documents arrive as the source system holds them, and many sources hold per-page image scans, so a multi-page upload can arrive as one attachment per page with the page number in the filename. Valtrix stores the document itself, so downloads keep working even when the source system is slow or unreachable.
idstringThe attachment ID. Pass it to the display link endpoint to show the document in your app, or to the download endpoint to fetch the bytes.
filenamestring or nullThe document filename when the source provided one. It names the original upload, so a filename ending in .pdf can belong to an image scan of one of its pages.
media_typestringThe MIME type of the stored document, such as application/pdf or image/jpeg. This is the authoritative format of the bytes; use it to pick the file extension when saving.
bytesnumberThe stored size in bytes.
content_hashstringSHA-256 hash of the document bytes. Two attachments with the same hash hold the same document, so compare hashes to skip downloads you already have.
created_atstringWhen Valtrix first stored the document, as an ISO 8601 timestamp.
{"id": "clx1a2b3c4d5e6f7g8h9i0j1k","filename": "invoice-4417.pdf (page 1)","media_type": "image/jpeg","bytes": 58369,"content_hash": "f3a91c0e7b2d...","created_at": "2026-07-10T04:00:00Z"}
/v1/records/:entityTypeQueries the canonical records of one entity type across every organization you hold an active grant for, synced and API-written alike. The same filter, order, select, and cursor grammar as table rows, applied to the entity's fields from GET /v1/schema. Each result is the entity's fields flat on the object plus meta fields, so a record here and a row from an untransformed table read the same way. Reference fields filter like ids, so following a reference is one query: collect customer_id values from one read and pass them as _record_id=in.(a,b) here, or filter this list by the reference column directly.
entityTypepathThe entity type slug. GET /v1/schema lists the valid ones.
<field>string, repeatableFilter as <op>.<value> with eq, neq, gt, gte, lt, lte, in.(a,b), is.null, and not.null, validated against the entity's field types.
_record_idstringPoint lookup by record ID, as eq.<id> or in.(a,b). Every id here is a rec_ prefixed record ID; there are no synthetic ids at the record layer.
_external_idstringPoint lookup by the record's id in its source system, as eq.<id> or in.(a,b). Every record carries it as _external_id: a connected system's own key for the row (an invoice's CFDI UUID, a vendor code) or the external_id an API write supplied. Exact match. The way to find the record behind an id you hold from the source system, since GET /v1/records/:entityType/:externalId covers API-written records only.
_changed_atstring, repeatableRange filter on when the record's data last actually changed, as gt., gte., lt., or lte. followed by an ISO 8601 timestamp. Combine two for a window. Deleted records never appear here, so this is a cutoff for one-off reads, not a way to stay in sync.
_synced_atstring, repeatableRange filter on when the record was last re-read from its source, with the same gt., gte., lt., and lte. forms as _changed_at. _synced_at=gte.<timestamp> is the same window as synced_since.
orderstringOne field as <field>.<asc|desc>. _synced_at and _changed_at are also orderable, and filterable by range.
selectstringComma separated field keys to return. The record's underscore envelope (_record_id, _external_id, _org_id, _connector, _connection_id, _source_id, _last_write_intent, _synced_at, _changed_at, _frozen) is always returned whatever you select, and naming one of those fields here is allowed.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from the previous page, sent back exactly as you received it.
orgstringNarrow to one organization by id.
connectorstringNarrow to records synced from one connector, by slug. GET /v1/connectors lists the valid ones. Records you wrote through this API carry the API source.
synced_sincestringOnly records written or synced at or after this ISO 8601 timestamp. A freshness window, not a change feed: deletions never appear here, so stay in sync through a table's change feed.
recordsarrayOne object per record: the entity's fields, plus _record_id, _external_id, _org_id, _connector, _connection_id, _source_id, _last_write_intent, _synced_at, _changed_at, and _frozen. The underscore fields are the record object's envelope, prefixed so they never collide with entity fields.
next_cursorstring or nullThe bookmark for the next page. null means the last page.
schema_versionstringThe entity schema version, as reported by GET /v1/schema.
$ curl https://api.valtrix.com/v1/records/customer \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "name=not.null" -d "order=_synced_at.desc" -d "connector=procore" -d "limit=50"{"records": [{ "name": "...", "email": "...", "_record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "_external_id": "customer_8f2", "_org_id": "org_7f3k2m", "_connector": "procore", "_connection_id": "conn_7f3k2m", "_source_id": "CR-88213", "_last_write_intent": null, "_synced_at": "2026-07-08T09:30:00Z", "_changed_at": "2026-07-06T15:12:00Z", "_frozen": false }],"next_cursor": "eyJpIjoiMDFo...","schema_version": "v1"}
/v1/records/:entityTypeCreates or updates a record, keyed by your external_id within the organization. Returns 201 on create, 200 on update, with the derived table rows. In tables that keep one row per record, the record id in the response is the same _record_id carried on that row. Aggregated tables report pending instead of a row: the write lands there through a rebuild, as changes to the bucket rows it feeds, under their own row ids. By default the write stays in Valtrix. Pass propagate: true to also push it into the organization's connected system: the record is still written immediately, the status becomes 202, and the response carries a write intent that tracks the push. The organization object's connections tell you which entity types and operations each connection can take. Each connection's write_guidance says what those writes take: the fields per operation with their meaning, the values a command field accepts and what each does, the documents an operation carries, and the connector's rules on refusals. Read it before the first propagated write to a system. How propagation works explains the lifecycle; send an Idempotency-Key on any write you might retry.
entityTypepathThe entity type slug. GET /v1/schema lists the valid ones.
Idempotency-Keyheader, optionalAny unique string, up to 255 characters. A repeat of the same key on this API key within 24 hours replays the original status and body instead of writing again, so a retried request never creates a second record or a second write intent. After 24 hours the key is forgotten and the same call writes again.
orgstring, requiredThe organization the record belongs to. The grant must cover this entity type.
external_idstring, requiredYour identifier, up to 255 characters. Same external_id updates the same record.
dataobject, requiredAn object of entity fields, under 100KB. Unknown fields are rejected. GET /v1/schema lists the writable fields. Updates merge: fields you omit keep their stored values, and sending null clears a field.
linesarray of line_item objects, optionalThe record's line items, written with it in the same call. Each entry is an object of line_item fields and becomes an ordinary line_item record, the same entity you read back from GET /v1/records/line_item; GET /v1/schema lists its writable fields. Accepted on contract, estimate, invoice, order, cost, and journal_entry records only (400 invalid_fields elsewhere, and for any unknown line field). The link to the parent record is stamped for you: document_entity the parent's entity (contract, cost, and so on), document_type its kind (purchase_order, bill, and so on), document_id its record id, position defaulting to the line's index, and external ids <external_id>/line/<position>. Sending lines replaces the record's previous lines. With propagate: true the header and its lines reach the source system in one write intent.
attachmentsarray of attachment inputs, optionalFiles written with the record, at most 10, each { "filename", "media_type", "content" } with content the file as a base64 string. media_type is one of application/pdf, application/xml, text/xml, image/png, image/jpeg, image/gif, image/webp, and the files must decode to at most 25 MB together (400 invalid_attachments otherwise, with fields naming each issue; 503 storage_unavailable when attachment storage is down). Each file becomes a record attachment, listed by GET /v1/records/:recordId/attachments and downloadable like a synced document; the same content sent again is stored once. With propagate: true the files reach the source system in the same write intent when the connector's write takes them, the way an invoice's XML and PDF are uploaded into a supplier portal.
propagatebooleanDefaults to false. When true, the write is also propagated to the organization's connected system and the response is 202 with a write intent. An external_id that a connected system already synced (its own id for the row, the _external_id of that synced record) makes the propagated write an update of that row on its connection; any other external_id creates the row at the source. Returns 409 write_not_supported when no connected system declares create or update for this entity type, 409 write_ambiguous when more than one does and connection is not given, and 409 write_fields_unsupported when the connection sends none of the fields in data on that operation, so the write would reach the source as nothing; fields the connection does not send are stored in Valtrix and listed under warnings.
connectionstringThe id of the connection to propagate to, from the organization object's connections or a record's connection_id. Only valid with propagate: true (400 invalid_propagation otherwise); 404 unknown_connection when it does not belong to the organization.
source_fieldsobjectConnector-specific SDK fields and operations, up to 100KB. Requires propagate: true and an SDK write connector. Sage Estimating accepts fields, references, estimate_id, phase_code, item_code, catalog_item_id, copy_from, collections, and lines. Options are stored on the write intent and do not become canonical columns. Unsupported connectors return 400 invalid_source_fields.
createdbooleantrue with status 201 on create, false with status 200 on update. With propagate: true the status is 202 either way.
recordobjectThe record object as stored.
warningsarrayPresent only when a field with a closed vocabulary received a value outside it, as [{ field, message }]. The write still lands, but filters written from the schema will not match that value, so switch to the canonical value the message names.
write_intentobjectThe queued write intent, present only with propagate: true, as { id, status, connection_id } with status pending. Poll GET /v1/write-intents/:id or listen for write_intent.applied and write_intent.failed to learn the outcome.
linesarray of record objectsPresent only when lines were sent: the line_item records written with this record, as full record objects in position order, each with document_entity, document_type, document_id, and position filled in. They are ordinary records from here on; read them back with GET /v1/records/line_item.
attachmentsarray of attachment objectsPresent only when attachments were sent: the stored attachment objects, in the order sent, each with its id, filename, media_type, bytes, and content_hash. Download them like any synced document.
tablesarrayThe write outcome per table: applied, quarantined, filtered, or pending, with the derived row where one exists.
$ curl -X POST https://api.valtrix.com/v1/records/customer \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "customer_8f2", "data": { "name": "...", "email": "..." } }'{"created": true,"record": { "id": "rec_9d4f2c81a7b3e650d21c4f8a", "entity_type": "customer", "external_id": "customer_8f2", "org_id": "org_7f3k2m", "connector": "valtrix-api", "connection_id": null, "source_id": null, "last_write_intent": null, "data": { ... }, "synced_at": "2026-07-08T09:30:00Z", "changed_at": "2026-07-08T09:30:00Z", "frozen": false },"tables": [{ "table": "customers_clean", "status": "applied", "row": { ... }, "quarantined_by": [] }]}
/v1/records/:recordIdFetches any record by its record ID, synced and API-written alike, with the current row it produces in each of your tables. The record ID is the rec_ prefixed id you see everywhere: the _record_id on a table row, a reference field's value, or the id of a record object. Rows from aggregating or reshaping transformations carry syn_ prefixed synthetic row ids with no single record behind them; those return 404 record_not_found. A table's row_identity field tells you upfront whether its rows resolve.
recordIdpathThe rec_ prefixed record ID, from a row's _record_id, a reference field, or a record object's id.
recordobjectThe record object. Its entity_type tells you what kind of record the ID resolved to, so you can follow a reference field without knowing its target type upfront.
tablesarrayThe current row it produces in each of your tables.
attachmentsarrayThe source documents Valtrix captured alongside the record, as attachment objects. Empty for most records.
$ curl https://api.valtrix.com/v1/records/rec_9d4f2c81a7b3e650d21c4f8a \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "record": { "id": "rec_9d4f2c81a7b3e650d21c4f8a", "entity_type": "customer", "external_id": "customer_8f2", "org_id": "org_7f3k2m", "connector": "procore", "connection_id": "conn_7f3k2m", "source_id": "CR-88213", "last_write_intent": null, "data": { ... }, "synced_at": "2026-07-08T09:30:00Z", "changed_at": "2026-07-06T15:12:00Z", "frozen": false }, "tables": [{ "table": "customers_clean", "row": { ... } }], "attachments": [] }
/v1/records/:entityType/:externalIdFetches a record by the external_id you supplied when writing it, with the current row it produced in each table. Covers records written through this API only; synced records are fetched by record ID. A null row means it was filtered out or quarantined.
entityTypepathThe entity type slug. GET /v1/schema lists the valid ones.
externalIdpathThe external_id you supplied when writing the record.
orgstring, requiredThe organization the record belongs to.
recordobjectThe record object.
tablesarrayThe current row the record produces in each table. A null row means it was filtered out or quarantined.
attachmentsarrayThe record's attachment objects. Records written through this API carry no documents, so this is empty today.
$ curl "https://api.valtrix.com/v1/records/customer/customer_8f2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "record": { "id": "rec_9d4f2c81a7b3e650d21c4f8a", "entity_type": "customer", "external_id": "customer_8f2", "org_id": "org_7f3k2m", "connector": "valtrix-api", "connection_id": null, "source_id": null, "last_write_intent": null, "data": { ... }, "synced_at": "2026-07-08T09:30:00Z", "changed_at": "2026-07-08T09:30:00Z", "frozen": false }, "tables": [{ "table": "customers_clean", "row": { ... } }], "attachments": [] }
/v1/records/:entityType/:externalIdDeletes a record you wrote and removes its derived rows. The removals also appear in each change feed. Pass propagate=true as a query parameter to also delete it from the organization's connected system: the Valtrix record is removed immediately, the status becomes 202, and the response carries a write intent. How propagation works explains the lifecycle.
entityTypepathThe entity type slug. GET /v1/schema lists the valid ones.
externalIdpathThe external_id you supplied when writing the record.
Idempotency-Keyheader, optionalAny unique string, up to 255 characters. A repeat of the same key on this API key within 24 hours replays the original status and body instead of deleting again. After 24 hours the key is forgotten and the same call deletes again.
orgstring, requiredThe organization the record belongs to.
propagatequery, optionalDefaults to false. When true, the delete is also propagated to the organization's connected system and the response is 202 with a write intent. Returns 409 write_not_supported when no connected system declares delete for this entity type, and 409 write_ambiguous when more than one does and connection is not given.
connectionquery, optionalThe id of the connection to propagate to, from the organization object's connections or the record's connection_id. Only valid with propagate=true (400 invalid_propagation otherwise); 404 unknown_connection when it does not belong to the organization.
deletedbooleantrue when the record and its derived rows were removed. With propagate=true the status is 202.
write_intentobjectThe queued write intent, present only with propagate=true, as { id, status, connection_id } with status pending. Poll GET /v1/write-intents/:id or listen for write_intent.applied and write_intent.failed to learn the outcome.
tablesarrayThe removal outcome per table.
$ curl -X DELETE "https://api.valtrix.com/v1/records/customer/customer_8f2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "deleted": true, "tables": [{ "table": "customers_clean", "status": "removed", "row": null, "quarantined_by": [] }] }
/v1/records/:recordId/attachmentsLists the source documents Valtrix captured alongside a record, such as the scanned vendor bill behind a cost record. Attachments exist where the source system exposes documents and the connector syncs them; most records have none and return an empty list. The list mirrors what is attached in the source system, including repeat uploads of the same document, and a multi-page document can appear as one attachment per page. The record ID is the rec_ prefixed id from a row's _record_id, a reference field, or a record object's id. The response carries metadata only; show the document through the display link endpoint or fetch its bytes through the download endpoint.
recordIdpathThe rec_ prefixed record ID.
attachmentsarrayThe record's attachment objects, oldest first.
$ curl https://api.valtrix.com/v1/records/rec_9d4f2c81a7b3e650d21c4f8a/attachments \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "attachments": [{ "id": "clx1a2b3c4d5e6f7g8h9i0j1k", "filename": "invoice-4417.pdf (page 1)", "media_type": "image/jpeg", "bytes": 58369, "content_hash": "f3a91c...", "created_at": "2026-07-10T04:00:00Z" }, { "id": "clx9z8y7x6w5v4u3t2s1r0q9p", "filename": "invoice-4417.pdf (page 2)", "media_type": "image/jpeg", "bytes": 61204, "content_hash": "7e167d...", "created_at": "2026-07-10T04:00:01Z" }] }
/v1/attachments/:attachmentId/urlReturns a short lived link to the document behind an attachment, for showing it inside your app. By default the link serves the document inline with its media type, so it works as the source of an image tag, an iframe, or a PDF viewer. Your backend requests the link and hands it to your frontend; the API key never reaches the browser. Each link expires within minutes, so request a fresh one per view rather than storing it. Treat the link as an opaque value: the storage host and URL format are not part of the API contract and can change, so embed the link exactly as returned rather than parsing it or pinning its domain.
attachmentIdpathThe attachment ID, from the record's attachment list.
dispositionstringinline (default) serves the document for viewing in the browser; attachment serves it as a file download with the original filename.
urlstringThe short lived link to the document. Requires no authentication, so it can be embedded directly. Opaque, its host and format can change between requests.
expires_innumberSeconds until the link stops working.
$ curl https://api.valtrix.com/v1/attachments/clx1a2b3c4d5e6f7g8h9i0j1k/url \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "url": "https://valtrix-attachments.s3.amazonaws.com/...&X-Amz-Signature=...", "expires_in": 300 }
/v1/attachments/:attachmentId/downloadFetches the document behind an attachment. The endpoint responds 302 with a short lived download link, and standard HTTP clients follow it automatically and receive the bytes. Each link expires within minutes, so request a fresh download per fetch rather than storing the link. Store the content_hash from the attachment list if you need to know whether you already hold a document. To show a document inside your app rather than fetch its bytes, use the display link endpoint.
attachmentIdpathThe attachment ID, from the record's attachment list.
A 302 redirect to a short lived download link for the document. Following it returns the raw bytes with the attachment's media type.
$ curl -L https://api.valtrix.com/v1/attachments/clx1a2b3c4d5e6f7g8h9i0j1k/download \-H "Authorization: Bearer $VALTRIX_API_KEY" \-o invoice-4417-page-1.jpg
The entity types accepted by the record endpoints, with the fields each one takes. These fields make up a record's data object; every record additionally carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at), so no entity declares an id field. The envelope id is the record ID that table rows expose as _record_id and that reference fields on other entities point at. GET /v1/schema returns these same definitions, so your integration can read them at runtime.
activityA logged interaction or note in the sales process: calls, emails, meetings, notes, and stage moves all map here. What it was logged on is polymorphic (subject_id/subject_type: opportunity, customer, contact, or project; reviewer comments and status changes on a permit or job log against the project). Scheduled future work maps to schedule_task; time worked maps to time_entry.
Writable through POST /v1/records/activity. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/activity/:externalId for records you wrote. The title key is note.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
notestringWhat happened
kindstringOne of: call, email, meeting, note, task, stage_change, other. Map the source activity types onto these; anything else is other with the raw label in source data
subject_idreferenceValtrix record ID of the record the activity was logged on
subject_typestringEntity type of the linked subject record: opportunity, customer, contact, or project
customer_idreferenceValtrix record ID of the customer the entry concerns when the subject is something of theirs (a project, an opportunity, an admission); a clinical entry charted on an admission links the admission as subject and the patient here. Sources whose subject is the customer itself leave it empty
occurred_atdateWhen the activity happened
created_atdateWhen the activity was logged in the source
categorystringThe source's own type or form name for the entry (a note type, an assessment tool, a problem list, a vitals reading), as it labels it; source-defined, pass it through
codestringA coded value the entry carries, as the source prints it (an ICD-10 diagnosis code, a procedure code), for clinical and coded sources
scorenumberThe numeric result the entry records, for scored assessments and measurements
outcomestringThe source's own status or interpretation of the entry (a problem status, an assessment interpretation, an allergy status), as it labels it; source-defined, pass it through
authorstringName of the person who recorded or signed the entry, for sources that keep one on it
$ curl -X POST https://api.valtrix.com/v1/records/activity \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "note": "...", "kind": "...", "subject_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/activity/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
bookingA customer's reservation of an event spot, a staff member's time, or a resource: class registrations, appointments, court hires, and desk bookings all map here. Group occurrences link via event_id; the purchase paying for it maps to order and a consumed pack credit to entitlement.
Writable through POST /v1/records/booking. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/booking/:externalId for records you wrote. The title key is customer_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
customer_namestringCustomer the booking is for
customer_idreferenceValtrix record ID of the customer the booking is for
event_idreferenceValtrix record ID of the event the booking reserves a spot in, for group occurrences
resource_idreferenceValtrix record ID of the resource the booking reserves, such as a court, room, or desk
employee_idreferenceValtrix record ID of the staff member the booking is with, such as the practitioner or stylist
item_idreferenceValtrix record ID of the catalog item for the booked service
location_idreferenceValtrix record ID of the location the booking takes place at
order_idreferenceValtrix record ID of the order that paid for the booking
entitlement_idreferenceValtrix record ID of the entitlement the booking consumed a credit from
statusstringOne of: booked, confirmed, attended, completed, no_show, cancelled, waitlisted. Map the source booking states onto these (an unconfirmed booking is booked)
start_atdateWhen the booking starts
end_atdateWhen the booking ends
created_atdateWhen the booking was made in the source
$ curl -X POST https://api.valtrix.com/v1/records/booking \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "customer_name": "...", "status": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/booking/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
budget_changeAn internal budget adjustment or transfer that moves budget between lines without a client-facing change order. Contractual changes map to change_order.
Writable through POST /v1/records/budget_change. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/budget_change/:externalId for records you wrote. The title key is description.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringBudget change number
titlestringBudget change title
descriptionstringWhat the budget change covers
statusstringOne of: draft, pending, approved, rejected, void. Map the source approval states onto these; the raw label stays in source data
amountnumberNet amount of the change
project_idreferenceValtrix record ID of the project the budget change belongs to
created_atdateWhen the budget change was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/budget_change \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "description": "...", "number": "...", "project_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/budget_change/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
budget_detailA computed budget report row per cost code: original, changes, committed, costs to date, projected, and over/under. Read-side rollup only; the budget's raw composition maps to budget_line.
Writable through POST /v1/records/budget_detail. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/budget_detail/:externalId for records you wrote. The title key is cost_code.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
cost_codestringCost code the row is budgeted against
cost_code_idreferenceValtrix record ID of the cost code the row is budgeted against
categorystringCost category or division as the source names it; source-defined, pass it through
project_idreferenceValtrix record ID of the project the row is budgeted against
original_amountnumberOriginal budgeted amount
budget_changesnumberApproved budget changes
approved_cosnumberApproved change orders
pending_cosnumberPending change orders
revised_amountnumberBudget after approved changes
committed_costsnumberCommitted costs
direct_costsnumberDirect costs to date
jtd_costsnumberJob-to-date costs
projected_costsnumberProjected total costs
estimated_finalnumberEstimated cost at completion
over_undernumberProjected over or under budget
$ curl -X POST https://api.valtrix.com/v1/records/budget_detail \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "cost_code": "...", "category": "...", "cost_code_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/budget_detail/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
budget_lineA single budgeted row of a project budget: cost code plus original and revised amounts. This is the budget's composition; computed report rollups with committed, to-date, and projected columns map to budget_detail.
Writable through POST /v1/records/budget_line. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/budget_line/:externalId for records you wrote. The title key is description.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
project_namestringProject or job the line is budgeted against
project_idreferenceValtrix record ID of the project the line is budgeted against
cost_codestringCost code the line is budgeted against
cost_code_idreferenceValtrix record ID of the cost code the line is budgeted against
descriptionstringWhat the budget line covers
categorystringCost category or division as the source names it; source-defined, pass it through
cost_typestringCost type as the source names it (labor, material, subcontractor); source-defined, pass it through
quantitynumberBudgeted quantity
unit_costnumberBudgeted cost per unit
original_amountnumberOriginal budgeted amount
revised_amountnumberRevised budget after approved changes
document_typestringOne of: bill, credit, expense, payroll, card_transaction, invoice, order, estimate, bid_request, prime_contract, subcontract, purchase_order, work_order, requisition, change_order, journal_entry, claim, treatment_plan, other. The canonical kind of the document the line belongs to
document_statusstringOne of: draft, pending, approved, rejected, void. Approval status of the source document the line belongs to, mapped the same way as that document
created_atdateWhen the budget line was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/budget_line \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "description": "...", "project_name": "...", "project_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/budget_line/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
change_eventAn early-stage record that something changed on a project, captured before or without a priced change order. Once priced and contractual it maps to change_order or commitment_change_order.
Writable through POST /v1/records/change_event. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/change_event/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringChange event number
titlestringChange event title
descriptionstringWhat changed and why
scopestringOne of: in_scope, out_of_scope, tbd. Whether the change is in or out of the contracted scope
statusstringOne of: open, pending, closed, void. Map the source change event states onto these (awaiting pricing or sent to client is pending)
change_typestringKind of change as the source names it (owner change, design change, weather); source-defined, pass it through
change_reasonstringReason for the change as the source names it; source-defined, pass it through
change_order_idreferenceValtrix record ID of the change order the event became once priced
change_order_typestringEntity type of the linked change order record: change_order or commitment_change_order
project_idreferenceValtrix record ID of the project the change event belongs to
created_atdateWhen the change event was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/change_event \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "number": "...", "change_order_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/change_event/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
change_orderA priced amendment to a client-facing contract, at the header level: construction prime contract change orders, scope or SOW amendments, and lease amendments all land here. Changes against a purchase order or subcontract map to commitment_change_order; unpriced early-stage changes map to change_event.
Writable through POST /v1/records/change_order. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/change_order/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringChange order number
titlestringChange order title
statusstringOne of: draft, pending, approved, rejected, void. Map the source approval states onto these (pricing and review states are pending); the raw label stays in source data
totalnumberTotal value of the change
contract_idreferenceValtrix record ID of the client-facing contract the change order belongs to
project_idreferenceValtrix record ID of the project the change order belongs to
executedbooleanWhether the change order is executed
created_atdateWhen the change order was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/change_order \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "number": "...", "contract_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/change_order/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
commitment_change_orderA priced amendment to a money-out commitment, linked to its parent via contract_id: subcontract change orders and post-issue purchase order revisions land here. Client-facing changes map to change_order.
Writable through POST /v1/records/commitment_change_order. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/commitment_change_order/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringChange order number
titlestringChange order title
statusstringOne of: draft, pending, approved, rejected, void. Map the source approval states onto these; the raw label stays in source data
totalnumberTotal value of the change
contract_idreferenceValtrix record ID of the commitment contract the change order belongs to
project_idreferenceValtrix record ID of the project the change order belongs to
executedbooleanWhether the change order is executed
created_atdateWhen the change order was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/commitment_change_order \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "number": "...", "contract_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/commitment_change_order/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
compliance_documentA signed instrument a party must provide before something else may proceed: lien waivers, insurance certificates, tax forms, liability and consent waivers, and staff certifications all collapse here, discriminated by kind. This is the tracked obligation, with its own signature lifecycle, validity window, and signatory; the rendered file rides along as a record attachment. What it gates is identified by subject_type and subject_id, so one shape covers a waiver blocking payment of a vendor invoice (cost), a certificate held against a subcontract or membership (contract), and a liability or consent form a customer signs before a booking. A standing credential belonging to the signing party rather than to any one document leaves subject_id null and is reached through counterparty_id.
Writable through POST /v1/records/compliance_document. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/compliance_document/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
titlestringName of the form or template the document was raised from
kindstringOne of: lien_waiver, insurance_certificate, tax_form, liability_waiver, consent_form, certification, other. Map the source document types onto these
statusstringOne of: pending, signed, declined, expired, released. Where the document sits in its signature lifecycle
subject_typestringEntity type of the record the document gates: cost, contract, or booking
subject_idreferenceValtrix record ID of the record this signature gates, null for a standing credential held against the counterparty itself
counterpartystringVendor, customer, or employee required to sign
counterparty_idreferenceValtrix record ID of the party required to sign
counterparty_typestringEntity type of the linked counterparty record: vendor, customer, or employee
project_idreferenceValtrix record ID of the project the document belongs to
location_idreferenceValtrix record ID of the location the document belongs to, for sources that hold compliance against a venue or site rather than a project
amountnumberAmount the document covers, which may be a partial release against the gated record total
effective_atdateDate the document takes effect from
expires_atdateDate the document lapses and must be renewed, for coverage and certifications that expire
signed_atdateWhen the counterparty signed, null while the document is unsigned
signed_by_namestringName of the individual who signed on the counterparty side
signed_by_titlestringRole or title of the individual who signed
$ curl -X POST https://api.valtrix.com/v1/records/compliance_document \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "kind": "...", "subject_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/compliance_document/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
contactAn individual person at a customer, vendor, or other external party, with their own name and contact details. Not the account itself (customer, vendor) and not the org's own workforce (employee).
Writable through POST /v1/records/contact. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/contact/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringContact name
titlestringRole or title
party_idreferenceValtrix record ID of the customer or vendor the contact belongs to
party_typestringEntity type of the linked party record: customer or vendor
emailstringPrimary email address
phonestringPrimary phone number
created_atdateWhen the contact was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/contact \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "title": "...", "party_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/contact/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
contractAn agreement with money attached, with either side of the business: prime contracts, subcontracts, purchase orders, work orders, memberships, subscriptions, and leases all collapse here, discriminated by kind. Their lines map to line_item and their changes to change_order or commitment_change_order.
Writable through POST /v1/records/contract. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/contract/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringContract number
titlestringContract title
kindstringOne of: prime, subcontract, purchase_order, membership, subscription, lease, other. Map the source agreement types onto these
counterpartystringCustomer or vendor the agreement is with
counterparty_idreferenceValtrix record ID of the customer or vendor the agreement is with
counterparty_typestringEntity type of the linked counterparty record: customer or vendor
project_idreferenceValtrix record ID of the project the contract belongs to
statusstringOne of: draft, pending, sent, approved, rejected, fulfilled, completed, void. Map the source contract and purchase order states onto these (out for signature or awaiting approval is pending, issued to the counterparty is sent, executed is approved, received in full is fulfilled); the raw label stays in source data
totalnumberTotal contract value
retainage_percentnumberRetainage percentage withheld
executedbooleanWhether the contract is executed
contract_atdateDate of the agreement
starts_atdateWhen the agreement takes effect (a membership start, a lease commencement, a subscription start)
ends_atdateWhen the agreement ends or expires
originstringOne of: new, renewal, reactivation, upgrade, transfer, other. How the agreement came about, for recurring client agreements whose source labels a first sale, a renewal, or a win-back
categorystringThe plan, product line, or agreement category the source files it under; source-defined, pass it through
sold_bystringName of the staff member who made the sale
commission_agentstringName of the staff member credited with the commission when the source tracks one separately from the seller
resource_idreferenceValtrix record ID of the piece of equipment the agreement is for, such as the machine a parts order or rental agreement was raised against
maintenance_order_idreferenceValtrix record ID of the maintenance work order or request a parts order was raised under
needed_bydateWhen the goods or work ordered are needed by
$ curl -X POST https://api.valtrix.com/v1/records/contract \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "number": "...", "counterparty_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/contract/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
costA money-out document at the header level: vendor invoices, expenses, payroll, and other direct costs all collapse here, discriminated by kind. Its lines map to line_item; the transaction settling it maps to payment, and when one payment settles several costs each share is a payment_allocation. Payroll costs carry per-department allocation lines: line_item rows with document_type payroll and the department column filled.
Writable through POST /v1/records/cost. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/cost/:externalId for records you wrote. The title key is description.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
descriptionstringWhat the cost covers
kindstringOne of: bill, credit, expense, payroll, card_transaction, other. A credit is a vendor credit or credit note that reduces what is owed (total stays positive); a vendor invoice is a bill
statusstringOne of: draft, pending, approved, rejected, paid, void. Map the source approval and payment states onto these (under review is pending, denied is rejected, cancelled is void, payment sent is paid); the raw label stays in source data
invoice_numberstringVendor invoice reference
payee_namestringVendor or employee paid
payee_idreferenceValtrix record ID of the vendor or employee paid
payee_typestringEntity type of the linked payee record: vendor or employee
project_idreferenceValtrix record ID of the project the cost belongs to
location_idreferenceValtrix record ID of the location the cost belongs to, for sources that book costs against a venue or site rather than a project
totalnumberTotal amount, in currency
currencystringISO currency code the total is denominated in, null when the source books everything in one implicit currency
issued_atdateThe bill or invoice date stated by the vendor on the document
due_atdateWhen payment is due
received_atdateWhen the cost was received
paid_atdateWhen the cost was paid
po_numberstringPurchase order number the cost was raised against, as the source prints it, for sources that match bills to purchase orders
subtotalnumberAmount before tax, in currency, for sources that print it separately from the total
taxnumberTax charged on the document, in currency, for sources that print it separately from the total
cost_centerstringCost center, business unit, or organization the cost is booked to, as the source names it; source-defined, pass it through
review_statusstringThe source's own review or audit state of the supporting document (audited, awaiting upload, rejected, cancelled), as it labels it; source-defined, pass it through. Distinct from status, which is the normalized lifecycle
match_statusstringThe source's purchase-order match result for the bill (matched, invalid, unmatched), as it labels it; source-defined, pass it through
resource_idreferenceValtrix record ID of the piece of equipment the cost was incurred on, for repair and service costs booked against a unit
maintenance_order_idreferenceValtrix record ID of the maintenance work order the cost was booked under
financial_account_idreferenceValtrix record ID of the card or bank instrument the cost was paid with at the moment it was incurred, for card transactions and other expenses settled on an instrument rather than through a later payment
employee_idreferenceValtrix record ID of the employee who incurred or submitted the cost: the cardholder of a card transaction, the submitter of an expense, for spend and expense sources that track who spent. Distinct from payee_id, which is who gets paid
$ curl -X POST https://api.valtrix.com/v1/records/cost \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "description": "...", "kind": "...", "payee_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/cost/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
cost_codeAn entry in the org's cost code structure used to code budgets, costs, and time. Structural reference data, not a money document.
Writable through POST /v1/records/cost_code. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/cost_code/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
codestringFull cost code
namestringCost code name
statusstringOne of: active, inactive
$ curl -X POST https://api.valtrix.com/v1/records/cost_code \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "code": "..." } }'$ curl "https://api.valtrix.com/v1/records/cost_code/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
cost_transactionOne actual cost posted to a project's job cost ledger: the line a bill, payroll run, equipment allocation, inventory issue, or journal entry books against a cost code and cost type, with the amount and hours. This is the ledger every job cost report is built from. The documents that produce postings stay where they are (cost, journal_entry) and are linked via document_type and document_id when synced; budget_detail is the per-cost-code rollup of these postings, and budget_line is what they are measured against.
Writable through POST /v1/records/cost_transaction. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/cost_transaction/:externalId for records you wrote. The title key is description.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
descriptionstringWhat the posting covers, as the source shows it
numberstringSource transaction or reference number the posting came from: the invoice, check, timecard, or journal number
source_kindstringOne of: bill, card_transaction, payroll, equipment, inventory, journal, other. What produced the posting; map the source screen or transaction type onto these and keep the raw label in source data
document_typestringEntity type of the synced document that produced the posting: cost or journal_entry
document_idreferenceValtrix record ID of the cost or journal entry that produced the posting, typed by document_type; null when the producing document is not synced
project_namestringProject or job the cost was posted to
project_idreferenceValtrix record ID of the project the cost was posted to
cost_codestringCost code the posting is coded against
cost_code_idreferenceValtrix record ID of the cost code the posting is coded against
cost_typestringCost type as the source names it (material, labor, equipment, subcontract, other); source-defined, pass it through
vendor_namestringVendor the cost was incurred with, for postings that came from a bill, card charge, or inventory receipt
vendor_idreferenceValtrix record ID of the vendor the cost was incurred with
employee_namestringEmployee whose labor the posting records, for postings that came from payroll or a timecard
employee_idreferenceValtrix record ID of the employee whose labor the posting records
location_idreferenceValtrix record ID of the location the cost was posted to, for sources that book costs against a venue or site rather than a project
amountnumberCost amount posted, negative for a credit or reversal
hoursnumberLabor or equipment hours the posting records, zero when the source posts none
quantitynumberUnits the posting records where the source counts them (pieces, equipment units); null otherwise
billablebooleanWhether the posting can be billed to the customer; false for non-billable postings and write-offs
currencystringISO currency code the amount is denominated in, null when the source books everything in one implicit currency
transaction_atdateAccounting date of the posting as the source books it
created_atdateWhen the posting was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/cost_transaction \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "description": "...", "number": "...", "document_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/cost_transaction/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
coverageOne insurance policy a customer holds with a payer: the member and group numbers, its rank among the customer's policies, and who holds it. The carrier itself maps to payer; a claim filed under the policy maps to invoice with its lines as line_item.
Writable through POST /v1/records/coverage. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/coverage/:externalId for records you wrote. The title key is payer_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
customer_idreferenceValtrix record ID of the customer the policy covers
payer_namestringCarrier or plan the policy is with
payer_idreferenceValtrix record ID of the carrier or plan the policy is with
rankstringOne of: primary, secondary, tertiary, other. The order the policy is billed in; map the source positions onto these
member_numberstringMember, subscriber, or policy number
group_numberstringGroup or plan number
policy_holderstringWho holds the policy, as the source records it: a name, or Self when the customer is the holder
policy_holder_birthdaydateDate of birth of the policy holder, for sources that keep one on the policy
copaynumberCopay due per visit under the policy, in currency
authorization_codestringAuthorization or pre-approval code on file for the policy
statusstringOne of: active, inactive. A terminated, replaced, or hidden policy is inactive
starts_atdateWhen the policy takes effect
ends_atdateWhen the policy ends
created_atdateWhen the policy was recorded in the source
$ curl -X POST https://api.valtrix.com/v1/records/coverage \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "payer_name": "...", "rank": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/coverage/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
customerA person or company the org sells to or performs work for, at the account level. Individual people at that account map to contact; parties the org buys from map to vendor.
Writable through POST /v1/records/customer. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/customer/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringFull customer name
emailstringPrimary email address
phonestringPrimary phone number
countrystringCountry code or name
statusstringOne of: prospect, active, inactive. Map the source lifecycle (archived, disabled, deleted, lead) onto these; the raw label stays in source data
created_atdateWhen the customer was created in the source
birthdaydateDate of birth, for sources that keep one on the customer (a member, a patient, a client)
genderstringOne of: female, male, other. Map the source labels onto these; the raw label stays in source data
addressstringStreet address as one line
citystringCity, town, or district of the address
regionstringState, province, or region of the address, as the source names it
marital_statusstringMarital or civil status as the source labels it; source-defined, pass it through
emergency_contactstringEmergency contact as one line (name and phone), for sources that keep one on the customer
ownerstringName of the staff member who owns the relationship: the assigned account executive, sales representative, or coach
email_opt_inbooleanWhether the customer accepts marketing email, as the source records the consent
$ curl -X POST https://api.valtrix.com/v1/records/customer \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "email": "..." } }'$ curl "https://api.valtrix.com/v1/records/customer/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
employeeA member of the org's own workforce, at the company level. A person's membership on a specific project maps to project_user, and their logged time maps to time_entry.
Writable through POST /v1/records/employee. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/employee/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringFull name
emailstringWork email address
phonestringPrimary phone number
job_titlestringRole within the company
departmentstringDepartment, team, or division the employee belongs to, as the source names it; source-defined, pass it through
annual_salarynumberAnnual base salary or salary-equivalent compensation, in the source currency
employee_idstringInternal employee number
start_datedateWhen the employee started at the company, the hire date in the source system
statusstringOne of: active, inactive. Map terminated, archived, or disabled employees to inactive
created_atdateWhen the employee was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/employee \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "email": "..." } }'$ curl "https://api.valtrix.com/v1/records/employee/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
entitlementA customer's prepaid or granted balance of visits, credits, or time: class packs, punch cards, session credits, and membership allowances all map here. The purchase creating it maps to order, the agreement granting it maps to contract, and each redemption is a booking holding entitlement_id.
Writable through POST /v1/records/entitlement. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/entitlement/:externalId for records you wrote. The title key is item_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
item_namestringCatalog item the entitlement was granted from
item_idreferenceValtrix record ID of the item the entitlement was granted from
customer_idreferenceValtrix record ID of the customer holding the entitlement
contract_idreferenceValtrix record ID of the membership or subscription agreement granting the entitlement
order_idreferenceValtrix record ID of the order that purchased the entitlement
kindstringOne of: visits, credits, minutes, currency. What the balance counts
quantitynumberQuantity granted
remainingnumberQuantity remaining
statusstringOne of: active, expired, exhausted, inactive. Map the source entitlement states onto these
starts_atdateWhen the entitlement becomes usable
expires_atdateWhen the entitlement expires
created_atdateWhen the entitlement was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/entitlement \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "item_name": "...", "kind": "...", "item_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/entitlement/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
estimateA priced proposal issued before any agreement exists: quotes, bids, estimates, and proposals all map here. Its lines map to line_item; once accepted, the resulting agreement maps to contract (linked back via contract_id) and the resulting bill maps to invoice.
Writable through POST /v1/records/estimate. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/estimate/:externalId for records you wrote. The title key is number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringEstimate or quote number
titlestringEstimate title
customer_namestringCustomer the estimate was issued to
customer_idreferenceValtrix record ID of the customer the estimate was issued to
project_idreferenceValtrix record ID of the project the estimate belongs to
contract_idreferenceValtrix record ID of the contract the estimate became once accepted
statusstringOne of: draft, sent, pending, approved, rejected, expired, void. Map the source estimate and proposal states onto these; the raw label stays in source data
totalnumberTotal proposed amount
currencystringISO currency code
issued_atdateWhen the estimate was issued
expires_atdateWhen the estimate expires
created_atdateWhen the estimate was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/estimate \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "number": "...", "title": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/estimate/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
eventA scheduled occurrence customers book into: group classes, courses, workshops, and open sessions, with capacity and start and end times. Each attendee's spot maps to booking; one-on-one reservations map straight to booking without an event; project schedule rows map to schedule_task.
Writable through POST /v1/records/event. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/event/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringEvent name
kindstringOne of: class, course, workshop, other. Map the source occurrence types onto these
item_idreferenceValtrix record ID of the catalog item for the service the event delivers
employee_idreferenceValtrix record ID of the employee leading the event, such as the instructor or teacher
resource_idreferenceValtrix record ID of the resource the event occupies, such as a room or court
location_idreferenceValtrix record ID of the location the event takes place at
capacitynumberMaximum number of bookings
booked_countnumberNumber of spots taken
statusstringOne of: scheduled, cancelled, completed. Map the source event states onto these
start_atdateWhen the event starts
end_atdateWhen the event ends
created_atdateWhen the event was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/event \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "...", "item_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/event/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
financial_accountA bank, card, or cash account the org moves money through, discriminated by kind: the payment instrument a payment is released from or received into, not the ledger account it posts to. gl_account_id, with gl_account_number and gl_account_name, carries the chart-of-accounts account the source maps the instrument to, so a payment can be traced from the account that released it to the ledger balance it reduces. Chart-of-accounts entries themselves map to gl_account.
Writable through POST /v1/records/financial_account. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/financial_account/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringAccount name as the source labels it
kindstringOne of: bank, card, cash, other. Instrument classification
subtypestringFiner-grained account type as the source names it (checking, savings, business); source-defined, pass it through
maskstringLast digits of the account number, never the full number
statusstringOne of: active, inactive
location_idreferenceValtrix record ID of the location the account is set up for, for sources that bank per venue or site
gl_account_numberstringNumber of the chart-of-accounts account the source maps the instrument to, as the source records the mapping
gl_account_namestringName of the chart-of-accounts account the source maps the instrument to
gl_account_idreferenceValtrix record ID of the mapped chart-of-accounts account, when that account is synced
$ curl -X POST https://api.valtrix.com/v1/records/financial_account \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "...", "location_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/financial_account/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
gl_accountAn entry in the org's chart of accounts used to code money movements in accounting sources. Structural reference data, not a money document; project-coding structures map to cost_code instead.
Writable through POST /v1/records/gl_account. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/gl_account/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringAccount number in the chart of accounts
namestringAccount name
kindstringOne of: asset, liability, equity, income, expense. Account classification
subtypestringFiner-grained account type as the source names it (accounts receivable, fixed asset, cost of goods sold); source-defined, pass it through
currencystringISO currency code
statusstringOne of: active, inactive
$ curl -X POST https://api.valtrix.com/v1/records/gl_account \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "number": "..." } }'$ curl "https://api.valtrix.com/v1/records/gl_account/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
inspectionA filed inspection of one piece of equipment against a checklist form, or one that was due and never filed: driver vehicle inspection reports, pre-use and daily walkarounds, and mechanic inspections all collapse here, discriminated by kind. The equipment maps to resource (linked via resource_id); a defect that becomes work maps to maintenance_order. A government building or permit inspection is not this: it stays on the project as schedule_task or activity.
Writable through POST /v1/records/inspection. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/inspection/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringName of the inspection form or checklist that was filed
kindstringOne of: dvir, inspection. A driver vehicle inspection report filed for regulatory compliance is dvir; every other checklist is inspection
statusstringOne of: completed, missed. A filed inspection is completed; one the source reports as due and not filed is missed
outcomestringOne of: pass, fail, other. The overall result of a filed inspection; a pass with noted defects is other. Null on a missed inspection
resource_namestringName of the piece of equipment inspected
resource_idreferenceValtrix record ID of the piece of equipment inspected
location_idreferenceValtrix record ID of the site or yard the equipment was at
inspector_namestringWho filed the inspection, or who it was assigned to when missed
inspector_idreferenceValtrix record ID of the employee who filed the inspection or was assigned it
meter_hoursnumberEngine or run hours recorded on the inspection
odometer_milesnumberOdometer reading in miles recorded on the inspection; convert a source that reports kilometres
latitudenumberLatitude where the inspection was filed
longitudenumberLongitude where the inspection was filed
commentsstringThe inspector's overall comments
due_atdateWhen the inspection was due, for sources that schedule them
started_atdateWhen the inspector started the checklist
completed_atdateWhen the inspection was filed
created_atdateWhen the record was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/inspection \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "...", "resource_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/inspection/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
inventory_levelThe stock position of one item at one location: quantity on hand as of the source's latest count or running balance. A point-in-time read model; the movements that produce it map to inventory_movement.
Writable through POST /v1/records/inventory_level. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/inventory_level/:externalId for records you wrote. The title key is item_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
item_namestringItem the level is for
item_idreferenceValtrix record ID of the item the level is for
location_idreferenceValtrix record ID of the location holding the stock
quantitynumberQuantity on hand
uomstringUnit of measure as the source names it; source-defined, pass it through
unit_costnumberCost per unit used to value the stock
as_ofdateWhen the level was measured or last updated
reorder_pointnumberQuantity at or below which the org reorders the item at this location
min_quantitynumberLowest quantity the org wants on hand at this location
max_quantitynumberHighest quantity the org wants on hand at this location
$ curl -X POST https://api.valtrix.com/v1/records/inventory_level \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "item_name": "...", "quantity": 42, "item_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/inventory_level/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
inventory_movementA single change to stock: receipts, transfers, adjustments, counts, waste, and sales depletion all map here, discriminated by kind. Transfers carry both locations; the resulting position maps to inventory_level.
Writable through POST /v1/records/inventory_movement. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/inventory_movement/:externalId for records you wrote. The title key is item_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
kindstringOne of: receipt, transfer, adjustment, count, waste, sale. Map the source movement types onto these
item_namestringItem the movement is for
item_idreferenceValtrix record ID of the item the movement is for
from_location_idreferenceValtrix record ID of the location the stock moved out of
to_location_idreferenceValtrix record ID of the location the stock moved into
quantitynumberQuantity moved, negative when stock decreases
uomstringUnit of measure as the source names it; source-defined, pass it through
unit_costnumberCost per unit of the moved stock
occurred_atdateWhen the movement happened
created_atdateWhen the movement was recorded in the source
$ curl -X POST https://api.valtrix.com/v1/records/inventory_movement \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "item_name": "...", "kind": "...", "item_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/inventory_movement/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
invoiceA bill the org issues to a customer for money owed to the org. Receivables only: bills the org has to pay map to cost, and the transactions settling either side map to payment.
Writable through POST /v1/records/invoice. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/invoice/:externalId for records you wrote. The title key is number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringInvoice number
customer_namestringBilled customer name
customer_idreferenceValtrix record ID of the billed customer
project_idreferenceValtrix record ID of the project the invoice bills against
location_idreferenceValtrix record ID of the location the invoice belongs to, for sources that bill per venue or site rather than a project
amountnumberTotal amount due
currencystringISO currency code
statusstringOne of: draft, pending, approved, rejected, sent, partially_paid, paid, void. Map the source approval and payment states onto these; the raw label stays in source data
issued_atdateIssue date
due_atdateDue date
patient_balancenumberPart of the open balance the customer owes personally, for sources that split a bill between the customer and an insurer
insurance_balancenumberPart of the open balance billed to the customer's insurance, for sources that split a bill between the customer and an insurer
descriptionstringThe short title the source prints on the invoice (the job or service it bills), for sources that carry one beside the number
notesstringFree-text notes on the invoice as the source keeps them: the summary of work performed, the memo printed for the customer
$ curl -X POST https://api.valtrix.com/v1/records/invoice \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "number": "...", "customer_name": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/invoice/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
itemA catalog master record for a good or service the org sells, stocks, or buys: products, SKUs, menu items, materials, and rate-card services all map here. Usages of an item on a document map to line_item via item_id; stock on hand maps to inventory_level.
Writable through POST /v1/records/item. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/item/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringItem name
skustringSKU or item code in the source system
categorystringCatalog category or family as the source names it; source-defined, pass it through
uomstringDefault unit of measure as the source names it; source-defined, pass it through
pricenumberStandard selling price per unit
costnumberStandard acquisition or production cost per unit
currencystringISO currency code
statusstringOne of: active, inactive. Map archived, draft, or discontinued items to inactive
created_atdateWhen the item was created in the source
manufacturerstringWho makes the item, for parts and materials catalogs
manufacturer_part_numberstringThe manufacturer's own part number, when it differs from the sku the org files the item under
vendor_idreferenceValtrix record ID of the vendor the org prefers to buy the item from
vendor_skustringThe preferred vendor's own number for the item
$ curl -X POST https://api.valtrix.com/v1/records/item \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "sku": "...", "vendor_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/item/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
journal_entryA general ledger journal entry at the header level, the most general money record an accounting source exposes. Its debit and credit lines map to line_item coded by gl_account_id; documents with business meaning map to invoice, cost, or payment instead.
Writable through POST /v1/records/journal_entry. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/journal_entry/:externalId for records you wrote. The title key is number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringJournal entry number
descriptionstringWhat the entry records
statusstringOne of: draft, posted, adjusted, void. Map the source posting states onto these
location_idreferenceValtrix record ID of the location the entry books for, for sources that journal per venue or site
posted_atdateWhen the entry was posted to the ledger
created_atdateWhen the entry was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/journal_entry \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "number": "...", "description": "...", "location_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/journal_entry/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
line_itemA single line on any financial document: contracts, purchase orders, invoices, orders, costs, estimates, and journal entries. The parent is identified by three columns: document_entity names the entity the parent record lives under (a bill line has document_entity cost), document_type carries the parent's kind (that same line has document_type bill), and document_id holds its record id. Every per-document line flavor lands here; budget rows are the one exception (budget_line).
Writable through POST /v1/records/line_item. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/line_item/:externalId for records you wrote. The title key is description.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
descriptionstringWhat the line covers
document_entitystringEntity type of the document the line belongs to: contract, cost, invoice, order, estimate, or journal_entry. Read document_id from this entity; filter on it to select the lines of every cost, every contract, and so on
document_typestringOne of: bill, credit, expense, payroll, card_transaction, invoice, order, estimate, bid_request, prime_contract, subcontract, purchase_order, work_order, requisition, change_order, journal_entry, claim, treatment_plan, other. The kind of the document the line belongs to, matching the parent record's kind rather than its entity (a line on a cost of kind bill has document_type bill and document_entity cost)
document_idreferenceValtrix record ID of the document the line belongs to, in the entity document_entity names
positionnumber1-based order of the line on its document as the source shows it; sort by this to render lines the way the source does
project_idreferenceValtrix record ID of the project the line belongs to
item_idreferenceValtrix record ID of the catalog item the line sells or consumes
cost_codestringCost code the line is coded against
cost_code_idreferenceValtrix record ID of the cost code the line is coded against
gl_account_idreferenceValtrix record ID of the general ledger account the line is coded to, for accounting sources
cost_typestringCost type as the source names it (labor, materials, subcontract); source-defined, pass it through
classstringAccounting class or tracking category the line is coded to (QuickBooks class, location or department class), as the source names it; source-defined, pass it through
departmentstringDepartment, team, or division the line amount is allocated to, for payroll and labor costs, as the source names it; source-defined, pass it through
quantitynumberQuantity on the line
uomstringUnit of measure as the source names it; source-defined, pass it through
unit_costnumberCost per unit
amountnumberLine amount
totalnumberExtended line total
created_atdateWhen the line was created in the source
discountnumberDiscount applied to the line, in currency, as a positive number
discount_codestringThe coupon or discount code the discount came from; source-defined, pass it through
taxnumberTax charged on the line, in currency
$ curl -X POST https://api.valtrix.com/v1/records/line_item \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "description": "...", "document_entity": "...", "document_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/line_item/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
locationA standalone physical place record, such as a store, site, or warehouse. Only sources that model places as their own records produce these; address fields on another entity stay on that entity.
Writable through POST /v1/records/location. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/location/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringLocation name
addressstringStreet address
created_atdateWhen the location was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/location \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "address": "..." } }'$ curl "https://api.valtrix.com/v1/records/location/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
maintenance_orderA unit of maintenance work on one piece of equipment: a maintenance or repair request, the work order that performs it, and a preventive service coming due all collapse here, discriminated by kind. The equipment maps to resource (linked via resource_id); labor booked to it maps to time_entry and outside service or parts costs to cost, both linked back via maintenance_order_id. A purchase order or subcontract with a vendor stays contract, and a project schedule row stays schedule_task.
Writable through POST /v1/records/maintenance_order. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/maintenance_order/:externalId for records you wrote. The title key is title.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
titlestringWhat the work is, as the source titles it: the complaint, the work order title, or the service coming due
numberstringWork order or request number as the source prints it
kindstringOne of: request, work_order, preventive. A reported problem or request for work is request; the job that carries out the work is work_order; a scheduled service coming due on a usage or time trigger, before any work order exists, is preventive
statusstringOne of: open, scheduled, in_progress, on_hold, completed, cancelled. Map the source states onto these: new, pending, requested, or due is open; approved, planned, or assigned with dates is scheduled; started is in_progress; waiting on parts or paused is on_hold; resolved, closed, or done is completed; denied, rejected, or voided is cancelled. The raw label stays in source data
prioritystringOne of: low, medium, high, urgent. Critical or emergency is urgent
maintenance_typestringThe org's own classification of the work (Repair, Preventative Maintenance, Damage, Warranty); source-defined, pass it through
descriptionstringThe problem reported or the work to perform, as plain text
resource_namestringName of the piece of equipment the work is on
resource_idreferenceValtrix record ID of the piece of equipment the work is on
location_idreferenceValtrix record ID of the site, yard, or shop the work was requested from or is performed at
work_order_idreferenceValtrix record ID of the work order a request or preventive service was rolled into; null on a work order itself
requested_by_namestringWho asked for the work
requested_by_idreferenceValtrix record ID of the employee who asked for the work
assigned_to_namestringThe mechanic or technician the work is assigned to; several names joined with a comma when the source assigns more than one
assigned_to_idreferenceValtrix record ID of the first mechanic or technician assigned
out_of_servicebooleanWhether the equipment is down until the work is done
meter_hoursnumberEngine or run hours on the equipment when the work was raised
odometer_milesnumberOdometer reading in miles when the work was raised; convert a source that reports kilometres
due_atdateWhen the work is needed by, or when a time-triggered preventive service comes due
due_meternumberThe meter reading a usage-triggered preventive service comes due at, in meter_unit
meter_unitstringUnit due_meter counts in, as the source names it (Hours, Miles, Kilometers); source-defined, pass it through
scheduled_start_atdateWhen the work is planned to start
scheduled_end_atdateWhen the work is planned to finish
completed_atdateWhen the work was completed or the request resolved
labor_costnumberLabor cost booked to the work, in currency
parts_costnumberParts cost booked to the work, in currency
total_costnumberTotal cost of the work including labor, parts, outside services, and markups, in currency
created_atdateWhen the record was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/maintenance_order \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "title": "...", "number": "...", "resource_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/maintenance_order/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
opportunityA potential sale the org is pursuing through the stages of a sales pipeline: deals, opportunities, and qualified pipeline leads all map here. The account being pursued maps to customer (linked via customer_id); a priced proposal issued along the way maps to estimate, and the agreement once won maps to contract.
Writable through POST /v1/records/opportunity. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/opportunity/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringOpportunity or deal name, commonly the prospect account name
pipelinestringSales pipeline the opportunity is tracked in, as the source names it
stagestringPipeline stage as the source names it; this is a source-defined label, not a vocabulary, so pass it through
statusstringOne of: open, won, lost. Map closed-won and closed-lost stages onto won and lost; everything still in play is open
customer_namestringProspect account name
customer_idreferenceValtrix record ID of the prospect account
contact_namestringPrimary contact name on the deal
contact_idreferenceValtrix record ID of the primary contact on the deal
emailstringPrimary contact email address
websitestringProspect website URL
amountnumberExpected deal value
currencystringISO currency code
notesstringFree-form notes on the opportunity
closed_atdateWhen the opportunity was won or lost
created_atdateWhen the opportunity was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/opportunity \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "pipeline": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/opportunity/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
orderA customer order for goods or services with a total and fulfilment status, such as a sales, e-commerce, or point-of-sale order. A signed agreement of any kind maps to contract, and the resulting bill maps to invoice.
Writable through POST /v1/records/order. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/order/:externalId for records you wrote. The title key is number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringOrder number
customer_namestringOrdering customer name
customer_idreferenceValtrix record ID of the ordering customer
project_idreferenceValtrix record ID of the project the order belongs to or kicked off
location_idreferenceValtrix record ID of the location the order was placed at, for sources that scope orders to a store or venue
totalnumberOrder total
currencystringISO currency code
statusstringOne of: draft, pending, approved, rejected, unfulfilled, partially_fulfilled, fulfilled, cancelled. Map the source approval and fulfilment states onto these; the raw label stays in source data
items_countnumberNumber of line items
placed_atdateWhen the order was placed
$ curl -X POST https://api.valtrix.com/v1/records/order \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "number": "...", "customer_name": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/order/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
payerAn insurance carrier or plan the org bills on behalf of its customers: vision, medical, and dental insurers and the plans they offer all map here, discriminated by kind. A customer's own policy with the payer maps to coverage; the money the payer sends maps to payment.
Writable through POST /v1/records/payer. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/payer/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringCarrier or plan name as the org lists it
kindstringOne of: medical, vision, dental, other. The line of insurance the payer covers; map the source's insurance types onto these
payer_codestringElectronic payer id the org files claims under (the clearinghouse payer id), as the source prints it
statusstringOne of: active, inactive. Map hidden, archived, or disabled payers to inactive
created_atdateWhen the payer was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/payer \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "..." } }'$ curl "https://api.valtrix.com/v1/records/payer/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
paymentA payment transaction, issued to a vendor or received from a client, discriminated by kind, typically settling an invoice or cost. invoice_id and cost_id name the primary document; when one payment settles several documents, every share lives in payment_allocation and those rows are authoritative. The document being settled is not a payment.
Writable through POST /v1/records/payment. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/payment/:externalId for records you wrote. The title key is number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
numberstringPayment number
kindstringOne of: issued, received. Issued to a vendor or received from a client
counterpartystringCustomer or vendor the payment settles with
counterparty_idreferenceValtrix record ID of the customer or vendor the payment settles with
counterparty_typestringEntity type of the linked counterparty record: customer or vendor
invoice_numberstringInvoice the payment settles
invoice_idreferenceValtrix record ID of the invoice the payment settles
check_numberstringCheck or reference number
cost_idreferenceValtrix record ID of the primary cost the payment settles; a payment covering several costs lists each share in payment_allocation
project_idreferenceValtrix record ID of the project the payment belongs to
location_idreferenceValtrix record ID of the location the payment belongs to, for sources that settle money per venue or site rather than a project
financial_account_idreferenceValtrix record ID of the bank, card, or cash account the payment was released from or received into, for sources that record the settling instrument
amountnumberAmount paid
statusstringOne of: pending, sent, paid, failed, void. Map the source payment states onto these (authorized or in a check run is pending, cancelled is void); the raw label stays in source data
paid_atdateDate of the payment
created_atdateWhen the payment was recorded in the source
methodstringOne of: cash, credit_card, debit_card, card, transfer, check, other. How the payment was made; use card when the source does not say credit or debit
installmentsnumberNumber of card installments the payment was split into, for sources that record one
currencystringISO currency code the amount is denominated in, for sources that pay in more than one currency
$ curl -X POST https://api.valtrix.com/v1/records/payment \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "number": "...", "kind": "...", "counterparty_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/payment/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
payment_allocationOne share of a payment applied to one document: the row that links a payment to each cost or invoice it settles, with the amount applied. A payment settling a single document still gets one allocation; a check covering several bills gets one per bill, and vendor credits applied against the payment carry negative amounts. The allocations of a payment sum to its amount.
Writable through POST /v1/records/payment_allocation. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/payment_allocation/:externalId for records you wrote. The title key is invoice_number.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
payment_idreferenceValtrix record ID of the payment this share belongs to
document_typestringEntity type of the settled document: cost or invoice
document_idreferenceValtrix record ID of the cost or invoice this share settles, typed by document_type
invoice_numberstringSource document number of the settled cost or invoice
amountnumberAmount of the payment applied to the document; negative for a credit applied against the payment
positionnumber1-based order of the share within its payment as the source shows it
$ curl -X POST https://api.valtrix.com/v1/records/payment_allocation \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "invoice_number": "...", "document_type": "...", "payment_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/payment_allocation/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
prescriptionA prescription written for a customer: spectacle and contact lens prescriptions with their per-eye values, and medication orders, discriminated by kind. The exam or visit it came from maps to activity or booking; the sale that fills it maps to invoice or order.
Writable through POST /v1/records/prescription. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/prescription/:externalId for records you wrote. The title key is summary.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
summarystringOne line naming the prescription as the source lists it (its type and date, or the drug and strength)
kindstringOne of: glasses, contacts, medication, other. Map the source prescription types onto these
customer_idreferenceValtrix record ID of the customer the prescription is for
location_idreferenceValtrix record ID of the location the prescription was written or entered at
prescriberstringName of the doctor who wrote the prescription, as the source prints it
issued_atdateDate of the prescription
expires_atdateWhen the prescription expires
notesstringFree-text notes on the prescription
created_atdateWhen the prescription was recorded in the source
right_spherenumberSphere power of the right eye, in diopters
right_cylindernumberCylinder power of the right eye, in diopters
right_axisnumberCylinder axis of the right eye, in degrees
right_addnumberNear addition of the right eye, in diopters
left_spherenumberSphere power of the left eye, in diopters
left_cylindernumberCylinder power of the left eye, in diopters
left_axisnumberCylinder axis of the left eye, in degrees
left_addnumberNear addition of the left eye, in diopters
pupillary_distancenumberBinocular distance pupillary distance, in millimetres
right_lensstringContact lens prescribed for the right eye, as the source names the product
right_base_curvenumberBase curve of the right contact lens, in millimetres
right_diameternumberDiameter of the right contact lens, in millimetres
right_lens_powernumberPower of the right contact lens, in diopters
left_lensstringContact lens prescribed for the left eye, as the source names the product
left_base_curvenumberBase curve of the left contact lens, in millimetres
left_diameternumberDiameter of the left contact lens, in millimetres
left_lens_powernumberPower of the left contact lens, in diopters
$ curl -X POST https://api.valtrix.com/v1/records/prescription \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "summary": "...", "kind": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/prescription/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
projectA job, project, or engagement that work is performed under, with stage, value, and dates. The scoping parent most project-scoped records hang off; every job or engagement flavor from any source maps here.
Writable through POST /v1/records/project. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/project/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringProject name
numberstringJob or project number
descriptionstringFree-text description or summary of the work as the source keeps it: a technician's summary of work on a field-service job, a project brief. Filled by sources that carry one; others leave it empty
customer_namestringCustomer the project is for
customer_idreferenceValtrix record ID of the customer the project is for
stagestringLifecycle stage as the source names it (estimating, pre-construction, warranty); source-defined, pass it through
lifecycle_stagestringOne of: draft, applied, in_review, corrections, approved, issued, inspections, on_hold, finaled, denied, withdrawn, void, expired, other. Where a permit, plan review, or application sits in its lifecycle, mapped from the portal state so counts combine across portals: unsubmitted or saved is draft; submitted, received, or pending intake is applied; under review, in process, or routing is in_review; resubmit, corrections, or waiting on the applicant is corrections; approved or ready to issue is approved; issued or permitted is issued; a temporary certificate or inspections pending is inspections; hold or stop work is on_hold; finaled, closed, completed, or certificate of occupancy is finaled; denied or rejected is denied; withdrawn is withdrawn; void or cancelled is void; expired or archived is expired; a state that fits none is other. Only permitting and plan-review sources fill it; construction and job sources leave it null and keep their own stage
citystringSite city
countrystringSite country code or name
jurisdictionstringPermitting authority the project is filed with: the city, county, or agency whose portal it lives in. Set per portal by the connector, not read off the record; sources that are not permit portals leave it empty
valuenumberContracted project value
statusstringOne of: active, closed, inactive. Completed or closed projects are closed; archived or disabled ones are inactive
start_atdatePlanned start date
completion_atdatePlanned completion date
created_atdateWhen the project was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/project \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "number": "...", "customer_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/project/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
project_userA person's membership in a specific project's directory. The person's company-level record maps to employee (internal) or contact (external).
Writable through POST /v1/records/project_user. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/project_user/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringFull name
emailstringWork email address
phonestringPrimary phone number
job_titlestringRole within the project
person_idreferenceValtrix record ID of the company-level record for this person, an employee for internal staff or a contact for external members
person_typestringEntity type of the linked person record: employee or contact
project_idreferenceValtrix record ID of the project the person is a member of
statusstringOne of: active, inactive
$ curl -X POST https://api.valtrix.com/v1/records/project_user \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "email": "...", "person_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/project_user/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
project_vendorA vendor's assignment to a specific project's directory. The vendor's company-level record maps to vendor.
Writable through POST /v1/records/project_vendor. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/project_vendor/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringVendor company name
tradestringTrade or specialty the vendor operates in, as the source names it; source-defined, pass it through
citystringCity of the primary address
countrystringCountry code or name
vendor_idreferenceValtrix record ID of the company-level vendor record
project_idreferenceValtrix record ID of the project the vendor is assigned to
statusstringOne of: active, inactive
$ curl -X POST https://api.valtrix.com/v1/records/project_vendor \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "trade": "...", "vendor_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/project_vendor/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
resourceA bookable or tracked asset at a location: courts, rooms, desks, chairs, operatories, lanes, and equipment all map here, discriminated by kind. A fleet unit from an equipment management source (a machine, vehicle, trailer, or attachment) is kind equipment and fills the make, model, serial, meter, and position columns. Reservations of it map to booking, work performed on it maps to maintenance_order, inspections of it map to inspection, and the venue itself maps to location.
Writable through POST /v1/records/resource. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/resource/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringResource name
kindstringOne of: court, room, desk, chair, operatory, lane, equipment, other. Map the source resource types onto these
location_idreferenceValtrix record ID of the location the resource belongs to
capacitynumberHow many people the resource accommodates at once
statusstringOne of: active, inactive
created_atdateWhen the resource was created in the source
makestringManufacturer of the equipment or vehicle (Caterpillar, Ford). Filled by equipment and fleet sources
modelstringManufacturer model designation (320 GC, F-550)
yearnumberModel year
serial_numberstringManufacturer serial number or product identification number
vinstringVehicle identification number, for on-road vehicles
fleet_numberstringThe number the org itself knows the unit by: fleet, unit, or equipment number
license_platestringRegistration plate, for on-road vehicles
categorystringEquipment category or class as the source names it (Excavator, Light Truck, Trenchbox); source-defined, pass it through
ownershipstringOne of: owned, rented, leased, other. How the org holds the unit; a rent-to-own unit still on rent is rented
operational_statusstringWhat the unit is doing right now as the source names it (available, in use, down, in transit); source-defined, pass it through. Whether the record is still in service is status
meter_hoursnumberLatest engine or run hours the source holds for the unit
odometer_milesnumberLatest odometer reading in miles; convert a source that reports kilometres
purchase_pricenumberWhat the org paid for the unit
purchase_datedateWhen the org acquired the unit
latitudenumberLatitude of the last known position, for tracked units
longitudenumberLongitude of the last known position, for tracked units
located_atdateWhen the last known position was reported
assigned_to_idreferenceValtrix record ID of the employee the unit is currently assigned to (its operator or driver)
engine_makestringManufacturer of the engine, which often differs from the maker of the machine
engine_modelstringEngine model designation
engine_serial_numberstringSerial number of the engine, the key parts suppliers look engine parts up by
$ curl -X POST https://api.valtrix.com/v1/records/resource \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "...", "location_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/resource/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
schedule_taskA task or milestone on a project schedule, with WBS position, dates, and percent complete. Any Gantt or schedule row flavor maps here. Each plan-review discipline on a government permit or plan-review portal also maps here, as one non-milestone task per discipline keyed to the project (name is the discipline, description is the review outcome), so reviews read as one uniform set of rows across every portal.
Writable through POST /v1/records/schedule_task. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/schedule_task/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringTask name; for a plan-review discipline, the discipline (Electrical, Mechanical, Structural)
descriptionstringWhat the task covers. For a plan-review discipline, carry the review outcome from the shared review vocabulary so counts combine across permit portals: Approved, Not Approved, In Progress, Not Started. Map the portal state onto these (corrections required and resubmit are Not Approved, under review and prescreen are In Progress); the raw portal label stays in source data
wbsstringWork breakdown structure position
resource_namestringAssigned resource or crew
project_idreferenceValtrix record ID of the project the task is scheduled on
percent_completenumberPercentage complete
criticalbooleanWhether the task is on the critical path
milestonebooleanWhether the task is a milestone
start_atdatePlanned start date
finish_atdatePlanned finish date
created_atdateWhen the task was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/schedule_task \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "description": "...", "project_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/schedule_task/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
templateA reusable document or message template the org authors in a source and fills per customer: contract and agreement templates, email and message templates, and forms, discriminated by kind, with the template body as stored. This is the authored template itself, not a filled copy; a signed instrument produced from one maps to compliance_document, and a file attached to a record rides along as a record attachment.
Writable through POST /v1/records/template. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/template/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringTemplate name as the source labels it
kindstringOne of: contract, email, message, form, other. Map the source template types onto these
codestringThe source's identifier for the template, when it has one apart from the name
subjectstringSubject line, for email and message templates
bodystringTemplate body as stored in the source, with its placeholders intact (HTML or text)
statusstringOne of: active, inactive. Map archived or disabled templates to inactive
updated_atdateWhen the template was last edited in the source
created_atdateWhen the template was created in the source
$ curl -X POST https://api.valtrix.com/v1/records/template \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "kind": "..." } }'$ curl "https://api.valtrix.com/v1/records/template/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
time_entryA single logged block of labor time: who worked, on what project and cost code, for how many hours or at what cost. Any timesheet or timecard flavor maps here.
Writable through POST /v1/records/time_entry. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/time_entry/:externalId for records you wrote. The title key is employee_name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
employee_namestringWho logged the time
employee_idreferenceValtrix record ID of the employee who logged the time
project_namestringProject or job the time was logged against
project_idreferenceValtrix record ID of the project the time was logged against
cost_codestringCost code the time was logged against
cost_code_idreferenceValtrix record ID of the cost code the time was logged against
location_idreferenceValtrix record ID of the location the time was worked at, for sources that schedule labor per venue or site rather than a project
hoursnumberHours worked
costnumberLabor cost of the entry
started_atdateWhen the entry started
ended_atdateWhen the entry ended
created_atdateWhen the entry was created in the source
resource_idreferenceValtrix record ID of the piece of equipment the time was spent on, for mechanic and shop time logged against a unit
maintenance_order_idreferenceValtrix record ID of the maintenance work order the time was logged against
$ curl -X POST https://api.valtrix.com/v1/records/time_entry \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "employee_name": "...", "project_name": "...", "employee_id": "rec_9d4f2c81a7b3e650d21c4f8a" } }'$ curl "https://api.valtrix.com/v1/records/time_entry/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
vendorA company the org buys from or subcontracts to, at the company-directory level. A vendor's assignment to a specific project maps to project_vendor; the agreement itself maps to contract.
Writable through POST /v1/records/vendor. Retrievable through GET /v1/records/:recordId with any record ID, or GET /v1/records/vendor/:externalId for records you wrote. The title key is name.
The fields below make up the record's data object. Every record also carries the envelope of the record object (id, entity_type, external_id, org_id, connector, synced_at, changed_at); external_id is yours for records written through this API, the source system's id for synced records.
namestringVendor company name
tradestringTrade or specialty the vendor operates in, as the source names it; source-defined, pass it through
emailstringPrimary contact email
phonestringPrimary phone number
citystringCity of the primary address
countrystringCountry code or name
statusstringOne of: active, inactive. Map archived, disabled, or deleted vendors to inactive
created_atdateWhen the vendor was created in the source
tax_idstringTax identification number of the vendor (RFC, EIN, VAT number), as the source prints it, for sources that key suppliers by it
$ curl -X POST https://api.valtrix.com/v1/records/vendor \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "external_id": "crm-9x2", "data": { "name": "...", "trade": "..." } }'$ curl "https://api.valtrix.com/v1/records/vendor/crm-9x2?org=org_7f3k2m" \-H "Authorization: Bearer $VALTRIX_API_KEY"
How propagation works. A write intent is created when you upsert or delete a record with propagate: true and one of the organization's connected systems declares that operation for the entity type. What a write to that system takes is on the connection: write_guidance lists, per entity type, the fields each operation sends with their meaning, the values a command field accepts with the effect of each, the documents an operation carries, and the connector's own rules on refusals; read it before the first propagated write to a system. The record is written in Valtrix immediately and the response is 202 with the write intent (its op is create until a propagated write on that record has applied, and update after, so a record whose first propagation failed is created at the source on the next propagated write rather than updated); a worker then applies it to the source system and, where the connector can read the write back, re-reads it, recording the outcome as verification, and moves the write intent from pending to applied (source_external_id filled, and the record's source_id along with it) or failed (error says why in plain words, with a code, whether a retry can help, the source's own text, the step that failed, and whether anything reached the source). Write intents are never created directly: POST /v1/records/:entityType and DELETE /v1/records/:entityType/:externalId are the only ways in. Learn the outcome by polling GET /v1/write-intents/:id, reconciling with GET /v1/write-intents, or by listening for write_intent.applied and write_intent.failed; a failed write intent can be re-enqueued with POST /v1/write-intents/:id/retry once you have fixed the input. Before writing, the organization object's connections list which entity types and operations each connection can take, and GET /v1/connectors shows the same per connector with whether each operation has been verified by a real write yet. Propagation is explicit opt-in per call, so a write without propagate never leaves Valtrix. Send an Idempotency-Key on every propagated write you might retry, so a retry within 24 hours replays the original response instead of queuing a second write intent.
A propagation of one record write to the organization's source system, created by a write with propagate: true. The record it belongs to carries the same write intent as its last_write_intent, and the record's source_id is filled from source_external_id once the write intent is applied.
idstringThe wi_ prefixed write intent id.
record_idstringThe record this write intent propagates.
entity_typestringThe entity type of the record.
opstringcreate, update, or delete.
statusstringpending, applied, or failed.
connection_idstringThe connection the write propagates to.
source_external_idstring or nullThe id the source system assigned once applied; null before then.
verificationstring or nullWhat the source showed after an applied write: confirmed (the connector read the write back and every field it sent came back as sent), unconfirmed (the operation has no read-back, such as a form submission, a document upload, or an API that does not echo the fields), or contradicted (the read-back showed a different value for at least one field). The status is applied in all three cases; contradicted means the source accepted the write and then showed something else, so read the record back before writing again. null while pending or failed.
errorobject or nullWhy the write did not land, when the status is failed; null otherwise. code is one of refused (the source refused the write in its own words), invalid_input (the record lacks something the source needs, or carries nothing the operation sends), precondition_failed (a rule of the connector stopped the write before anything was sent), not_found (the source has no row for the record), exists (the source already holds the row), source_error (the source failed internally), session_refused (the connection's sign-in was refused), rate_limited, unreachable, reference_pending (a referenced record has no source id yet), grant_missing (the login the connection signs in with lacks a permission the operation needs at the source; the message names the objects and, for a bridge connection, the grant script that adds them), connector_error (the connector could not express the write; Valtrix is notified), or write_failed (unclassified). message says what went wrong in plain words and what to do; hint explains the code; retryable says whether retrying the same write can change the outcome; source_message is the source system's own text when message was translated from it, else null; step names the connector step that failed, else null; source_changed says whether anything had been posted to the source before the failure, null when the connector cannot tell.
attemptsnumberHow many times propagation has been attempted.
created_atstringWhen the write intent was created, as an ISO 8601 timestamp.
applied_atstring or nullWhen the write landed in the source, or null.
{"id": "wi_9k2f7c81a7b3e650","record_id": "rec_9d4f2c81a7b3e650d21c4f8a","entity_type": "vendor","op": "create","status": "applied","connection_id": "conn_7f3k2m","source_external_id": "CR-88213","verification": "confirmed","error": null,"attempts": 1,"created_at": "2026-07-08T09:30:00Z","applied_at": "2026-07-08T09:30:04Z"}
/v1/write-intentsLists write intents across every organization this key can reach, newest first. Filter by record_id, entity_type, op, status, or connection to reconcile what has and has not landed.
record_idstringOnly write intents for this record.
entity_typestringOnly write intents for this entity type.
opstringcreate, update, or delete.
statusstringpending, applied, or failed.
connectionstringOnly write intents propagating to this connection.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from a previous page.
write_intentsarrayThe write intent objects.
next_cursorstring or nullPass as cursor to fetch the next page, or null when there are no more.
$ curl https://api.valtrix.com/v1/write-intents?status=failed \-H "Authorization: Bearer $VALTRIX_API_KEY"{"write_intents": [{ "id": "wi_9k2f7c81a7b3e650", "record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "entity_type": "vendor", "op": "create", "status": "failed", "connection_id": "conn_7f3k2m", "source_external_id": null, "verification": null, "error": { "code": "exists", "message": "Vendor code ACME01 already exists with a different name.", "retryable": false, "hint": "The source already holds a row with this identity, so the create was not repeated. Write it as an update instead.", "source_message": "Duplicate key ACME01", "step": null, "source_changed": false }, "attempts": 1, "created_at": "2026-07-08T09:30:00Z", "applied_at": null }],"next_cursor": null}
/v1/write-intents/:idFetches one write intent by id, for polling a propagation to completion. The id comes from the write intent in a 202 write response or from a record's last_write_intent.
idpathThe wi_ prefixed write intent id.
The write intent object itself, not wrapped, or 404 write_intent_not_found when no write intent with that id is visible to this key.
$ curl https://api.valtrix.com/v1/write-intents/wi_9k2f7c81a7b3e650 \-H "Authorization: Bearer $VALTRIX_API_KEY"{"id": "wi_9k2f7c81a7b3e650", "status": "applied", "source_external_id": "CR-88213", "applied_at": "2026-07-08T09:30:04Z"}
/v1/write-intents/:id/retryRe-enqueues a failed write intent, moving it back to pending. Use it after fixing your input with POST /v1/records/:entityType or once a connector is repaired. Returns 409 write_intent_not_retryable for a write intent that is not failed. Requires a key with write access.
idpathThe wi_ prefixed write intent id.
The write intent object itself, not wrapped, now pending with its attempts count carried over.
$ curl -X POST https://api.valtrix.com/v1/write-intents/wi_9k2f7c81a7b3e650/retry \-H "Authorization: Bearer $VALTRIX_API_KEY"{"id": "wi_9k2f7c81a7b3e650", "status": "pending", "attempts": 3}
An invitation for an organization to connect one of its systems through the hosted Connect flow. Links stay valid for 45 days. On an organization-scoped key the same object describes the organization's own sign-in link for one system: whoever holds that system's login opens it, no Valtrix account is needed, it grants nothing, and it stays valid for 7 days.
idstringThe session id. Store it against your own record of who you minted the link for: when the flow completes, org.connected carries it as connect_session_id, which ties the connection back to this session without relying on the browser redirect.
org_idstringThe organization the link is bound to. When you omit org, Valtrix creates the organization up front (named from org_display_name) and it stays pending until the person completes Connect; it appears in GET /v1/orgs once access is granted. If the person signs into a different existing organization instead, that organization takes over and org.connected carries its id, so correlate on external_id or the connect_session_id rather than on this value alone.
connect_urlstringThe single use hosted Connect link. Send the organization through it; when they finish they land on your redirect_uri. The link embeds a secret, so share it only with the organization and correlate on id, never on this URL.
expires_atstringWhen the link stops working, as an ISO 8601 timestamp, 45 days after creation (7 days for a sign-in link minted on an organization-scoped key). Mint a new session after expiry.
{"id": "cts_8m2kq","org_id": "org_3f9k2m","connect_url": "https://valtrix.com/connect/vct_...","expires_at": "2026-07-09T09:30:00Z"}
/v1/connect/sessionsMints a single use hosted Connect link bound to an organization. Pass org to reconnect or extend an existing organization; omit it and Valtrix creates the organization first (named from org_display_name) and binds the link to it. Send the organization through the link to grant access; your tables build from every connection the organization has. Connections belong to the organization, not to you: if it already syncs the connector you name, the link asks it to approve your access without signing in to that system again, and a link with no connector lets it approve access to everything it already syncs or connect another system first. On an organization-scoped key the same call mints the organization's own sign-in link instead: it names one system (connector is required, 400 connector_required), whoever opens it enters that system's sign-in details with no Valtrix account and no access to any data, it grants nothing, and it expires after 7 days; org_display_name, external_id, scope, and redirect_uri are ignored on such keys.
org_display_namestringThe name for the organization Valtrix creates when org is omitted. The person completing Connect can override it, and the organization can rename itself later, so do not rely on it to recognize the organization afterwards; use external_id or the id from GET /v1/orgs.
external_idstringYour own identifier for the organization, such as your customer id, up to 255 characters. Stored on the grant when the flow completes and echoed as external_id on organization responses and org webhooks. Minting another session with a known external_id targets the same organization, so you can always say connect customer 123 without tracking whether they already exist on Valtrix. Sending it alongside an org id that belongs to a different organization returns 409 external_id_conflict.
orgstringAn existing organization id, to reconnect or extend access.
connectorstringA connector slug to preselect. GET /v1/connectors lists the valid ones. Required on an organization-scoped key, which mints a sign-in link for that one system.
credential_sectionsstringEither "login" or "api". Some connectors capture both a product sign-in and API credentials on one Connect page; this restricts the link to one of the two sections. Omit it to request everything the connector captures, which is the recommended default. Ignored by connectors that only capture one kind of credential.
redirect_uristringWhere the organization lands after finishing.
scopeobjectLimits the requested grant, as { "entityTypes": ["..."] }. Defaults to full access.
history_fromstringAn ISO 8601 date. A one-time backfill for migrations: the first sync loads date-bounded history (invoices, bills, transactions) from this date, and every sync after it fetches only the connector's default trailing window, so the backfilled records stay in the organization's tables but are not refreshed. Master data such as customers, vendors, and items always loads in full. Set it when a customer needs more history than the connector fetches by default, for example an organization changing systems that needs its full fiscal year in its tables. It widens an existing connection to the same connector and never narrows it, and the connection object shows the value as history_from. A source with its own retention ceiling clamps the start and the sync run report says provider_limit. Must not be in the future: 400 invalid_history_from.
The Connect session object, with status 201.
$ curl -X POST https://api.valtrix.com/v1/connect/sessions \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org_display_name": "Acme Inc", "external_id": "cus_123", "redirect_uri": "https://yourapp.com/connected" }'{ "id": "cts_8m2kq", "org_id": "org_3f9k2m", "connect_url": "https://valtrix.com/connect/vct_...", "expires_at": "2026-07-09T09:30:00Z" }
The systems your organizations can connect through the hosted Connect flow. Every place the API takes or returns a connector identifies it by slug: the connector parameter on record reads and Connect sessions, the _connector meta field on table rows and records, the connector on a record object, the connections on an organization, and the connector field on webhook events. This endpoint is the source of those slugs, so resolve display names and build connector pickers from it instead of hardcoding the catalog.
A system Valtrix syncs data from, as available to you.
slugstringThe connector's identifier everywhere in this API. Stable, lowercase, safe to store.
display_namestringThe connector's name, for display.
categorystringThe kind of system the connector covers, such as construction or accounts_payable.
entity_typesarrayThe entity type slugs this connector syncs. GET /v1/schema describes the shape of each one.
writesobjectThe entity types this connector can take propagated writes for, each mapped to { create, update, delete, verified }. The three booleans say which operations the connector declares; verified becomes true once a real propagated write has landed and been re-read from the source. An empty object means writes to this connector stay in Valtrix. The organization object's connections carry the same capability per connection.
write_guidanceobjectWhat to send when writing each entity type into this connector, keyed like writes: fields lists the keys each operation sends under data with their meaning, whether they are required, and, for a field that acts as a command, the values it takes with the effect each has at the source; documents lists the files an operation carries with their media types; notes are the connector's own rules on preconditions, side effects, and refusals. Read it before a propagated write instead of inferring from column names; an entity type with nothing declared is absent.
{"slug": "procore","display_name": "Procore","category": "construction","entity_types": ["project", "customer", "invoice"],"writes": { "vendor": { "create": true, "update": true, "delete": false, "verified": true } },"write_guidance": { "vendor": { "fields": [{ "op": "create", "column": "name", "description": "Display name", "required": true, "values": [] }], "documents": [], "notes": ["A vendor code the system already holds is reused, never duplicated."] } }}
Tracks a connector you added for a provider Valtrix had not built support for yet, and where the build stands. The provider is the third-party system your customers use; once support is built, it appears in the catalog as a connector and is identified by its slug everywhere else in the API.
idstringThe request id.
provider_namestringThe provider name as you submitted it.
product_namestring or nullThe specific product Valtrix resolved the request to, when the provider sells more than one.
homepage_urlstringThe provider homepage you submitted.
statusstringrequested when the request is queued, building while Valtrix prepares the connector, live once customers' data syncs, or not_available when the provider cannot be supported. The two terminal transitions also fire the connector_request.live and connector_request.not_available webhooks.
connectorstring or nullThe live connector's slug once there is one, usable everywhere the API takes a connector.
created_atstringWhen you added it, as an ISO 8601 timestamp.
updated_atstringWhen the status last changed, as an ISO 8601 timestamp.
{"id": "creq_9f2k7d","provider_name": "Aspire","product_name": "Aspire","homepage_url": "https://www.youraspire.com","status": "building","connector": null,"created_at": "2026-08-13T09:00:00Z","updated_at": "2026-08-13T09:00:00Z"}
/v1/connectorsLists every connector available to you, in display name order. The slugs here are the valid values for the connector parameter on record reads and Connect sessions, and entity_types tells you which connectors can supply the data you care about before you send an organization through Connect.
connectorsarrayConnector objects, in display name order. Not paginated.
$ curl https://api.valtrix.com/v1/connectors \-H "Authorization: Bearer $VALTRIX_API_KEY"{"connectors": [{ "slug": "procore", "display_name": "Procore", "category": "construction", "entity_types": ["project", "customer", "invoice"], "writes": { "vendor": { "create": true, "update": true, "delete": false, "verified": true } }, "write_guidance": {} }]}
/v1/connector-requestsAdds a connector for a provider your customers use that is not in the catalog yet. The provider is the third-party system itself; what Valtrix builds and ships is the connector. The add is tracked as a connector request: Valtrix resolves the product, prepares it for connection, and builds the connector; the request's status tracks progress, and the terminal transitions push webhooks: connector_request.live when customers' data starts syncing, connector_request.not_available when the provider cannot be supported. Console members are also emailed on live. As soon as the connector is added it appears in your catalog, so you can include it in connect links and send customers through Connect right away. If the provider is already available, it is added to your catalog and the response returns the existing connector; if another platform already added it, your platform is attached to the open request. Requires a write key. Organization-scoped keys minted through POST /v1/keys file the request for your platform on that organization's behalf.
provider_namestringThe provider or product name, as your customer says it.
homepage_urlstringThe provider's homepage URL.
product_choicestringWhen a previous attempt returned product_choice_required, the product_name you picked from its candidates.
An outcome field: created or attached with the connector request object, already_available with the slug of the existing connector that was added to your catalog, or a 409 product_choice_required with candidate products to choose between (the SDK returns this as an outcome instead of throwing).
$ curl https://api.valtrix.com/v1/connector-requests \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "provider_name": "Aspire", "homepage_url": "https://www.youraspire.com" }'{"outcome": "created","request": { "id": "creq_9f2k7d", "provider_name": "Aspire", "product_name": "Aspire", "homepage_url": "https://www.youraspire.com", "status": "building", "connector": null, "created_at": "2026-08-13T09:00:00Z", "updated_at": "2026-08-13T09:00:00Z" }}
/v1/connector-requestsLists your connector requests (one for each connector you added that was not already in the catalog), newest first, including requests where you joined an existing build for the same provider. Status is one of requested, building, live, or not_available; connector carries the live connector's slug once there is one. Poll this for the intermediate stages, or subscribe to the connector_request.live and connector_request.not_available webhooks to be pushed the terminal ones. Organization-scoped keys see the requests filed for their organization.
requestsarrayConnector request objects, newest first. Not paginated.
$ curl https://api.valtrix.com/v1/connector-requests \-H "Authorization: Bearer $VALTRIX_API_KEY"{"requests": [{ "id": "creq_9f2k7d", "provider_name": "Aspire", "product_name": "Aspire", "homepage_url": "https://www.youraspire.com", "status": "live", "connector": "aspire", "created_at": "2026-08-13T09:00:00Z", "updated_at": "2026-08-14T07:30:00Z" }]}
Every organization that has connected to you through Connect, including those that later revoked access. Webhooks like grant.revoked tell you something changed; these endpoints tell you where things stand now, so they are the source of truth to reconcile your own records against. Data itself is not read here: rows carry their organization in _org_id, and the org parameter narrows any table read. Called with an organization-scoped key, these endpoints see only that key's organization.
An organization and the state of its grant to you. Organizations stay listed after revocation or expiry so a missed webhook never leaves your records unreconcilable.
idstringThe organization id. The same value rows carry as _org_id and the org parameter accepts across the API. Stable for the life of the organization, so this is the value to store on your side.
display_namestringThe organization's name. The organization owns this and can change it at any time, so treat it as display only and never use it to identify an organization in your records; store the id instead.
external_idstring or nullYour identifier for the organization, as sent when minting the Connect session that connected it. Stable and controlled by you, unlike display_name, so it is the field to join organizations back to your own customer records. Null when you have never sent one.
statusstringactive while the grant is live, revoked after the organization revokes your access, expired when the grant passed its expiry. Only active organizations are readable through tables and records.
scopeobjectThe entity types the grant covers, as { "entity_types": ["..."] }. A single "*" means full access.
connectionsarrayThe organization's connected systems, in the same shape as the connection object without org_id. Each carries an id (the value to pass as connection on a propagated write, and the value records expose as connection_id), the connector slug, a status: pending, connected, error, expired, disconnected, or paused, history_from, the date its date-bounded history was backfilled from once or null for the connector's default window, last_synced_at, when data last synced from it, null before the first sync, initial_sync_completed_at, when its first full load finished, paused_at and paused_until while it is paused, and writes: the entity types this connection can take propagated writes for, mapped to the operations among create, update, and delete. An empty writes object means writes to this connection stay in Valtrix. Use it to decide whether to send propagate: true and which connection to address; GET /v1/connectors shows the same per connector, with verification status per operation. write_guidance, keyed the same way, says what to send: the fields each operation takes with their meaning, the values a command field accepts with the effect of each, the documents an operation carries, and the connector's notes on refusals.
connected_atstringWhen the organization granted you access, as an ISO 8601 timestamp.
expires_atstring or nullWhen the grant expires, or null when it has no expiry.
revoked_atstring or nullWhen the organization revoked your access, or null while it has not.
{"id": "org_7f3k2m","display_name": "Acme Inc","external_id": "cus_123","status": "active","scope": { "entity_types": ["*"] },"connections": [{ "id": "conn_7f3k2m", "connector": "procore", "status": "connected", "last_synced_at": "2026-07-08T09:30:00Z", "writes": { "vendor": ["create", "update"] } }],"connected_at": "2026-06-01T09:00:00Z","expires_at": null,"revoked_at": null}
/v1/orgsLists every organization that has connected to you, in the order they first connected. Revoked and expired organizations are included so the list is complete for reconciliation; filter by status to narrow.
statusstringNarrow to one status: active, revoked, or expired.
external_idstringLook up the organization carrying this external_id, exact match. The way to resolve one of your customer ids to its Valtrix organization.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from the previous page, sent back exactly as you received it.
orgsarrayOrganization objects, in the order they first connected.
next_cursorstring or nullThe bookmark for the next page. null means the last page.
$ curl https://api.valtrix.com/v1/orgs \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "status=active" -d "limit=50"{"orgs": [{ "id": "org_7f3k2m", "display_name": "Acme Inc", "external_id": "cus_123", "status": "active", "scope": { "entity_types": ["*"] }, "connections": [{ "id": "conn_7f3k2m", "connector": "procore", "status": "connected", "last_synced_at": "2026-07-08T09:30:00Z", "writes": { "vendor": ["create", "update"] } }], "connected_at": "2026-06-01T09:00:00Z", "expires_at": null, "revoked_at": null }],"next_cursor": null}
/v1/orgs/:idFetches one organization by id, whatever its status. The place to check where you stand after a grant.revoked or connection.broken webhook, or after any request returns org_not_granted.
idpathThe organization id.
The organization object, or 404 org_not_found when no organization with that id has connected to you.
$ curl https://api.valtrix.com/v1/orgs/org_7f3k2m \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "org_7f3k2m", "display_name": "Acme Inc", "external_id": "cus_123", "status": "revoked", "scope": { "entity_types": ["*"] }, "connections": [{ "id": "conn_7f3k2m", "connector": "procore", "status": "disconnected", "last_synced_at": "2026-07-08T09:30:00Z", "writes": { "vendor": ["create", "update"] } }], "connected_at": "2026-06-01T09:00:00Z", "expires_at": null, "revoked_at": "2026-07-08T09:30:00Z" }
Every connected system of every organization that has connected to you, as a resource of its own. The organization object nests the same connections; these endpoints are where you act on one: decide how far back its history loads, run a full sync on demand and read the run's report, and pause it while its data needs to stand still. They also cover an organization changing systems. Connect the outgoing system with history_from covering the period the organization needs and keep reading it live while you reconcile. At the switch, start a full sync, wait for its run to settle, replay GET /v1/tables/:name/changes from the cursors it returns for anything that changed after the run, and pause the connection with no expiry: it stays a source of the organization's tables for as long as the organization keeps it, so the queries you already run keep serving that era. Connect the new system alongside it, and where its connector takes propagated writes, carry records across with propagate: true so the new system's row and the record you wrote stay one record in Valtrix. Read the organization through Valtrix before, during, and after the switch rather than copying its data into a store of your own; the cursors are a bookmark for following changes, not a replication log. Called with an organization-scoped key, these endpoints see only that key's organization.
One connected system of one organization. Connections belong to the organization, not to you; a connection stays listed while the organization keeps it, whatever your grant's state.
idstringThe connection id. The value records expose as connection_id and the value to pass as connection on a propagated write.
org_idstringThe organization the connection belongs to.
connectorstringThe connector slug of the connected system, as listed by GET /v1/connectors.
statusstringpending before the organization finishes connecting, connected while it syncs, error after a persistent sync failure, expired when the stored credentials stopped working, disconnected after the organization removed it, or paused while syncing is stopped on purpose. A paused connection's data stays readable through every read endpoint; only ingestion stops.
history_fromstring or nullThe date its date-bounded history was backfilled from, or null when only the connector's default trailing window applies. The backfill runs once, on the first sync after the date is set; later syncs fetch the default window and keep the backfilled records without refreshing them. Set at connect time or widened later with PATCH; never narrowed.
last_synced_atstring or nullWhen data last synced from it, null before the first sync.
initial_sync_completed_atstring or nullWhen the first full load finished, null until then. The moment the organization's data is complete enough to build on.
paused_atstring or nullWhen the connection was paused, null while it is not.
paused_untilstring or nullWhen a pause lifts on its own, null for a pause with no expiry or while not paused.
writesobjectThe entity types this connection can take propagated writes for, mapped to the operations among create, update, and delete. Empty when writes to this connection stay in Valtrix.
write_guidanceobjectThe connector's write guidance for this connection, keyed by entity type like writes: the fields each operation sends with their meaning and whether they are required, the values a command field takes with the effect of each, the documents an operation carries, and the connector's notes on preconditions, side effects, and refusals. The same object as write_guidance on the connector object, here so a caller holding the organization knows what to send before propagating a write.
{"id": "conn_7f3k2m","org_id": "org_7f3k2m","connector": "sage-100-contractor","status": "paused","history_from": "2022-01-01T00:00:00Z","last_synced_at": "2026-09-08T09:30:00Z","initial_sync_completed_at": "2026-08-01T10:12:00Z","paused_at": "2026-09-08T09:31:00Z","paused_until": "2026-09-11T09:31:00Z","writes": { "vendor": ["create", "update"] },"write_guidance": { "vendor": { "fields": [{ "op": "create", "column": "name", "description": "Display name", "required": true, "values": [] }], "documents": [], "notes": [] } }}
One sync of a connection, returned when you start a sync and when you read a run back. Once it finishes, the report says what was fetched per resource and how far back, and once the organization's tables have rebuilt from it, tables gives the change-feed position to replay later deltas from.
idstringThe run id.
connection_idstringThe connection the run belongs to.
triggerstringWhat started it: initial for the first load, scheduled, manual for a requested sync, webhook when the source pushed a change, or field_selection after a field change.
modestringfull or incremental.
statusstringqueued, running, succeeded, partial when some resources failed, failed, or cancelled.
started_atstring or nullWhen the run started, null while queued.
finished_atstring or nullWhen the run finished, null until then.
errorstring or nullThe failure summary of a partial or failed run.
reportobject or nullThe completeness report, null until the run finishes. resources maps each resource type to fetched, created, updated, attachments, failed_scopes, skipped_scopes, denied_scopes, error, error_code, effective_window_start (the date the fetch started from) and window_reason: template_window when the connector's default window applied, history_from when the connection's history date widened it, provider_limit when the source's own ceiling clamped it, or watermark on an incremental run that resumed from the last sync. deleted_by_entity_type counts records the run swept as deleted per entity type. bounded is true when any resource fetch was bounded by a date, which means items dated before that start that are still open in the source, such as an old unpaid invoice, are not guaranteed to be present; treat them as opening balances. error and error_code are null when the resource synced. Otherwise error says in plain words what stopped the fetch and error_code is the value to branch on: auth_rejected (the source rejected the stored credentials), permission_denied (the credentials work but this resource is not permitted), shape_drift (the source answered in a shape the connector no longer recognizes, or a full fetch came back empty where records were expected), session_ended (the source ended the session twice in one run, before and after a fresh login), provider_error (the source answered with a server error), timeout (the source did not answer in time), not_found (the source has no such resource), and scopes_failed (some scopes of a fan-out could not be fetched and are retried next run). Every code except auth_rejected and permission_denied is retried on the next run without action on your side.
derivationstring or nullpending while the organization's tables are still rebuilding from this run, settled once they have, null while the run itself has not finished.
tablesarray or nullOne { table, cursor } per published table once derivation is settled, null before. cursor is the current position of that table's change feed for this organization, the same opaque string GET /v1/tables/:name/changes accepts, or null for a table with no changes yet. Read the tables, then replay the change feed from these cursors to pick up everything that changed after this run.
{"id": "run_3c2b1a","connection_id": "conn_7f3k2m","trigger": "manual","mode": "full","status": "succeeded","started_at": "2026-09-08T09:00:00Z","finished_at": "2026-09-08T09:24:00Z","error": null,"report": {"resources": { "invoices": { "fetched": 4120, "created": 12, "updated": 40, "attachments": 0, "failed_scopes": 0, "skipped_scopes": 0, "denied_scopes": 0, "error": null, "error_code": null, "effective_window_start": "2022-01-01T00:00:00.000Z", "window_reason": "history_from" } },"deleted_by_entity_type": {},"bounded": true},"derivation": "settled","tables": [{ "table": "invoices_clean", "cursor": "eyJjIjoiODIzMiJ9" }]}
/v1/connectionsLists every connection of every organization that has connected to you, oldest first. Filter by org, connector, or status to narrow.
orgstringOnly this organization's connections. 404 org_not_granted when it has not connected to you.
connectorstringOnly connections of this connector slug.
statusstringOnly connections in this status: pending, connected, error, expired, disconnected, or paused.
limitnumberDefaults to 50. Values above 200 are clamped to 200.
cursorstringThe next_cursor from the previous page, sent back exactly as you received it.
connectionsarraynext_cursorstring or nullThe bookmark for the next page. null means the last page.
$ curl https://api.valtrix.com/v1/connections \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "status=connected" -d "limit=50"{"connections": [{ "id": "conn_7f3k2m", "org_id": "org_7f3k2m", "connector": "sage-100-contractor", "status": "connected", "history_from": "2022-01-01T00:00:00Z", "last_synced_at": "2026-09-08T09:30:00Z", "initial_sync_completed_at": "2026-08-01T10:12:00Z", "paused_at": null, "paused_until": null, "writes": { "vendor": ["create", "update"] }, "write_guidance": {} }],"next_cursor": null}
/v1/connections/:idFetches one connection by id.
idpathThe connection id.
The connection object, or 404 connection_not_found when no organization that connected to you owns it.
$ curl https://api.valtrix.com/v1/connections/conn_7f3k2m \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "conn_7f3k2m", "org_id": "org_7f3k2m", "connector": "sage-100-contractor", "status": "connected", "history_from": null, "last_synced_at": "2026-09-08T09:30:00Z", "initial_sync_completed_at": "2026-08-01T10:12:00Z", "paused_at": null, "paused_until": null, "writes": {}, "write_guidance": {} }
/v1/connections/:idUpdates the settings of a connection. history_from is the one updatable field today: setting it to an earlier date queues one full sync that backfills the added history. That backfill runs once per resource; every sync after it is back on the connector's default trailing window, so the added records stay put but are not refreshed. The date can only move earlier; a later date returns 400 invalid_history_from, because narrowing would sweep records the organization already relies on. Pausing and resuming are state changes and have their own endpoints.
idpathThe connection id.
history_fromstringAn ISO 8601 date, earlier than the current history_from and not in the future.
The updated connection object.
$ curl -X PATCH https://api.valtrix.com/v1/connections/conn_7f3k2m \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "history_from": "2022-01-01" }'{ "id": "conn_7f3k2m", "history_from": "2022-01-01T00:00:00.000Z", ... }
/v1/connections/:id/runsStarts a full sync of the connection and returns the run with status 202. Poll GET /v1/connections/:id/runs/:runId, or subscribe to the connection.sync_completed webhook event, for the report and the per-table cursors. 409 sync_in_progress while a sync is already queued or running, 409 connection_paused while the connection is paused.
idpathThe connection id.
modestringOnly "full" is accepted; omit it. Incremental syncs run on the connection's own schedule.
The sync run object, with status 202.
$ curl -X POST https://api.valtrix.com/v1/connections/conn_7f3k2m/runs \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{}'{ "id": "run_3c2b1a", "connection_id": "conn_7f3k2m", "trigger": "manual", "mode": "full", "status": "queued", "started_at": null, "finished_at": null, "error": null, "report": null, "derivation": null, "tables": null }
/v1/connections/:id/runs/:runIdFetches one sync run of the connection. While it runs, report and tables are null. Once it finishes, report is filled; once the organization's tables have rebuilt from it, derivation is settled and tables carries the cursor per table. Note the cursors, read the tables, then replay the change feed from the cursors for anything that changed afterwards.
idpathThe connection id.
runIdpathThe run id, as returned by POST /v1/connections/:id/runs or the connection.sync_completed event.
The sync run object, or 404 run_not_found.
$ curl https://api.valtrix.com/v1/connections/conn_7f3k2m/runs/run_3c2b1a \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "run_3c2b1a", "status": "succeeded", "report": { "resources": { "invoices": { "fetched": 4120, ... } }, "deleted_by_entity_type": {}, "bounded": true }, "derivation": "settled", "tables": [{ "table": "invoices_clean", "cursor": "eyJjIjoiODIzMiJ9" }], ... }
/v1/connections/:id/pauseStops syncing the connection while keeping every record, table row, change, and attachment readable and the organization's credentials stored. Queued and running syncs are cancelled, nothing is scheduled, and source webhooks are ignored. The connection stays billable, since its data is still served. Use it to hold the organization's data still while you reconcile a point-in-time read, or for a source the organization has switched off, whose data stays part of its tables for as long as the organization keeps the connection. A platform may pause only a connection it is the sole reader of; when another platform also reads it, 409 connection_shared names them and the organization pauses it from its console. Resume with POST /v1/connections/:id/resume, or let a pause with until lift on its own.
idpathThe connection id.
untilstringAn ISO 8601 timestamp at which the pause lifts and syncing resumes from where it stopped, at most a year away. Omit it to pause with no expiry.
The paused connection object. 409 connection_not_pausable when the connection is pending, disconnected, or already paused.
$ curl -X POST https://api.valtrix.com/v1/connections/conn_7f3k2m/pause \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "until": "2026-09-11T09:31:00Z" }'{ "id": "conn_7f3k2m", "status": "paused", "paused_at": "2026-09-08T09:31:00Z", "paused_until": "2026-09-11T09:31:00Z", ... }
/v1/connections/:id/resumeLifts a pause. The connection returns to connected and its next sync runs at once, incrementally from the watermark it stopped at, so nothing that changed in the source during the pause is lost.
idpathThe connection id.
The connection object. 409 connection_not_paused when it is not paused.
$ curl -X POST https://api.valtrix.com/v1/connections/conn_7f3k2m/resume \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{}'{ "id": "conn_7f3k2m", "status": "connected", "paused_at": null, "paused_until": null, ... }
Mint and revoke organization-scoped API keys, so each app or environment that acts for one organization holds a credential that can only reach that organization's data. These endpoints require a platform-wide key with manage access, the same level that governs transformation deploys and webhook endpoints: scoped keys cannot mint, list, or revoke keys, and keys minted here are always scoped and carry read or write access, never manage. Read-only keys suit dashboards, analytics tools, and anything else that only consumes data. Platform-wide keys are minted only in the console, under Developers. Revoking the grant for an organization also cuts off its scoped keys, because every request re-checks the grant; revoking a single key retires one credential without touching the grant. Handing scoped keys to agent-built apps is covered in Embedded coding agents.
An API key's metadata. The secret itself appears once, in the key field of the create response, and never again; only the prefix is kept for display.
idstringThe key id. Pass it to DELETE /v1/keys/:id to revoke the key.
namestringThe label the key was created with. Name keys after the app or environment that holds them.
prefixstringThe first characters of the secret, for display and log correlation. Organization-scoped secrets start with vlt_org_; platform-wide secrets start with vlt_; read-only secrets with vlt_read_ or vlt_org_read_.
orgstring or nullThe organization the key is scoped to, or null for a platform-wide key.
accessstringThe key's access level: read, write, or manage. Read-only keys cannot write or delete records or create connect sessions. Organization-scoped keys are never manage.
managebooleanWhether access is manage. Kept for backwards compatibility; read access instead.
created_atstringWhen the key was created, as an ISO 8601 timestamp.
last_used_atstring or nullWhen the key last authenticated a request, or null if it never has.
revoked_atstring or nullWhen the key was revoked, or null while it is live. A revoked key fails authentication immediately.
{"id": "key_8m2kf4","name": "Acme production app","prefix": "vlt_org_a1b2c3d4","org": "org_7f3k2m","access": "write","manage": false,"created_at": "2026-07-10T08:00:00Z","last_used_at": null,"revoked_at": null}
/v1/keysMints a new API key pinned to one organization. The organization must hold an active grant. The full secret is returned once, in the key field. Store it immediately.
orgstring, requiredThe organization id to scope the key to, as listed by GET /v1/orgs and carried on every organization object. Required: keys created through the API are always organization-scoped.
namestring, requiredA label for the key, non-empty and at most 100 characters. Name it after the app or environment that will hold it.
accessstringread or write. Defaults to write. Read-only keys can read everything the grant covers but cannot write or delete records or create connect sessions.
The API key object plus a key field carrying the full secret, shown only in this response. 404 org_not_granted when the organization has no active grant.
$ curl https://api.valtrix.com/v1/keys \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "org": "org_7f3k2m", "name": "Acme production app" }'{"id": "key_8m2kf4","name": "Acme production app","prefix": "vlt_org_a1b2c3d4","org": "org_7f3k2m","access": "write","manage": false,"created_at": "2026-07-10T08:00:00Z","last_used_at": null,"revoked_at": null,"key": "vlt_org_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"}
/v1/keysLists every API key, including revoked ones, newest first. Not paginated. Filter by org to see the keys scoped to one organization, for example when offboarding it.
orgstringNarrow to keys scoped to one organization, by organization id.
keysarrayAPI key objects, newest first.
$ curl https://api.valtrix.com/v1/keys \-H "Authorization: Bearer $VALTRIX_API_KEY" \-G -d "org=org_7f3k2m"{"keys": [{ "id": "key_8m2kf4", "name": "Acme production app", "prefix": "vlt_org_a1b2c3d4", "org": "org_7f3k2m", "access": "write", "manage": false, "created_at": "2026-07-10T08:00:00Z", "last_used_at": "2026-07-12T14:00:00Z", "revoked_at": null }]}
/v1/keys/:idRevokes a key immediately: any request made with it fails authentication from this point on. Revoking an already-revoked key is a no-op, so retries are safe.
idpathThe key id.
The API key object with revoked_at set, or 404 key_not_found when no key with that id exists.
$ curl -X DELETE https://api.valtrix.com/v1/keys/key_8m2kf4 \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "key_8m2kf4", "name": "Acme production app", "prefix": "vlt_org_a1b2c3d4", "org": "org_7f3k2m", "access": "write", "manage": false, "created_at": "2026-07-10T08:00:00Z", "last_used_at": "2026-07-12T14:00:00Z", "revoked_at": "2026-07-14T09:00:00Z" }
The management surface for transformations as code: read every transformation as a canonical YAML definition and apply definitions through the same draft-then-publish pipeline the dashboard uses. This section is the contract: the endpoints, followed by the full definition file format (the file envelope, every step type, the expression functions, condition groups, and quality rules). The step-by-step workflow around them (repo layout, pull requests, CI wiring, ownership and adoption) lives in the Transformations as code guide, in the console under Developers. Reads and plans work with any API key; apply requires a key with manage access, the same level that governs API key management under Keys. To author a transformation from scratch, write a definition file, validate it with POST /v1/transformations/plan until the plan is clean, then publish it with POST /v1/transformations/apply.
A transformation and its canonical definition. The table name is the identity: definitions match remote transformations by the table they publish.
tablestringThe output table the transformation publishes. The same name you query at /v1/tables/:name, and the identity a definition file matches on.
namestringThe display name.
managed_bystringui while the dashboard editor owns the steps, code once a deploy has adopted it. Code-managed transformations are read-only in the dashboard editor.
statusstringdraft before the first publish, published after.
published_versionnumber or nullThe current published version number, or null before the first publish.
has_draftbooleanWhether an unpublished dashboard draft exists.
definitionstringThe canonical YAML definition. Re-applying it unchanged plans as a noop.
{"table": "vendors_enriched","name": "Vendors enriched","managed_by": "code","status": "published","published_version": 3,"has_draft": false,"definition": "version: 1\ntable: vendors_enriched\n..."}
The canonical YAML shape of a transformation, one file per output table. Steps and quality rules are the same validated shapes the dashboard editor produces; the file adds only this envelope. Step types, expression functions, condition groups, and quality rules are each documented below.
versionnumber, default 1The definition format version. The only current version is 1.
tablestringThe output table name and the definition's identity: lowercase letters, digits, and underscores, starting with a letter, at most 63 characters. Renaming a table is a delete plus a create, not a rename.
namestringThe display name shown in the console.
source{ entity: string } or { table: string }The input rows. { entity } reads the canonical records of an entity type (slugs listed by GET /v1/schema); { table } reads another published table, so transformations chain.
schedulenone | hourly | daily | weekly, default "none"A timed rebuild cadence, on top of the rebuild that runs whenever new synced data arrives.
org_scopeall | selected, default "all"all runs for every granted organization; selected runs only for organizations picked in the dashboard. Deploys never change which organizations are selected.
stepslist of stepsApplied in order; every step type is documented under Step types.
qualitylist of quality rules, optionalDocumented under Quality rules.
version: 1table: vendors_enrichedname: Vendors enrichedsource:entity: vendorschedule: dailyorg_scope: allsteps:- type: standardizecolumn: emailformat: lowercaseonUnparseable: keep- type: filter_rowswhere:logic: andconditions:- { column: status, operator: eq, value: active }- type: derived_columncolumn: region_labelexpression:fn: casecases:- when:logic: andconditions:- { column: region, operator: not_null }then: { column: region }else: { literal: unknown }quality:- type: not_nullcolumn: emailseverity: warn- type: accepted_valuescolumn: statusseverity: errorconfig:values: [active]
/v1/transformationsLists every transformation with its canonical definition. The starting point for exporting existing transformations into files.
transformationsarrayTransformation objects, oldest first.
$ curl https://api.valtrix.com/v1/transformations \-H "Authorization: Bearer $VALTRIX_API_KEY"{"transformations": [{ "table": "vendors_enriched", "name": "Vendors enriched", "managed_by": "code", "status": "published", "published_version": 3, "has_draft": false, "definition": "version: 1\n..." }]}
/v1/transformations/:tableFetches one transformation by the table it publishes.
tablepathThe output table name.
The transformation object, or 404 transformation_not_found when no transformation publishes that table.
$ curl https://api.valtrix.com/v1/transformations/vendors_enriched \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "table": "vendors_enriched", "name": "Vendors enriched", "managed_by": "code", "status": "published", "published_version": 3, "has_draft": false, "definition": "version: 1\n..." }
/v1/transformations/planValidates definitions server-side and returns the structured diff a deploy would apply, without writing anything. Works with any API key, so it runs in CI on pull requests; the CLI exits non-zero while changes are pending. Plan is also the way to iterate on a definition before publishing: fix what each item's error reports, re-plan, and apply once every item is clean.
definitionsarray, requiredAn array of { "file", "content" } entries, one per definition file. At most 100 per call.
adoptbooleanWhen true, the plan treats dashboard-managed transformations as adoptable instead of reporting them blocked. Defaults to false.
itemsarrayOne item per definition, in dependency order. Each carries the action (create, update, noop, missing_remote, or error), setting, step, and quality-rule changes, the schema impact (output columns added or removed), and warnings. missing_remote means the definition reads a source table that neither exists nor is defined in the same call. Items blocked by dashboard ownership carry blocked: "managed_by_ui"; invalid definitions carry the validation message in error.
$ curl https://api.valtrix.com/v1/transformations/plan \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "definitions": [{ "file": "vendors_enriched.yaml", "content": "version: 1\n..." }] }'{"items": [{ "file": "vendors_enriched.yaml", "table": "vendors_enriched", "action": "update", "managed_by": "code", "blocked": null, "settings": [], "steps": { "added": [], "removed": [], "modified": ["standardize (s1)"] }, "quality": null, "schema_impact": { "added": [], "removed": [], "dynamic": false }, "warnings": [], "error": null }]}
/v1/transformations/applyWrites definitions into drafts and publishes each changed transformation through the standard publish pipeline, stamping the publish with the API key's identity. Transformations apply in dependency order and the deploy stops at the first failure; later items report skipped. An unchanged definition is a noop and creates no new version. Preview the diff first with POST /v1/transformations/plan. Requires a key with manage access (the level that also governs API key management); applying marks each transformation managed_by code. The CLI has no bare apply command: valtrix deploy always plans first, shows the diff, and only applies with --yes.
definitionsarray, requiredAn array of { "file", "content" } entries, one per definition file. At most 100 per call.
adoptbooleanWhen true, dashboard-managed transformations are adopted into code management instead of skipped. Defaults to false.
itemsarrayOne item per definition, in dependency order. Each carries the action (create, update, noop, missing_remote, or error), setting, step, and quality-rule changes, the schema impact (output columns added or removed), a status (applied, noop, skipped, or failed), and the new version number and publish run_id for applied items. Items blocked by dashboard ownership carry blocked: "managed_by_ui".
$ curl https://api.valtrix.com/v1/transformations/apply \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "definitions": [{ "file": "vendors_enriched.yaml", "content": "version: 1\n..." }] }'{"items": [{ "file": "vendors_enriched.yaml", "table": "vendors_enriched", "action": "update", "status": "applied", "version": 4, "run_id": "run_9", "managed_by": "code", "blocked": null, "settings": [], "steps": { "added": [], "removed": [], "modified": ["standardize (s1)"] }, "quality": null, "schema_impact": { "added": [], "removed": [], "dynamic": false }, "warnings": [], "error": null }]}
Steps run in order within each organization's rows; rows never mix across organizations. Every step also accepts an optional id (string), a stable identifier used in deploy diffs; ids like step_1 are minted when omitted. No step can produce the reserved row-metadata columns _record_id, _external_id, _org_id, _connector, _synced_at, _changed_at, _attachment_id, _attachment_ids.
rename_columnsRename columns; mapping is {"old_name": "new_name"}.
mappingobjectdrop_columnsRemove the listed columns.
columnslist of stringskeep_columnsKeep only the listed columns and remove every other column.
columnslist of stringscastConvert a column to the given type; onError controls values that cannot be converted. "json" parses JSON text (an object or array) into structured data, so SQL step output built with json_object or json_group_array is served as real nested objects instead of a string.
columnstringtostring | number | boolean | date | jsononErrorkeep | nullstandardizeNormalize the format of a column. defaultRegion is a 2-letter country code used by phone_e164.
columnstringformattrim | lowercase | uppercase | title_case | phone_e164 | date_iso | currency_codedefaultRegionstring, optionalonUnparseablekeep | nullmap_valuesReplace observed values with canonical ones; fallback controls values missing from the mapping.
columnstringmappingobjectfallbackkeep | null | valuefallbackValuestring, optionalreplace_textFind and replace every occurrence inside a column's text; mode "regex" treats find as a regular expression.
columnstringfindstringreplaceWithstringmodeplain | regexsplit_columnSplit a column on a separator into the listed new columns, in order. The original column is removed unless keepOriginal is true.
columnstringseparatorstringintolist of stringskeepOriginalboolean, optionalextract_jsonExtract a nested value from a JSON object or array column into a new column. path uses dots and [n] indexes, e.g. "address.city" or "items[0].sku".
columnstringpathstringtargetstringbinLabel numeric ranges: the first bin whose exclusive upper bound "upTo" exceeds the value wins; a bin without upTo catches everything above. Non-numeric values get fallbackLabel or null.
columnstringtargetstringbinslist of objectsEach item: upTo (number, optional), label (string).
fallbackLabelstring, optionalfilter_rowsKeep only rows matching the condition group.
wherecondition groupDocumented under Conditions.
set_defaultFill empty values in a column with a default.
columnstringvaluestringderived_columnCreate a new column from an expression over existing columns.
columnstringexpressionexpressionOne of the functions documented under Expression functions.
dedupeRemove duplicate rows sharing the key columns. With orderBy, rows are ranked by that column and the first survives; otherwise keep decides ("latest" = most recently synced).
keyslist of stringskeeplatest | firstorderByobject, optionalFields: column (string), direction (asc | desc).
lookupJoin columns from another relation by matching localKey to remoteKey. With source "table" (the default), tableName names a published table; with source "entity", tableName is the slug of another entity type and the join runs against its canonical records. Rows join within the same org only. Every relation and the input rows carry "_record_id", each record's own id; a reference column (like document_id or customer_id) holds the _record_id of the record it points at, so a child-to-parent join uses the reference column as localKey and "_record_id" as remoteKey.
sourcetable | entity, optionaltableNamestringlocalKeystringremoteKeystringtakelist of stringsprefixstring, optionalrollupAggregate the matching rows of another relation into one new column per row, e.g. the number of orders per customer. Matches localKey to remoteKey within the same org; fn runs over the matches' "column" ("count" may omit it). For per-parent child aggregates, use "_record_id" as localKey and the child's reference column (like document_id) as remoteKey.
sourcetable | entity, optionaltableNamestringlocalKeystringremoteKeystringfncount | sum | avg | min | maxcolumnstring, optionaltargetstringaggregateGroup rows by the groupBy columns (within each org) and replace them with one row per group holding the groupBy columns plus the aggregation targets. An empty groupBy produces one row per org.
groupBylist of stringsaggregationslist of objectsOne output column: fn over the group's rows. "count" without a column counts rows; every other fn needs a source column. Each item: target (string), fn (count | count_distinct | sum | avg | min | max | first), column (string, optional).
unionAppend the rows of another relation (a published table, or an entity type with source "entity") below the current rows. mapping optionally renames source columns, {"source_column": "target_column"}; unmapped columns keep their names. Only rows from orgs present in the current rows are appended.
sourcetable | entity, optionaltableNamestringmappingobject, optionalpivotReshape long to wide: group by the groupBy columns and create one column per listed headerValue, filled with agg over valueColumn of the rows whose headerColumn equals it. headerValues must list the expected values explicitly.
groupBylist of stringsheaderColumnstringvalueColumnstringaggcount | count_distinct | sum | avg | min | max | firstheaderValueslist of stringsunpivotReshape wide to long: each row becomes one row per listed column, with the column name in keyColumn and its value in valueColumn. dropEmpty skips empty values.
columnslist of stringskeyColumnstringvalueColumnstringdropEmptyboolean, optionalexplodeFan out a JSON-array column into one row per element. mode "value" puts each element in the column; mode "flatten" spreads object elements into columns (optionally prefixed). Rows without an array are dropped unless keepEmpty is true.
columnstringmodevalue | flattenprefixstring, optionalkeepEmptyboolean, optionalsqlRun a single SQLite SELECT (or WITH) over the current rows, separately per org. The table is named "input" and has the data columns plus _record_id, _external_id, _org_id, _connector, _synced_at (when the record was last re-read from its source), _changed_at (when its data last actually differed; use it for "last updated" columns), _attachment_id (the id of the stored source document behind the record, the first when there are several, null when none) and _attachment_ids (a JSON array of all of them). Keep _record_id (select input.*) when the query preserves rows; a query that groups or reshapes may omit _record_id and row ids are generated. Set key to the output columns that identify such a row (an invoice id plus a payment id, a status plus a month) so the generated id stays the same when the other columns change; without key the id is derived from every value in the row, so any edit to the query or to a value replaces the row with a new one and consumers of the change feed see a delete and an insert. Published tables can be joined by their name, and entity types by their slug when no table shares that name; a joined relation has ONLY _record_id and _org_id plus its data columns (no _synced_at or _changed_at) and already contains only rows from the same org, so no org filtering is needed. Use only when the other step types cannot express the logic.
querystringkeylist of strings, optionalThe functions a derived_column step's expression field accepts, selected by fn. Column arguments name existing columns.
concatpartslist of { column: string } or { literal: string }coalescecolumnslist of stringsarithmeticArithmetic over columns and numbers; left/right are a column name, a number, or a nested arithmetic expression.
leftcolumn name, number, or nested arithmetic expressionoperator+ | - | * | /rightcolumn name, number, or nested arithmetic expressionyear_ofcolumnstringcaseIf/else over conditions: the first matching case wins; "then"/"else" are a column reference or a literal. Without a matching case and no "else", the value is null.
caseslist of objectsEach item: when (condition group), then ({ column: string } or { literal: string or number }).
else{ column: string } or { literal: string or number }, optionalsubstringSlice of the text; start is 1-based, length optional (to the end when omitted).
columnstringstartnumberlengthnumber, optionalreplacecolumnstringfindstringreplaceWithstringregexboolean, optionalregex_extractFirst regex match; group defaults to capture group 1 when the pattern has one, else the whole match.
columnstringpatternstringgroupnumber, optionalsplit_partSplit the text on the separator and take the 1-based part.
columnstringseparatorstringindexnumberlengthcolumnstringdate_partcolumnstringpartyear | month | daydate_trunccolumnstringunitday | week | month | yeardate_diffDifference "to" minus "from" in the unit; both are date columns, "to" defaults to now.
unitdays | months | yearsfromstringtostring, optionaldate_addcolumnstringamountnumberunitdays | months | yearsroundcolumnstringdigitsnumber, optionalabscolumnstringfloorcolumnstringceilcolumnstringCondition groups appear in filter_rows steps, in case expressions, and in expression quality rules.
condition groupA group of column conditions combined with "and" or "or". Operators "in"/"not_in" read the "values" list; "not_null"/"is_null" need no value and treat blank strings, empty lists, and empty objects as empty; "contains"/"not_contains" check list elements when the column holds a list; "gt"/"gte"/"lt"/"lte" and "within_last_days" compare numerically; "before"/"after" compare as dates; every other operator compares against "value" as text.
logicand | orconditionslist of conditionsconditionOne column check inside a condition group.
columnstringoperatorenumOne of: eq (equals), neq (does not equal), contains (contains), not_contains (does not contain), starts_with (starts with), ends_with (ends with), regex (matches regex), in (is one of), not_in (is not one of), gt (greater than), gte (greater than or equal), lt (less than), lte (less than or equal), before (is before), after (is after), within_last_days (within last N days), not_null (is not empty), is_null (is empty).
valuestring, optionalThe comparison value; unused by not_null, is_null, in, and not_in.
valueslist of strings, optionalThe list read by in and not_in.
Rules evaluate separately within each organization's rows after every rebuild. With severity error, not_null, unique, accepted_values, regex, relationship, expression rules quarantine failing rows, removing them from the table until they pass; freshness and volume report without quarantining. With severity warn, every rule reports without quarantining.
not_nullFails rows where the column is empty. Blank strings, empty lists, and empty objects count as empty.
columnstringThe column the rule checks.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
uniqueFails every row whose column value appears more than once within the organization's rows.
columnstringThe column the rule checks.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
accepted_valuesFails rows whose column value is not in the accepted list.
columnstringThe column the rule checks.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.valueslist of stringsThe accepted values, compared as text.
regexFails rows whose column value does not match the pattern.
columnstringThe column the rule checks.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.patternstringA JavaScript regular expression source, without slashes.
freshnessFails rows whose last sync is older than the age limit. Takes no column.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.maxAgeHoursnumber, default 24The maximum age in hours.
volumeFails when an organization has fewer rows than the minimum. Takes no column.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.minRowsnumber, default 1The minimum expected row count per organization.
relationshipFails rows whose column value does not exist as a key in the related table or entity type, checked within the same organization.
columnstringThe column the rule checks.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.relationobjectFields: name (string, the related published table, or an entity type slug), remoteKey (string, the key column in the relation), source (table | entity, optional, resolved automatically when omitted).
expressionFails rows that do not match the condition group.
severityerror | warn, default "error"namestring, optionalThe display name shown in the console.
config.wherecondition groupDocumented under Conditions.
Webhooks push notifications to your server so you never poll on a timer. Register an endpoint with POST /v1/webhooks, or under Developers, Webhooks in the console, pick the event types it receives, and you get a signing secret shown once. Two paths cover data changes. table.changed is the thin one: it carries ids, never row data, and you drain the change feed from your own stored cursor. table.row_changed is the fat one, opt-in per endpoint and per table: one delivery per changed row with the row, the columns that changed, and their previous values, optionally narrowed to changes touching the columns you name. Deliveries retry with backoff for about 3 days and can repeat, so acknowledge with any 2xx within 5 seconds and deduplicate on the event id. Events arrive as fast as the data changes, and a sync often surfaces a whole day of changes at once, so expect bursts of hundreds of deliveries within seconds and process them asynchronously rather than inline in the request. An event still undelivered after that is marked dead and kept for 14 days; GET /v1/webhooks/:id/events lists the queue and POST /v1/webhooks/:id/replay re-queues the dead ones. Webhooks are the wake-up and the change feed is the record: subscribe to table.changed, drain GET /v1/tables/:name/changes from your stored cursor each time it fires, and reconcile from the same cursor every 15 to 60 minutes to cover a delivery you missed, never on a tight timer. Every delivery posts the event object unless the endpoint has a payload_template, in which case the receiver gets your JSON with placeholders filled in from the event: write the exact body the receiver expects, such as a chat tool's incoming-webhook message, and reference event fields as {{data.row.status}} or {{data.previous.status | default 'none'}}. A string that is exactly one placeholder takes the value's own JSON type, so a number stays a number and an object stays an object; placeholders inside text are interpolated, with objects and arrays as compact JSON. A delivery whose every placeholder resolves to nothing is skipped instead of sent. POST /v1/webhooks/preview renders a template against a sample event before you save it, and the placeholders it returns are the paths you can use.
A URL you registered to receive events, with the event types it subscribes to and its delivery health. Managing endpoints needs a platform-wide key with manage access; the signing secret is returned only by the create and rotate-secret calls.
idstringThe endpoint id.
urlstringWhere events are posted. https on a public host in production. Fixed for the life of the endpoint: register a new endpoint to change it.
descriptionstring or nullA label for your own reference.
statusstringactive while deliveries succeed, failing once 20 deliveries in a row have failed (deliveries continue), disabled when you turned it off; a disabled endpoint receives nothing, and events keep queuing for it until you re-enable it or delete it, for up to 90 days; an event still waiting after that is dropped.
event_typesarrayThe event types the endpoint receives, sorted. Any of the names under Event types except endpoint.test, which every endpoint accepts.
payload_templatestring or nullnull when deliveries post the event object as is. Otherwise a JSON document, as a string, that is rendered per event and posted instead: every {{path}} in its strings is replaced with the value at that path of the event object (id, type, occurred_at, or data and its fields, down to data.row.column and data.previous.column for table.row_changed), {{path | default 'text'}} supplies a value when the path is missing, {{path | escape}} writes the value with &, <, and > replaced by &, <, and > (the escaping a chat tool such as Slack asks for, so a value is never read as its markup; it combines with default in either order), and a string that is exactly one placeholder is replaced by the raw value rather than text. A Slack incoming webhook URL takes a template of the form { "text": "..." } and nothing else: Slack requires text and answers 200 with the body ok. Deliveries whose placeholders all resolve to nothing are recorded as skipped and not sent. The signature covers the rendered body.
stale_after_secondsnumber or nullThe age past which a queued event is discarded instead of delivered, in seconds, or null to retry every event for the full three-day window. Set it on an endpoint that drives alerts so a receiver that comes back after a day gets the last day of changes, not everything that queued while it was down. Discarded events are listed with status discarded and can be replayed.
tablesarrayThe table.row_changed subscriptions as { table, columns, change_types, resync_required_at }: the published table by name, when columns is non-empty the columns an update must touch to be delivered, and when change_types is non-empty the kinds of change delivered, any of insert (a new row), update, and delete. An empty change_types delivers all three, and columns never holds back a new row or a delete, so an endpoint that should hear about one column changing and nothing else sets both. resync_required_at is null unless the subscription fell so far behind that changes it had not received were pruned (an endpoint left disabled for over 90 days); it then marks when that happened, and the gap is closed by a full read of the table, after which sending the table again in PATCH /v1/webhooks/:id clears it. Empty unless event_types includes table.row_changed.
consecutive_failuresnumberFailed deliveries since the last success. Resets to zero on a successful delivery or when you re-enable the endpoint.
last_success_atstring or nullWhen a delivery last got a 2xx, as an ISO 8601 timestamp.
last_failure_atstring or nullWhen a delivery last failed, as an ISO 8601 timestamp.
pending_eventsnumberEvents queued for the endpoint and not yet delivered, including those waiting out a retry backoff.
dead_eventsnumberEvents that exhausted their retries and can be re-queued with POST /v1/webhooks/:id/replay. Kept for 14 days.
discarded_eventsnumberEvents dropped without delivery, by queued_events: discard on PATCH /v1/webhooks/:id or by the endpoint's stale_after_seconds. Replayable like dead events, kept for 14 days.
oldest_pending_atstring or nullWhen the oldest event still waiting was queued, as an ISO 8601 timestamp; null when nothing is pending. Together with pending_events it says how far behind a receiver is.
created_atstringWhen the endpoint was registered, as an ISO 8601 timestamp.
{"id": "whe_4k2m9p","url": "https://yourapp.com/webhooks/valtrix","description": "Production app","status": "active","event_types": ["connection.broken", "table.row_changed", "write_intent.applied", "write_intent.failed"],"payload_template": null,"stale_after_seconds": null,"tables": [{ "table": "customers_clean", "columns": ["email"], "change_types": ["update"], "resync_required_at": null }],"consecutive_failures": 0,"last_success_at": "2026-07-10T08:05:00Z","last_failure_at": null,"pending_events": 0,"dead_events": 0,"discarded_events": 0,"oldest_pending_at": null,"created_at": "2026-07-10T08:00:00Z"}
An event in an endpoint's queue: waiting to be delivered, delivered, dead after exhausting its retries, or discarded without delivery. Read them from GET /v1/webhooks/:id/events to see what an endpoint has not received yet, and re-queue dead or discarded ones with POST /v1/webhooks/:id/replay. Delivered, dead, and discarded events are kept for 14 days.
idstringThe event id, the same value your endpoint receives in the Valtrix-Event-Id header and the envelope.
typestringThe event type, like table.row_changed. Event types lists every one.
statusstringpending while delivery is still being attempted, delivered once your endpoint answered 2xx, dead once every retry failed, discarded when it was dropped without delivery, either by queued_events: discard or because it aged past the endpoint's stale_after_seconds (the delivery log records which).
attemptsnumberHow many deliveries have been tried. A replay resets it to zero.
next_attempt_atstring or nullWhen the next delivery is due for a pending event, as an ISO 8601 timestamp; null once delivered, dead, or discarded.
delivered_atstring or nullWhen your endpoint accepted the event, as an ISO 8601 timestamp; null until then.
created_atstringWhen the event was queued, as an ISO 8601 timestamp; the envelope's occurred_at.
dataobjectThe event payload exactly as the envelope carries it. Event types lists the fields per event.
{"id": "evt_01h9x...","type": "table.row_changed","status": "dead","attempts": 19,"next_attempt_at": null,"delivered_at": null,"created_at": "2026-07-10T08:05:00Z","data": { "table": "customers_clean", "record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "change_type": "upsert" }}
One attempt to post an event to an endpoint, kept for 14 days, with the body that was sent and the start of the receiver's reply. Read them from GET /v1/webhooks/:id/deliveries to see why an endpoint is failing.
idstringThe delivery id.
event_idstring or nullThe queued event this delivery attempted, as listed by GET /v1/webhooks/:id/events; null for table.changed pings and test events, which are not queued.
event_typestringThe event type that was posted, like table.row_changed or endpoint.test.
statusstringsucceeded when your endpoint answered 2xx within 5 seconds, failed otherwise, skipped when the endpoint's payload_template resolved no placeholder for this event so nothing was sent. A 429 is recorded as failed but retried after the Retry-After header without counting toward the endpoint's failure count or the event's attempts, so a rate-limited event waits as long as the endpoint keeps asking.
http_statusnumber or nullThe HTTP status your endpoint returned, or null when the request never completed.
errorstring or nullWhat went wrong on a failed delivery: the non-2xx status, a timeout, or a connection error.
request_bodystring or nullThe exact body that was posted: the event object, or the rendered payload_template when the endpoint has one. Truncated past 128 KB. Null when nothing was sent, as on a skipped delivery.
response_bodystring or nullThe first 2 KB of what your endpoint answered, or null when it sent nothing or never answered. The place to read a receiver's own error message. Always null on a test delivery, whose reply is not stored.
duration_msnumber or nullHow long the request took.
created_atstringWhen the attempt was made, as an ISO 8601 timestamp.
{"id": "whd_9s2ph1","event_id": "evt_01h9x...","event_type": "table.row_changed","status": "failed","http_status": 500,"error": "Endpoint responded 500","request_body": "{\"id\":\"evt_01h9x...\",\"type\":\"table.row_changed\",\"occurred_at\":\"2026-07-10T08:05:00Z\",\"data\":{...}}","response_body": "internal error","duration_ms": 212,"created_at": "2026-07-10T08:05:00Z"}
The envelope every webhook delivery posts to your endpoint. The same shape for every event type; only data varies. An endpoint with a payload_template posts the rendered template instead, with this object as the source of its placeholder values.
idstringUnique per event. Deliveries can repeat, so treat an id you have already seen as handled.
typestringThe event name, like table.changed. Event types lists every one.
occurred_atstringWhen the event happened, as an ISO 8601 timestamp.
dataobjectThe event payload. Carries ids and names; only table.row_changed carries row data. Event types lists the fields per event.
{"id": "evt_01h9x...","type": "table.changed","occurred_at": "2026-07-08T09:30:00Z","data": { "table": "customers_clean", "latest_cursor": "eyJjIjoiODIzMiJ9" }}
Every event the type field can name, with the fields each one puts in data.
connection.brokenAn organization's connection stopped syncing. Rows already synced stay in your tables but stop updating until it recovers.
data.org_idstringThe organization the connection belongs to.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
data.connectorstringThe connector slug of the affected system.
data.reasonstringreauthorization_required when the organization must reconnect through a new Connect session, sync_failed when syncing hit a persistent error, bridge_offline when the Valtrix Bridge on the organization's machine stopped reporting in, bridge_revoked when the organization's bridge pairing was revoked.
connection.pausedA connection was paused, by you, by the organization, or by an admin. Its data stays readable but stops updating until it resumes. Not in an endpoint's default subscriptions; opt in.
data.org_idstringThe organization the connection belongs to.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
data.connection_idstringThe paused connection.
data.connectorstringThe connector slug of the paused system.
data.paused_untilstring or nullWhen the pause lifts on its own, or null for a pause with no expiry.
connection.resumedA paused connection resumed, by request or because its pause expired. Its next sync runs at once. Not in an endpoint's default subscriptions; opt in.
data.org_idstringThe organization the connection belongs to.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
data.connection_idstringThe resumed connection.
data.connectorstringThe connector slug of the resumed system.
connection.sync_completedA sync run of a connection finished with data: succeeded, or partial when some resources failed. Carries the same report GET /v1/connections/:id/runs/:runId returns: on a partial run each failed resource carries error, a sentence for a person, and error_code, the value to branch on. Read the run for the per-table cursors once the organization's tables have rebuilt. Fires for every run, scheduled ones included, so filter on trigger and mode. Not in an endpoint's default subscriptions; opt in.
data.org_idstringThe organization the connection belongs to.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
data.connection_idstringThe connection that synced.
data.connectorstringThe connector slug of the synced system.
data.run_idstringThe run id, to read back with GET /v1/connections/:id/runs/:runId.
data.triggerstringinitial, scheduled, manual, webhook, or field_selection.
data.modestringfull or incremental.
data.statusstringsucceeded or partial.
data.reportobjectThe completeness report of the sync run object.
connector_request.liveA connector you added through POST /v1/connector-requests went live: the connector is in the catalog and requesters' customer data is syncing. Sent to every platform on the request, including ones that joined an existing build.
data.request_idstringThe connector request id, matching the id in GET /v1/connector-requests.
data.provider_namestringThe provider name as the request submitted it.
data.product_namestring or nullThe specific product the request resolved to, when the provider sells more than one.
data.statusstringAlways "live" for this event.
data.connectorstringThe new connector's slug, valid everywhere the API takes a connector, so you can start minting Connect sessions with it.
connector_request.not_availableA connector request ended without a connector: the provider cannot be supported. The request stays visible in GET /v1/connector-requests with status not_available.
data.request_idstringThe connector request id, matching the id in GET /v1/connector-requests.
data.provider_namestringThe provider name as the request submitted it.
data.product_namestring or nullThe specific product the request resolved to, when the provider sells more than one.
data.statusstringAlways "not_available" for this event.
data.connectornullAlways null for this event.
endpoint.testA test delivery triggered from the console, for verifying your handler end to end. data is empty.
grant.revokedAn organization revoked your access; its rows disappear from your tables.
data.org_idstringThe organization that revoked access.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
org.connectedAn organization granted you access, through a new connection or by extending an existing grant.
data.org_idstringThe organization that granted access. Use it with the org parameter across the API.
data.external_idstring or nullYour identifier for the organization, as sent on the Connect session. Null when you have never sent one.
data.connect_session_idstring or nullThe id of the Connect session the organization completed, matching the id returned when you minted the link. The reliable way to attribute the event to the customer you minted the link for, even if the browser never reached your redirect_uri. Null when access was granted outside a Connect session.
org.initial_sync.completedThe first sync of a new connection finished and its data is ready to query.
data.org_idstringThe organization the connection belongs to.
data.external_idstring or nullYour identifier for the organization, or null when you have never sent one.
data.connectorstringThe connector slug of the system that synced.
table.changedA table has new changes. Fetch GET /v1/tables/:name/changes from your own stored cursor. Coalesced, at most one delivery per table every 30 seconds.
data.tablestringThe name of the table that changed, as used in /v1/tables/:name paths.
data.latest_cursorstringThe bookmark of the newest change at send time. Use it only to compare against your own stored cursor: if they match you are already caught up and can skip the drain. Never fetch the change feed starting from this value; that starts at the end of the feed and skips everything between your last drain and now. Always drain from your own stored cursor.
table.publishedA table was published for the first time and now appears in GET /v1/schema.
data.tablestringThe name of the new table.
data.schema_versionstringThe version it published at, like "v1". The same form GET /v1/schema reports.
table.row_changedOne row of a subscribed table changed. Opt in per endpoint with the tables field on POST /v1/webhooks or under Developers, Webhooks, choosing the tables and optionally the columns and the change types: with columns set, an update is delivered only when one of them changed, and with change_types set, only those kinds of change (insert, update, delete) are delivered. Columns alone never hold back a new row or a delete; leave insert and delete out of change_types for that. An insert arrives as change_type upsert with changed_columns null, an update as upsert with the columns that changed. Delivered in feed order per table, one event per change, starting from when the subscription was created; a table's initial load after a new connection is skipped, so you only hear about changes to data that was already there. Republishing the table's transformation is not skipped: every row the new version changes is delivered, and a row whose id changed arrives as a delete of the old id followed by an insert of the new one, deletes first. Rows from an aggregating or reshaping SQL step keep their syn_ id across republishes only when the step declares a key, so a step without one re-emits the whole table when a column is added. Rows larger than 64 KB arrive without the row, with row_omitted set, so fetch by record_id in that case.
data.tablestringThe name of the table the row belongs to, as used in /v1/tables/:name paths.
data.org_idstringThe organization the row belongs to.
data.external_idstring or nullYour identifier for the organization, as sent on the Connect session. Null when you have never sent one.
data.record_idstringThe id of the affected row, the same value the row carries as _record_id. For deletes it identifies which row to drop.
data.change_typestringupsert when the row was created or updated, delete when it was removed.
data.cursorstringThis change's position in the table's change feed, the same value GET /v1/tables/:name/changes returns for it. Store it if you also drain the feed, so the two paths never double-process a change.
data.changed_columnsarray or nullThe columns whose values differ from the row's previous version. null when the row is new to the table, and null for deletes.
data.previousobject or nullThe values the changed columns held before this change, keyed by column. null whenever changed_columns is null.
data.rowobject or nullThe full row after the change, in the row object shape. null for deletes, and null when the row was too large to deliver.
data.row_omittedbooleantrue when row is null because the row exceeded 64 KB. Read it with GET /v1/records/:recordId or the table's rows endpoint.
data.schema_versionstring or nullThe table's schema version at the time of the change, like "v3". Compare it with the version your client was generated from before reading columns.
table.schema_updatedA published table's schema changed. Compare against GET /v1/schema and regenerate your SDK types.
data.tablestringThe name of the table whose schema changed.
data.schema_versionstringThe new schema version, like "v3". The same form GET /v1/schema reports.
write_intent.appliedA propagated write landed in the organization's connected system. Where the connector can read the write back, verification says whether the source showed the values as sent. Fetch GET /v1/write-intents/:id for the full write intent, or the record for its updated source_id.
data.org_idstringThe organization the write belongs to.
data.external_idstring or nullYour identifier for the organization, as sent when minting its Connect session.
data.write_intentobjectThe write intent summary: id, status (always applied), op, entity_type, record_id, source_external_id, the id the source system assigned, and verification (confirmed, unconfirmed, or contradicted).
write_intent.failedA propagated write could not be applied after its retries. Inspect error, fix the input with POST /v1/records/:entityType if needed, then POST /v1/write-intents/:id/retry.
data.org_idstringThe organization the write belongs to.
data.external_idstring or nullYour identifier for the organization, as sent when minting its Connect session.
data.write_intentobjectThe write intent summary: id, status (always failed), op, entity_type, record_id, and error, why the write did not land, as { code, message, retryable, hint, source_message, step, source_changed }; the same error the write intent object carries.
/v1/webhooksRegisters a URL to receive events and returns the signing secret once, in the secret field. Store it immediately; it is never shown again, only rotated. With no event_types, the endpoint receives every event type except table.row_changed; pass tables to receive table.row_changed for those tables (it is added to event_types for you). Requires a platform-wide key with manage access.
urlstring, requiredWhere to post events. Must be https on a public host in production; localhost and private addresses are rejected.
descriptionstringA label for your own reference.
event_typesarrayThe event types to receive, from the names under Event types. Omit for every type except table.row_changed. At least one is required when sent.
tablesarraytable.row_changed subscriptions as { table, columns, change_types }: the published table by name, as listed by GET /v1/tables, optionally the columns an update must touch to be delivered (omit or send [] for every update), and optionally the kinds of change to deliver, any of insert, update, and delete (omit or send [] for all three). columns narrows updates only, so new rows and deletes still arrive unless change_types leaves them out: { columns: ["status"], change_types: ["update"] } delivers a row only when its status changes. Each subscription starts at the table's current position, so only changes after this call are delivered.
payload_templatestringA JSON document, as a string, to post instead of the event object, with {{path}} placeholders filled in from each event; see payload_template on the webhook endpoint object for the rules. Placeholders are checked against the event types and tables subscribed here, so a template naming a column no subscribed table has is rejected. Omit or send null to post the event object.
stale_after_secondsnumberHow old a queued event may get before it is discarded instead of delivered, in seconds (60 to 2592000, thirty days). Set it on an endpoint that drives alerts so a receiver that was down for a while catches up on recent changes only, not on everything since it went down. Omit or send null to keep retrying every event for the full retry window.
The webhook endpoint object plus a secret field carrying the signing secret, shown only in this response. 400 invalid_url when the URL is rejected, 400 invalid_subscriptions when an event type, table, column, or change type is unknown, 400 invalid_payload_template when the template does not parse or names a field the subscriptions do not carry, 400 invalid_request when stale_after_seconds is out of range.
$ curl https://api.valtrix.com/v1/webhooks \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "url": "https://yourapp.com/webhooks/valtrix", "event_types": ["table.row_changed", "connection.broken"], "tables": [{ "table": "customers_clean", "columns": ["email"], "change_types": ["update"] }] }'{"id": "whe_4k2m9p","url": "https://yourapp.com/webhooks/valtrix","description": null,"status": "active","event_types": ["connection.broken", "table.row_changed"],"tables": [{ "table": "customers_clean", "columns": ["email"], "change_types": ["update"] }],"consecutive_failures": 0,"last_success_at": null,"last_failure_at": null,"created_at": "2026-07-10T08:00:00Z","secret": "whsec_7c1f0b2e9a4d5f6a8b9c0d1e2f3a4b5c"}
/v1/webhooksLists every endpoint that has not been deleted, newest first, with its subscriptions and delivery health. Not paginated. Requires a platform-wide key with manage access.
webhooksarrayWebhook endpoint objects, newest first.
$ curl https://api.valtrix.com/v1/webhooks \-H "Authorization: Bearer $VALTRIX_API_KEY"{"webhooks": [{ "id": "whe_4k2m9p", "url": "https://yourapp.com/webhooks/valtrix", "description": null, "status": "active", "event_types": ["connection.broken", "table.row_changed"], "tables": [{ "table": "customers_clean", "columns": ["email"], "change_types": ["update"] }], "consecutive_failures": 0, "last_success_at": "2026-07-10T08:05:00Z", "last_failure_at": null, "created_at": "2026-07-10T08:00:00Z" }]}
/v1/webhooks/:idFetches one endpoint by id. Requires a platform-wide key with manage access.
idpathThe endpoint id.
The webhook endpoint object, or 404 webhook_not_found.
$ curl https://api.valtrix.com/v1/webhooks/whe_4k2m9p \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "whe_4k2m9p", "url": "https://yourapp.com/webhooks/valtrix", "description": null, "status": "active", "event_types": ["connection.broken", "table.row_changed"], "tables": [{ "table": "customers_clean", "columns": ["email"], "change_types": ["update"] }], "consecutive_failures": 0, "last_success_at": "2026-07-10T08:05:00Z", "last_failure_at": null, "created_at": "2026-07-10T08:00:00Z" }
/v1/webhooks/:idChanges where an endpoint posts, what it receives, what it posts, how long it keeps trying, or turns it off and on. Send only the fields to change: url moves the endpoint (its queue, secret, and subscriptions stay, its failure count resets, and every waiting event is sent to the new URL straight away), description relabels it, event_types replaces the event list, tables replaces the table.row_changed subscriptions (a table already subscribed keeps its position and has its resync_required_at cleared, a new one starts from now), payload_template replaces or clears the custom payload, stale_after_seconds sets or clears the age past which a queued event is discarded instead of delivered, enabled pauses or resumes delivery, and queued_events sends or discards what is waiting. Sending tables without event_types adds table.row_changed to the current event list. Requires a platform-wide key with manage access.
idpathThe endpoint id.
urlstringThe new https URL to post to. Fixing a wrong URL this way keeps everything the endpoint has queued: pending events go to the new URL at once instead of waiting out their backoff, and dead events stay replayable.
descriptionstring or nullA new label for the endpoint, or null to clear it.
event_typesarrayThe full list of event types to receive from now on. At least one.
tablesarrayThe full list of table.row_changed subscriptions as { table, columns, change_types }, with the same meaning as on POST /v1/webhooks: change_types is any of insert, update, and delete, and an entry sent without it goes back to all three. Send [] to drop them all, together with an event_types list that omits table.row_changed.
enabledbooleanfalse pauses the endpoint: nothing is sent, but events keep queuing for it. true resumes it, clears its failure count, and sends the queue straight away unless queued_events says discard.
queued_eventsstringWhat to do with every pending event on the endpoint. send clears the failure count and delivers them all straight away instead of waiting out their backoff, the move after a receiver that was down is back; discard marks them discarded so they are never sent, the move when the receiver should start from now, for example an alerting endpoint that would otherwise post days of stale notifications. Discarded events stay listed and can be re-queued with POST /v1/webhooks/:id/replay for 14 days. Combine with enabled: true to choose what a resumed endpoint does with its queue; on its own it acts on an active or failing endpoint, and send is refused on a disabled one.
payload_templatestring or nullReplaces the payload template: a JSON string with {{path}} placeholders to render per event, or null to go back to posting the event object. Checked against the subscriptions as they stand after this call. Queued events not yet delivered are rendered with the new template.
stale_after_secondsnumberThe age past which a queued event is discarded instead of delivered, in seconds (60 to 2592000), or null to retry every event for the full retry window. Applies to events already waiting as well as new ones.
The updated webhook endpoint object. 404 webhook_not_found, 400 invalid_url when the URL is rejected, 400 invalid_subscriptions when an event type, table, or column is unknown, 400 invalid_payload_template when the template does not fit, or when new subscriptions would orphan a placeholder in the stored template (change the template in the same call, or first), 400 invalid_request when stale_after_seconds is out of range or queued_events is send on a disabled endpoint.
$ curl -X PATCH https://api.valtrix.com/v1/webhooks/whe_4k2m9p \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "tables": [{ "table": "customers_clean", "columns": [], "change_types": [] }] }'{ "id": "whe_4k2m9p", "url": "https://yourapp.com/webhooks/valtrix", "description": null, "status": "active", "event_types": ["connection.broken", "table.row_changed"], "tables": [{ "table": "customers_clean", "columns": [], "change_types": [] }], "consecutive_failures": 0, "last_success_at": "2026-07-10T08:05:00Z", "last_failure_at": null, "created_at": "2026-07-10T08:00:00Z" }
/v1/webhooks/previewRenders a payload template against a sample event and returns what an endpoint with that template would post, without registering or changing anything. The sample is the most recent change on a subscribed table when there is one, so the preview shows real values; otherwise a synthetic event with plausible values. Pass the event_types and tables the endpoint will have so placeholders are checked against the same subscriptions the endpoint will be saved with; with neither, the template is checked against every event type and every published table. Requires a platform-wide key with manage access.
payload_templatestring, requiredThe template to render, a JSON document as a string with {{path}} placeholders.
event_typesarrayThe event types the endpoint will receive. Omit for every type.
tablesarrayThe table.row_changed subscriptions the endpoint will have, as { table, columns, change_types } (change_types any of insert, update, and delete). Omit for every published table.
eventobjectThe sample event object the template was rendered against.
bodyobject or nullThe rendered payload, exactly what would be posted; null when the delivery would be skipped.
skippedbooleantrue when every placeholder resolved to nothing against the sample, so this event would not be sent.
samplestringrecent_change when the sample is a real change from a subscribed table, synthetic when it was made up.
placeholdersarrayEvery path the template may reference for these subscriptions: id, type, occurred_at, data.<field> for each field the subscribed event types carry, and data.row.<column> and data.previous.<column> for each column of the subscribed tables.
400 invalid_payload_template when the template does not parse or names a path the subscriptions do not carry, 400 invalid_subscriptions when event_types or tables are not valid.
$ curl https://api.valtrix.com/v1/webhooks/preview \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{ "payload_template": "{ \"text\": \"{{data.table}} row {{data.record_id}} changed {{data.changed_columns}}\" }", "tables": [{ "table": "customers_clean", "columns": ["email"] }] }'{"event": { "id": "evt_01h9x...", "type": "table.row_changed", "occurred_at": "2026-07-10T08:05:00Z", "data": { "table": "customers_clean", "record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "changed_columns": ["email"], "...": "..." } },"body": { "text": "customers_clean row rec_9d4f2c81a7b3e650d21c4f8a changed [\"email\"]" },"skipped": false,"sample": "recent_change","placeholders": ["id", "type", "occurred_at", "data.table", "..."]}
/v1/webhooks/:id/rotate-secretIssues a new signing secret and returns it once. Deliveries are signed with the new secret from this moment, so update your server before calling it or accept a brief window of failed verifications. Requires a platform-wide key with manage access.
idpathThe endpoint id.
The webhook endpoint object plus a secret field carrying the new signing secret, or 404 webhook_not_found.
$ curl -X POST https://api.valtrix.com/v1/webhooks/whe_4k2m9p/rotate-secret \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "id": "whe_4k2m9p", "url": "https://yourapp.com/webhooks/valtrix", "description": null, "status": "active", "event_types": ["connection.broken", "table.row_changed"], "tables": [{ "table": "customers_clean", "columns": [], "change_types": [] }], "consecutive_failures": 0, "last_success_at": "2026-07-10T08:05:00Z", "last_failure_at": null, "created_at": "2026-07-10T08:00:00Z", "secret": "whsec_0d3e5f7a9b1c2d4e6f8a0b1c2d3e4f5a" }
/v1/webhooks/:id/testPosts an endpoint.test event to the endpoint right away, signed like any other delivery, and reports how your server answered. Use it to prove the receiver and the stored secret before real events flow. An endpoint with a payload_template instead posts the template rendered against the same sample event POST /v1/webhooks/preview uses, so the receiver sees a realistic message; the delivery is still logged as endpoint.test. The attempt is recorded in the delivery log and counts toward the endpoint's failure count. Requires a platform-wide key with manage access.
idpathThe endpoint id.
deliveredbooleantrue when your endpoint answered 2xx within 5 seconds, or when the delivery was skipped.
skippedbooleantrue when the endpoint's payload_template resolved no placeholder against the sample event, so nothing was posted.
http_statusnumber or nullThe status your endpoint returned, or null when the request never completed.
duration_msnumber or nullHow long the request took.
errorstring or nullWhy the delivery failed, or null when it was delivered.
200 with the outcome either way; the endpoint's own failure is reported in the body, not as an API error. 404 webhook_not_found when the endpoint does not exist.
$ curl -X POST https://api.valtrix.com/v1/webhooks/whe_4k2m9p/test \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "delivered": true, "skipped": false, "http_status": 200, "duration_ms": 143, "error": null }
/v1/webhooks/:id/deliveriesThe endpoint's delivery log, newest first, kept for 14 days. Page with limit and offset. Requires a platform-wide key with manage access.
idpathThe endpoint id.
limitnumberDeliveries per page, 1 to 200. Defaults to 50.
offsetnumberHow many deliveries to skip. Defaults to 0.
deliveriesarrayDelivery objects, newest first.
totalnumberHow many deliveries the log holds for this endpoint.
404 webhook_not_found when the endpoint does not exist.
$ curl "https://api.valtrix.com/v1/webhooks/whe_4k2m9p/deliveries?limit=20" \-H "Authorization: Bearer $VALTRIX_API_KEY"{"deliveries": [{ "id": "whd_9s2ph1", "event_id": "evt_01h9x...", "event_type": "table.row_changed", "status": "failed", "http_status": 500, "error": "Endpoint responded 500", "request_body": "{...}", "response_body": "internal error", "duration_ms": 212, "created_at": "2026-07-10T08:05:00Z" }],"total": 1}
/v1/webhooks/:id/eventsThe endpoint's event queue, newest first: what is waiting to be delivered, what was delivered, what died after exhausting its retries, and what was discarded, by you or by the endpoint's stale limit. Filter with status to see just the dead or discarded ones before a replay. Page with limit and offset. Requires a platform-wide key with manage access.
idpathThe endpoint id.
statusstringpending, delivered, dead, or discarded. Omit for every event.
limitnumberEvents per page, 1 to 200. Defaults to 50.
offsetnumberHow many events to skip. Defaults to 0.
eventsarrayQueued event objects, newest first.
totalnumberHow many events match, across pages.
404 webhook_not_found when the endpoint does not exist, 400 invalid_status for a status outside the four.
$ curl "https://api.valtrix.com/v1/webhooks/whe_4k2m9p/events?status=dead" \-H "Authorization: Bearer $VALTRIX_API_KEY"{"events": [{ "id": "evt_01h9x...", "type": "table.row_changed", "status": "dead", "attempts": 19, "next_attempt_at": null, "delivered_at": null, "created_at": "2026-07-10T08:05:00Z", "data": { "table": "customers_clean", "record_id": "rec_9d4f2c81a7b3e650d21c4f8a", "change_type": "upsert" } }],"total": 1}
/v1/webhooks/:id/replayRe-queues events that died after exhausting their retries or were discarded, so they are delivered again from the next tick with a fresh attempt budget. Send event_ids to replay specific ones, or an empty body to replay every dead and discarded event on the endpoint. Delivered and pending events are never touched. Requires a platform-wide key with manage access.
idpathThe endpoint id.
event_idsarrayThe dead or discarded events to replay, by id. Omit to replay all of them.
replayednumberHow many events were re-queued.
404 webhook_not_found when the endpoint does not exist, 400 invalid_request when event_ids is sent empty.
$ curl -X POST https://api.valtrix.com/v1/webhooks/whe_4k2m9p/replay \-H "Authorization: Bearer $VALTRIX_API_KEY" \-H "Content-Type: application/json" \-d '{}'{ "replayed": 3 }
/v1/webhooks/:idRemoves the endpoint. Queued events are dropped and nothing more is sent to it; its delivery log stays readable in the console until it expires. Deleting again returns 404, so treat webhook_not_found as already done when retrying. Requires a platform-wide key with manage access.
idpathThe endpoint id.
deletedbooleanAlways true.
404 webhook_not_found when no endpoint with that id exists.
$ curl -X DELETE https://api.valtrix.com/v1/webhooks/whe_4k2m9p \-H "Authorization: Bearer $VALTRIX_API_KEY"{ "deleted": true }
(your endpoint URL)Every event is a JSON POST with the event object as the body. Verify the signature before trusting the payload: recompute HMAC-SHA256 over "<t>.<raw body>" with your signing secret and compare it to v1, rejecting timestamps older than 5 minutes. Respond with any 2xx within 5 seconds; anything else is retried with backoff, and an endpoint that keeps failing shows status failing on GET /v1/webhooks/:id and in the console.
Valtrix-SignatureheaderFormatted as t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" keyed with your signing secret>.
Valtrix-Event-IdheaderUnique per event. Deliveries may repeat, so treat events with a seen id as already handled.
This endpoint is yours, so the response is too: any 2xx within 5 seconds acknowledges the event, and the response body is ignored. The delivery itself carries the event object as its body.
POST https://yourapp.com/webhooks/valtrixValtrix-Signature: t=1783686600,v1=5f3a...Valtrix-Event-Id: evt_01h9x...{ "id": "evt_01h9x...", "type": "table.changed", "occurred_at": "2026-07-08T09:30:00Z", "data": { "table": "customers_clean", "latest_cursor": "eyJjIjoiODIzMiJ9" } }