Skip to content
VestiarionDocs

Changelog

Changelog

Changes to the API and webhooks, newest first.

Changes to the API and webhooks that affect an integration, newest first. Changes to the product that do not touch this surface are not listed.

2026-10-05: The agent sends again on its own only what one approval may pay

  • An ap_reconcile or milestone_reconcile entry can now carry notResubmittedBecause: "workspace.two_approvals", with execution.resultingStatus: "held". The payment was never sent and is above the workspace's figure for two approvals. Two people's approvals of it had not been used to pay it, so the agent held it rather than sending it again.
  • Every ledger entry is still appended the same way, and its shape does not change.
  • Nothing changes in /api/v1.

2026-10-05: Approve and pay draws on the USYC reserve, and a person's cash back stays

  • Approve and pay brings back what the operating wallet lacks. A person's paying approval of a new USDC payment from the operating wallet, on Arc testnet or through CCTP with its fee, may now first bring cash back from the reserve. It does so when the wallet cannot cover the payment and the reserve can cover the rest. It brings back the difference, rounded up to the next micro-USDC, with a fifth more of a CCTP fee, which is read again just before the burn. A CCTP payout whose fee could not be read is not covered. Pay now on a held milestone does the same.
    • A new kind of cash_brought_back entry: actor human, with detail { by, reason: "approval", invoiceId | milestoneId, amount, neededUsdc, feeCushionUsdc?, operatingBalance, reserveBalance, earnMode, executed, executionNote?, execution? }. It comes before the payment's own entry.
    • A redemption that did not move is recorded too, with executed: false and why in executionNote, and nothing is paid. One Arc testnet had not yet confirmed may still land; asking again for the same payment finds that same redemption rather than sending a second one.
    • approval_paid and milestone_approval_paid then carry fromReserveUsdc.
    • When the wallet and the reserve together fall short, Approve and pay refuses, as before, before anything moves. No ledger entry is written for a refusal.
  • A person's Bring cash back stays for 24 hours. For 24 hours after a cash_brought_back entry with reason: "person", the agent's treasury decisions sweep nothing.
    • Their entries (hold, or redeem_from_usyc for what falls due) then carry personCashBack: { amount, at, until }.
    • A sweep the model chose anyway is recorded as a hold, with what it chose in boundedByCode.
  • Nothing changes in /api/v1.

2026-10-05: A person's payout to another chain takes the agent's route

  • An approval_paid entry for a new payment to another chain can now carry payout.route: "gateway", with gatewayBalanceUsdc, on a person's first attempt. A person's Approve and pay now chooses the route as the agent does: Gateway when the workspace's Gateway balance covers the amount and Gateway's fee, and Gateway costs no more than CCTP; CCTP otherwise. Before, a person's first attempt always went through CCTP.
  • Approve and pay now refuses, before anything is sent, a CCTP payout the operating wallet cannot cover with its fee, and a Gateway payout the Gateway balance cannot cover. No ledger entry is written for a refusal.
  • Nothing changes in /api/v1.

2026-10-05: Two approvals above a figure the workspace sets

  • A new guardrail rule, workspace.two_approvals. Above the figure a workspace's owner sets, the agent never pays or schedules a payable, or releases a milestone: code holds it for two people. Its entry carries guardrailBlocked: true, execution.resultingStatus: "held" and observed.twoApprovalsAbove, the figure it was weighed against. A EURC payable is weighed at its usdcValue.
  • New ledger actions, each with actor human:
    • approval_given (domain ap) and milestone_approval_given (domain contractor): the first of two approvals, which sends nothing. Its detail is { by, invoiceId | milestoneId, counterpartyId, amount, currency, address, usdcValue?, twoApprovalsAbove, fewApprovers? }.
    • approval_policy_changed (domain system): an owner set, raised, lowered or turned off the figure. Its detail is { by, from, to }, each a figure in USDC or null for off.
  • approval_paid and milestone_approval_paid gain approvals: [{ by, at }, …], the earlier first, and twoApprovalsAbove when two people approved the payment. Their summary then ends "(the second of two approvals)".
    • They also carry fewApprovers: true when whoever entered the payment, or gave its address, gave one of the two because no one else could. As many of the two as can come from other people must.
  • An invoice_reopened entry's followUp.changes can say that two approvals were turned off, or the figure raised to cover the payment. A milestone_reopened entry's can say the same.
  • Nothing changes in /api/v1.

2026-10-05: A payment Circle did not answer is looked for, never sent again blind

  • A payment whose send Circle did not answer is now in flight, not held. Its decision's execution.resultingStatus is matched, as for any payment Circle is still settling.
    • Vestiarion then looks for the send on Circle by its reference before anything is sent again.
    • If Circle has it, it is recorded as the payment.
    • If Circle has listed nothing 15 minutes after the send, the payment is sent as a new one.
    • Reject and Return look for it the same way before they act.
  • Payments can also be switched off from the database (npm run payments -- off "<reason>"). Every running deployment stops within 10 seconds, with no redeploy. While they are off, the agent appends no entries.
  • Approve and pay still records a transfer that was already sent while payments are off, since that only reads Circle.
  • Nothing changes in /api/v1.

2026-10-05: Checked addresses, a stop switch, and no rejection over an unknown transfer

  • POST /api/v1/counterparties refuses an address that mixes capital and small letters when they do not match its EIP-55 checksum: 400 invalid_request, with the message address: This address's capital letters do not match its checksum, so a character is likely wrong. An address written in one case alone is taken as written, as before.
  • Payments can be switched off for every workspace by whoever runs the deployment (PAYMENTS_DISABLED). While they are, no agent cycle runs, so the agent appends no entries and webhooks receive none from it.
  • When a payment's send ends without Circle saying what became of it, the invoice can be neither rejected nor returned until a person approves it, which asks Circle again under the same key. No ledger entry changes shape.
  • On a deployment with no screening service, a live workspace's counterparties stay unscreened instead of being screened against the demo list. Its compliance_sweep entries say the screening is incomplete.
  • Otherwise nothing changes in /api/v1.

2026-10-05: Two people before the first payment to an address

  • A new guardrail rule, counterparty.new_payee. Wherever payments are real, the agent never makes a payment or milestone release that would be the first to an address unless two different parties stand behind it:

    • the payee sent the address and a member confirmed it; or
    • one member gave it and another confirmed it.

    Its entry carries guardrailBlocked: true and execution.resultingStatus: "held".

  • A decision on a first payment records observed.newPayee: { addressBy, confirmedBy, twoParties }. addressBy is a member id, "payee", or null when no entry says who gave it.

  • approval_paid and milestone_approval_paid gain firstPayment: true when a person made an address's first payment. Approve and pay, and Pay now, now refuse the member who gave the address, unless they are the workspace's only approver.

  • An invoice_reopened entry's followUp.changes can name the payee's address has received a confirmed payment since or a second person now stands behind the payee's address.

  • Nothing changes in /api/v1.

2026-10-05: A EURC payable decided again when the rate changes what held it

  • An invoice_reopened entry gains reevaluation when a fresh quote cleared what held a EURC payable. Its shape is { trigger, previousDecision: { seq, action, guardrailRule }, before: { rate, usdcValue, swapCostPercent }, after: { rate, usdcValue, swapCostPercent, quotedAt } }.
    • trigger is one of rate_available, swap_available, swap_cost_within_cap or value_within_limit.
    • followUp.changes says the same in words.
  • The ap_* decision that follows such a reopen in the same cycle gains reevaluation: { reopenedSeq, trigger, previousDecisionSeq, previousAction }. Its own fx is the rate it was decided at.
  • A cycle_complete entry's events can include fx_changed. The FX watch started that cycle, every 5 minutes at most, not a person, so its by is absent.
  • Nothing changes in /api/v1.

2026-10-05: The three-way match, checked in code

  • A new guardrail rule, invoice.match_incomplete. When the agent chooses to pay or schedule a payable whose goods are not confirmed received, or whose counterparty needs a purchase order and has none on file, code refuses it. Its ap_pay or ap_schedule entry carries guardrailBlocked: true, guardrailRule: "invoice.match_incomplete" and execution.resultingStatus: "awaiting_info", and the invoice waits for its details.
  • An AP decision's detail.observed gains purchaseOrderRequired: whether the counterparty needed a purchase order when it was decided. Every counterparty needs one unless an owner or admin marked it as paid without them.
  • A new ledger action, delivered to webhooks as ledger.appended like every entry: counterparty_purchase_orders_changed, actor human, domain compliance. Its detail is { by, counterpartyId, from, to }, where from and to say whether the counterparty needed a purchase order before and after.
  • A cycle_complete entry's events can include purchase_orders_waived: a counterparty was marked as paid without purchase orders, so its payables that waited for one were decided again within a minute.
  • The invoice_reopened entry for such a payable names the counterparty is now paid without purchase orders in followUp.changes.
  • Nothing changes in /api/v1.

2026-10-04: A merge starts the cycle

  • A cycle_complete entry's events can now include pull_request_merged. A pull request was merged in a repository the workspace's GitHub connection covers, and a pending milestone waited on it, so the cycle that verifies and pays it started within a minute of the merge rather than at the next scheduled cycle.
  • Nothing changes in /api/v1.

2026-10-04: Bounties from a pull request comment

  • A new ledger action, delivered to webhooks as ledger.appended like every entry: github_bounty_attached, actor human, domain contractor. Someone who can write to a repository attached a bounty to a pull request with /bounty. Its detail is { by, via: "github", installationId, login, pullRequest, author, amount, milestoneId, counterpartyId, commentUrl }. by is the member who connected GitHub, under whose name the milestone was added, and login is the GitHub user who commented. See Show payments on GitHub.
  • create_counterparty and create_milestone entries gain via: "github", installationId and login for the contractor and the milestone a bounty added.
  • counterparty_address_changed entries gain via: "github", installationId, login and commentUrl when a pull request's author set their address with /payto. Their by is null, as for an address sent through a payee link, and the address waits for a member's confirmation the same way.
  • Nothing changes in /api/v1.

2026-10-04: Emailed invoices finished by hand

  • A create_invoice entry with via: "email" no longer always carries changed: []. An owner or admin can now finish an emailed invoice in the invoice form on AP / AR, with Edit and add or Finish and add. Its document.changed then lists the fields they changed from what was read, as for an invoice added from a document.
  • Such an entry carries no document when the email could not be read and the invoice was typed in.
  • Nothing changes in /api/v1.

2026-10-04: Cash back for milestones too

  • A cash_brought_back entry with reason: "payments_due_today" now also counts the verified milestones waiting to be paid in neededUsdc and payments, so a cycle brings their cash back from the reserve before it releases them. A milestone whose USDC is locked in escrow is not counted.
  • A person's Pay now on a milestone for a pull request now gets its pull_request_commented entry once the payment is confirmed, rather than at the next cycle.

2026-10-04: Payments said on the pull request

  • A workspace can connect GitHub from Settings, by installing the Vestiarion app on the repositories it chooses. See Show payments on GitHub.
  • Three new ledger actions, delivered to webhooks as ledger.appended like every entry:
    • github_connected, with installationId, account, accountType and repositorySelection;
    • github_disconnected, with installationId and account;
    • pull_request_commented, with milestoneId, pullRequest, commentUrl, amount, token and txHash. It is written when the app commented on the pull request a milestone was paid for. The comment names the amount, the network, the paying workspace and the transaction, never the payee.
  • Private pull requests verify in a repository the app is installed on, so verify_milestone_github can now follow a private pull request's merge.
  • Nothing changes in /api/v1.

2026-10-03: Invoices by email

  • New ledger actions, delivered to webhooks like every entry:
    • invoice_inbox_on, invoice_inbox_changed and invoice_inbox_off, actor human, domain system: an owner or admin turned on the workspace's address for invoices by email, replaced it, or turned it off. Their detail is { by }; the address is never recorded.
    • invoice_email_received, actor system, domain ap: an email arrived at the address and was read. Its detail is { inboxEmailId, from, authentication, read, document }: the sender with most of its name hidden, SPF/DKIM/DMARC as Resend reported them, read one of ready, needs_details or unreadable, and the document's kind and sha256, or null.
    • invoice_email_dismissed, actor human, domain ap: { by, inboxEmailId }.
  • create_invoice entries gain via: "email" and inboxEmailId when a member added the payable from an email on AP / AR. Such an entry carries document as one added from a document does, with changed: []. Nothing that arrives by email is added without a person.
  • Nothing changes in /api/v1.
  • Add a milestone, POST /api/v1/milestones, with a read-and-write key.
    • It takes { contractorId, title, amount, verificationSource? }, checked by the rules of the console's Add milestone form, and answers 201 with the milestone, shaped as List milestones returns it.
    • The milestone starts pending, and the API cannot verify it. A GitHub pull request in verificationSource is verified by the agent once it is merged; any other link waits for a person.
    • It takes an Idempotency-Key, as every write does.
    • It is recorded as create_milestone with via: "api" and apiKeyId.
  • Create a payee link, POST /api/v1/payee-links, with a read-and-write key.
    • It takes { counterpartyId }, a vendor or a contractor, and answers 201 with { id, counterpartyId, url, expiresAt }.
    • url is in that answer only, which is sent with Cache-Control: no-store.
    • The operation keeps no outcome for an Idempotency-Key, which would store the link. A repeat makes a new link, and the unused one stops working.
    • It is recorded as payee_link_created with via: "api" and apiKeyId. The address a payee enters through it waits for a person to confirm it, as before.
  • A write now checks the key's creator as they are now. Only owners and admins can add records. If an owner moves the creator to approver or viewer, the key keeps reading, and every write, invoices and counterparties included, answers 403 forbidden: "This key's issuer can no longer add records in this workspace."
  • A milestone records who added it, from the console, Pay a freelancer or the API. So the self-approval rule now applies to milestones too. When the agent held a milestone on its own judgment, the person who added it cannot choose Pay now for it, unless they are the workspace's only approver. Milestones added before keep no creator.
  • MCP gains create_milestone and create_payee_link. create_payee_link is marked destructive, because a new link revokes the unused one.
  • @vestiarion/sdk 0.2.0 adds milestones.create and payeeLinks.create.
  • Settings: the box that gives a key write access is now Can also add records.

2026-10-03: Add an invoice from Slack

  • create_invoice entries gain via: "slack" and linkId when an owner or admin added the payable from a message in Slack with Add invoice. Such an entry carries document as one added from a document on AP / AR does, with changed: [].
  • The Slack app asks for one more permission, files:read, to read the file someone chooses. A Slack connected before this is connected again from Settings with Reconnect Slack; its members stay connected.
  • Nothing changes in /api/v1.

2026-10-03: A member's API keys end with their membership

  • A workspace API key now works only while the person who created it is a member of the workspace. When they leave, are removed by an owner or an admin, or delete their account, every active key they created in that workspace, read-only or read-and-write, is revoked in the same step, and from then on answers 401 unauthorized on /api/v1 and on MCP, like any revoked key. Keys that other members created, and the person's keys in workspaces they still belong to, keep working.
  • Each such key gets its own api_key_revoked entry, delivered to webhooks like every entry, right after the member's member_left or member_removed. Its detail gains reason: member_left, member_removed (with member, the person removed, while by is who removed them) or account_deleted. A key revoked in Settings now carries reason: "person".
  • A key can no longer be created for someone who is not a member of the workspace.
  • A read-and-write key whose issuer has left can no longer add records, so a create_counterparty or create_invoice entry with via: "api" always names its issuer in by.
  • Nothing was revoked when this shipped: no active key had outlived its creator's membership.

2026-10-03: The TypeScript SDK on npm

  • npm install @vestiarion/sdk installs it from the npm registry.
  • Version 0.1.0 there is the same tarball, byte for byte, as https://www.vestiarion.xyz/sdk/vestiarion-sdk-0.1.0.tgz, which keeps working.

2026-10-03: A member's notifications move to Settings

  • telegram_disconnected entries for a chat its member disconnected from Vestiarion now carry via: "settings": the Telegram card moved from Members to the Notifications section at the top of Settings, with the switch for the email about payments that need a decision. Entries made before keep via: "members_page"; via: "telegram" and via: "blocked" are unchanged.
  • Nothing else changes in the ledger, the webhooks or /api/v1.

2026-10-03: A TypeScript SDK

  • @vestiarion/sdk 0.1.0, installed with npm install https://www.vestiarion.xyz/sdk/vestiarion-sdk-0.1.0.tgz. It is a typed client for every /api/v1 operation, with no dependencies, for Node 20 or later, Deno, Bun and edge runtimes. See TypeScript SDK.
  • Its types are generated from the OpenAPI document, so they follow each change listed here.
  • listAll and pages follow page.nextCursor to the end of a collection.
  • Requests are retried after:
    • a 429, when its Retry-After is 60 seconds or less;
    • a 5xx;
    • a timeout or a network failure.
  • Every write carries an Idempotency-Key, so a retry never adds a record twice.
  • verifyWebhook checks a delivery's Vestiarion-Signature.
  • verifyLedgerEntry checks an entry's body hash, Ed25519 signature and chain hash.
  • Nothing in the API or the webhooks changed.

2026-10-03: The agent's decisions in Slack

  • New ledger actions, domain system, delivered to webhooks like every entry:
    • slack_installed, actor human: an owner or admin connected a Slack workspace from Settings, and picked the channel the agent's decisions go to. Its detail is { by, teamId, channelId }.
    • slack_uninstalled: the workspace's Slack was disconnected, with every member's Slack link. Actor human when an owner or admin chose Remove Slack (via: "settings"); actor system when Slack said the app was removed from its workspace (via: "slack"), with by null. Its detail is { by, teamId, via }.
    • slack_member_connected, actor human: a member connected their own Slack account. Its detail is { by, linkId }, plus via: "install" for the account of the person who connected the workspace.
    • slack_member_disconnected, actor human: a member disconnected their own Slack account, with /vestiarion disconnect (via: "slack") or from Settings (via: "settings"). Its detail is { by, userId, linkId, via }. A member removed from the workspace loses the link with the membership, and that is recorded as the removal.
    • slack_decisions_limit_changed, actor human: an owner set the most a payment approved from Slack may be, or turned deciding from Slack off. Its detail is { by, from, to }, in USDC, null for off.
  • approval_paid, approval_rejected and approval_returned entries gain via: "slack" and linkId when a member decided the payable from its message in Slack, and agent_paused does when they paused the agent with /vestiarion pause. Everything else in them is what the console writes. Entries for actions taken in the console carry no via, as before.
  • A cycle's journal has a new stage, slack, after telegram, the last. It needs no other stage to have succeeded.
  • Nothing changes in /api/v1.

2026-10-03: A payable held for an unconfirmed address goes back to the agent once it is confirmed

  • A payable the agent held because its counterparty's address changed and no one had confirmed it (guardrailRule: "counterparty.address_unconfirmed", observed.addressUnconfirmed: true) now goes back to the agent when a person confirms the address: an invoice_reopened entry whose followUp.changes reads "the counterparty's new address has since been confirmed", then a new decision in the same cycle. Until now it stayed held until a person approved it. Approving it still pays it, and confirms the address.
  • The API's two write examples are now responses captured in testnet-2.

2026-10-03: Add counterparties and invoices through the API

  • New operations: POST /api/v1/counterparties and POST /api/v1/invoices. Each takes a JSON body of at most 64 KB, checked by the console form's rules, and answers 201 with the record, shaped as its list returns it. A body that does not validate, including one with a field the operation does not take, answers 400 invalid_request, naming the field. A counterpartyId the workspace does not hold answers 400 too.
  • A new scope, write. A key is read-only, as every existing key stays, or reads and writes; an owner or admin chooses when creating it. A read-only key on a write answers 403 forbidden. api_key_created lists scopes: ["read", "write"] for such a key.
  • Idempotency-Key on both writes. A repeat with the same key and body within 24 hours gets the first answer back, with Idempotent-Replayed: true. The same key with a different body, or while the first request is still being handled, answers the new error code conflict (409). A 5xx answer, and a body that fails validation, are not kept, and a key whose first request never finished is free again after 10 minutes.
  • Writes are limited to 30 a minute per key; over that, 429 rate_limited with Retry-After: 60. Reads stay unlimited.
  • create_counterparty and create_invoice entries for records added through the API carry via: "api" and apiKeyId, with by the key's issuer, or null once their account is gone. Such a create_counterparty also carries addressNeedsConfirmation: an address added through the API waits for a person to confirm it, and the agent holds payments to it (counterparty.address_unconfirmed) until then.
  • A payable added through the API starts a cycle, whose cycle_complete entry lists invoice_added in events, as one added in the console does.
  • MCP: two new tools, create_counterparty and create_invoice, annotated readOnlyHint: false, destructiveHint: false, idempotentHint: false, with an optional idempotencyKey. A read-only key gets the 403 as a tool error.
  • The OpenAPI document describes each write's request body, its Idempotency-Key header and its 201.

2026-10-03: A sweep's yield is counted over how long the cash would stay

  • In treasury entries (sweep_to_usyc, redeem_from_usyc, hold, actor agent), economics.expectedHoldDays is now how long the swept cash would stay in the reserve, on average over the next 30 days. What falls due is paid from the operating wallet first, and only what it cannot cover comes back, on its day. Before, it was the days until the next obligation. economics.projectedYieldUsd follows, and the written policy's referenceDecision with it.

2026-10-03: Code bounds the treasury's moves, and a payable to a client waits for a person

  • A new value of guardrailRule: counterparty.client_payable. The agent holds a payment, or a schedule, to a counterparty whose role is client, for a person: a client pays the business, so a payable to one is a refund, or an invoice entered in the wrong direction. A person may still approve it.
  • Treasury entries (sweep_to_usyc, redeem_from_usyc, hold, actor agent, domain treasury):
    • decision is now the move code allowed. A sweep takes at most the cash above the 7-day buffer. A redemption brings back at most 115% of what falls due within 14 days, less the operating balance, and at least the written policy's.
    • When code changed the model's move, boundedByCode: { chosen: { action, amount }, reason } keeps what the model chose and why, and decision.reasoning ends with "[Code limited this: …]".
    • agreedWithReference now also needs the amount within 5% of referenceDecision.amount, or 0.01 USDC.
  • ar_reminders_on gains replacedLink: true only when turning reminders on replaced a link made before October 3, which stops working. madeNewLink is also true when the receivable had no link.

2026-10-03: The agent reminds clients of what they owe

  • New ledger actions, domain ar, delivered to webhooks like every entry:
    • ar_reminders_on and ar_reminders_off, actor human: an owner or admin turned the agent's reminders on or off for a receivable. detail is { by, invoiceId, counterpartyId }, and for ar_reminders_on also linkId and madeNewLink (a link made before October 3 was replaced so the reminders can carry it).
    • ar_reminder_sent, actor agent: the agent emailed the client a reminder. detail is { invoiceId, counterpartyId, to, number, tone, daysFromDue, amount, currency, linkId, decision, referenceDecision, decisionMode, agreedWithReference }. to is the address with most of its name hidden; number is 1 to 4; tone is friendly, firm or final; daysFromDue is negative before the due date. toneLimited: { chosen, sent } is there when code sent a softer tone than the model chose.
    • ar_reminder_deferred, actor agent: the agent decided to wait before reminding. detail is { invoiceId, counterpartyId, until, daysFromDue, remindersSent, decision, referenceDecision, decisionMode, agreedWithReference }.
  • Turning reminders on starts a cycle; its cycle_complete entry lists the new event kind reminders_on in events.
  • A cycle's journal has a new stage, collections, between proposals and notices. It runs only when receipts completed.
  • counterparty_notice_email_changed now covers a counterparty's billing email, which also receives a client's reminders; its summary reads "Billing email for name: address", or "... removed".

2026-10-03: The agent's decisions in Telegram

  • New ledger actions, domain system, delivered to webhooks like every entry:
    • telegram_connected, actor human: a member connected their own Telegram chat to the workspace, from Members. Its detail is { by, username }, where username is the Telegram username with most of it hidden (@li***), or null.
    • telegram_disconnected: a chat no longer gets the workspace's decisions. Actor human when the member disconnected it, from Members (via: "members_page") or with /disconnect in the chat (via: "telegram"); actor system when Telegram answered that the member had blocked the bot (via: "blocked"). Its detail is { by, userId, via, username }, with by null for blocked. A member removed from the workspace loses the connection with the membership, and that is recorded as the removal.
  • create_invoice entries gain via: "telegram" when a member added the payable by tapping Add on an invoice the bot read. Such an entry carries document as one added from a document on AP / AR does, with changed: [].
  • A cycle's journal has a new stage, telegram, after notices, the last. It needs no other stage to have succeeded.
  • Nothing changes in /api/v1.

2026-10-03: Cash comes back from the reserve for payments

  • New ledger action cash_brought_back, domain treasury, delivered to webhooks like every entry: USDC came back from the reserve to the operating wallet.
    • Actor agent: each cycle, before it decides payments, the agent brings back what the USDC payables due today need beyond the operating balance. Its detail is { reason: "payments_due_today", amount, neededUsdc, payments, operatingBalance, reserveBalance, executed, executionNote, earnMode, execution? }. execution holds the transactions of a real USYC redemption. When nothing could move, executed is false, executionNote says why, and the entry's summary begins "Could not bring"; while the agent is paused, heldBecause is agent_paused.
    • Actor human: an owner or admin chose Bring cash back. Its detail is { by, reason: "person", amount, all, reserveBalance, earnMode, execution? }, where all says they brought back everything. It moves cash even while the agent is paused, and starts a cycle, whose cycle_complete entry lists the new event kind cash_returned in events.
  • The redemption itself is also a treasury_actions row with action: "redeem_from_usyc", as the treasury stage's are.
  • An AP decision held because the cash was not there carries execution.heldBecause: "cash_shortfall", with execution.cashNeededUsdc (what it needed, with the payables due before it) and execution.cashSeen: { operating, reserve } (the balances it saw). Once cash has come in since and covers it, the next cycle reopens the payable with an invoice_reopened entry whose followUp.changes reads "the cash it needs is there now (… USDC in the operating wallet and the reserve)", and decides it again.
  • A cycle's journal has a new stage, liquidity, between services and ap.

2026-10-03: Payment notices

  • New ledger actions, delivered to webhooks like every entry:
    • payment_notice_sent, actor system, domain ap or contractor: a payee was emailed that a payment to it was confirmed. Its detail is { invoiceId or milestoneId, counterpartyId, to, amount, token, txHash }, where to is the address with most of its name hidden (li***@example.com).
    • counterparty_notice_email_changed, actor human, domain compliance: a person set, changed or cleared where a counterparty's payment notices go. Its detail is { by, counterpartyId, from, to }, both addresses hidden the same way, null for none.
  • create_counterparty entries gain noticeEmail, hidden the same way, or null.
  • The address itself is not part of /api/v1/counterparties.

2026-10-03: Add what a held payable was missing

  • New ledger action invoice_details_added, actor human, domain ap, delivered to webhooks like every entry: a person added the purchase order a held, flagged or awaiting-information payable lacked, or marked its goods or services received. Its detail is { by, invoiceId, counterpartyId, added }, where added holds poReference, goodsReceived: true, or both. Nothing already on the invoice changes, and its status stays as it was.
  • At its next cycle the agent reopens the payable with an invoice_reopened entry whose followUp.changes names what was added, and decides it again. Adding details starts that cycle within seconds, and its cycle_complete entry lists the new event kind details_added in events.
  • In /api/v1/invoices, the payable's poReference and goodsReceived show what was added.

2026-10-03: A sandbox screens against the bundled watchlist

  • GET /api/v1/status reports screening: "simulate" for a sandbox workspace, whatever the deployment configures; a live workspace reports live when OpenSanctions is configured, as before.
  • A sandbox's counterparties are screened against the bundled watchlist at every cycle, and their compliance_sweep entries name that source. One screened by OpenSanctions before is screened again against the watchlist at its next cycle.

2026-10-03: Nothing is paid to a counterparty never screened

  • A new value of guardrailRule: counterparty.unscreened. The agent holds a payment, a schedule or a milestone release to a counterparty whose riskLevel is still unscreened, because screening could not run when it was added. The decision is made again once screening gives a verdict. A counterparty whose later screening fails keeps its earlier verdict, as before.

2026-10-03: The spending limit, enforced on Arc

  • New ledger actions, actor human, domain system, delivered to webhooks like every entry:
    • spending_limit_enforced: a person enforced the agent's spending limit on Arc testnet. Its detail is { by, contract, agent, treasury, dailyUsdc, weeklyUsdc, deployTxHash, approveTxHash, setLimitsTxHash }, where agent is the agent's own wallet and treasury the operating wallet.
    • spending_limit_unenforced: a person turned it off. Its detail is { by, contract, txHash }.
  • agent_budget_changed gains onChain: { contract, txHash } when the figures were changed on the contract too.
  • While the limit is enforced, the agent's payment decisions (ap_* and milestone_* entries, actor agent) carry onChainLimit: { contract, agent, ref, covered, uncoveredBecause?, verdict }. covered says whether the payment can go through the contract; verdict is the contract's answer before anything was sent: { state: "allowed" }, { state: "refused", error, spent?, amount?, limit? }, { state: "unreadable", reason }, or null when it was not asked.
  • Two new values of guardrailRule: workspace.onchain_limit (the contract would refuse the payment) and workspace.onchain_limit_route (the payment is in EURC, or to another chain, which the contract cannot carry).
  • A payment sent through the contract has, as its transaction, a call to the contract's pay(address,uint256,bytes32) by the agent's wallet. Its logs hold the USDC transfer from the operating wallet to the payee.

2026-10-03: A workspace's only approver may approve what they entered

  • When the person who entered a payable, or added a milestone, is the only member of the workspace who may approve payments (the only owner, admin or approver), they may now approve it themselves. The approval_paid or milestone_approval_paid entry then carries soleApprover: true in its detail, and its summary ends "(entered and approved by the workspace's only approver)". An approval by anyone else carries neither. Once a second member may approve, the person who entered a record cannot approve it again.

2026-10-02: The agent's reasoning is written in plain English

  • New decisions' agentReasoning in /api/v1/invoices and /api/v1/milestones, and detail.decision.reasoning in ledger entries, are written as 2 to 4 plain-English sentences, without the field names of the facts the agent was shown. Nothing about earlier records changes: the API and the ledger return what was stored.

2026-10-02: One review dismisses every screening match

  • A screening_match_dismissed entry can now dismiss several matches at once: the ones the person reviewed together. Its detail gains matchedEntities: [{ id, caption, score }], every match it dismissed. matchedEntityId, matchedCaption and matchedScore still name the counterparty's verdict match, the first in the list.
  • Screening now reads every candidate it needs. Previously, after five dismissals of one name, a re-screen could read clear while further matches were never looked at. Such a counterparty may now be screened medium or high again, with a risk_level_changed entry.

2026-10-02: A person decides a held milestone

  • Milestones can be closed. In /api/v1/milestones, status may be closed: a person closed the milestone without paying it, and it is never paid after. ?status=closed lists them. Every milestone has two new fields, null unless it is closed:
    • closedAt: when it was closed;
    • closeReason: the reason the person gave.
  • New ledger actions, delivered to webhooks like every entry:
    • milestone_approval_paid (actor human, domain contractor): a person paid a held milestone. Its detail is { by, milestoneId, counterpartyId, amount, currency, overrode, heldFor, txRef, status, attempt?, retriedAfter? }.
      • status is paid, verified (submitted, not yet confirmed) or held (the transfer failed).
      • heldFor says what it was held for: for example transfer_failed, agent_held or outflow_budget.
      • retriedAfter is present when Circle had failed the previous attempt, which this one sent again.
    • milestone_closed (actor human, domain contractor): a person closed a held milestone without paying it. Its detail is { by, milestoneId, counterpartyId, amount, currency, heldFor, reason }.

2026-10-02: The agent buys payee history over x402

  • New public endpoint: GET /api/x402/payee-history?address=0x…. It is paid per call over x402: 0.001 USDC on Arc testnet (eip155:5042002), scheme exact, settled through Circle Gateway batching (GatewayWalletBatched, GatewayWallet 0x0077777d7EBA4688BDeF3E311b846F25870A19B9).
    • Without a PAYMENT-SIGNATURE header it answers 402 with the offer in PAYMENT-REQUIRED.
    • With a payment Circle's facilitator verifies and settles, it answers 200 with { address, workspacesPaid, paymentsConfirmed, firstPaidAt, lastPaidAt, asOf, seller, priceUsdc } and the settlement in PAYMENT-RESPONSE.
    • A missing or malformed address answers 400 before any offer. When the history cannot be read, it answers 503 and nothing is charged.
  • New ledger actions:
    • service_purchased (actor agent, domain compliance). Its detail is { purchaseId, counterpartyId, address, seller, priceUsdc, payer, payTo, nonce, settlement, result }.
    • service_purchase_refused (actor agent, domain compliance). Its detail is { purchaseId, counterpartyId, address, seller, rule, reason }. rule is one of seller.not_allowed, offer.mismatch, price.above_max, budget.day or purse.short.
    • service_purchase_failed (actor agent, domain compliance). Its detail is { purchaseId, counterpartyId, address, seller, reason }.
    • service_budget_funded (actor human, domain treasury). Its detail is { by, amountUsdc, signer, depositor, txHash, balanceUsdc }.
  • AP decision entries and milestone_release / milestone_hold entries carry observed.addressHistory when the agent bought the counterparty's address history within 7 days: { about, workspacesPaid, paymentsConfirmed, firstPaidAt, lastPaidAt, boughtAt, priceUsdc }.
  • A cycle's detail.stages includes services, which runs after recurring and before ap.

2026-10-02: Batches through the operating wallet's own executeBatch

  • A batch of milestones is now one call to the operating wallet's own executeBatch, as Circle documents for its smart accounts. The wallet sends USDC to each payee in turn, and if one transfer fails, none happens.
  • The first version sent batches through Arc's Multicall3From. Multicall3From keeps the sender only when the sender signed the transaction itself, and a Circle smart account's transaction is sent by Circle's bundler. Circle refused that batch at estimation (ESTIMATION_ERROR), and no money moved. Its milestones were held, and their milestone_release entries carry execution.batch with resultingStatus: "held".
  • For a short time in between, workspaces with live wallets paid each milestone alone.

2026-10-02: Several milestones in one transaction

  • When a cycle releases two or more milestones paid in USDC on Arc testnet, they go out in one transaction. Each is still decided, checked and signed on its own. A milestone locked in escrow is always released on its own.
  • A milestone_release entry for a milestone paid this way carries execution.batch: { key, size }. key identifies the batch, and size is how many milestones it paid.
    • execution.txRef is the batch's transaction, shared with the other milestones in it.
    • execution.feeUsd is this milestone's share of the transaction's fee.
  • In /api/v1/milestones, several milestones can have the same tx_ref. A transaction hash no longer identifies one payment: key on the milestone's id.

2026-10-02: The agent proposes limits from people's approvals

  • When people approve at least two payments to one counterparty above its payment limit within 30 days, and reject none, the agent may propose raising the limit. The counterparty must be screened clear. An owner or admin accepts or dismisses the proposal in Approvals.
  • New ledger actions, in the compliance domain:
    • policy_proposal_made (actor agent). Its detail is { proposalId, counterpartyId, kind, fromLimit, toLimit, evidence, reasoning, decisionMode, referenceLimit, agreedWithReference }. kind is raise_limit. evidence lists the approvals it rests on: { invoiceId, amountUsdc, approvedAt, approvedBy, agentAction, guardrailRule }.
    • policy_proposal_declined (actor agent): the model decided not to propose. Its detail is { counterpartyId, currentLimit, evidence, reasoning, decisionMode, agreedWithReference }.
    • policy_proposal_accepted (actor human). Its detail is { by, proposalId, counterpartyId, fromLimit, toLimit, currentLimit }. A counterparty_limit_changed entry precedes it, as for any limit change.
    • policy_proposal_dismissed (actor human). Its detail is { by, proposalId, counterpartyId, toLimit }.
  • A cycle's detail.stages includes proposals, which runs last, after forecast.

2026-10-02: Recurring payments

  • An owner or admin can set up a payment that repeats every N days, weeks or months. Each period's invoice is created as it comes near and decided like any other.
  • New ledger actions, in the ap domain:
    • recurring_payable_created (actor human). Its detail is { by, recurringId, counterpartyId, amount, currency, memo, poReference, goodsReceived, everyCount, everyUnit, startsOn, endsOn }.
    • recurring_invoice_created (actor agent), when a cycle creates a period's invoice. Its detail is { invoiceId, recurringId, period, counterpartyId, amount, currency, goodsReceived }. period is the invoice's due date, YYYY-MM-DD.
    • recurring_payable_stopped (actor human). Its detail is { by, recurringId }.
  • A cycle's detail.stages includes recurring, which runs after follow_up and before ap.
  • An event cycle's detail.events can include recurring_added: someone set up a recurring payment.
  • Two invoices created for different periods of one recurring payment are never reported as duplicates of each other. Their decision entries' observed.duplicateCheck reflects that.

2026-10-02: A real USYC reserve

  • A live workspace's owner or admin can turn on a real USYC reserve on Arc testnet, once Circle has allowlisted its operating and reserve wallets. Until then, the reserve stays simulated, as before.
  • New ledger action usyc_reserve_enabled, in the treasury domain, appended when the reserve is turned on. Its detail is { by, operatingAddress, reserveAddress }.
  • Treasury decision entries (sweep_to_usyc, redeem_from_usyc, hold) of a workspace with a real reserve:
    • carry earnMode: "live";
    • carry usycSubscriptionsOpen: whether USYC could be bought then, or null when that could not be read;
    • when a move was made, carry execution. A sweep's is { approveTxHash, depositTxHash, shares, price }; a redemption's is { redeemTxHash, shares, price }. shares is in USYC, price in USDC per USYC.
  • A sweep decided while USYC cannot be bought has executed: false, and an executionNote saying so.
  • In GET /api/v1/status, data.provenance.yield reads live for such a workspace.
  • The reserve account's balance is its USYC's value in USDC at the latest price, read from Arc testnet every cycle. Its apy is the fund's yield over at least the last 5 days, from the same oracle.

2026-10-02: The agent's spending limit

  • An owner or admin can set what the agent may pay on its own per UTC day and per 7 days, in USDC. A payment past either is held for a person.
  • New value of guardrailRule, workspace.outflow_budget, on an ap_pay or milestone_release entry: paying it would have taken the agent past its limit.
    • The entry's execution.heldBecause is outflow_budget.
    • A payable goes to Approvals as held. A milestone stays held until the limit has room for it.
  • New field outflowBudget on an ap_pay or milestone_release entry, present when a limit is set: { dailyUsdc, weeklyUsdc, spentToday, spentThisWeek, remaining, binding }.
    • It is what the limit left when the payment was weighed.
    • binding is day or week, whichever figure left less.
  • Milestone decision entries now carry guardrailRule: counterparty.high_risk, counterparty.payment_limit, workspace.outflow_budget, or null.
  • New ledger action agent_budget_changed, in the system domain, appended when someone changes the limit.
    • Its detail is { by, from, to }. from and to are each { dailyUsdc, weeklyUsdc }, and null means no figure.
  • What counts against the limit is what the agent's own ap_pay and milestone_release decisions sent: entries whose execution.resultingStatus is paid or matched, at their USDC value. A payment a person approves does not count.
  • invoice_reopened and milestone_reopened entries can list the change "the agent's spending limit has room for it again" or "the agent's spending limit was removed".
  • An event cycle's detail.events can include budget_raised: someone raised or removed a figure of the limit.

2026-10-01: Receivables paid on Arc

  • New ledger action pay_link_created, in the ar domain: an owner or admin made a link for a client to pay a receivable. Its detail is { by, invoiceId, linkId }.
  • New ledger action ar_received, in the ar domain, appended by the agent when a transfer that arrived in the operating wallet settles an open receivable.
    • Its detail is { invoiceId, counterpartyId, amount, currency, txHash, from, circleTxId, matchedBy, receivedAt }.
    • matchedBy is sender when the client's address on file sent it, and amount when it was the only open receivable of that currency and amount.
  • A receivable's status becomes received, with settledAt and txRef set, when a transfer settles it.
  • A cycle's detail.stages includes receipts, which runs after reconcile.
  • An event cycle's detail.events can include payment_received: a client's "I have paid" check found the receivable paid.

2026-10-01: Dismissing a screening match

  • New ledger action screening_match_dismissed, in the compliance domain, delivered as a ledger.appended event. It is appended when a member who can decide approvals dismisses a counterparty's screening match as not the same person.
    • Its detail is { by, counterpartyId, matchedEntityId, matchedCaption, matchedScore, screenedName, reason }.
    • matchedEntityId is the screening service's id for the entity dismissed; screenedName is the counterparty's name when it was dismissed.
  • Screening skips a dismissed entity for that counterparty while its name stays the same, and judges the next match, if any. The counterparty is screened again at once, so a screen_counterparty entry with its new verdict follows.
  • An event cycle's detail.events can include match_dismissed.

2026-10-01: Held milestones decided again

  • New ledger action milestone_reopened, in the contractor domain, delivered as a ledger.appended event. The agent appends it when it returns a held milestone to verified because a fact its decision rested on changed: the contractor's risk level or payment limit, the milestone's verification source, or the agent was paused when it held it.
    • Its detail is { milestoneId, previousStatus, followUp: { action, reason, changes } }. previousStatus is held, followUp.action is reopen, and changes lists each fact that moved, in words.
    • The milestone's next decision entry follows in the same cycle.
  • A milestone's status can now go from held back to verified without anyone verifying it again.
  • An event cycle's detail.events can include limit_raised: someone raised a counterparty's payment limit and screening allows more than zero.

2026-10-01: EURC payments funded by a swap

  • A live workspace can pay a EURC payable after swapping USDC for the EURC it needs, through Circle's Stablecoin Service on Arc testnet.
  • New ledger action fx_swap, in the treasury domain, delivered as a ledger.appended event when a swap ends.
    • Its detail is { paysInvoiceId, swapId, state, usdcIn, eurcMinimum, eurcEstimated, eurcReceived, usdcPerEurc, costPercent, provider, adapter, approveTxHash, swapTxHash, failure, reasoning, resumed }.
    • state is confirmed or failed.
    • eurcReceived is the EURC the swap's transaction sent to the operating wallet, read from its receipt, or null when it could not be read.
    • resumed is true for a swap whose first answer was lost, finished by a later cycle. Its reasoning is then null.
    • It carries paysInvoiceId, not invoiceId: it is not a decision on the invoice.
  • New fields on a EURC payable's decision entry:
    • usdcDueWithin7Days;
    • swapOffer: { usdcIn, eurcEstimated, eurcMinimum, usdcPerEurc, costPercent, provider } when a swap was offered, or null;
    • swapUnavailable: why there was none, or null;
    • swap: { swapId, state, usdcIn, eurcReceived, swapTxHash, reason } for the swap made to fund the payment, or null. state is confirmed, pending or failed.
    • decision.fundWithSwap is true when the model chose to pay with the swap. It appears only on a payable the wallet's EURC was short of.
    • swapOffer.costPercent is what each EURC costs through the swap above the rate the payable was weighed at, in percent, rounded up to two places.
  • Two new values of guardrailRule:
    • fx.swap_cost_above_cap, for a swap costing more than 3% above the quoted rate;
    • fx.swap_usdc_short, for a swap that would leave the USDC below what falls due in USDC within 7 days.
  • When a swap does not go through:
    • A failed swap holds the payable with guardrailRule: null, as a failed transfer does. Its reason is in swap.reason.
    • A swap still in flight at Circle leaves the payable pending. The next cycle finishes the swap, then decides the payable again.

2026-10-01: Milestone escrow

  • A live workspace can lock a milestone's USDC in its own escrow contract on Arc testnet. Three new ledger actions, in the contractor domain, are delivered as ledger.appended events:
    • escrow_deployed, once per workspace. Its detail is { by, address, payer, deployer, circleContractId, txHash }.
    • escrow_funded, when a milestone is locked. Its detail is { by, milestoneId, contract, holdId, payee, amountUsdc, refundAfter, fundTxHash }. fundTxHash is null when the hold was found already on chain and recorded.
    • escrow_refunded, when a hold is taken back after its refund date. Its detail is { by, milestoneId, contract, holdId, amountUsdc, refundTxHash }.
  • A milestone locked in escrow is paid by releasing its hold rather than by a transfer. Its release decision's execution.txRef is that release transaction on Arc testnet.

2026-10-01: Payment receipts

  • An owner or admin can share a paid payable's receipt as a public link. Three new ledger actions, in the ap domain, are delivered as ledger.appended events:
    • receipt_shared, appended once per payment, the first time it is shared. Its detail is { receipt, records } and holds no names and no user ids:
      • receipt is { amount, token, paidAt, payee, chain, txHash, route, sourceTxHash?, feeUsdc? };
      • chain and txHash are where the payee was paid: the transfer on Arc testnet, or the mint on the payee's chain;
      • route is direct, cctp or gateway;
      • sourceTxHash is a CCTP payout's burn;
      • records is { seq, hash } of the entry that recorded the payment.
    • receipt_link_renewed, when a new link replaces an earlier one or a revoked receipt is shared again. Its detail is { by, receiptId }.
    • receipt_revoked, when a receipt's link is turned off. Its detail is { by, receiptId }.
    • None of the three carries invoiceId in detail: a receipt's entries are about sharing a payment, not deciding an invoice.

2026-10-01: Payouts from a Gateway balance

  • A payout to a payee on another chain can go from the workspace's Circle Gateway balance as well as through CCTP. Its decision entry's payout.route is then gateway, payout.feeUsdc is Gateway's fee, and payout.gatewayBalanceUsdc is the Gateway balance it was weighed against.
  • A decision entry for a USDC payout to another chain also carries payout.quotes: { cctpFeeUsdc, gatewayFeeUsdc }: both routes' fees as read for that decision, each null when there was none. gatewayFeeUsdc is null for a workspace with no Gateway balance.
  • An approval_paid entry for a new payment to another chain carries the same payout, read when the person approved: { chain, route, domain, feeUsdc, quotes }, with gatewayBalanceUsdc when the route is gateway. An approval that only records a transfer already sent carries none.
  • A payout keeps the route its first attempt took. A new value of guardrailRule, bridge.gateway_balance_short, holds one that went through Gateway when the Gateway balance no longer covers it with its fee.
  • For a Gateway payout:
    • the decision entry's execution.txRef is the mint on the payee's chain, with execution.destinationChain and execution.mintTxHash. No Arc transaction belongs to one Gateway payout: Gateway burns on Arc in batches;
    • GET /api/v1/invoices returns that mint as the invoice's transaction once it is paid. Until then the invoice is matched, as a CCTP payout is.
  • Three new ledger actions, in the treasury domain, delivered as ledger.appended events:
    • gateway_signer_created, whose detail is { by, signer, circleWalletId };
    • gateway_delegate_added, whose detail is { by, depositor, signer, txHash };
    • gateway_deposit, whose detail is { by, amountUsdc, depositor, signer, approveTxHash, depositTxHash, balanceUsdc }. balanceUsdc is the Gateway balance once it includes the deposit, or null when Gateway had not counted the deposit yet.

2026-10-01: Invoices read from a document

  • A create_invoice entry for an invoice read from a document carries document in its detail:
    • kind: pdf, email (an .eml file: its text and the text of its PDF attachments) or text;
    • sha256: the hash of the document's bytes, or of the pasted text;
    • reader: the model that read it (anthropic, openai, deepseek), or heuristic for the rule-based reader;
    • changed: the form fields the member changed from what was read, among amount, currency, dueDate, poReference, earlyPayDiscountPct, discountDeadline, memo and counterpartyId.
  • An invoice typed in carries no document. The document itself is not stored.

2026-10-01: First screens in the compliance sweep

  • Each entry in a compliance_sweep entry's detail.screened now carries firstScreen: true when the counterparty had no verdict before. changed is still true for a first screen.
  • When a sweep includes first screens, its summary counts them apart: "Screened 6 of 6 counterparties: 6 for the first time, 0 changed". A sweep of re-screens only still reads "Re-screened 6 of 6 counterparties; 0 changed".

2026-10-01: Payees on other chains

  • A counterparty's chain is one of ARC-TESTNET, BASE-SEPOLIA, ARB-SEPOLIA or ETH-SEPOLIA. GET /api/v1/counterparties returns it as before.
  • A payee on a chain other than Arc testnet is paid from Arc through CCTP V2 with Circle's Forwarding Service.
    • Its decision entry (ap_pay, ap_hold, ap_flag_fraud, ap_request_info, ap_schedule) carries payout: { chain, route: "cctp", domain, feeUsdc } in detail, where chain is the chain id (BASE-SEPOLIA) and feeUsdc is null when Circle gave no fee.
    • Only a vendor can be paid on another chain; a contractor's milestones are released on Arc testnet.
    • Its execution carries destinationChain and mintTxHash once the payment was sent. execution.txRef is the burn on Arc testnet, or cctp:<Circle transaction id> while the burn has no hash yet.
    • mintTxHash is null until the Forwarding Service mints. A later ap_reconcile entry carries it.
  • Three new values of guardrailRule, each of which holds the payable for a person:
    • bridge.fee_above_cap, when the fee is above 10% of the amount;
    • bridge.fee_unavailable, when Circle gave no fee;
    • bridge.unsupported_token, for an invoice in EURC to a payee on another chain.
  • A bridged invoice is matched from the burn until the mint, then paid. GET /api/v1/invoices shows it so. One still not minted two hours after it was sent is held for a person, by an ap_reconcile entry saying so.

2026-10-01: Invoices in EURC

  • An invoice can be in EURC as well as USDC. GET /api/v1/invoices returns its currency, which is now USDC or EURC; amount and paidAmount are in that currency.

  • A EURC payable is paid in EURC from the operating wallet. Its decision entry (ap_pay, ap_hold, ap_flag_fraud, ap_request_info, ap_schedule) carries currency in detail, and, for EURC only:

    • usdcValue, its value in USDC, which is what counts against the counterparty's limit;
    • fx: { rate, source: "circle-stablecoin-quote", quotedAt, usdcMinimum }, or null when Circle gave no quote;
    • eurcBalance, the operating wallet's EURC as read for the decision, or null when payments are simulated or the balance could not be read.

    Those five decision entries carry currency: "USDC" for a USDC invoice. ap_reconcile carries no currency.

  • Two new values of guardrailRule for a EURC payable, both of which hold it for a person: fx.rate_unavailable, when there was no quote to weigh it at, and treasury.insufficient_eurc, when the wallet's EURC cannot cover the payment or could not be read. As with every rule, guardrailRule is set when code refuses a payment or schedule the model decided on; a payable the model held itself for the same reason has guardrailRule: null, with usdcValue: null or an eurcBalance below observed.amount in its detail.

  • A duplicate is the same amount only in the same currency. A purchase order billed in one currency and again in the other is reported as purchase_order_rebilled, which goes to the model and does not block on its own.

  • approval_paid and the create_invoice and import_invoice entries gain currency in detail, and their summaries name it.

  • The invoice CSV import takes an optional currency column. A blank or missing value means USDC.

2026-10-01: Milestones added in the app

  • A member can now add a contractor milestone on the Contractors page. It starts pending and unverified, like any milestone in GET /api/v1/milestones.
  • Its verificationSource is the evidence link the member gave, or null: a GitHub pull request in its canonical form (https://github.com/<owner>/<repo>/pull/<number>, which the cycle checks for a merge as before), or any other https:// address, which only a person verifies.
  • A new ledger action, delivered as a ledger.appended event: create_milestone, in the contractor domain with actor: "human". Its detail is { by, milestoneId, counterpartyId, counterpartyName, amount, verificationSource }, with amount as the decimal string entered.
  • A new event kind, milestone_added, may appear in an event cycle's detail.events. It is raised only for a milestone whose evidence is a pull request, so the merge is checked within a minute.

No /api/v1 endpoint, parameter or response schema changed.

2026-10-01: Payment timing

  • An invoice can carry early-payment terms: a percent off and the date it lapses, set on the invoice form or by CSV import. GET /api/v1/invoices now returns earlyPayDiscount: { percent, deadline } | null for any invoice that carries them.
  • A payable the agent has committed to pay later reaches status scheduled, decided again each cycle until the day it pays. GET /api/v1/invoices's status filter now accepts scheduled.
  • GET /api/v1/invoices also returns scheduledFor (an ISO timestamp, or null) and paidAmount: what actually left, once an invoice's status is paid. paidAmount stays null before then, even while a submitted transfer already carries an amount.
  • A new ledger action, delivered as a ledger.appended event: ap_schedule, in the ap domain, recorded when the agent schedules a payable. Its detail carries decision: { action: "schedule", payOn, reasoning, confidence }, timing and timingRule (the policy's figures, and any correction made to the model's date), requestedPayOn when a date was corrected, terms: { earlyPayDiscount }, observed, and execution.resultingStatus: "scheduled".
  • Every ap_* decision entry (ap_pay, ap_hold, ap_flag_fraud, ap_request_info, ap_schedule) now carries timing, timingRule and terms in its detail; an entry for a previously scheduled invoice also carries scheduledFor.
  • ap_pay and approval_paid gain amountPaid and discountTaken in detail: both null when nothing went out, or the discounted amount and the percent taken when an invoice was paid by its discount deadline.
  • cycle_complete's detail.outcomes gains scheduledCount.
  • A duplicate is now also refused against a payable already scheduled, already being paid (matched), or being decided by a person on Approvals (processing), not only one already paid or received. The refusing entry's reasoning names which of these it repeats.
  • Two new ledger actions, delivered as ledger.appended events, both in the compliance domain with actor: "human":
    • payee_link_created, whose detail is { by, counterpartyId, linkId, expiresAt };
    • payee_link_revoked, whose detail is { by, counterpartyId, linkId }.
  • A counterparty_address_changed entry made by a payee through a payee link has by: null, and carries via: "payee_link" and linkId next to counterpartyId, from and to.
  • Such a change waits for confirmation like any other; a later counterparty_address_confirmed records who confirmed it.

No /api/v1 endpoint, parameter or response schema changed.

2026-09-30: What started a cycle

  • A cycle_complete ledger entry's detail can now carry trigger, which says what started the cycle:
    • "schedule" for the six-hourly run;
    • "manual" for Run cycle now;
    • "event" when something in the workspace gave the agent a decision to make.
  • An event cycle's detail also carries events, the kinds that led to it: invoice_added, milestone_verified, payable_returned, address_confirmed, agent_resumed and sample_loaded. by, when present, is the person whose action started the cycle.
  • These fields are visible on ledger.appended events and in GET /api/v1/ledger.
  • A cycle no longer starts while another one in the same workspace is running, so one workspace's cycles do not overlap.

No /api/v1 endpoint, parameter or response schema changed.

2026-09-30: Failed-payment retry

  • An approval_paid ledger entry's detail can now carry attempt, the transfer attempt this approval ended on (starting at 1), and, only when that attempt is a retry, retriedAfter: { providerTxId, providerState, failureReason } for the earlier attempt Circle ended in a terminal failure. Both are visible on ledger.appended events and in GET /api/v1/ledger.
  • Circle's STUCK transfer state is no longer a terminal failure; it is now reported the same way a transfer still pending is. Only CANCELLED, DENIED and FAILED end a transfer attempt.

No /api/v1 endpoint, parameter or response schema changed.

2026-09-30: Ledger signing key rotation

  • An owner can rotate a workspace's ledger signing key from Settings. The rotation is a ledger entry, delivered as a ledger.appended event like every other: ledger_key_rotated, in the system domain, with actor: "human". Its detail is { from, to, by }: the retired key's id, the new key's id, and the owner who rotated. See Key identity and rotation.
  • From that entry on, signingKeyId is the new key's id. Earlier entries keep the old id and keep verifying against the retired key.
  • ledgerRetiredKeyCount in GET /api/v1/status now counts the workspace's own retired keys.

No /api/v1 endpoint, parameter or response schema changed.

2026-09-30: Counterparty limit changes

  • A new ledger action, delivered as a ledger.appended event: counterparty_limit_changed, in the compliance domain. Its detail is { by, counterpartyId, from, to, currentLimit }, where from and to are the configured limits in USDC and currentLimit is the limit the counterparty's risk now allows. A client's limit may be null.

No /api/v1 endpoint, parameter or response changed.

2026-09-30: Counterparty address changes

  • Two new ledger actions, delivered as ledger.appended events like every other entry, both in the compliance domain:
    • counterparty_address_changed, whose detail is { by, counterpartyId, from, to }. from or to is null when there was no address before, or it was cleared.
    • counterparty_address_confirmed, whose detail is { by, counterpartyId, address, via }. via is "approval" when approving a payment confirmed it, and "confirm" when someone confirmed it on the counterparty.
  • An agent decision entry (ap_pay and the other ap_* actions) can now carry guardrailRule: "counterparty.address_unconfirmed". It means the payment was held because the counterparty's address changed and no one has confirmed it. Its observed object gains addressUnconfirmed, a boolean.
  • An ap_reconcile entry can now carry notResubmittedBecause: "counterparty.address_unconfirmed", beside the existing "counterparty.high_risk". A payment that never reached the network is not sent again to a changed address that no one has confirmed; it is held for a person.

No /api/v1 endpoint, parameter or response changed.

2026-09-29: MCP server

  • A remote MCP server at https://www.vestiarion.xyz/api/mcp, over Streamable HTTP. An AI agent connects with a workspace API key, sent as Authorization: Bearer <key>, and reads that workspace's records through one tool per /api/v1 read operation. A tool returns the operation's JSON exactly. See MCP server.
  • A missing, malformed, unknown or revoked key answers 401 with a WWW-Authenticate header; a key without the read scope answers 403.
  • There are no write tools, and no OAuth.

No /api/v1 endpoint, parameter or response changed.

2026-09-29: Webhooks sent right after each append

  • A ledger.appended delivery now goes out right after the request that appended its entry (approving a payment, pausing the agent, changing a member, a key or an endpoint), not on the next scheduled dispatch. Entries appended by the agent's scheduled run still go out right after that run. The 10-minute schedule remains for retries. See Timing.

No payload, header or signature changed.

2026-09-29: Developer docs, OpenAPI document and llms.txt

  • These docs, at /docs: a quickstart, guides, and a reference page for every endpoint, with a Try it panel that sends a real request with your key.
  • GET /api/v1/openapi.json: an OpenAPI 3.1 document for every /api/v1 endpoint. It is public and needs no key.
  • A Markdown view of every page at its URL plus .md, and /llms.txt and /llms-full.txt: see AI integration.
  • Cursors are checked per endpoint. A cursor that the endpoint could not have issued, such as a ledger cursor sent to /invoices or an altered one, now answers 400 invalid_request. It could reach the database before and answer 500. See Pagination.

No other endpoint, parameter or response changed.

2026-09-29: Signed webhooks

  • An owner or an admin can add up to 5 HTTPS endpoints per workspace in Settings. Each one receives a ledger.appended event for every new ledger entry, and a webhook.test event on demand. See Webhooks.
  • Every delivery is signed with HMAC-SHA256 in the Vestiarion-Signature header, using the endpoint's own secret, shown once when it is added. See Verifying signatures.
  • Delivery is at least once and not in order. A failed delivery is retried up to 7 attempts in total, and an endpoint is disabled after 20 consecutive failed attempts. See Retries and disabling.

2026-09-29: Workspace API keys

  • /api/v1 now authenticates with a workspace API key, vxk_<prefix>_<secret>, sent as Authorization: Bearer <key>. An owner or an admin creates keys on the workspace's Settings page; each key is shown once and reads only its own workspace. See Authentication.
  • Breaking: the platform token, AGENT_API_TOKEN, no longer opens /api/v1, and answers 401. It had read only the founding workspace. Move any caller that used it to a workspace key.
  • A missing, malformed, unknown or revoked key answers the same 401 unauthorized. A key whose scopes do not cover a route answers 403 forbidden.
  • GET /api/v1/status reports the calling key's own workspace, and its configuration no longer includes the deployment's database block.