Skip to content
Last updated

Changelog

All notable changes to the Talli API are documented here.

Entries are tagged by API version: [V2] for Campaign Manager, [V3] for Distribution Manager, [Beneficiary] for the unversioned beneficiary API, [Public] for the anonymous /public/* endpoints.

2026-09-09

Changed

  • [V3] POST /v3/distribution-manager/distribution-drafts/{id}/:promote now carries each entry's isDisabled and disableReason from the draft's payoutMethods onto the promoted distribution's payoutMethods.
  • [V3] POST /v3/distribution-manager/payout-instructions/{id}/:try-restore now accepts a payout instruction whose selected payout method is a CheckTransfer, while the check is funded and waiting for the print run. The instruction moves to Restored once the funds are back in the distribution.
  • [V3] POST /v3/distribution-manager/payout-instructions/{id}/:try-restore for a check transfer already sent for printing, or whose funding has failed, is rejected with 400.
  • [V3] POST /v3/distribution-manager/templates and PUT /v3/distribution-manager/templates/{id} reject a content field holding a {{tag:Key}} placeholder whose Key contains a character that is not allowed in a tag key, with 400.

2026-09-08

Added

  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/payout — a check-transfer payout now carries requestedOn, the moment the beneficiary submitted the check request.
  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/activity now returns a CheckTransferRequested activity when a beneficiary requests a check payout, with Check transfer requested in description.
  • [V3] GET /v3/distribution-manager/simulate/emails lists the organization's most recent emails, newest first, with recipient, templateType, deliveryStatus, payoutInstructionId, subject and htmlBody. limit is between 1 and 50 and defaults to 10; there is no paging.
  • [V3] subject and htmlBody are null for an email whose content is not available yet; poll again for an email sent seconds ago.

2026-09-04

Added

  • [V3] The simulate endpoints exist in the sandbox environment only; in production they return 404.
  • [V3] POST /v3/distribution-manager/simulate/bank-accounts/{id}/:receive-ach funds the bank account with a simulated incoming ACH deposit. It accepts amount and an optional idempotencyKey, returns 202, and the account's available balance rises within seconds.
  • [V3] POST /v3/distribution-manager/simulate/payout-instructions/{id}/:settle-ach settles the payout instruction's ACH transfer and returns 202. The payout instruction reaches Fulfilled within seconds; a payout with no settleable ACH transfer is rejected with 409.

Changed

  • [V3] Icon uploads — POST /v3/distribution-manager/distributions, PATCH /v3/distribution-manager/distributions/{id}/distribution-icon and the distribution-draft equivalents — now accept PNG and JPEG only; GIF and WebP are rejected with 400, same as any other unsupported type. This applies to file uploads only — an icon supplied as iconUrl on create or draft patch is not re-validated against the accepted set. Icon URLs stored before this change, including GIF/WebP ones, are unaffected and keep rendering.

2026-09-02

Added

  • [V3] The payout method category gains the value CheckTransfer: a payout instruction whose beneficiary requests a check moves from PayoutMethodSelected to Requested.
  • [V3] A CheckTransfer method can be attached to a distribution only while GET /v3/distribution-manager/payout-methods returns it for the caller.

Changed

  • [V3] A distribution can offer at most one check payout method, counting PaperCheck and CheckTransfer together. A create, patch, promote or clone request naming more than one of them, on a distribution or a distribution draft, is rejected with 400.

2026-08-31

Changed

  • [V3] Tag keys on one payout instruction are now compared exact-case, so {"BusinessName": "John Doe", "Businessname": "EMEA"} is two tags instead of a 400. This applies to POST /v3/distribution-manager/payout-instructions and to PATCH /v3/distribution-manager/payout-instructions/{id} with /Tags.
  • [V3] The same comparison applies to the tag:-prefixed dynamic fields on POST /v3/distribution-manager/templates/:send-preview: tag:BusinessName alongside tag:Businessname is now accepted.
  • [V3] Two tag keys that are equal after trimming are still rejected with 400.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload is unchanged: a file with two tag columns whose names differ only by case is still rejected with 400.

2026-08-26

Changed

  • [V3] {{tag:Key}} placeholders in email template content now match the payout instruction's tag key exact-case, not ignoring case. {{tag:teamid}} no longer resolves a TeamId tag; it renders as tag:teamid, the same fallback used for an instruction that does not carry the tag at all. Tag-key uniqueness rules on POST/PATCH /v3/distribution-manager/payout-instructions, file uploads, and the tag:-prefixed dynamic fields on :send-preview are unchanged — keys are still unique per instruction regardless of case.

2026-08-25

Changed

  • [V3] PATCH /v3/distribution-manager/payout-instructions/{id} — an /amount increase on a Restored payout is rejected synchronously with 409 INSUFFICIENT_DISTRIBUTION_BALANCE when the distribution's unallocated balance can't cover the difference.
  • [V3] POST /v3/distribution-manager/payout-instructions/{id}/:reinitiate — on a "Cancelled" payout, is rejected synchronously with 409 INSUFFICIENT_DISTRIBUTION_BALANCE when the distribution's unallocated balance can't cover the payout amount.
  • [V3] A payout instruction tag value can be at most 128 characters instead of 256. The limit applies everywhere tags are set: POST /v3/distribution-manager/payout-instructions, PATCH /v3/distribution-manager/payout-instructions/{id} with /Tags, file uploads via POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload (per cell), and the tag:-prefixed dynamic fields on POST /v3/distribution-manager/templates/:send-preview. A longer value is rejected with 400; an upload names the tag column and the rows. Values stored before this change keep their full length and are returned unchanged on reads and in exports, but a write that carries one is now rejected. The key limit of 64 characters and all other tag rules are unchanged.
  • [V3] Tag keys can no longer contain &. The rule applies everywhere tags are set: POST /v3/distribution-manager/payout-instructions, PATCH /v3/distribution-manager/payout-instructions/{id} with /Tags, and file uploads via POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload; it also covers the tag:-prefixed dynamic fields on POST /v3/distribution-manager/templates/:send-preview. A request carrying one is rejected with 400, and an upload names the offending tag column. Tag values are unaffected and may still contain &. Tag keys already stored with & have been renamed, each & replaced with _, so a key returned by GET /v3/distribution-manager/payout-instructions/{id} may differ from the one originally sent. The existing key rules — no comma or double quote, none of < > { }, no upload file column name, unique regardless of case — and the length caps are unchanged.

2026-08-21

Changed

  • [V3] A beneficiary who unsubscribes from emails no longer has their payout instructions cancelled. Previously every payout instruction with the same email in the distribution moved to Cancelled with cancellationReason: UnsubscribedFromEmails; now the instructions keep their status and stay claimable, and the platform stops emailing that beneficiary instead. Payout instructions are no longer cancelled with the UnsubscribedFromEmails reason by the platform (the value remains valid on historical rows and as a manual cancellation reason). If the beneficiary resubscribes, emailing resumes without any further action.
  • [V3] POST /v3/distribution-manager/payout-instructions/{id}/:initiate, .../:reinitiate, and .../:send-reminder return 400 when the beneficiary has unsubscribed from emails. :initiate with channel: None is not affected, since it sends no email.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:initiate — count-based selection skips payout instructions whose beneficiary has unsubscribed. When payoutInstructionIds are supplied explicitly and any of them belongs to an unsubscribed beneficiary, the whole selection is rejected with 409 PAYOUT_INSTRUCTIONS_NOT_INITIABLE; the response now carries an unsubscribedIds list alongside unknownIds and notReadyIds, and nothing is initiated.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:send-reminders — payout instructions whose beneficiary has unsubscribed are no longer eligible and are skipped by the selection, as they are by the automatic reminder schedule.
  • [V3] A payout instruction restored after a failed payout stays in the Restored status instead of being automatically re-initiated when its beneficiary has unsubscribed from emails.
  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/activity now returns a PayoutInstructionEmailSubscriptionStatusUpdated activity when the beneficiary unsubscribes from emails or resubscribes — Beneficiary unsubscribed from emails or Beneficiary resubscribed to emails, with the new status in details.

2026-08-20

Changed

  • [V3] Tag keys and values can no longer contain <, >, { or }. The rule applies everywhere tags are set: POST /v3/distribution-manager/payout-instructions, PATCH /v3/distribution-manager/payout-instructions/{id} with /Tags, and file uploads via POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload; it also covers the tag:-prefixed dynamic fields on POST /v3/distribution-manager/templates/:send-preview. A request carrying one is rejected with 400; a value violation names the tag. An upload names the tag column, with the rows for a value violation. The existing key rules — no comma or double quote, no upload file column name, unique regardless of case — and the length caps are unchanged.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload — the six known column names are matched without regard to case or surrounding whitespace, so a column called EMAIL is now read as Email rather than rejected. A file naming the same known column twice is rejected with 400, as is a file with no header row. Column names and cell values are trimmed, so two of either that differ only by surrounding whitespace are now the same: two tag columns whose names differ only by padding are rejected as duplicates, a file whose Id cells differ only by padding is rejected for a duplicate row, and a payout instruction stored earlier under a padded Id is no longer matched by a re-upload of the same file — it gets a new row. A cell holding only whitespace now reads as empty, exactly as a cell left blank does: a row whose Amount holds only whitespace imports without an amount instead of the file being rejected, and re-uploading such a row against an existing payout instruction clears its amount and returns it to Draft. Header problems come back under the file key, where the column-format error previously used File.
  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/activity — the entry for a tag change now reports the tags the change removed as <old value> → (removed) in details, beside the tags that remain, and reads Tags cleared when the change left none. Previously the entry carried only the new tag set, so clearing every tag produced an entry with an empty details. Entries recorded before this change carry no removals and render as they did.

2026-08-19

Added

  • [V3] Email template content now resolves {{tag:Key}} placeholders to the payout instruction's tag values. A placeholder works in any content field of the templates sent for a payout instruction — the initiation, reminder, push, reinitiation and payout-available emails — and may be mixed freely with the existing placeholders, for example Team {{tag:TeamId}} for {{FirstName}}. Key matching ignores case, so {{tag:teamid}} resolves the TeamId tag. A placeholder naming a tag the instruction does not carry renders as the key text itself, tag:TeamId, so the surrounding wording stays intact. For emails sent gradually — bulk initiation and bulk reminders — the values are those the instruction carried when the send was accepted; a tag edited afterwards does not change an email already queued.
  • [V3] POST /v3/distribution-manager/templates/:send-preview accepts sample values for the {{tag:Key}} placeholders as dynamic fields keyed with the tag: prefix — {"dynamicFields": {"FirstName": "John", "tag:TeamId": "42"}}. Such keys are exempt from the dynamic-field name check and instead follow the payout instruction tag rules (at most 20, key up to 64 characters, value up to 256, no comma or double quote, no duplicate keys ignoring case, and no key matching an upload-file column: Id, Amount, FirstName, LastName, Email, PhoneNumber); a violation is rejected with 400. Dynamic fields without the prefix are validated against the template type as before. A placeholder with no matching key renders its fallback text, exactly as it would for an instruction lacking that tag.
  • [V3] Email template schemas returned by GET /v3/distribution-manager/templates/template-type-schemas gain a DistributionName dynamic field on the InitiateEmail, ReminderEmail, FailureReinitiateEmail, and PushEmail types. Template content may reference {{DistributionName}}; it renders as the distribution's name. The existing CampaignName field stays valid as a legacy alias of the same value — stored templates using {{CampaignName}} keep rendering unchanged, and dynamicFields on POST /v3/distribution-manager/templates/:send-preview accepts either key.

Changed

  • [V3] The default content values in the template type schemas (Subject and Paragraph1 of InitiateEmail and ReminderEmail) now use the {{DistributionName}} placeholder instead of {{CampaignName}}. Templates created from these defaults render identically.
  • [V3] The CampaignLogo content field is renamed to DistributionLogo across the template type schemas, and POST /v3/distribution-manager/templates and PUT /v3/distribution-manager/templates/{id} expect the new key in contentFields. CampaignLogo stays accepted as a legacy alias on create, update, and :send-preview — but sending both keys in one request is rejected with 400. Templates stored with the old key are unaffected and keep rendering; GET responses echo whichever key a template was saved with.

2026-08-14

Added

  • [V3] cancellationReason on payout instruction responses gains the value DirectAchRequestRejected. A direct ACH request that is accepted with 202 but cannot then be executed — the distribution does not cover the amount, or ACH is not available for it — resolves to a cancelled payout instruction carrying this reason. Such a request may be corrected and resubmitted with the same externalId; it continues on the same payout instruction, so the identifier from the original response stays valid and the resubmitted amount and payee details are the ones used.

2026-08-12

Fixed

  • [V3] GET /v3/distribution-manager/reports/distributions/{distributionId}/basic — requesting a different page, page size, or sort order for the same distribution within 2 minutes of an earlier request no longer returns that earlier request's cached page. Each combination of page, size, and sort now caches separately.
  • [V3] GET /v3/distribution-manager/reports/distributions/{distributionId}/basic — rows now come back in a stable order: the requested sortBy column, or creation date descending when none is given, with the payout instruction id breaking ties either way. The order was previously undefined when the sort column held duplicates, so consecutive pages could repeat or skip a row.

2026-08-11

Added

  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:initiate accepts a new optional payoutInstructionIds array, initiating exactly the payout instructions listed rather than a count or an upload batch. It cannot be combined with uploadBatchId or count (400). The selection is all-or-nothing: every id must belong to the distribution and be in the Ready status, otherwise the request is rejected with 409 PAYOUT_INSTRUCTIONS_NOT_INITIABLE — listing the offending ids in unknownIds and notReadyIds — and nothing is initiated. The funds, daily email limit and dryRun behaviour are unchanged. Existing callers are unaffected.
  • [V3] Payout instruction responses gain isDirectPayout. It is true for a payout requested directly by the distribution manager, which has no claim journey and never receives a notification. Reminders and reinitiation are rejected with 400 for such a payout.
  • [V3] cancellationReason on payout instruction responses gains the value DirectAchFailed, reported when the bank transfer for a direct payout fails and the payout is cancelled. Existing values are unchanged.

Changed

  • [V3] Payout instructions now allow up to 20 tags instead of 10. The limit applies everywhere tags are set: POST /v3/distribution-manager/payout-instructions, PATCH /v3/distribution-manager/payout-instructions/{id} with /Tags, and file uploads via POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload (up to 20 tag columns). All other tag rules are unchanged.
  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:direct-ach now requires the payoutInstructions:directPayout permission instead of payoutInstructions:initiate.
  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:direct-ach now rejects a repeated externalId with 400. Previously a repeat returned 202 with the same payout instruction identifier; the request is now accepted only once per externalId within a distribution.

2026-08-07

Changed

  • [V3] POST /v3/distribution-manager/bank-accounts/{id}/:withdraw now requires the bankAccounts:withdraw permission instead of admin:manage.

2026-08-06

Added

  • [V3] New POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:direct-ach endpoint requests a direct ACH payout to a vendor/payee supplied entirely by the distribution manager, with no beneficiary claim journey. The body carries the payout amount, the payee identity (name, email, phone, date of birth, address) and the destination bank details (routing/account number, account type, accountOwnerType of Individual or Business, and businessName for a business account). On acceptance returns 202 with the identifier of the payout instruction the request will produce; that identifier is derived from the distribution and externalId, so repeating a request yields the same one. Returns 404 if the distribution does not exist, 400 if the request is rejected — including when ACH is not enabled for the distribution — and 429 when the rate limit is exceeded.

Changed

  • [V3] Icon uploads — POST /v3/distribution-manager/distributions, PATCH /v3/distribution-manager/distributions/{id}/distribution-icon and the distribution-draft equivalents — accept PNG, JPEG, GIF and WebP only, and reject anything else with 400. The returned icon URL no longer contains the uploaded filename; icon URLs stored before this change are unaffected.
  • [Beneficiary] POST /beneficiary/request-otp no longer accepts the currentDateTimeString query parameter — the time shown in the one-time-code message is now set by Talli, in UTC (for example 21 Jul 2026, 14:32 UTC). The parameter is ignored rather than rejected, so existing clients keep working.

Removed

  • [V3] otpThresholdAmount is no longer part of the distribution or distribution-draft API — it is gone from the create payloads, the GET responses, and the paths PATCH accepts. It is now managed internally by Talli; contact support if you need a threshold configured.

2026-08-05

Fixed

  • [V3] GET /v3/distribution-manager/payout-instructions — the email filter no longer rejects terms containing %, _ or \ with 400. Those characters are now matched literally as part of the address instead — for example email=jo_hn@example.com matches only that exact address, not any address with a different character in place of the underscore.

2026-08-04

Added

  • [V3] GET /v3/distribution-manager/payout-instructions accepts optional tagKey and tagValue query parameters: tagKey together with tagValue returns the payout instructions carrying that exact tag pair, while tagKey alone returns the instructions carrying the key with any value. Matching is exact, including case; both parameters are trimmed and an empty value counts as absent. tagValue without tagKey is rejected with 400. The tag filter combines with the existing distributionId, status, externalId, email, and batchId filters.
  • [V3] New GET /v3/distribution-manager/tag-keys endpoint returns the distinct tag keys across the payout instructions visible to the caller, as a plain array of strings sorted alphabetically without regard to case — [] when no payout instruction carries a tag. Use it to offer tag-key suggestions before filtering the payout-instructions list.

Changed

  • [V3] GET /v3/distribution-manager/reports/distributions/{distributionId}/basic — the exported CSV gains one column per distinct tag key in the distribution, appended after the existing columns in alphabetical order (ignoring case). A row's cell holds its value for that key, or stays empty when the payout instruction does not carry the tag. Every page of one distribution carries the same tag columns, so paged exports stay concatenable. The existing columns are unchanged.

2026-08-03

Added

  • [V3] GET /v3/distribution-manager/payout-instructions gains filters: statuses (repeatable or comma-separated — matches any of the listed statuses; each must be a defined status name, and an unknown or numeric value is rejected with 400), distributionIds (repeatable or comma-separated — matches any of the listed distributions; each must be a valid guid), phoneNumber (exact match on the beneficiary's number, normalized first so common US formats such as +15551234567, 15551234567, or (555) 123-4567 all match; a number written in full international form — a leading + followed by its country code — matches too), and fromDate / toDate (inclusive range on the payout instruction's creation date, YYYY-MM-DD). Filters of different kinds combine with AND. Supplying both a singular filter and its plural form (status with statuses, or distributionId with distributionIds) returns 400, as does a fromDate later than toDate. The singular status is now validated the same way as statuses — a numeric or unknown value, or a comma-separated list, is rejected with 400 rather than silently reinterpreted.

Changed

  • [V3] PATCH /v3/distribution-manager/payout-instructions/{id}/amount and /email operations are now accepted only in statuses Draft, Ready, Restored, and Cancelled. To change amount or email on a payout that was already sent, cancel it, edit while cancelled, then reinitiate.
  • [V3] PATCH /v3/distribution-manager/payout-instructions/{id} — an /email change in Cancelled no longer auto-restores the payout.
  • [V3] POST /v3/distribution-manager/payout-instructions/{id}/:reinitiate — Now accepts payout instructions in status Cancelled. A new allocation is created for the current amount, if the campaign has insufficient unallocated funds the reinitiation fails asynchronously and the payout stays in Cancelled.
  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/activityPayout instruction updated rows now include a details object of { fieldName: "old → new" } for each field that changed on the update (e.g. amount: "50 → 100", email: "old@x.com → new@y.com").

Fixed

  • [V3] GET /v3/distribution-manager/payout-instructions returns payout instructions in a stable order: newest first. The order was previously undefined, so consecutive pages could repeat or skip a row.

Changed

  • [Beneficiary] POST /beneficiary/payout-instruction/ach-transfer accepts two new optional fields for paying into a business bank account: accountOwnerType (Individual or Business; omitted or null means Individual, so existing requests are unaffected) and businessName. For Business, businessName is required and must be 1–100 characters, and dateOfBirth is no longer required — it is ignored if sent. For Individual, dateOfBirth remains required and businessName must be omitted. A request whose accountOwnerType differs from the owner type the same bank account was already registered with is rejected with 400.

2026-07-31

Added

  • [V3] POST /v3/distribution-manager/payout-instructions accepts an optional tags object of free-form key/value labels, e.g. {"TeamId": "Alpha"}. At most 10 tags per payout instruction; keys up to 64 characters and values up to 256, both trimmed; keys must be unique regardless of case, must not contain a comma or a double quote, and must not be one of the known upload file columns (Id, Amount, FirstName, LastName, Email, PhoneNumber); values must not be empty. A request that breaks any of these is rejected with 400.
  • [V3] GET /v3/distribution-manager/payout-instructions and GET /v3/distribution-manager/payout-instructions/{id} return tags for each payout instruction — always present, {} when it has none.
  • [V3] PATCH /v3/distribution-manager/payout-instructions/{id} accepts /Tags as a patchable path, and accepts it in any status, including after the payout was requested or redeemed — the not-yet-requested restriction still applies to every other patchable property, and to a patch that mixes /Tags with one of them. Tags are replaced as a whole set: replace /Tags with {} clears them, a remove operation on /Tags is rejected with 400, and paths into a single tag such as /Tags/TeamId stay unsupported. The tag limits above are enforced here too, with a 400 for violations.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload and GET /v3/distribution-manager/distributions/{id}/payout-instructions/upload-status/{batchId} both return detectedTagColumns: the columns of the uploaded file that were imported as tags, in file order, [] for a file without them. Compare it with the columns you sent to catch a mistyped column name.
  • [V3] GET /v3/distribution-manager/payout-instructions/{id}/activity includes a Tags updated entry every time an instruction's tags change, carrying the new tag set in details.

Changed

  • [V3] GET /v3/distribution-manager/payout-instructions — the email filter now accepts a partial address and matches from the beginning of it, so email=john returns every beneficiary whose address starts with john. It matches from the start only: email=ohn does not match john@example.com. A full address still matches that beneficiary, and now additionally any address that starts with it, so email=bob@gmail.com also returns bob@gmail.com.br. %, _ and \ are rejected with 400.
  • [Beneficiary] POST /beneficiary/payout-instruction/prepaid-cardaddressLine2 is now optional.
  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload no longer rejects a file whose header carries columns beyond the known ones with 400 "Unexpected columns format or invalid delimiter" — those columns are now imported as tags on every row. The known six columns are still all required. At most 10 tag columns; names up to 64 characters and values up to 256; names must be unique regardless of case and must not match a known column name in any casing (a column called EMAIL is rejected rather than read as Email); an empty cell means the row carries no such tag. A file that breaks a rule is rejected with 400 under the Tags key, quoting the row numbers for over-long values. A file with tag columns is the desired tag set for every row it re-uploads, so an empty cell clears that tag; a file without tag columns leaves the stored tags untouched.

2026-07-29

Changed

  • [V3] POST /v3/distribution-manager/reports/tax/export — the exported CSV gains seven columns, which shifts the position of most existing ones: Business Name after Name, Other Classification after Tax Classification, then Exempt Payee Code, FATCA Code, Account Numbers, and Foreign Partners/Owners/Beneficiaries after Zip, and TIN Type after TIN. TIN moves from the ninth column to the fifteenth and each address column moves by two. Every existing column keeps its header label, so read the CSV by header rather than by column index. Rows with no business data leave the new cells empty and report Ssn as the TIN type.

2026-07-27

Added

  • [V3] New GET /v3/distribution-manager/distributions/{id}/payout-instructions/upload-status/{batchId} endpoint returns exact progress for one upload batch (the batchId returned by the upload endpoint): totalRows, processedRows, pendingRows, the per-outcome readyRows / draftRows / skippedRows / failedRows counts, and isComplete. Poll it after an upload instead of polling the payout-instructions list. Counts reflect each row's outcome at import time and can keep settling for a short window after a processing failure — a row can flip from Failed to Ready — so do not treat the first complete snapshot as immutable. Returns 404 when the distribution or the upload batch is unknown, or the batch belongs to another distribution.
  • [V3] New GET /v3/distribution-manager/distributions/{id}/payout-instructions/upload-status/{batchId}/rows endpoint lists the per-row outcomes of an upload batch, ordered by each row's line number in the uploaded file. Each row carries rowNumber, externalId (the file's Id column value), outcome (Pending | Ready | Draft | Skipped | Failed), payoutInstructionId (the payout instruction the row created or updated — null for Pending, Skipped, and Failed rows), and processedAt. Supports pageNumber/pageSize paging (defaults 1/20; pageSize must be 1–100, otherwise 400) and an optional outcome filter (e.g. outcome=Failed); an unrecognised outcome value is rejected with 400. Wait until upload-status reports isComplete before sweeping rows — outcomes settle while processing runs. A Skipped row's live payout instruction can be found by filtering GET /v3/distribution-manager/payout-instructions on its externalId. Returns 404 under the same conditions as upload-status.

2026-07-24

Changed

  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:initiate and .../:send-reminders — a count that exceeds the daily email send limit is no longer rejected with a 400 validation error. count is now purely an upper bound on how many payout instructions to select, and the daily-limit check is applied to the selected batch rather than to count itself. The value is still range-checked — greater than 0 and, when supplied, at most 25000, otherwise 400; only the daily-limit ceiling on count is gone. Two caller-visible consequences: (1) a request whose count exceeds the limit but whose actual selection fits the remaining quota now succeeds200 for a dry run, 202 otherwise — where it previously failed with 400; (2) a request whose selection would exceed the remaining quota returns 409 with error code EMAIL_SEND_QUOTA_EXCEEDED, carrying limit, used, requested, and resetsAt, where requested is the size of the selected batch, not the submitted count. Every over-quota condition now produces one consistent, machine-readable response instead of two different shapes; the 409 surfaces on the dry run too.

2026-07-23

Changed

  • [V3] GET /v3/distribution-manager/bank-accounts/{id}/withdrawal-accountsroutingNumber and accountNumber are now masked to the last four characters (previously the last three), consistent with how account numbers are surfaced elsewhere.

2026-07-17

Added

  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:initiate accepts a new optional uploadBatchId: when set, only Ready payout instructions created or updated by that upload batch (the batchId returned by the upload endpoint) are selected; when omitted, all Ready instructions of the distribution remain eligible as before. The batch must belong to the distribution and have finished processing: an unknown uploadBatchId is rejected with 404, and a still-processing one with 409 and error code UPLOAD_STILL_PROCESSING (carrying totalRows and processedRows) — poll the upload-status endpoint until it reports complete.

Changed

  • [V3] :initiatecount is now optional. When omitted, all Ready instructions in scope are selected, up to the 25,000-per-initiate maximum; if more are Ready, the request is rejected with 409 and error code TOO_MANY_PAYOUT_INSTRUCTIONS_TO_INITIATE (carrying readyCount and maxBatchSize). The rejection surfaces on the dry run too, so nothing is initiated unknowingly. An explicit count behaves exactly as before.

Fixed

  • [V3] :initiate now selects oldest-first (longest-waiting instructions first), as its documentation always stated; the selection had actually been newest-first. Only observable when initiating fewer instructions than are Ready.

2026-07-15

Changed

  • [V3] POST /v3/distribution-manager/distributions and POST /v3/distribution-manager/distribution-drafts/{id}/:promote now reject with 400 any payoutMethodIds entry that is not among the payout methods available for the organization's default bank (the set returned by GET /v3/distribution-manager/payout-methods). Previously any active payout method was accepted.
  • [V3] PATCH /v3/distribution-manager/distributions/{id} now rejects with 400 a patch that enables a payout method not available for that distribution's bank (the set returned by GET /v3/distribution-manager/payout-methods?distributionId={id}). Payout methods already enabled on the distribution are not re-checked, so unrelated edits to a distribution that already carries such a method still succeed.

2026-07-14

Removed

  • [Beneficiary] POST /beneficiary/tax/w9 — the certified request field has been removed. It is no longer required and requests that omit it now succeed; requests that still send it are accepted (the field is ignored). A request without a User-Agent header is no longer rejected with 400.
  • [Beneficiary] GET /beneficiary/tax/w9 — the certifiedOn field has been removed from the submission object.
  • [V3] POST /v3/distribution-manager/reports/tax/export — the Certified On column has been removed from the exported CSV.

2026-07-13

Added

  • [V3] POST /v3/distribution-manager/distributions and POST /v3/distribution-manager/distribution-drafts accept an optional taxCollectionMode form field (None | Standard), so tax collection can be configured at creation instead of requiring a follow-up PATCH. For distributions, omitting the field defaults to None.

2026-07-10

Added

  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload now returns { batchId } in its 202 Accepted body. The batchId identifies the upload; use it to poll upload progress and to filter the payout-instructions list by upload (see below).
  • [V3] GET /v3/distribution-manager/payout-instructions accepts a new optional batchId query parameter that filters the list to the payout instructions created by that upload batch.

Changed

  • [V3] POST /v3/distribution-manager/distributions/{id}/payout-instructions/:upload now rejects a file with more than 100,000 rows with a 400; files at or below the limit are unaffected. Support for larger imports was intentionally dropped from the supported envelope.

2026-07-08

Added

  • [V3] POST /v3/distribution-manager/reports/tax/export — exports certified W-9 tax records as a password-protected ZIP. Request body: scope (Distribution | DateRange | Beneficiary), distributionId (required when scope is Distribution), from/to (required when scope is DateRange; exact UTC instants, inclusive, matched against the payout's fulfillment date), payoutInstructionId (required when scope is Beneficiary), includeNotFulfilledPayouts (optional, default false — when false, payout instructions that were never fulfilled are excluded from the Distribution and Beneficiary scopes even if their W-9 is completed; has no effect on DateRange, which is always keyed to fulfillment date), and zipPassword (required, minimum 12 characters with at least one uppercase letter, one lowercase letter, one digit, and one special character, used to encrypt the returned file — not stored by Talli). Returns application/zip containing a CSV of the matching records. Returns 400 when a scope-specific field is missing/invalid or zipPassword doesn't meet the length/complexity requirements, and 404 when the given distributionId or payoutInstructionId does not exist.

Fixed

  • [V3] GET /v3/distribution-manager/invitations no longer returns expired invitations (those past their expiresAt); totalItems and pagination now count only pending invitations. To re-invite someone whose invitation expired, create a new invitation via POST /v3/distribution-manager/invitations/bulk.

2026-07-06

Added

  • [Beneficiary] POST /beneficiary/tax/w9 — submits the beneficiary's W-9 tax information in a single call. Request body: name (required), taxClassification (optional; only Individual is accepted), tin (required, exactly 9 digits), address (line1, city, state, zip required; line2 optional), certified (required, must be true), and consent ({ electronicDelivery }). Returns 202 on success. Returns 400 when the mandatory set is incomplete, tin is not 9 digits, taxClassification is not Individual, or certified is not true. Returns 409 with code TAX_ALREADY_SUBMITTED if a W-9 has already been submitted for the beneficiary.

2026-07-03

Added

  • [V3] Distribution and draft responses now include a taxCollection object with mode (None | Standard) and nullable enabledOn. Existing distributions default to { mode: "None", enabledOn: null }.
  • [V3] PATCH /v3/distribution-manager/distributions/{id} and PATCH /v3/distribution-manager/distribution-drafts/{id} accept /taxCollection (e.g. [{ "op": "replace", "path": "/taxCollection", "value": { "mode": "Standard" } }]). Requires distributions:edit. enabledOn is server-stamped on first None → Standard and retained on Standard → None; unknown mode strings return 400.
  • [V3] POST /v3/distribution-manager/distribution-drafts/{id}/:promote — a Standard draft yields a distribution created as Standard with enabledOn stamped at promote time.

Changed

  • [V3] GET /v3/distribution-manager/payout-methods now returns only the payout methods available for the relevant bank and organization, rather than every active payout method. It accepts a new optional distributionId query parameter: pass it to scope the list to an existing distribution's bank; omit it to scope the list to the organization's default bank (e.g. when creating a distribution or editing a draft). Returns 404 when the distributionId does not identify an existing distribution in your organization.

2026-07-02

Changed

  • [Beneficiary] POST /beneficiary/payout-instructions/{id}/prepaid-card now requires dateOfBirth (format YYYY-MM-DD, must be a date in the past). Requests that omit it or supply today's/a future date are rejected with 400.

2026-06-29

Removed

  • [V3] Removed the instrument-read endpoints GET /v3/distribution-manager/ach-transfers/{id}, /ach-transfers/?payoutInstructionId=, /paypal-transfers/{id}, /paypal-transfers/?payoutInstructionId=, /venmo-transfers/{id}, /venmo-transfers/?payoutInstructionId=, /gift-cards/{id}, /gift-cards/?payoutInstructionId=, /prepaid-cards/{id}, /prepaid-cards/?payoutInstructionId=. These endpoints now return 404.

2026-06-24

Added

  • [V3] POST /v3/distribution-manager/distributions/{id}/:reassign-subclient — reassigns a distribution to a different sub-client. Request body: nullable subClientId; provide a sub-client id to move the distribution to that sub-client, or null to make it parent-only. The new subClientId cascades to every payout instruction of the distribution. Returns 202 on success. Returns 400 if subClientId is provided but is not a sub-client in the caller's organization, and 404 if the distribution does not exist.

2026-06-22

Changed

  • [V3] GET /v3/distribution-manager/users/{id} now returns the user object directly, with role nested on it. The previous { "user": { ... }, "role": { ... } } wrapper and its top-level role field have been removed.

2026-06-18

Added

  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:send-reminders — bulk send reminders for a distribution. Request body: count (required) and optional dryRun (default false). Selects up to count of the distribution's payout instructions that are still awaiting beneficiary action and have not been notified within the reminder interval, oldest reminder first, and sends each a reminder; the emails are queued and delivered gradually rather than all at once. On success returns 202 with { dryRun, batchId, acceptedCount }batchId tags every email produced by the batch and acceptedCount is how many reminders were accepted. With dryRun: true the same selection and validation run but nothing is sent: returns 200 with batchId: null. count must be greater than 0 and not exceed the organization's daily email send limit, otherwise 400. Returns 409 with errorCode NO_PAYOUT_INSTRUCTIONS_TO_REMIND (no eligible recipients) or EMAIL_SEND_QUOTA_EXCEEDED (the batch would exceed the organization's daily email send limit — the response includes limit, used, requested, and resetsAt). Returns 404 if the distribution does not exist and 429 when the per-organization rate limit is exceeded.

2026-06-16

Added

  • [V3] Distribution responses (GET /v3/distribution-manager/distributions and .../{id}) now include a read-only, nullable subClientId identifying the owning sub-client; null for parent-only distributions.
  • [V3] POST /v3/distribution-manager/distributions accepts an optional subClientId (the owning sub-client); omitting it leaves the distribution parent-only. The field is not patchable via PATCH /v3/distribution-manager/distributions/{id}.
  • [V3] Payout instruction responses now include a read-only, nullable subClientId, inherited from the owning distribution.
  • [V3] Distribution drafts now carry an optional subClientId: settable on POST /v3/distribution-manager/distribution-drafts, patchable via PATCH, returned on GET, and inherited by the distribution (and its payout instructions) on POST .../distribution-drafts/{id}/:promote. Drafts are sub-client read-scoped: a sub-client viewer sees only their own drafts; unassigned drafts are staff-only.

2026-06-15

Added

  • [V3] Sub-client management: GET /v3/distribution-manager/subclients (list, with member counts), POST /v3/distribution-manager/subclients (create, 201), PATCH /v3/distribution-manager/subclients/{id}. PATCH /v3/distribution-manager/users/{id}/subclient assigns a user to a sub-client or detaches them when subClientId is omitted/null.
  • [V3] CreateInvitationRequest accepts an optional subClientId; when set, the invitee is provisioned as a sub-client-viewer of that sub-client. UserResponse and InvitationResponse now include a nullable nested subClient ({ id, name }) — null for non-sub-client invitees.
  • [V3] The user object returned by GET /v3/distribution-manager/users and GET /v3/distribution-manager/users/{id} now includes role — the user's role as { id, name }. It is always present (an active user is expected to have exactly one role) and is now returned on the list as well, not only when fetching a single user by id.

Fixed

  • [V3] GET /v3/distribution-manager/distributions/{id}/account-details now resolves the distribution through the organization-scoped (filter-respecting) read path. A distribution belonging to another organization now returns 404 instead of being read by id regardless of tenant. The two distinct 404 messages ("distribution not found" vs "bank account not found") were also unified into a single message so the response cannot be used as a cross-organization GUID-existence oracle. (TALLI-2834)
  • [V3] POST /v3/distribution-manager/distributions/{id}/:refund (deprecated) now resolves the distribution through the organization-scoped read path; a distribution belonging to another organization returns 404 instead of being read by id regardless of tenant. (TALLI-2834)

2026-06-12

Added

  • [V3] BankAccountResponse now includes acceptsNewDistributions (boolean). true only when the bank account is on the organization's default bank partner AND has status Active; false for run-off accounts on a former partner or accounts still being provisioned. Consumers should use this flag (not status) when deciding whether an account is eligible for new distribution creation. The listing endpoint GET /v3/distribution-manager/bank-accounts still returns every account regardless of provider, so references to run-off accounts remain resolvable.

Changed

  • [V3] POST /v3/distribution-manager/distributions and POST /v3/distribution-manager/distribution-drafts/{id}/:promote now return 400 ValidationProblem (keyed on bankAccountId) when the chosen bank account is not on the organization's default bank partner. Existing distributions referencing a former partner's bank account continue to function — only new distribution creation is gated. Draft POST and PATCH endpoints intentionally do not gate on the partner match; the check fires at promote.

2026-06-11

Added

  • [V3] POST /v3/distribution-manager/distributions/{distributionId}/payout-instructions/:initiate — bulk initiate payout instructions. Takes a count and initiates up to that many of the distribution's Ready payout instructions (oldest first) as a single batch; the initial emails are sent gradually rather than all at once. The batch is atomic: the distribution's bank account must hold enough unallocated funds to cover it, and it must fit within the organization's daily email send limit. Rejections return 409 Problem Details with errorCode NO_PAYOUT_INSTRUCTIONS_TO_INITIATE, INSUFFICIENT_DISTRIBUTION_BALANCE, or EMAIL_SEND_QUOTA_EXCEEDED (the quota rejection includes limit, used, and requested). On success returns 202 with the batchId, the accepted count, and the total amount. An optional dryRun flag runs the same selection and validations (including the 409 rejections) and returns the would-be summary with 200 and a null batchId, without initiating anything. Rate-limited per organization (429). The singular POST /payout-instructions/{id}/:initiate is unchanged and keeps sending immediately.

2026-06-02

Removed

  • [V2] The entire Campaign Manager API (/v2/campaign-manager/*) has been removed (sunset). All endpoints now return 404. Consumers must migrate to the Distribution Manager API (/v3/distribution-manager/*), which exposes the equivalent functionality. The campaign-manager-v2 OpenAPI/Swagger documents are no longer published, and the default API version is now 3. Request/response shapes shared with V3 (e.g. the Campaign* types, user-management and invitation contracts) are unaffected.

2026-06-09

Added

  • [V3] PUT /v3/distribution-manager/brand/slug — set the brand slug (string). The slug is required for the branding to take effect and is immutable once set. Returns 409 BRAND_SLUG_LOCKED (slug already set to a different value) or BRAND_SLUG_TAKEN (slug used by another organization).

Changed

  • [V3] Brand config is now treated as always-existing (1:1 per organization). All brand PUT endpoints are upsert ("create or update"): no separate create step is required and call order does not matter.
  • [V3] GET /v3/distribution-manager/brand now returns 200 with an empty configuration (null slug/displayName) when none has been set up yet, instead of 404.
  • [V3] PUT /v3/distribution-manager/brand/company-details now also accepts displayName (required) and supportEmail (optional), alongside companyName + companyAddress. displayName is the short UI label (tab names, browser history); companyName is the legal name used in contact details.

Changed (Public/Beneficiary)

  • [Public] [Beneficiary] GET /public/brand/* and GET /beneficiary/brand/* now return 404 until the brand config is "live" (both slug and displayName set — the minimum identity; colors/logo remain optional). Since these fields are set via separate PUTs, a partially-configured org no longer leaks a half-branded response — callers fall back to default Talli branding on 404. When 200 is returned, slug and displayName are guaranteed present.

Removed

  • [V3] POST /v3/distribution-manager/brand (bootstrap) — removed. Brand config is now created implicitly via the individual PUT endpoints (upsert).
  • [V3] PUT /v3/distribution-manager/brand/contact-details — removed. displayName and supportEmail moved to /company-details; fromEmail moved to the new admin-only /from-email endpoint. This removes the duplicate displayName that previously appeared in both branding and contact details.

2026-06-05

Fixed

  • [V2, V3] POST /v3/distribution-manager/bank-accounts, POST /v3/distribution-manager/funds-holders, and POST /v2/campaign-manager/funds-holders no longer return 500 ("Bank Provider is not configured for this organization.") for correctly-onboarded organizations that had no explicit default bank provider set. These calls now succeed for any properly onboarded organization.

2026-05-29

Added

  • [V3] GET /v3/distribution-manager/brand — read merged brand config.
  • [V3] POST /v3/distribution-manager/brand — bootstrap brand config with slug + displayName. Returns 409 BRAND_CONFIG_EXISTS if config already exists for the organization; 409 BRAND_SLUG_TAKEN if slug used by another organization.
  • [V3] PUT /v3/distribution-manager/brand/colors — set/clear primary + secondary brand colors (paired). Body: BrandColors { primaryColor, secondaryColor }.
  • [V3] PUT /v3/distribution-manager/brand/company-details — set company name + address. Body: SetBrandCompanyDetailsRequest { companyName, companyAddress }.
  • [V3] PUT /v3/distribution-manager/brand/contact-details — set displayName, supportEmail, fromEmail. Changing the fromEmail domain resets fromEmailActive to false until the new domain is verified.
  • [V3] POST /v3/distribution-manager/brand/logo — upload brand logo (multipart). Accepts SVG ≤ 200KB or PNG ≤ 400×120px ≤ 200KB.
  • [V3] POST /v3/distribution-manager/brand/favicon — upload brand favicon (multipart). Accepts ICO ≤ 32×32px ≤ 20KB.

2026-05-22

Deprecated

  • [V2] GET /campaigns/{id}/withdrawal-accounts and POST /campaigns/{id}/withdrawal-accounts are deprecated. Use the bank-account-scoped equivalents GET /bank-accounts/{bankAccountId}/withdrawal-accounts and POST /bank-accounts/{bankAccountId}/withdrawal-accounts instead (pass the campaign's bank-account id rather than the campaign id). The campaign-scoped routes continue to work and are now flagged as deprecated in the OpenAPI/Swagger definition.

Added

  • [V2, V3] TemplateType enum — added PayoutAvailableEmail = 6 for payout available email notifications sent when gift card payouts are fulfilled.

2026-05-21

Added

  • [V2, V3] BankAccountResponse now includes a status field (Pending | Active). Existing callers continue to work; the new field is additive.
  • [V2, V3] GET /bank-accounts query parameter includePending (default false). When false (default), Pending bank accounts are excluded from the list and the total count. When true, all bank accounts are returned including Pending ones.

Changed

  • [V2, V3] GET /bank-accounts/{id} always returns the bank account regardless of status. The response includes the new status field so callers can distinguish Pending from Active. No request shape change.

2026-05-20

Changed

  • [V3] POST /v3/distribution-manager/funds-holders now returns CreateFundsHolderResponse { fundsHolderId, bankAccountId } instead of the bare fundsHolderId Guid. The bank account is returned in Pending status and becomes Active automatically once the funds holder is approved.
  • [V2] POST /v2/campaign-manager/funds-holders now creates the associated bank account immediately (in Pending status) rather than only after KYB approval, so its bankAccountId is available straight away. The response shape is unchanged (still the bare fundsHolderId).
  • [V2, V3] POST /v2/campaign-manager/campaign-drafts/{id}/:promote and POST /v3/distribution-manager/distribution-drafts/{id}/:promote now return 400 ValidationProblem (keyed on bankAccountId) when the draft's assigned bank account is still Pending. Wait until the bank account's status is Active before promoting.

2026-05-14

Added

  • [Beneficiary] GET /beneficiary/payout-instruction/ response now includes bankPartner (BankProvider enum) — the issuing bank partner, so callers can render issuer-specific copy (T&C, disclaimers, cardholder agreement URLs) keyed by the partner.
  • [Beneficiary] payoutMethodGroups.prepaidCards[].description — optional per-partner description for the prepaid-card row.

Changed

  • [Beneficiary] payoutMethodGroups.prepaidCards[].name for the prepaid-card row is now resolved per bank partner. Patriot campaigns keep "Easy Prepaid Mastercard®"; Stearns campaigns return "Prepaid Mastercard®". Other payout methods are unchanged.

2026-05-14

Changed

  • [V3] POST /v3/distribution-manager/bank-accounts no longer requires fundsHolderId in the request body. The request body now only accepts an optional name field.

2026-05-18

Changed

  • [V2, V3] POST /bank-accounts/{id}/:withdrawdescription now rejected if longer than 512 characters

2026-05-05

Added

  • [V3] Distribution Manager API (/v3/distribution-manager/*) — new public API for distribution management