Everything you need to wire your AI agent into Truss: get a key in under a minute,
authenticate with a single header, and verify your first invoice call against the
complete POST /api/agents field reference below.
For copy-paste Python and Node.js examples, see the SDK quickstart.
For exact failure bodies and corrective actions, see the API error reference.
GET A KEY. MAKE YOUR FIRST CALL.
Step 1 — Get your key.
Go to /developers, fill out the form with your name, work email, and a brief note on
what you’re wiring in. Your truss_<48-hex> key is provisioned instantly — no waiting,
no approval queue. Copy it now; we only store the hash on our side.
Step 2 — Make your first call.
Send the minimal payload below. The invoice_id value doesn’t need to exist yet —
a 200 with verified: false and an invoice not found anomaly
means your key is valid and the endpoint is reachable. That’s the verification call.
curl -X POST https://api.truss.polsia.app/api/agents \
-H "Authorization: Bearer truss_<your-key>" \
-H "Content-Type: application/json" \
-d '{"invoice_id": "inv_001"}'
A valid key always returns 200. Check verification.verified for data correctness.
The response below shows what a new key holder should expect on a first call with a non-existent invoice:
// 200 OK — key is working; inv_001 doesn't exist yet, that's expected
{
"authenticated": true,
"owner_label": "your-key-label",
"invocation_id": 123,
"verification": {
"verified": false,
"invoice_id": null,
"payment_id": null,
"matched_fields": [],
"anomalies": [
{
"field": "invoice",
"severity": "error",
"message": "invoice not found"
}
],
"checked_at": "2026-08-21T12:00:00.000Z"
}
}
SIGN EVERY REQUEST.
Every request to /api/agents requires a Truss API key. Pass it in either of the two
accepted header forms — both are equivalent:
Authorization: Bearer truss_<your-48-hex-key>
x-api-key: truss_<your-48-hex-key>
Auth failure responses:
| Status | Body | Cause |
|---|---|---|
401 |
{ "error": "API key required" } |
No key supplied in either header |
401 |
{ "error": "Invalid or revoked API key" } |
Supplied key is malformed, truncated, otherwise unrecognized, or revoked. See the revoked-key guidance. |
429 |
{"error":"rate_limited",...} |
Plan quota exceeded for your key |
POST https://api.truss.polsia.app/api/agents
Auth: Authorization: Bearer <key> or x-api-key: <key> (required)
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
invoice_id |
string |
required | ID of the invoice to verify. Omitting or passing an empty value triggers anomaly invoice_id is required. |
expected_amount |
number |
optional | Cross-checks against invoice.amount. Tolerance ±0.01. Mismatch triggers an error-severity anomaly. |
expected_currency |
string |
optional | Cross-checks against payment.currency (case-insensitive, e.g. "USD"). |
expected_vendor_name |
string |
optional | Cross-checks against invoice.vendor_name (case-insensitive). |
expected_routing_number |
string |
optional | Cross-checks against payment.routing_number (exact string match). |
expected_account_last4 |
string |
optional | Cross-checks against payment.account_last4 (exact string match). |
webhook_url |
string|null |
optional | Sets a credential-free http:// or https:// callback URL (maximum 2048 characters) for this API key. The URL is persisted and reused when this field is omitted; pass null to clear it. The verdict is still returned inline in the same response. |
| any additional fields | any | optional | Persisted verbatim to api_invocations.request_payload and ignored by the verifier. Use for agent metadata (agent_id, intent, etc.). |
Add "mode": "three_way_match" to the request body to cross-check a purchase order, invoice, and receipt in a single synchronous call. Truss compares each document’s amount, vendor_name, and date (case/space-insensitive) and returns one of four verdicts:
| Verdict | When |
|---|---|
verified | All three cross-checks pass (amount / vendor / date line up across PO and invoice and receipt). |
partial | One or two cross-checks pass; the rest fail. |
mismatch | All three cross-checks fail — documents are present but don’t agree. |
missing | One or more documents are absent, or any required field on a document is empty. |
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | "three_way_match" — required to dispatch the three-way flow. All other request-body fields above are ignored when this is set. | |
po | object | PO document — must include amount, vendor_name, date, po_number. | |
invoice | object | Invoice document — must include amount, vendor_name, date. | |
receipt | object | Receipt document — must include amount, vendor_name, date. | |
curl -X POST https://api.truss.polsia.app/api/agents \
-H "Authorization: Bearer truss_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"mode": "three_way_match",
"po": { "po_number": "PO-001", "amount": 100.00, "vendor_name": "Acme", "date": "2026-07-01" },
"invoice": { "amount": 100.00, "vendor_name": "Acme", "date": "2026-07-01" },
"receipt": { "amount": 100.00, "vendor_name": "Acme", "date": "2026-07-01" }
}'
// 200 OK — all three line up
{
"authenticated": true,
"owner_label": "your-key-label",
"invocation_id": 123,
"verification": {
"mode": "three_way_match",
"verdict": "verified",
"verified": true,
"missing_documents": [],
"comparisons": [
{ "field": "amount", "matched": true, "values": { "po": 100, "invoice": 100, "receipt": 100 } },
{ "field": "vendor_name", "matched": true, "values": { "po": "Acme", "invoice": "Acme", "receipt": "Acme" } },
{ "field": "date", "matched": true, "values": { "po": "2026-07-01", "invoice": "2026-07-01", "receipt": "2026-07-01" } }
],
"matched_fields": [...],
"anomalies": [],
"checked_at": "2026-08-22T12:00:00.000Z"
}
}
Every successful response includes an invocation_id. Keep this ID with the result if a
callback is configured; it is the correlation ID for checking delivery status.
When an amount mismatch is detected, the verdict flips to "mismatch" (or "partial" if other cross-checks still line up) and verification.anomalies surfaces the offending field with the per-document values, e.g.:
{
"verification": {
"mode": "three_way_match",
"verdict": "mismatch",
"verified": false,
"missing_documents": [],
"comparisons": [
{ "field": "amount", "matched": false, "values": { "po": 100, "invoice": 110, "receipt": 130 } },
...
],
"anomalies": [{ "field": "amount", "severity": "error", "values": {...} }],
...
}
}
When a valid webhook_url is active, Truss returns the verdict synchronously and asynchronously POSTs the same response envelope to that URL. The callback adds the top-level API-key tier; its nested verification object is unchanged, including all three-way match fields.
// POST to your webhook_url
{
"authenticated": true,
"owner_label": "your-key-label",
"invocation_id": 123,
"verification": {
"mode": "three_way_match",
"verdict": "verified",
"verified": true,
"missing_documents": [],
"comparisons": [
{ "field": "amount", "matched": true, "values": { "po": 100, "invoice": 100, "receipt": 100 } },
{ "field": "vendor_name", "matched": true, "values": { "po": "Acme", "invoice": "Acme", "receipt": "Acme" } },
{ "field": "date", "matched": true, "values": { "po": "2026-07-01", "invoice": "2026-07-01", "receipt": "2026-07-01" } }
],
"matched_fields": [ ... ],
"anomalies": [],
"checked_at": "2026-08-22T12:00:00.000Z"
},
"tier": "paid"
}
Truss sends the case-insensitive X-Truss-Signature header in the format sha256=<lowercase-hex-digest>. The digest is an HMAC-SHA256 made with the API key’s plaintext value as the secret and the exact UTF-8 JSON request body as the message. Verify the signature against the raw body before parsing JSON. For Python and Node.js request examples, see the SDK examples. The callback includes the same top-level invocation_id as the inline response. Any 2xx response counts as delivered; non-2xx responses, timeouts, and network errors are retried up to three total attempts with short backoff (100ms, then 200ms). A final failure is recorded, but never delays or changes the inline verdict response.
Poll delivery state with the invocation_id returned by POST /api/agents. This endpoint
uses the same API key, so an invocation belonging to another key is indistinguishable from an unknown ID. The
public delivery_state enum is not_configured, retrying, delivered,
or permanently_failed. The persisted internal pending state is returned as
retrying until delivery settles.
curl https://api.truss.polsia.app/api/agents/123/webhook-status \
-H "Authorization: Bearer truss_<your-key>"
// 200 OK
{
"invocation_id": 123,
"delivery_state": "retrying",
"attempt_count": 2,
"created_at": "2026-08-22T12:00:00.000Z",
"delivered_at": null,
"latest_failure_reason": "HTTP 502"
}
| Lifecycle example | Representative status fields | Meaning |
|---|---|---|
| Pending (internal) | delivery_state: "retrying"attempt_count: 0delivered_at: nulllatest_failure_reason: null | Callback is configured but delivery attempts have not settled. Callers see this persisted pending state as the transient retrying state. |
| Successful delivery | delivery_state: "delivered"attempt_count: 1delivered_at: "2026-08-22T12:00:01.000Z"latest_failure_reason: null | A callback received a 2xx response. Delivery is complete, whether it succeeded on the first attempt or after retries. |
| In-progress retry | delivery_state: "retrying"attempt_count: 2delivered_at: nulllatest_failure_reason: "HTTP 502" | An attempt failed and another attempt may still run. The reason is the latest failure so far. |
| Terminal failure | delivery_state: "permanently_failed"attempt_count: 3delivered_at: nulllatest_failure_reason: "HTTP 500" | All three attempts failed. The reason is now the terminal error and no further delivery attempt will run. |
Delivery makes one immediate attempt, then retries after 100ms and 200ms, for three
total attempts. A retry count is therefore max(attempt_count - 1, 0).
| Field | Meaning |
|---|---|
invocation_id | The invocation ID returned by the verification request and included in the callback envelope. |
delivery_state | Publicly, not_configured means no callback was active, retrying means delivery is still transient (including the internal pending state), delivered means a 2xx response was received, and permanently_failed means all three attempts failed. |
attempt_count | Total callback attempts made so far, including the first send, from 0 through 3. The retry count is max(attempt_count - 1, 0). |
created_at | UTC timestamp when the invocation was recorded; it is not a timestamp for each individual delivery attempt. |
delivered_at | UTC timestamp when delivery succeeded, or null while delivery is pending/retrying or has permanently failed. |
latest_failure_reason | null before any failure and after successful delivery; while retrying, it is the most recent error, and after all three attempts fail, it is the terminal error. |
When a callback is configured, retrying is the transient state to poll, including the internal
pending period and the backoff between attempts. Stop when it settles at the terminal
delivered or permanently_failed state; for permanent failure, use
latest_failure_reason as the terminal error. not_configured is also terminal. A
malformed, nonexistent, or other-account invocation returns 404 with
{"error":"Invocation not found"}. Missing or invalid credentials retain the standard
401 responses.
Register this route before a global express.json() parser so the raw request bytes remain available:
const crypto = require('node:crypto');
const express = require('express');
const app = express();
const TRUSS_API_KEY = process.env.TRUSS_API_KEY;
app.post('/truss/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const received = req.get('X-Truss-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', TRUSS_API_KEY)
.update(req.body)
.digest('hex');
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const callback = JSON.parse(req.body.toString('utf8'));
// callback.verification.verdict is ready for your async workflow.
console.log(callback.tier, callback.verification);
return res.sendStatus(204);
}
);
{
"authenticated": true,
"owner_label": "your-key-label",
"verification": {
"verified": false,
"invoice_id": null,
"payment_id": null,
"matched_fields": [],
"anomalies": [
{
"field": "invoice_id",
"severity": "error",
"message": "invoice_id is required"
}
],
"checked_at": "2026-08-21T12:00:00.000Z"
}
}
Each entry in anomalies carries field, severity
("error" or "warning"), and optionally expected,
actual, and message.
verified is true only when the invoice and payment both resolve
and no error-severity anomalies fire.
| Code | When |
|---|---|
200 |
Always on a valid key — check verification.verified for data correctness |
401 |
Missing header: { "error": "API key required" }; supplied malformed, truncated, unrecognized, or revoked key: { "error": "Invalid or revoked API key" } |
400 |
Malformed, credential-bearing, or non-HTTP(S) webhook_url |
429 |
Rate limit exceeded for your plan |
500 |
Internal server error |
For the exact failure bodies and the corrective action for each status, see the API error reference.
Every authenticated call — including verified: false outcomes — is logged
to api_invocations and visible in the
/dashboard/api-usage
view. The verification object is stored in response_payload; the verified
flag and anomaly count are surfaced as columns in that dashboard so you can audit all agent traffic
and spot mismatches before they reach payment.