Finero public API
A versioned, tenant-scoped REST API for Finero — invoices, payment links, confirmed payments, ERP sync, and automation history. Built for external systems, automation platforms, and AI agents. All requests and responses are JSON. Timestamps are ISO 8601 UTC. Decimal money amounts are exact strings (never floats); *_minor amounts are integers in the currency's minor unit with an explicit ISO 4217 currency.
Base URL
https://api.getfinero.com/functions/v1/apiYour first call
A workspace admin creates an API key in Settings → API (the full key is shown once — store it safely). Then confirm it works — this read is safe for both key types and returns your workspace context:
curl "https://api.getfinero.com/functions/v1/api/v1/me" \
-H "Authorization: Bearer $FINERO_API_KEY"Versioning. All paths live under /v1. Changes within v1 are additive and backward-compatible (new fields, new operations). A breaking change would ship as /v2 — existing v1 behavior is never silently changed. The machine-readable spec lives at https://api.getfinero.com/functions/v1/api/openapi.json.
AI agents (MCP)
This reference describes the API-key surface — for systems, scripts and unattended automation. If instead you want an AI assistant to work with Finero on a person's behalf, use the MCP server. It speaks the Model Context Protocol, and the assistant acts as the person who approved it rather than as the workspace.
Connect an assistant, step by step
- A workspace admin turns on Settings → API & MCP → Enable new AI agent connections.
- In Claude: Settings → Connectors → Add custom connector. Paste the server URL below and choose register automatically. There is no client ID or secret to copy — the connector registers itself.
- Approve the connection when Finero asks. You will be shown the agent's name and where it returns you, which workspace it is for, and exactly what it may do. This happens once per person.
https://app.getfinero.com/mcpWhat an assistant can do: twenty-four tools, including summarize_aging, list_customer_aging and list_aging_installments, and no more — list_invoices and get_invoice (invoices, installments and collection status), summarize_invoices (how many sit in each collection status, and how much is outstanding per currency), list_payments (what was actually collected, and whether it reached your ERP), list_payment_integrations (who collects the money), list_workflows and list_workflow_executions (what your automations do, which are switched on, and what they actually did), list_erp_connections, list_sync_runs and get_sync_run (connection state and sync history), start_erp_sync (starts a pull; an admin's agent only), list_payment_links and get_payment_link (the links you have issued, and the page a customer would pay at), list_tasks and dismiss_task (the invoices waiting for a person to set up their payment links, and returning one to Auto Generate), create_payment_links (one link, or a split in one all-or-nothing call; it takes an idempotency_key, so a retry never creates the links twice), deactivate_payment_link, list_scheduled_emails, update_scheduled_email, delete_scheduled_email, and — for an assistant connected to an admin only — push_payment_receipts (pushes the payments your workspace is holding for manual release).
Reading your automations shows the switches and what each automation does — never the addresses behind them. The address your team is notified at and the reply-to on customer email are not sent to an assistant at all.
About payment links: an assistant can do exactly what the person who connected it can do in Finero. It can issue a link for an installment — for the whole amount, for part of it, or split into several links in one call that creates all of them or none — and can withdraw one again. It can never ask for more than the invoice still owes: the database caps every link at what is genuinely outstanding, so an assistant cannot overcharge anyone even if it is told to. Withdrawing a link only switches it off; the record stays and a new link can be issued straight away. It can use your payment-link email workflow, send the standard email now, schedule it in your workspace timezone, suppress it, or reschedule/cancel a pending send — but it cannot change a word of the message.
What it cannot do, ever: send your customers an email it has written itself, take a payment, refund one, move money out of your account, write anything to your ERP beyond pushing the payments your workspace is already holding for manual release, change members, integrations or API keys, or delete invoices, links, payments, members, or integrations. Those tools do not exist, and every write path in the database refuses an agent session outside the operations above — so an instruction hidden in invoice text cannot cause one indirectly.
When a tool call fails it says why in a stable code, and whether trying again could help — so an assistant corrects a bad argument instead of retrying it, and retries a passing fault instead of giving up. It never repeats anything your database or payment provider said.
Revoking a connection stops any assistant using it immediately, not when its token expires.
Authentication
Every request needs a Finero API key, created by a workspace admin in Settings → API. Keys are tenant-owned: all data access is scoped to the key's workspace, and nothing in a request can address another workspace. Send the key as a bearer token:
curl "https://api.getfinero.com/functions/v1/api/v1/me" \
-H "Authorization: Bearer fnr_0123456789abcdef_your43charSecretPart..."- Keep keys in a secrets manager. Never commit them, never put them in URLs, client code, or logs.
- The full key is shown exactly once at creation and can never be retrieved.
- Handing a key to an AI agent: you choose the permission, not the agent —
readonlyreads every resource in this workspace,adminadds the write operations. Enter it in the agent's credential store rather than typing it into a conversation: a key pasted into chat stays in that history, and the fix is to revoke it and issue a new one. The same guidance is in /llms.txt, which is what an agent reads. - To rotate: create a new key, switch your integration, then revoke the old key (revocation is immediate).
- Authentication failures return a generic 401 — the API never confirms whether a key identifier exists.
Permissions
A key has exactly one permission, chosen at creation:
- Read-only — may call only operations guaranteed to have no side effects (marked Admin or Read-only key below). It can never create, change, trigger, or send anything.
- Admin — everything Read-only can, plus the small set of deliberately exposed mutations (marked Admin key required): creating payment links (one, or a batch of up to 12) and deactivating them, rescheduling or cancelling their emails, starting an ERP sync, setting a connection's sync cadence and payment-push mode, and pushing held payments to the ERP. An Admin key does not get any other write — invoices, installments, customers and workflow history stay read-only for every key.
Enforcement is per-operation (each operation is classified in the spec via x-permission), not by HTTP method. Calling an admin-only operation with a read-only key returns 403 permission_denied.
Pagination, filtering & sorting
List endpoints return { data: [...], pagination: { next_cursor, has_more, limit } }. Pass ?limit= (1–100, default 25) and follow pagination.next_cursor via ?cursor= until has_more is false. Cursors are opaque — don't parse or construct them. Results are ordered by creation time (newest first by default; ?order=asc for oldest first).
Filters are per-endpoint allowlists (documented on each endpoint) applied as exact matches or since-timestamps. Unknown query parameters are rejected with 400 validation_failed — there is no generic column filtering.
Idempotency
Operations marked Idempotency-Key required must send a unique Idempotency-Key header (1–255 printable ASCII characters, e.g. a UUID). Retrying with the same key and same body returns the original result (with Idempotency-Replayed: true) instead of executing again — safe for network retries. Reusing a key with a different body returns 409 idempotency_conflict. Keys expire after 24 hours. Retry guidance for agents: on a timeout or 5xx, retry the identical request with the identical key; on 4xx, fix the request and use a fresh key.
Rate limits
- 120 requests/minute per API key
- 600 requests/minute per workspace (all its keys)
- Repeated failed authentication is limited per source address
Exceeding a limit returns 429 rate_limit_exceeded with a Retry-After header (seconds). Request bodies are capped at 64 KiB (413 payload_too_large).
End-to-end examples
A common flow: find a collectible invoice, read its installments, create a payment link for one installment, then poll its status. All data is fictional; replace $FINERO_API_KEY and the ids with real values from earlier responses.
1. List invoices (Admin or Read-only key, no side effects)
List invoices (add ?collection_ready=true to find collectible ones).
curl "https://api.getfinero.com/functions/v1/api/v1/invoices" \
-H "Authorization: Bearer $FINERO_API_KEY"2. Get an invoice with its installments (Admin or Read-only key, no side effects)
Read one invoice with its installments; take a collectible installment's id for the next step.
curl "https://api.getfinero.com/functions/v1/api/v1/invoices/<invoiceId>" \
-H "Authorization: Bearer $FINERO_API_KEY"3. Create a payment link (Admin key, has side effects)
Create a hosted payment link for that installment. The response's url is the customer-facing checkout page.
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/payment-links" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Idempotency-Key: <unique-key>" \
-H "Content-Type: application/json" \
-d '{"invoice_id":"8f14e45f-ceea-4a5b-9d2c-167ce7de1a10","installment_id":"a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70"}'4. Get a payment link (Admin or Read-only key, no side effects)
Poll the link until its status is paid.
curl "https://api.getfinero.com/functions/v1/api/v1/payment-links/<paymentLinkId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Step 3 is the only mutation and requires an Idempotency-Key: if the response is lost, retry with the same key to get the original link back instead of creating a second one. There is no standalone customers resource — customer display fields live on the invoice, and confirmed payments are read via /v1/payments.
Errors
Errors share one shape. request_id echoes the X-Request-Id response header — include it when reporting a problem. Validation errors add a details array of { field, message }.
{
"error": {
"code": "permission_denied",
"message": "The API key does not have permission to perform this operation.",
"request_id": "req_2f7c1a9b3d5e4f60718293a4"
}
}| Code | Status | Retry? | Meaning |
|---|---|---|---|
| missing_credentials | 401 | fix request | No Authorization: Bearer header was sent. |
| invalid_credentials | 401 | fix request | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. |
| revoked_credentials | 401 | fix request | The presented key was revoked. Create a new key in Settings → API. |
| feature_unavailable | 403 | fix request | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. |
| permission_denied | 403 | fix request | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). |
| not_found | 404 | fix request | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. |
| invalid_cursor | 400 | fix request | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. |
| validation_failed | 400 | fix request | The request failed schema validation. See error.details for field messages. |
| conflict | 409 | retry | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. |
| idempotency_key_required | 400 | fix request | This operation requires the Idempotency-Key header (1–255 characters). |
| idempotency_conflict | 409 | fix request | The Idempotency-Key was already used with a DIFFERENT request body. Use a fresh key for a new request. |
| rate_limit_exceeded | 429 | retry | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. |
| payload_too_large | 413 | fix request | The request body exceeds the 64 KiB limit. |
| method_not_allowed | 405 | fix request | The path exists but not with this HTTP method. |
| installment_not_collectible | 422 | fix request | The invoice or installment target cannot be collected right now — it is already paid, excluded from collections, has no outstanding balance, its invoice is not collection-ready, or its invoice is no longer present in your ERP. GET /v1/invoices/{invoiceId} reports the invoice's collection_status and each installment's collectible flag. |
| amount_exceeds_available | 422 | fix request | The installment is collectible, but the amount asked for is more than is still available on it. The available amount is the installment's collection_outstanding minus the amounts of its live payment links; what Finero already collected is netted off collection_outstanding and must not be subtracted twice. Ask for that amount or less, or omit amount_minor to take exactly what is left. |
| sync_disabled | 422 | fix request | Invoice sync is switched off for this ERP connection, so there is nothing to pull. A workspace administrator turns it back on in the Finero app; retrying will not change the answer. |
| connection_disconnected | 422 | fix request | This ERP connection has been disconnected, so Finero holds no credential for it and a pull cannot be started. A workspace administrator reconnects the ERP in the Finero app; retrying will not change the answer. |
| integration_unavailable | 422 | fix request | The tenant has no connected payments integration able to mint this link, or the one selected is not ready. Connect or repair a payment provider in the Finero app, then retry. |
| currency_not_supported | 422 | fix request | The selected payment provider cannot collect the invoice's currency (single-currency providers can only charge in their store currency). Use a provider that supports it, or collect the invoice outside Finero. |
| internal_error | 500 | retry | Unexpected server error. Safe to retry with the same Idempotency-Key. |
Endpoint reference
All examples use fictional data. Replace path placeholders like <invoiceId> with real ids from list responses.
Tasks
/v1/tasksList tasks
Lists this workspace's tasks, newest first. A task is work Finero asks a person to do on an invoice; today the one type is payment_arrangement: the Payment Arrangement workflow matched the invoice, so a person sets up its payment links. Filter state=open for the invoices still waiting for their first payment link; each row names its invoice (`invoice_number`). Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| invoice_id (query) | uuid | no | Filter by invoice. |
| state (query) | "open" | "configured" | "dismissed" | "not_needed" | no | Filter by task state. |
| type (query) | "payment_arrangement" | no | Filter by task type. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/tasks" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"type": "payment_arrangement",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"invoice_number": "INV-1042",
"assignee_id": "5b0f6a7e-2f5d-4d24-9e6b-3c1a2b4d5e6f",
"state": "open",
"configured_at": null,
"dismissed_at": null,
"created_at": "2026-09-29T12:00:00Z"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Tasks, newest first. |
| data[].id | uuid | yes | Finero task ID. |
| data[].type | "payment_arrangement" | yes | payment_arrangement: the Payment Arrangement workflow matched this invoice, so it needs payment links set up by a person. |
| data[].invoice_id | uuid | yes | Invoice; use get_invoice to load its balances and collection targets. |
| data[].invoice_number | string | null | yes | The invoice's human-readable number (local or ERP), so a person can be told which invoice without loading it; null when the invoice has none. |
| data[].assignee_id | uuid | null | yes | The workspace member who was notified; null if that member was removed. |
| data[].state | "open" | "configured" | "dismissed" | "not_needed" | yes | open: no payment link yet. configured: the invoice got its first payment link (it stays configured unless dismissed; later links change nothing). dismissed: released by a person or the API. not_needed: the invoice was retired or nothing is owed in the ERP. Until a task is dismissed, Auto Generate does not create links for its invoice. |
| data[].configured_at | date-time | null | yes | When the invoice got its first payment link. |
| data[].dismissed_at | date-time | null | yes | When the task was dismissed. |
| data[].created_at | date-time | yes | When a match opened the task. |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/tasks/{taskId}/dismissDismiss a task
Dismisses the task, which returns its invoice to Auto Generate: when that workflow is on, it may then create a payment link and email the customer. A CONFIGURED task too: its links stay, and Auto Generate may cover what they leave. Existing links and payments are not changed. Obtain the user's approval before calling. Dismissing a dismissed task changes nothing.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| taskId (path) | uuid | yes | Task ID. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/tasks/<taskId>/dismiss" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"ok": true
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| ok | boolean | yes |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Aging
/v1/aging/summarySummarize installment aging
Current installment-level aging in the workspace timezone, read from the PAYMENT SCHEDULE rather than the invoice header. A row counts when the installment is live, still owes money, is not closed in the source, and has a due date; removed and retired records never count. The invoice's status is not consulted, so debt the ERP has locked is still counted; `collectible` on each row says whether Finero can act on it. Balances are the maintained ERP/Finero reconciled figures, aged by due date: today or future, 1–30, 31–60, 61–90, or over 90 days. Absent by design: an installment with NO due date, which cannot be aged, and a CREDIT balance, which Finero never allocates against a debt. No historical reconstruction or FX. Totals cover all matching rows, per currency. Summary aggregates into `totals` and carries NEITHER `data` nor `pagination`: it is not a list. Customer and installment lists carry both. Use customer_key and bucket for drilldown. Cursors require unchanged query and report revision; restart on invalid_cursor. Invoice-summary due filters select whole invoices, not installment aging.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| currency (query) | string | no | Currency code, or UNKNOWN for missing currency. Omit for separate totals per currency; never sum currencies. |
| search (query) | string | no | Literal, case-insensitive customer name or number search across the full report. |
| customer_key (query) | string | no | Stable key returned by customer aging: customer UUID, or unassigned:invoice UUID. Never group customers by name. |
| bucket (query) | "current" | "days_1_30" | "days_31_60" | "days_61_90" | "over_90" | no | Installment bucket: current includes today; 1–30, 31–60, 61–90, over 90 are disjoint. The report holds positive balances only: an installment with no due date, or a credit balance, is not in it at all. |
| overdue_only (query) | boolean | no | Only positive balances with invoice or installment due dates before the workspace's current business day. |
| sort (query) | "overdue_desc" | "outstanding_desc" | "customer_asc" | no | Customer ordering, default overdue_desc. Money ranks WITHIN a currency, never across two: rows group by currency, largest first inside each — filter by currency to rank one book. Breakdown always orders by due date, invoice and installment; unknown dates last. |
| limit (query) | integer | no | Rows per page, default 50, maximum 100. Totals cover all matching rows. |
| cursor (query) | string | no | Return next_cursor unchanged, with identical filters and limit. Do not parse or build one — the format may change. Restart without it on invalid_cursor. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/aging/summary" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"report_date": "2026-09-10",
"timezone": "UTC",
"generated_at": "2026-09-10T12:00:00Z",
"balance_basis": "finero_reconciled_current",
"revision": "example",
"currencies": [
"USD"
],
"data_updated_at": "2026-09-10T11:00:00Z",
"totals": [
{
"currency": "USD",
"outstanding": "300.00",
"overdue": "200.00",
"current": "100.00",
"days_1_30": "0.00",
"days_31_60": "200.00",
"days_61_90": "0.00",
"over_90": "0.00"
}
]
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| report_date | date | yes | Current business day in workspace timezone; not a historical balance date. |
| timezone | string | yes | |
| generated_at | date-time | yes | |
| balance_basis | "finero_reconciled_current" | yes | Current maintained invoice or installment balance after existing ERP/Finero reconciliation. Includes debt Finero cannot currently collect, whatever blocks it: `collectible` on each row says whether it can be acted on. Always positive: a credit balance is not reported here, and Finero never nets one against a debt. No FX or historical reconstruction. |
| revision | string | yes | Content hash of the rows behind this answer. It changes whenever they do, in every mode, so it is usable as a change token; a cursor also carries it, which is how a paged walk detects that the report moved underneath it. |
| currencies | array of string | yes | The workspace's currencies, not this response's — a filter never removes one. Empty totals beside a full list means nothing matched, not that nothing is owed. |
| data_updated_at | date-time | null | yes | Latest source record change, not proof that the ERP is fully synchronized. |
| totals | array of object | yes | |
| totals[].currency | string | yes | |
| totals[].outstanding | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].overdue | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].current | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_1_30 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_31_60 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_61_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].over_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/aging/customersList customer aging balances
Current installment-level aging in the workspace timezone, read from the PAYMENT SCHEDULE rather than the invoice header. A row counts when the installment is live, still owes money, is not closed in the source, and has a due date; removed and retired records never count. The invoice's status is not consulted, so debt the ERP has locked is still counted; `collectible` on each row says whether Finero can act on it. Balances are the maintained ERP/Finero reconciled figures, aged by due date: today or future, 1–30, 31–60, 61–90, or over 90 days. Absent by design: an installment with NO due date, which cannot be aged, and a CREDIT balance, which Finero never allocates against a debt. No historical reconstruction or FX. Totals cover all matching rows, per currency. Summary aggregates into `totals` and carries NEITHER `data` nor `pagination`: it is not a list. Customer and installment lists carry both. Use customer_key and bucket for drilldown. Cursors require unchanged query and report revision; restart on invalid_cursor. Invoice-summary due filters select whole invoices, not installment aging.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| currency (query) | string | no | Currency code, or UNKNOWN for missing currency. Omit for separate totals per currency; never sum currencies. |
| search (query) | string | no | Literal, case-insensitive customer name or number search across the full report. |
| customer_key (query) | string | no | Stable key returned by customer aging: customer UUID, or unassigned:invoice UUID. Never group customers by name. |
| bucket (query) | "current" | "days_1_30" | "days_31_60" | "days_61_90" | "over_90" | no | Installment bucket: current includes today; 1–30, 31–60, 61–90, over 90 are disjoint. The report holds positive balances only: an installment with no due date, or a credit balance, is not in it at all. |
| overdue_only (query) | boolean | no | Only positive balances with invoice or installment due dates before the workspace's current business day. |
| sort (query) | "overdue_desc" | "outstanding_desc" | "customer_asc" | no | Customer ordering, default overdue_desc. Money ranks WITHIN a currency, never across two: rows group by currency, largest first inside each — filter by currency to rank one book. Breakdown always orders by due date, invoice and installment; unknown dates last. |
| limit (query) | integer | no | Rows per page, default 50, maximum 100. Totals cover all matching rows. |
| cursor (query) | string | no | Return next_cursor unchanged, with identical filters and limit. Do not parse or build one — the format may change. Restart without it on invalid_cursor. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/aging/customers" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"report_date": "2026-09-10",
"timezone": "UTC",
"generated_at": "2026-09-10T12:00:00Z",
"balance_basis": "finero_reconciled_current",
"revision": "example",
"currencies": [
"USD"
],
"data_updated_at": "2026-09-10T11:00:00Z",
"totals": [
{
"currency": "USD",
"outstanding": "300.00",
"overdue": "200.00",
"current": "100.00",
"days_1_30": "0.00",
"days_31_60": "200.00",
"days_61_90": "0.00",
"over_90": "0.00"
}
],
"data": [
{
"currency": "USD",
"outstanding": "300.00",
"overdue": "200.00",
"current": "100.00",
"days_1_30": "0.00",
"days_31_60": "200.00",
"days_61_90": "0.00",
"over_90": "0.00",
"customer_key": "11111111-1111-4111-8111-111111111111",
"customer_id": "11111111-1111-4111-8111-111111111111",
"customer_name": "Example customer",
"customer_number": "C-001",
"invoice_count": 1,
"installment_count": 2
}
],
"pagination": {
"total_count": 1,
"limit": 50,
"next_cursor": null
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| report_date | date | yes | Current business day in workspace timezone; not a historical balance date. |
| timezone | string | yes | |
| generated_at | date-time | yes | |
| balance_basis | "finero_reconciled_current" | yes | Current maintained invoice or installment balance after existing ERP/Finero reconciliation. Includes debt Finero cannot currently collect, whatever blocks it: `collectible` on each row says whether it can be acted on. Always positive: a credit balance is not reported here, and Finero never nets one against a debt. No FX or historical reconstruction. |
| revision | string | yes | Content hash of the rows behind this answer. It changes whenever they do, in every mode, so it is usable as a change token; a cursor also carries it, which is how a paged walk detects that the report moved underneath it. |
| currencies | array of string | yes | The workspace's currencies, not this response's — a filter never removes one. Empty totals beside a full list means nothing matched, not that nothing is owed. |
| data_updated_at | date-time | null | yes | Latest source record change, not proof that the ERP is fully synchronized. |
| totals | array of object | yes | |
| totals[].currency | string | yes | |
| totals[].outstanding | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].overdue | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].current | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_1_30 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_31_60 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_61_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].over_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data | array of object | yes | |
| data[].currency | string | yes | |
| data[].outstanding | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].overdue | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].current | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].days_1_30 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].days_31_60 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].days_61_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].over_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].customer_key | string | yes | |
| data[].customer_id | string | null | yes | |
| data[].customer_name | string | yes | |
| data[].customer_number | string | null | yes | |
| data[].invoice_count | integer | yes | Invoices behind this row: those with an installment still owing a dated positive balance, after this report's filters. One collected in full is not among them — which is why `summarize_invoices.by_customer.invoice_count` can be HIGHER for the same customer: it counts invoices carrying money, owed or collected. |
| data[].installment_count | integer | yes | How many installments are behind this row — the same population `invoice_count` counts, one row deep. Not the size of an invoice's schedule: `invoice_installment_count` on the breakdown is that, and counts paid siblings too. |
| pagination.total_count | integer | yes | How many rows match the filters, ignoring paging — customers and currencies in customer mode, invoice or installment targets in the breakdown. Not a count of invoices. |
| pagination.limit | integer | yes | |
| pagination.next_cursor | string | null | yes |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/aging/installmentsExplain aging with invoice installments
Current installment-level aging in the workspace timezone, read from the PAYMENT SCHEDULE rather than the invoice header. A row counts when the installment is live, still owes money, is not closed in the source, and has a due date; removed and retired records never count. The invoice's status is not consulted, so debt the ERP has locked is still counted; `collectible` on each row says whether Finero can act on it. Balances are the maintained ERP/Finero reconciled figures, aged by due date: today or future, 1–30, 31–60, 61–90, or over 90 days. Absent by design: an installment with NO due date, which cannot be aged, and a CREDIT balance, which Finero never allocates against a debt. No historical reconstruction or FX. Totals cover all matching rows, per currency. Summary aggregates into `totals` and carries NEITHER `data` nor `pagination`: it is not a list. Customer and installment lists carry both. Use customer_key and bucket for drilldown. Cursors require unchanged query and report revision; restart on invalid_cursor. Invoice-summary due filters select whole invoices, not installment aging.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| currency (query) | string | no | Currency code, or UNKNOWN for missing currency. Omit for separate totals per currency; never sum currencies. |
| search (query) | string | no | Literal, case-insensitive customer name or number search across the full report. |
| customer_key (query) | string | no | Stable key returned by customer aging: customer UUID, or unassigned:invoice UUID. Never group customers by name. |
| bucket (query) | "current" | "days_1_30" | "days_31_60" | "days_61_90" | "over_90" | no | Installment bucket: current includes today; 1–30, 31–60, 61–90, over 90 are disjoint. The report holds positive balances only: an installment with no due date, or a credit balance, is not in it at all. |
| overdue_only (query) | boolean | no | Only positive balances with invoice or installment due dates before the workspace's current business day. |
| sort (query) | "overdue_desc" | "outstanding_desc" | "customer_asc" | no | Customer ordering, default overdue_desc. Money ranks WITHIN a currency, never across two: rows group by currency, largest first inside each — filter by currency to rank one book. Breakdown always orders by due date, invoice and installment; unknown dates last. |
| limit (query) | integer | no | Rows per page, default 50, maximum 100. Totals cover all matching rows. |
| cursor (query) | string | no | Return next_cursor unchanged, with identical filters and limit. Do not parse or build one — the format may change. Restart without it on invalid_cursor. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/aging/installments" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"report_date": "2026-09-10",
"timezone": "UTC",
"generated_at": "2026-09-10T12:00:00Z",
"balance_basis": "finero_reconciled_current",
"revision": "example",
"currencies": [
"USD"
],
"data_updated_at": "2026-09-10T11:00:00Z",
"totals": [
{
"currency": "USD",
"outstanding": "300.00",
"overdue": "200.00",
"current": "100.00",
"days_1_30": "0.00",
"days_31_60": "200.00",
"days_61_90": "0.00",
"over_90": "0.00"
}
],
"data": [
{
"customer_key": "11111111-1111-4111-8111-111111111111",
"customer_id": "11111111-1111-4111-8111-111111111111",
"customer_name": "Example customer",
"customer_number": "C-001",
"invoice_id": "22222222-2222-4222-8222-222222222222",
"invoice_number": "INV-100",
"installment_id": "33333333-3333-4333-8333-333333333333",
"sequence_number": 1,
"invoice_installment_count": 2,
"amount": "200.00",
"currency": "USD",
"due_date": "2026-07-27",
"days_overdue": 45,
"bucket": "days_31_60",
"collectible": true
},
{
"customer_key": "11111111-1111-4111-8111-111111111111",
"customer_id": "11111111-1111-4111-8111-111111111111",
"customer_name": "Example customer",
"customer_number": "C-001",
"invoice_id": "22222222-2222-4222-8222-222222222222",
"invoice_number": "INV-100",
"installment_id": "44444444-4444-4444-8444-444444444444",
"sequence_number": 2,
"invoice_installment_count": 2,
"amount": "100.00",
"currency": "USD",
"due_date": "2026-10-10",
"days_overdue": 0,
"bucket": "current",
"collectible": true
}
],
"pagination": {
"total_count": 2,
"limit": 50,
"next_cursor": null
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| report_date | date | yes | Current business day in workspace timezone; not a historical balance date. |
| timezone | string | yes | |
| generated_at | date-time | yes | |
| balance_basis | "finero_reconciled_current" | yes | Current maintained invoice or installment balance after existing ERP/Finero reconciliation. Includes debt Finero cannot currently collect, whatever blocks it: `collectible` on each row says whether it can be acted on. Always positive: a credit balance is not reported here, and Finero never nets one against a debt. No FX or historical reconstruction. |
| revision | string | yes | Content hash of the rows behind this answer. It changes whenever they do, in every mode, so it is usable as a change token; a cursor also carries it, which is how a paged walk detects that the report moved underneath it. |
| currencies | array of string | yes | The workspace's currencies, not this response's — a filter never removes one. Empty totals beside a full list means nothing matched, not that nothing is owed. |
| data_updated_at | date-time | null | yes | Latest source record change, not proof that the ERP is fully synchronized. |
| totals | array of object | yes | |
| totals[].currency | string | yes | |
| totals[].outstanding | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].overdue | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].current | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_1_30 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_31_60 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].days_61_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| totals[].over_90 | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data | array of object | yes | |
| data[].customer_key | string | yes | |
| data[].customer_id | string | null | yes | |
| data[].customer_name | string | yes | |
| data[].customer_number | string | null | yes | |
| data[].installment_id | uuid | null | yes | |
| data[].invoice_id | uuid | yes | |
| data[].invoice_number | string | yes | |
| data[].sequence_number | integer | null | yes | |
| data[].invoice_installment_count | integer | yes | Zero in invoice-only mode. Otherwise all non-removed installments of this invoice, including paid and filtered-out siblings. Use to distinguish single-installment invoices. |
| data[].due_date | date | null | yes | |
| data[].days_overdue | integer | null | yes | |
| data[].amount | string | yes | Exact decimal amount in major currency units. Outstanding sums the positive balances the report holds; credits are not among them. |
| data[].currency | string | yes | |
| data[].bucket | "current" | "days_1_30" | "days_31_60" | "days_61_90" | "over_90" | yes | |
| data[].collectible | boolean | yes | |
| pagination.total_count | integer | yes | How many rows match the filters, ignoring paging — customers and currencies in customer mode, invoice or installment targets in the breakdown. Not a count of invoices. |
| pagination.limit | integer | yes | |
| pagination.next_cursor | string | null | yes |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
API context
/v1/meIdentify the calling API key
Returns the tenant and key metadata for the presented credential. Useful as a connectivity/permission check before other calls. No side effects.
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/me" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"tenant_id": "5b0f6a7e-2f5d-4d24-9e6b-3c1a2b4d5e6f",
"tenant_name": "Acme Industries",
"api_key": {
"id": "4d3c2b1a-0f9e-48d7-b6c5-a4b3c2d1e0f9",
"name": "Zapier integration",
"permission": "readonly",
"created_at": "2026-07-01T08:00:00+00:00",
"last_used_at": "2026-07-14T06:59:31+00:00"
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| tenant_id | uuid | yes | The tenant this API key belongs to. All data is scoped to it. |
| tenant_name | string | yes | Tenant display name. |
| api_key.id | uuid | yes | API key id. |
| api_key.name | string | yes | Key name given at creation. |
| api_key.permission | "admin" | "readonly" | yes | admin can call every operation; readonly only side-effect-free ones. |
| api_key.created_at | date-time | yes | Key creation time (ISO 8601 UTC). |
| api_key.last_used_at | date-time | null | yes | Previous use of this key (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Invoices
/v1/invoicesList invoices
Lists the tenant's synced invoices, newest first by creation time. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count. Filters are allowlisted; combine freely. collection_status filters like any other field — it is a stored, indexed value, so it pages and counts exactly as the rest do and combines with ?include_total=true.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| collection_status (query) | "paid" | "open" | "inactive" | no | Return only invoices with this collection status — Finero's own payment record, so paid means FINERO collected it and an invoice settled elsewhere is inactive. Combines with every other filter, and with include_total. |
| currency (query) | string | no | Filter by ISO 4217 currency code. |
| invoice_number (query) | string | no | Exact-match filter on the ERP invoice number. |
| connection_id (query) | uuid | no | Filter by ERP connection. |
| updated_since (query) | date-time | no | Only invoices updated at or after this ISO 8601 timestamp. `updated_at` moves when the invoice's own data changes (an ERP delivery, a retirement, an edit), NOT when only Finero's collection state does (`collection_status`, `collection_outstanding`, `amount_collected`, `erp_push_state`, `has_active_payment_link`): poll `GET /v1/payments?confirmed_since=` for collections. The bound INCLUDES its own timestamp, so polling with the newest `updated_at` you have seen returns that row again every time - expected, and not a change. |
| customer_id (query) | uuid | no | Only invoices for this customer, by the stable `customer.id` on the invoice. This is the ONLY safe way to total one customer's exposure — grouping by name merges two customers who share one, and grouping by email splits a customer across its billing addresses. |
| erp_push_state (query) | "held" | "pending" | "processing" | "posted" | "failed" | no | Only invoices whose payment write-back to the ERP is in this state. `failed` is the one to watch: money Finero collected is not represented in the ERP, so its own balance overstates what is owed — and without this filter a stuck invoice is invisible unless you already know its number. |
| has_active_payment_link (query) | boolean | no | Only invoices with (`true`) or without (`false`) an active Finero payment link. Combine `false` with `collection_status=open` and `due_before` for the overdue-and-unlinked worklist in one call. |
| due_before (query) | date | no | Only invoices whose collection_due_date is on or before this date (YYYY-MM-DD), INCLUSIVE. The overdue worklist is due_before=<today> with collection_status=open. FILTERS ON collection_due_date, so an invoice that has none is excluded — and one has none precisely when nothing on it is collectible, which is also when it can still owe money. Date bands therefore never sum to the whole book: check a total against a call with no date filter, never against the bands. `amount_collected` under ANY date filter is PARTIAL and never a collected total: a fully paid invoice has no `collection_due_date` so its collections drop out, while a partly collected OPEN invoice keeps its own and stays in. Ask with no date filter for what was collected. |
| due_after (query) | date | no | Only invoices whose collection_due_date is on or after this date (YYYY-MM-DD), INCLUSIVE. Combine with due_before for one aging bucket — but because BOTH ends include their own date, adjacent bands must not share a boundary: use due_before=X then due_after=<X plus one day>, or every invoice dated exactly X is counted twice and the profile sums to more than the book. Every `collection_due_date` exclusion and the partial-`amount_collected` caveat described on `due_before` applies here identically. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/invoices" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"collection_mode": "installment",
"id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"invoice_number": "INV-10421",
"currency": "USD",
"total_amount": "1250.00",
"invoice_date": "2026-07-01",
"due_date": "2026-08-01",
"payment_terms": "NET30",
"last_synced_at": "2026-07-10T06:30:00+00:00",
"collection_status": "open",
"collection_outstanding": "1250.00",
"amount_collected": "0.00",
"erp_push_state": null,
"has_active_payment_link": false,
"collection_due_date": "2026-08-20",
"customer": {
"id": "b7c8d9e0-1f2a-4b3c-8d4e-5f60718293a4",
"number": "CUST-2201",
"name": "Acme Industries Ltd",
"contact_name": "Marco Johnson",
"email": "ap@acme-industries.example"
},
"origin": "erp",
"connection_id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-10T06:30:00+00:00"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Invoices, newest first by creation time. |
| data[].collection_mode | "invoice" | "installment" | yes | Collect from the invoice header or its real installment schedule. |
| data[].block_reasons | array of string | no | INVOICE MODE ONLY (`collection_mode: invoice`), where the invoice itself is what Finero collects and there are no installments to carry this: WHY it is not collectible, as stable codes — empty when nothing blocks it. The same codes, and the same derivation, as an installment's `block_reasons`; in installment mode read them there instead. Meanings: https://app.getfinero.com/guide. |
| data[].id | uuid | yes | Invoice id (stable Finero identifier). |
| data[].invoice_number | string | null | yes | Human-readable local or ERP invoice number. |
| data[].currency | string | null | yes | Invoice currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].total_amount | string | null | yes | Invoice total. Null while the ERP has none yet (a draft). Exact decimal string at the CURRENCY'S own scale — USD "1250.00", JPY "1250", BHD "1250.000". Never parse as float for arithmetic. |
| data[].invoice_date | date | null | yes | Invoice date (YYYY-MM-DD). |
| data[].due_date | date | null | yes | Invoice due date (YYYY-MM-DD). |
| data[].payment_terms | string | null | yes | The invoice's payment terms exactly as your ERP names them (for example `NET30`). Informational: due dates come from `due_date` and `collection_due_date`, never from the terms. Null when the ERP sent none, when the connection does not collect them, and for local invoices. |
| data[].collection_due_date | date | null | yes | Due date for collectible invoice worklists: the invoice header date in invoice mode, or the earliest collectible installment date in installment mode. Null if nothing is collectible or no due date is available. Use Aging operations for the full dated balance report. Filter with due_before / due_after. |
| data[].last_synced_at | date-time | null | yes | When Finero last read this invoice FROM the ERP. Use it to ask whether a row is still current with your ERP — `updated_at` cannot answer that, because Finero's own writes to the invoice move it too (marking an invoice no longer present; recording a payment on an invoice created in Finero), so a row can look freshly updated while the ERP has not been heard from in weeks. Finero's collection state alone does not move `updated_at`. Null for invoices created in Finero, which have no ERP to read from, and for a few rows that predate the field. (ISO 8601 UTC). |
| data[].collection_outstanding | string | null | yes | What Finero still has to collect on this invoice, as an exact decimal string. Null while the ERP has no amount yet (a draft): unknown, never zero. THIS IS THE FIELD THAT ACCOUNTS FOR PAYMENTS FINERO HAS ALREADY TAKEN, including before your ERP has been told about them; payments and changes made in your ERP arrive with the next sync. Zero for an invoice no longer present in your ERP. Sum this for what is owed: the summary's totals are this sum. |
| data[].amount_collected | string | yes | How much Finero has confirmed collected on this invoice, as an exact decimal string. Money taken through a Finero payment link only - a payment made directly to you, or settled in your ERP, is not counted here and leaves this at zero. |
| data[].erp_push_state | "held" | "pending" | "processing" | "posted" | "failed" | null | yes | How far the money Finero collected has got into your ERP, or null when Finero has collected nothing on this invoice. `held` means the connection pushes receipts manually and an admin has not released this one yet; `pending` means the push is queued and `processing` that it is in flight - under all three, the ERP has not been told yet. `posted` means the ERP accepted the receipt - Finero's part ends there, and what the ERP does with it afterwards (allocating it, or reversing it) is the ERP's business. `failed` means the receipt never reached the ERP, so your ERP still shows that money as owed until someone acts. Finero retries a `failed` push every 15 minutes for 24 hours when the ERP was simply unreachable; any other cause waits for a person, because retrying cannot fix it. The WORST state across the invoice's pushes is reported, because this field exists to surface a problem and a majority would hide one. NOT TO BE CONFUSED WITH a payment's `erp_push_status`: that one is a SINGLE payment's own push, while this rolls up every push on the invoice. |
| data[].collection_status | "paid" | "open" | "inactive" | yes | ANSWER "HAS THIS INVOICE BEEN PAID?" FROM THIS FIELD. `paid` MEANS THE INVOICE IS PAID - nothing is left to collect on it - and it is the most current answer available: Finero knows the moment it collects, and your ERP finds out afterwards. `paid` REQUIRES TWO THINGS: Finero collected on it, AND no collection target has a balance outstanding. An invoice where Finero collected part and the remainder is excluded or otherwise blocked reads `inactive`, not `paid` — so `paid` can never hide money that is still owed. `open` means the invoice or a real installment is COLLECTIBLE by Finero — that, and nothing more. Whether a payment link or reminder actually goes out is a SEPARATE question, decided by which workflows this workspace has switched on, and no status on this invoice answers it. `inactive` is everything else. THE CONVERSE DOES NOT HOLD, and this is the one trap: NOT-`paid` does not mean unpaid. An invoice the customer settled directly with you, outside Finero, reads `inactive` - Finero did not take that money, so it cannot report having collected it. `paid` is conclusive; anything else means find out why. `paid` ALSO DOES NOT SAY HOW MUCH FINERO COLLECTED: an invoice settled by mixed paths - one installment direct to you, the rest through Finero - is still `paid`. Compare `amount_collected` against `total_amount` to attribute it. YOUR ERP LAGS THIS FIELD: it may still show a balance on a `paid` invoice until it applies Finero's receipt - NOT a reason to doubt the status. Read `erp_push_state` to tell ordinary lag from a push that will stay unreflected until someone acts. An invoice finalised with a balance still reads `inactive` when nothing on it can be collected - every installment excluded from collections, closed in your ERP, or the invoice has no billing email or a currency no connected payment provider can charge - and THE MONEY IS NOT WRITTEN OFF: `collection_outstanding` still carries it. Sum `collection_outstanding` for what is OWED - but it is populated on invoices nothing can be collected from, so filter to `collection_status=open` for what can actually be collected, and read each installment's `collectible` to see which parts. Invoice status blocks collection only when enabled on the connection; re-enabling requires fresh status. Other safeguards still apply. Inactive may also mean no balance outstanding, the ERP no longer has the invoice, or no payment schedule in installment mode. Invoice-only mode needs no schedule. A removed invoice can resume collection if a later sync finds it. |
| data[].has_active_payment_link | boolean | yes | Whether a Finero payment link is currently active on any installment of this invoice. `false` on an open invoice is the worklist: something is owed and nothing has been sent to collect it. Filter on it rather than joining against the payment-link list. |
| data[].customer.id | uuid | null | yes | STABLE CUSTOMER KEY — group by this, never by name or email. Finero resolves each ERP customer into one row per (workspace, connection, ERP customer id), so this is the same value across every invoice for that customer. Name and email are NOT keys and fail in opposite directions: two different customers can share a name ("Globex Corporation #1" twice), and one customer can bill from several addresses. Null only on invoices created in Finero with no ERP customer behind them. |
| data[].customer.number | string | null | yes | The business account number from the linked customer; id is Finero's generated UUID. For Oracle, this is trimmed BillToCustomerNumber, preserving case and leading zeros and scoped to the workspace and ERP connection. Shared with Aging and receipt posting. Null when the invoice has no resolved account. |
| data[].customer.name | string | null | yes | The bill-to customer's name (for example a company). For Oracle, this is BillToCustomerName: the customer's party name, not the account's own name. Distinct from contact_name, which is a person. |
| data[].customer.contact_name | string | null | yes | The person named on the invoice as the bill-to contact. Display only: it is never used to identify anyone and nothing in Finero branches on it, because an ERP can carry two people with the same name on one account. Null when the ERP sends none, or when the workspace has switched the Contact name pull field off for this connector. For a sole trader it may equal name. |
| data[].customer.email | string | null | yes | Billing email the payment-link email is sent to. |
| data[].origin | "local" | "erp" | yes | Provenance / system of record: local = the invoice exists only in Finero; erp = an ERP connection owns (or will own) the record. |
| data[].connection_id | uuid | null | yes | The exact ERP connection associated with this invoice. |
| data[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| data[].updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/invoices/{invoiceId}Get an invoice with its installments
Returns one invoice including its installments (ordered by sequence). Use an installment's id to create a payment link. Returns not_found for ids outside your tenant.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| invoiceId (path) | uuid | yes | Invoice id. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/invoices/<invoiceId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"collection_mode": "installment",
"id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"invoice_number": "INV-10421",
"currency": "USD",
"total_amount": "1250.00",
"invoice_date": "2026-07-01",
"due_date": "2026-08-01",
"payment_terms": "NET30",
"last_synced_at": "2026-07-10T06:30:00+00:00",
"collection_status": "open",
"collection_outstanding": "1250.00",
"amount_collected": "0.00",
"erp_push_state": null,
"has_active_payment_link": false,
"collection_due_date": "2026-08-20",
"customer": {
"id": "b7c8d9e0-1f2a-4b3c-8d4e-5f60718293a4",
"number": "CUST-2201",
"name": "Acme Industries Ltd",
"contact_name": "Marco Johnson",
"email": "ap@acme-industries.example"
},
"origin": "erp",
"connection_id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-10T06:30:00+00:00",
"installments": [
{
"id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"sequence": 1,
"original_amount": "1250.00",
"due_date": "2026-08-01",
"collection_status": "open",
"collection_outstanding": "1250.00",
"amount_collected": "0.00",
"collectible": true,
"block_reasons": [],
"excluded_from_collections": false,
"created_at": "2026-07-01T09:15:00+00:00",
"updated_at": "2026-07-10T06:30:00+00:00"
}
]
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| collection_mode | "invoice" | "installment" | yes | Collect from the invoice header or its real installment schedule. |
| block_reasons | array of string | no | INVOICE MODE ONLY (`collection_mode: invoice`), where the invoice itself is what Finero collects and there are no installments to carry this: WHY it is not collectible, as stable codes — empty when nothing blocks it. The same codes, and the same derivation, as an installment's `block_reasons`; in installment mode read them there instead. Meanings: https://app.getfinero.com/guide. |
| id | uuid | yes | Invoice id (stable Finero identifier). |
| invoice_number | string | null | yes | Human-readable local or ERP invoice number. |
| currency | string | null | yes | Invoice currency. ISO 4217 alphabetic code, e.g. "USD". |
| total_amount | string | null | yes | Invoice total. Null while the ERP has none yet (a draft). Exact decimal string at the CURRENCY'S own scale — USD "1250.00", JPY "1250", BHD "1250.000". Never parse as float for arithmetic. |
| invoice_date | date | null | yes | Invoice date (YYYY-MM-DD). |
| due_date | date | null | yes | Invoice due date (YYYY-MM-DD). |
| payment_terms | string | null | yes | The invoice's payment terms exactly as your ERP names them (for example `NET30`). Informational: due dates come from `due_date` and `collection_due_date`, never from the terms. Null when the ERP sent none, when the connection does not collect them, and for local invoices. |
| collection_due_date | date | null | yes | Due date for collectible invoice worklists: the invoice header date in invoice mode, or the earliest collectible installment date in installment mode. Null if nothing is collectible or no due date is available. Use Aging operations for the full dated balance report. Filter with due_before / due_after. |
| last_synced_at | date-time | null | yes | When Finero last read this invoice FROM the ERP. Use it to ask whether a row is still current with your ERP — `updated_at` cannot answer that, because Finero's own writes to the invoice move it too (marking an invoice no longer present; recording a payment on an invoice created in Finero), so a row can look freshly updated while the ERP has not been heard from in weeks. Finero's collection state alone does not move `updated_at`. Null for invoices created in Finero, which have no ERP to read from, and for a few rows that predate the field. (ISO 8601 UTC). |
| collection_outstanding | string | null | yes | What Finero still has to collect on this invoice, as an exact decimal string. Null while the ERP has no amount yet (a draft): unknown, never zero. THIS IS THE FIELD THAT ACCOUNTS FOR PAYMENTS FINERO HAS ALREADY TAKEN, including before your ERP has been told about them; payments and changes made in your ERP arrive with the next sync. Zero for an invoice no longer present in your ERP. Sum this for what is owed: the summary's totals are this sum. |
| amount_collected | string | yes | How much Finero has confirmed collected on this invoice, as an exact decimal string. Money taken through a Finero payment link only - a payment made directly to you, or settled in your ERP, is not counted here and leaves this at zero. |
| erp_push_state | "held" | "pending" | "processing" | "posted" | "failed" | null | yes | How far the money Finero collected has got into your ERP, or null when Finero has collected nothing on this invoice. `held` means the connection pushes receipts manually and an admin has not released this one yet; `pending` means the push is queued and `processing` that it is in flight - under all three, the ERP has not been told yet. `posted` means the ERP accepted the receipt - Finero's part ends there, and what the ERP does with it afterwards (allocating it, or reversing it) is the ERP's business. `failed` means the receipt never reached the ERP, so your ERP still shows that money as owed until someone acts. Finero retries a `failed` push every 15 minutes for 24 hours when the ERP was simply unreachable; any other cause waits for a person, because retrying cannot fix it. The WORST state across the invoice's pushes is reported, because this field exists to surface a problem and a majority would hide one. NOT TO BE CONFUSED WITH a payment's `erp_push_status`: that one is a SINGLE payment's own push, while this rolls up every push on the invoice. |
| collection_status | "paid" | "open" | "inactive" | yes | ANSWER "HAS THIS INVOICE BEEN PAID?" FROM THIS FIELD. `paid` MEANS THE INVOICE IS PAID - nothing is left to collect on it - and it is the most current answer available: Finero knows the moment it collects, and your ERP finds out afterwards. `paid` REQUIRES TWO THINGS: Finero collected on it, AND no collection target has a balance outstanding. An invoice where Finero collected part and the remainder is excluded or otherwise blocked reads `inactive`, not `paid` — so `paid` can never hide money that is still owed. `open` means the invoice or a real installment is COLLECTIBLE by Finero — that, and nothing more. Whether a payment link or reminder actually goes out is a SEPARATE question, decided by which workflows this workspace has switched on, and no status on this invoice answers it. `inactive` is everything else. THE CONVERSE DOES NOT HOLD, and this is the one trap: NOT-`paid` does not mean unpaid. An invoice the customer settled directly with you, outside Finero, reads `inactive` - Finero did not take that money, so it cannot report having collected it. `paid` is conclusive; anything else means find out why. `paid` ALSO DOES NOT SAY HOW MUCH FINERO COLLECTED: an invoice settled by mixed paths - one installment direct to you, the rest through Finero - is still `paid`. Compare `amount_collected` against `total_amount` to attribute it. YOUR ERP LAGS THIS FIELD: it may still show a balance on a `paid` invoice until it applies Finero's receipt - NOT a reason to doubt the status. Read `erp_push_state` to tell ordinary lag from a push that will stay unreflected until someone acts. An invoice finalised with a balance still reads `inactive` when nothing on it can be collected - every installment excluded from collections, closed in your ERP, or the invoice has no billing email or a currency no connected payment provider can charge - and THE MONEY IS NOT WRITTEN OFF: `collection_outstanding` still carries it. Sum `collection_outstanding` for what is OWED - but it is populated on invoices nothing can be collected from, so filter to `collection_status=open` for what can actually be collected, and read each installment's `collectible` to see which parts. Invoice status blocks collection only when enabled on the connection; re-enabling requires fresh status. Other safeguards still apply. Inactive may also mean no balance outstanding, the ERP no longer has the invoice, or no payment schedule in installment mode. Invoice-only mode needs no schedule. A removed invoice can resume collection if a later sync finds it. |
| has_active_payment_link | boolean | yes | Whether a Finero payment link is currently active on any installment of this invoice. `false` on an open invoice is the worklist: something is owed and nothing has been sent to collect it. Filter on it rather than joining against the payment-link list. |
| customer.id | uuid | null | yes | STABLE CUSTOMER KEY — group by this, never by name or email. Finero resolves each ERP customer into one row per (workspace, connection, ERP customer id), so this is the same value across every invoice for that customer. Name and email are NOT keys and fail in opposite directions: two different customers can share a name ("Globex Corporation #1" twice), and one customer can bill from several addresses. Null only on invoices created in Finero with no ERP customer behind them. |
| customer.number | string | null | yes | The business account number from the linked customer; id is Finero's generated UUID. For Oracle, this is trimmed BillToCustomerNumber, preserving case and leading zeros and scoped to the workspace and ERP connection. Shared with Aging and receipt posting. Null when the invoice has no resolved account. |
| customer.name | string | null | yes | The bill-to customer's name (for example a company). For Oracle, this is BillToCustomerName: the customer's party name, not the account's own name. Distinct from contact_name, which is a person. |
| customer.contact_name | string | null | yes | The person named on the invoice as the bill-to contact. Display only: it is never used to identify anyone and nothing in Finero branches on it, because an ERP can carry two people with the same name on one account. Null when the ERP sends none, or when the workspace has switched the Contact name pull field off for this connector. For a sole trader it may equal name. |
| customer.email | string | null | yes | Billing email the payment-link email is sent to. |
| origin | "local" | "erp" | yes | Provenance / system of record: local = the invoice exists only in Finero; erp = an ERP connection owns (or will own) the record. |
| connection_id | uuid | null | yes | The exact ERP connection associated with this invoice. |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| installments | array of object | yes | The invoice's installments, ordered by sequence. |
| installments[].id | uuid | yes | Installment id — use as installment_id when creating a payment link. |
| installments[].sequence | integer | yes | 1-based installment sequence within the invoice. |
| installments[].original_amount | string | yes | Original installment amount. Exact decimal string at the CURRENCY'S own scale — USD "1250.00", JPY "1250", BHD "1250.000". Never parse as float for arithmetic. |
| installments[].due_date | date | null | yes | Installment due date (YYYY-MM-DD). |
| installments[].collection_status | "paid" | "open" | "inactive" | yes | What Finero is doing about collecting THIS installment. Same three values as the invoice's `collection_status`, and it inherits that verdict: an installment of an `inactive` invoice is `inactive`. `paid` MEANS THIS INSTALLMENT IS PAID - nothing is left on it - and it is the current answer even while your ERP still shows a balance, which lags. The converse does not hold: an installment settled directly in your ERP reads `inactive`, because Finero did not take that money, so NOT-`paid` is not the same as unpaid. `open` requires THREE things together: an outstanding balance, `collectible` being true, and the ERP's own status saying the installment is still open - each connector declares which of its status values mean that. Where there is no provider status to judge (an invoice created in Finero) the first two decide alone. IT REPORTS WHAT FINERO IS DOING, NOT WHAT IS OWED: an installment that is genuinely outstanding but blocked - excluded, no billing email, a currency no connected provider can charge - reads `inactive`, because Finero is not collecting it. The money has not been written off; `collection_outstanding` still carries it, and `block_reasons` says why. |
| installments[].collection_outstanding | string | yes | What is still outstanding on this installment, as an exact decimal string. Accounts for Finero payments the ERP has not been told about yet. It is what is OWED, not what is collectible: it stays non-zero when `collectible` is false. Check `collectible` and `block_reasons` before treating it as money you can ask for. |
| installments[].amount_collected | string | yes | How much Finero has confirmed collected on this installment, as an exact decimal string. Money taken through a Finero payment link only; an installment settled directly in your ERP leaves this at zero. It reports what Finero collected AGAINST THIS INSTALLMENT, which is not necessarily where your ERP applied it — the ERP allocates across the whole invoice. `list_payments` records where Finero collected. |
| installments[].collectible | boolean | yes | Whether Finero considers this installment collectible right now. False once there is nothing left to collect, and also for installments blocked by an exclusion, a non-final invoice, a missing billing email, or an invoice that is no longer present in your ERP. |
| installments[].block_reasons | array of string | yes | WHY this installment is not collectible, as stable codes — empty when it is. Invoice-level blocks propagate here, so this answers the question without a second lookup. One code is derived from the installment's own facts rather than stored: `excluded_from_collections` when that flag is set — the same derivation the Collections board makes, so the two surfaces name the same cause. Plain-English meanings for every code are published at https://app.getfinero.com/guide, and the workflows catalogue returns the same dictionary in full - every code with its label, what it means and what resolves it. |
| installments[].excluded_from_collections | boolean | yes | Manually excluded from collections. |
| installments[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| installments[].updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/invoices/summaryCount invoices by collection status, and total the money
Answers 'how many, and how much' in ONE request. Returns counts per collection_status, outstanding and collected money per currency, and the customers owed the most (`by_customer`, capped by `customer_limit`), over the filters listInvoices accepts (all but `invoice_number`). Use this instead of paging every invoice and adding it up: collection_status is derived from Finero's own payment record and is not something a caller can compute from the fields it receives, and money must never be summed or ranked across currencies, so `by_customer` ranks within each. Exact at any workspace size: the counts come from an indexed column, so nothing is capped and nothing is sampled. Every filter listInvoices accepts except `invoice_number` works here too — pair `due_after` with `due_before` to select whole invoices by their earliest collectible due date. For installment-level aging use the Aging operations; an invoice can span multiple aging buckets.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| collection_status (query) | "paid" | "open" | "inactive" | no | Count and total only invoices with this collection status. NOT derivable from the unfiltered response: `by_collection_status` already gives the counts, but `by_currency` aggregates money across every status — so 'how much is still open, per currency' needs this filter. An `inactive` invoice can carry a real balance it will never collect, and summing across statuses buries that in the same number. |
| currency (query) | string | no | Filter by ISO 4217 currency code. |
| connection_id (query) | uuid | no | Filter by ERP connection. |
| updated_since (query) | date-time | no | Only invoices updated at or after this ISO 8601 timestamp. `updated_at` moves when the invoice's own data changes (an ERP delivery, a retirement, an edit), NOT when only Finero's collection state does (`collection_status`, `collection_outstanding`, `amount_collected`, `erp_push_state`, `has_active_payment_link`): poll `GET /v1/payments?confirmed_since=` for collections. The bound INCLUDES its own timestamp, so polling with the newest `updated_at` you have seen returns that row again every time - expected, and not a change. |
| customer_id (query) | uuid | no | Only count invoices for this customer, by the stable `customer.id`. One call gives that customer's whole exposure, per currency — the safe alternative to paging the book and grouping by name. |
| erp_push_state (query) | "held" | "pending" | "processing" | "posted" | "failed" | no | Only count invoices whose payment write-back to the ERP is in this state. `erp_push_state=failed` answers 'how much money has Finero collected that the ERP does not know about?' in one call. |
| has_active_payment_link (query) | boolean | no | Only count invoices with (`true`) or without (`false`) an active Finero payment link — the same filter the list accepts, so the money behind an unlinked worklist is one call. |
| due_before (query) | date | no | Only invoices whose collection_due_date is on or before this date (YYYY-MM-DD), INCLUSIVE. The overdue worklist is due_before=<today> with collection_status=open. FILTERS ON collection_due_date, so an invoice that has none is excluded — and one has none precisely when nothing on it is collectible, which is also when it can still owe money. Date bands therefore never sum to the whole book: check a total against a call with no date filter, never against the bands. `amount_collected` under ANY date filter is PARTIAL and never a collected total: a fully paid invoice has no `collection_due_date` so its collections drop out, while a partly collected OPEN invoice keeps its own and stays in. Ask with no date filter for what was collected. |
| due_after (query) | date | no | Only invoices whose collection_due_date is on or after this date (YYYY-MM-DD), INCLUSIVE. Combine with due_before for one aging bucket — but because BOTH ends include their own date, adjacent bands must not share a boundary: use due_before=X then due_after=<X plus one day>, or every invoice dated exactly X is counted twice and the profile sums to more than the book. Every `collection_due_date` exclusion and the partial-`amount_collected` caveat described on `due_before` applies here identically. |
| customer_limit (query) | integer | no | How many `by_customer` rows to return (default 25, max 100), drawn evenly across currencies so a short list never omits one. Read `by_customer_total` to know whether the cap hid rows. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/invoices/summary" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"total": 386,
"by_collection_status": {
"paid": 41,
"open": 302,
"inactive": 43
},
"inactive_nothing_to_collect": 39,
"by_currency": [
{
"currency": "USD",
"collection_outstanding": "184320.50",
"amount_collected": "20450.00",
"amount_collected_test_mode": "0.00"
}
],
"by_customer": [
{
"customer_id": "b7c8d9e0-1f2a-4b3c-8d4e-5f60718293a4",
"customer_name": "Acme Industries Ltd",
"currency": "USD",
"collection_outstanding": "37476.76",
"amount_collected": "1250.00",
"invoice_count": 6
}
],
"by_customer_total": 24
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| total | integer | yes | Invoices matching the filters. An exact count, never an estimate — and the number to trust for 'how many are there'. |
| by_collection_status.paid | integer | yes | Invoices with nothing left to collect that Finero collected on. See `collection_status` on the invoice for the full rule. |
| by_collection_status.open | integer | yes | Invoices with an invoice-level or installment balance Finero can collect. See `collection_status` on the invoice for the full rule. |
| by_collection_status.inactive | integer | yes | Invoices Finero is not collecting and did not collect — settled entirely outside Finero, cancelled, or with nothing collectible left. NOT the same as paid, and NOT the same as unpaid. |
| inactive_nothing_to_collect | integer | yes | The subset of `inactive` with nothing left to collect — settled outside Finero, cancelled, a draft, empty, a credit, or no longer present in your ERP. `inactive` minus this is the count with money STUCK on it: owed, and not collectible right now. This is Finero's own answer (its `collection_outstanding` at or below zero), not the ERP's balance, so it stays correct while the ERP lags. |
| by_currency | array of object | yes | Money per ISO 4217 currency, over every invoice matching the filters. NEVER summed across currencies — a single total over a mixed-currency workspace is a wrong number, not a shorter one. An invoice no longer present in your ERP owes nothing here (money already collected on it still counts), as in Aging. |
| by_currency[].currency | string | null | yes | Uppercased. NULL when the connector supplied no currency: that money is still owed and still counted, but nothing can be collected against it until the ERP supplies a code. Never merge that bucket into another. ISO 4217 alphabetic code, e.g. "USD". |
| by_currency[].collection_outstanding | string | yes | What is still outstanding after Finero's own collections, in this currency. Exact decimal string at that currency's own scale — USD reads 2989.00, JPY 1560817, BHD 2.010. CAUTION: this is what is OWED, not what is collectible — it includes invoices Finero is blocked from collecting (see an installment's block_reasons). Filtering to collection_status=open is the right total of what Finero can act on. It can still include an OPEN invoice's blocked installment — when one specific invoice matters, its detail says per installment what is collectible; that is a per-invoice check, never a workspace walk. |
| by_currency[].amount_collected | string | yes | What Finero has collected, in this currency. Exact decimal string at that currency's scale. IT INCLUDES TEST-MODE MONEY, exactly as the invoice's own field does, so the two cannot disagree — but test money is NOT revenue. Real cash is this figure MINUS `amount_collected_test_mode` beside it; when they are equal, nothing real has been collected and reporting this as income would be wrong. |
| by_currency[].amount_collected_test_mode | string | yes | How much of `amount_collected` came from TEST-mode payments, in this currency. Exact decimal string at that currency's scale. Subtract for real cash. The only aggregate that separates the two — `list_payments` has `mode` per payment, but paging it is what this call avoids. |
| by_customer | array of object | yes | WHO OWES THE MOST: the customers with money outstanding, over the same filters as everything else here. One row per (customer, currency) — a customer billed in two currencies is two rows, because money is never summed across currencies and, for the same reason, never ranked across them: `collection_outstanding` ranks WITHIN a currency, grouped by currency and largest first inside each. Narrow with `currency` to rank one book. Capped by `customer_limit` (default 25, max 100), drawn evenly across currencies so a short list never omits one; `by_customer_total` says how many rows exist, so a capped answer never reads as complete. This is the aggregate that replaces paging every invoice and grouping by `customer.id` client-side — there is deliberately no customer endpoint. |
| by_customer[].customer_id | uuid | null | yes | The stable `customer.id` carried on each invoice. NULL for invoices the connector attached to no customer: that money is still owed and is one honest row, not dropped. |
| by_customer[].customer_name | string | null | yes | The customer's display name as the ERP supplied it; null when it supplied none. |
| by_customer[].currency | string | null | yes | Uppercased; NULL when the connector supplied no currency. ISO 4217 alphabetic code, e.g. "USD". |
| by_customer[].collection_outstanding | string | yes | What this customer still owes in this currency after Finero's collections, as an exact decimal string at that currency's scale — the same figure and format as `by_currency`, so the rows for one currency sum to that bucket. |
| by_customer[].amount_collected | string | yes | What Finero has collected from this customer in this currency, as an exact decimal string at that currency's scale. TEST-MODE MONEY IS INCLUDED, as on the invoice, and is not split out per customer: before reporting it as revenue, check the currency's `amount_collected_test_mode` in `by_currency` (or `mode` per payment in list_payments). |
| by_customer[].invoice_count | integer | yes | How many of this customer's invoices in this currency carry money (outstanding or collected). An invoice collected in full still counts here — the aging report's `invoice_count` counts only what is still owed, so it can be lower for the same customer. |
| by_customer_total | integer | yes | How many (customer, currency) rows matched in total. When it exceeds the length of `by_customer`, the list was capped by `customer_limit` — raise it, or narrow the filters. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Payment links
/v1/payment-linksList payment links
Lists the tenant's payment links, newest first by creation time. Links created in one call share that time, so their order relative to each other here is not significant — the create answers in the order given. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count. The url field is the live hosted checkout page.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| status (query) | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | no | Filter by lifecycle status. |
| invoice_id (query) | uuid | no | Filter by invoice. |
| installment_id (query) | uuid | no | Filter by installment. |
| customer_id (query) | uuid | no | Only links on invoices belonging to this customer (`customer.id`). Resolved through the invoice — a link carries no customer of its own — so 'every link this customer has' is one call, not one per invoice. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/payment-links" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "active",
"provider": "stripe",
"mode": "live",
"amount_minor": 125000,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": "https://app.getfinero.com/pay/EXAMPLEtokenEXAMPLEtokenEXAMPLEtokenEXAMPL",
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": null,
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Payment links, newest first by creation time. |
| data[].id | uuid | yes | Payment link id. |
| data[].invoice_id | uuid | yes | Invoice the link collects. |
| data[].installment_id | uuid | null | yes | Installment the link collects. An installment may carry SEVERAL live links (0280) — their amounts together may not exceed the installment's `collection_outstanding`, which the database enforces under a row lock. What Finero already collected is ALREADY netted off that figure; subtracting it a second time under-states the room. |
| data[].status | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | yes | Lifecycle status. active = payable now; processing = a payment is being confirmed; paid = settled; inactive = deactivated or superseded; needs_replacement = the provider-side link stopped being usable (expired or removed at the provider), so a new link must be created; action_required = frozen pending a human decision — Finero will not act on it automatically. A payment whose provider report does not match the request is frozen in processing, not here. |
| data[].provider | "stripe" | "wix" | yes | Payment provider hosting the checkout. |
| data[].mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| data[].amount_minor | integer | yes | Amount the link collects. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].currency | string | yes | Link currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].payment_due_date | date | null | yes | Business due date owned by this payment link (YYYY-MM-DD). It is snapshotted at creation and does not change when the source installment changes. Null only for legacy links whose source had no date. |
| data[].url | string | null | yes | Hosted customer-facing pay-page URL. Treat as a capability: anyone with the URL can open the checkout. |
| data[].paid_at | date-time | null | yes | When the link was paid (ISO 8601 UTC). |
| data[].paid_amount_minor | integer | null | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].paid_currency | string | null | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].deactivated_at | date-time | null | yes | When the link was deactivated (ISO 8601 UTC). |
| data[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| data[].updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| data[].incident.reason | "amount_mismatch" | "currency_mismatch" | "mode_mismatch" | "attempt_request_mismatch" | "attempt_tenant_mismatch" | "attempt_not_found" | "duplicate_settlement" | yes | Why Finero refused to record the payment. duplicate_settlement = the link was already paid and a second payment arrived; the others = what the provider reported does not match what this link asked for. |
| data[].incident.provider | "stripe" | "wix" | yes | Provider that reported the payment. |
| data[].incident.reported_amount_minor | integer | yes | Amount the provider reported. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].incident.reported_currency | string | yes | Currency the provider reported. ISO 4217 alphabetic code, e.g. "USD". |
| data[].incident.recorded_at | date-time | yes | When Finero recorded the incident (ISO 8601 UTC). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payment-links/{paymentLinkId}Get a payment link
Returns one payment link, including its hosted checkout URL and settlement state.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| paymentLinkId (path) | uuid | yes | Payment link id. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/payment-links/<paymentLinkId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "active",
"provider": "stripe",
"mode": "live",
"amount_minor": 125000,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": "https://app.getfinero.com/pay/EXAMPLEtokenEXAMPLEtokenEXAMPLEtokenEXAMPL",
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": null,
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Payment link id. |
| invoice_id | uuid | yes | Invoice the link collects. |
| installment_id | uuid | null | yes | Installment the link collects. An installment may carry SEVERAL live links (0280) — their amounts together may not exceed the installment's `collection_outstanding`, which the database enforces under a row lock. What Finero already collected is ALREADY netted off that figure; subtracting it a second time under-states the room. |
| status | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | yes | Lifecycle status. active = payable now; processing = a payment is being confirmed; paid = settled; inactive = deactivated or superseded; needs_replacement = the provider-side link stopped being usable (expired or removed at the provider), so a new link must be created; action_required = frozen pending a human decision — Finero will not act on it automatically. A payment whose provider report does not match the request is frozen in processing, not here. |
| provider | "stripe" | "wix" | yes | Payment provider hosting the checkout. |
| mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| amount_minor | integer | yes | Amount the link collects. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| currency | string | yes | Link currency. ISO 4217 alphabetic code, e.g. "USD". |
| payment_due_date | date | null | yes | Business due date owned by this payment link (YYYY-MM-DD). It is snapshotted at creation and does not change when the source installment changes. Null only for legacy links whose source had no date. |
| url | string | null | yes | Hosted customer-facing pay-page URL. Treat as a capability: anyone with the URL can open the checkout. |
| paid_at | date-time | null | yes | When the link was paid (ISO 8601 UTC). |
| paid_amount_minor | integer | null | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| paid_currency | string | null | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| deactivated_at | date-time | null | yes | When the link was deactivated (ISO 8601 UTC). |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| incident.reason | "amount_mismatch" | "currency_mismatch" | "mode_mismatch" | "attempt_request_mismatch" | "attempt_tenant_mismatch" | "attempt_not_found" | "duplicate_settlement" | yes | Why Finero refused to record the payment. duplicate_settlement = the link was already paid and a second payment arrived; the others = what the provider reported does not match what this link asked for. |
| incident.provider | "stripe" | "wix" | yes | Provider that reported the payment. |
| incident.reported_amount_minor | integer | yes | Amount the provider reported. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| incident.reported_currency | string | yes | Currency the provider reported. ISO 4217 alphabetic code, e.g. "USD". |
| incident.recorded_at | date-time | yes | When Finero recorded the incident (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payment-linksCreate a payment link
Creates a hosted payment link for one collectible invoice or installment through the tenant's connected payment provider — the same validation and creation path the Finero app uses. An invoice target or installment may carry SEVERAL links at once, each collecting part of it: an installment of 100.00 can be offered as four links of 25.00. Their amounts may not exceed what is still outstanding. When live links already cover the installment the request is refused as a conflict, exactly as a duplicate link always was; when there is room but less than was asked for, it is refused as an invalid amount and names what was available. If the tenant's email-notification workflow is enabled, the customer payment-link email is sent as a consequence.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_id | uuid | yes | Invoice to collect. |
| installment_id | uuid | null | yes | Real installment id in installment mode; null in invoice-only mode. Read the invoice collection_mode first. |
| integration_id | uuid | no | Optional: a specific connected payments integration. Defaults to the tenant's connected provider. |
| amount_minor | integer | no | Optional: how much this link collects, in integer minor units (100 = USD 1.00; 100 = JPY 100). Fractional minor units are rejected, never rounded. Omit to collect everything still available on the current collection target, which is the usual case. Supply it to split an installment across several links — the amounts of all live links may not exceed the installment's `collection_outstanding`. That figure already has what Finero collected netted off it, so do not subtract `amount_collected` as well. For a three-decimal currency (BHD, JOD, KWD, OMR, TND) the amount must be a multiple of 10: the payment provider drops the last digit, so 1005 fils would be refused as `validation_failed` rather than minted as a link the checkout cannot honour. |
| payment_due_date | date | no | Optional link-owned due date (YYYY-MM-DD). Omit it to snapshot the invoice or installment target's current due date when the link is committed. Past dates are accepted for already-overdue receivables. |
| email_delivery_mode | "workflow" | "send_now" | "schedule" | "suppress" | no | Optional link-specific email decision. Omit or use workflow to preserve tenant workflow behaviour. Explicit modes override it for this link. |
| email_scheduled_local | string | no | Tenant-local YYYY-MM-DDTHH:mm wall clock. Required only when email_delivery_mode is schedule; the backend resolves it through the tenant timezone. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/payment-links" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Idempotency-Key: <unique-key>" \
-H "Content-Type: application/json" \
-d '{"invoice_id":"8f14e45f-ceea-4a5b-9d2c-167ce7de1a10","installment_id":"a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70"}'Request body example
{
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70"
}Success response (201)
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "active",
"provider": "stripe",
"mode": "live",
"amount_minor": 125000,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": "https://app.getfinero.com/pay/EXAMPLEtokenEXAMPLEtokenEXAMPLEtokenEXAMPL",
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": null,
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Payment link id. |
| invoice_id | uuid | yes | Invoice the link collects. |
| installment_id | uuid | null | yes | Installment the link collects. An installment may carry SEVERAL live links (0280) — their amounts together may not exceed the installment's `collection_outstanding`, which the database enforces under a row lock. What Finero already collected is ALREADY netted off that figure; subtracting it a second time under-states the room. |
| status | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | yes | Lifecycle status. active = payable now; processing = a payment is being confirmed; paid = settled; inactive = deactivated or superseded; needs_replacement = the provider-side link stopped being usable (expired or removed at the provider), so a new link must be created; action_required = frozen pending a human decision — Finero will not act on it automatically. A payment whose provider report does not match the request is frozen in processing, not here. |
| provider | "stripe" | "wix" | yes | Payment provider hosting the checkout. |
| mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| amount_minor | integer | yes | Amount the link collects. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| currency | string | yes | Link currency. ISO 4217 alphabetic code, e.g. "USD". |
| payment_due_date | date | null | yes | Business due date owned by this payment link (YYYY-MM-DD). It is snapshotted at creation and does not change when the source installment changes. Null only for legacy links whose source had no date. |
| url | string | null | yes | Hosted customer-facing pay-page URL. Treat as a capability: anyone with the URL can open the checkout. |
| paid_at | date-time | null | yes | When the link was paid (ISO 8601 UTC). |
| paid_amount_minor | integer | null | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| paid_currency | string | null | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| deactivated_at | date-time | null | yes | When the link was deactivated (ISO 8601 UTC). |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| incident.reason | "amount_mismatch" | "currency_mismatch" | "mode_mismatch" | "attempt_request_mismatch" | "attempt_tenant_mismatch" | "attempt_not_found" | "duplicate_settlement" | yes | Why Finero refused to record the payment. duplicate_settlement = the link was already paid and a second payment arrived; the others = what the provider reported does not match what this link asked for. |
| incident.provider | "stripe" | "wix" | yes | Provider that reported the payment. |
| incident.reported_amount_minor | integer | yes | Amount the provider reported. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| incident.reported_currency | string | yes | Currency the provider reported. ISO 4217 alphabetic code, e.g. "USD". |
| incident.recorded_at | date-time | yes | When Finero recorded the incident (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | idempotency_key_required | This operation requires the Idempotency-Key header (1–255 characters). → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 409 | idempotency_conflict | The Idempotency-Key was already used with a DIFFERENT request body. Use a fresh key for a new request. → fix the request |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 422 | amount_exceeds_available | The installment is collectible, but the amount asked for is more than is still available on it. The available amount is the installment's collection_outstanding minus the amounts of its live payment links; what Finero already collected is netted off collection_outstanding and must not be subtracted twice. Ask for that amount or less, or omit amount_minor to take exactly what is left. → fix the request |
| 422 | currency_not_supported | The selected payment provider cannot collect the invoice's currency (single-currency providers can only charge in their store currency). Use a provider that supports it, or collect the invoice outside Finero. → fix the request |
| 422 | installment_not_collectible | The invoice or installment target cannot be collected right now — it is already paid, excluded from collections, has no outstanding balance, its invoice is not collection-ready, or its invoice is no longer present in your ERP. GET /v1/invoices/{invoiceId} reports the invoice's collection_status and each installment's collectible flag. → fix the request |
| 422 | integration_unavailable | The tenant has no connected payments integration able to mint this link, or the one selected is not ready. Connect or repair a payment provider in the Finero app, then retry. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payment-links/batchCreate several payment links at once
Splits one collectible invoice or installment into 1 to 12 payment links in ONE atomic call: every link is created or none is, so a split never stops half-way. Each payment names its amount and may name its due date; each row passes createPaymentLink's checks, counting the rows before it, and together they may not exceed what is outstanding. A refused row is named in the error details as `payments[<index>].amount_minor` (0-based); nothing was created, so fix it and send the whole request again. The email decision applies to each link and is carried out after the links exist; a failed email never undoes a link.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_id | uuid | yes | Invoice to collect. |
| installment_id | uuid | null | yes | Real installment id in installment mode; null in invoice-only mode. Read the invoice collection_mode first. |
| integration_id | uuid | no | Optional: a specific connected payments integration. Defaults to the tenant's connected provider. |
| payments | array of object | yes | The links to create, in order. The response lists them in the same order. |
| payments[].amount_minor | integer | yes | How much this link collects, in integer minor units (100 = USD 1.00). Required: a batch is a decided split. The amounts of these links plus every live link may not exceed the target's `collection_outstanding`. For a three-decimal currency (BHD, JOD, KWD, OMR, TND) each amount must be a multiple of 10. |
| payments[].payment_due_date | date | no | Optional link-owned due date (YYYY-MM-DD). Omit it to snapshot the target's current due date. Past dates are accepted for already-overdue receivables. |
| email_delivery_mode | "workflow" | "send_now" | "schedule" | "suppress" | no | Optional email decision applied to EACH link. Omit or use workflow to preserve tenant workflow behaviour. |
| email_scheduled_local | string | no | Tenant-local YYYY-MM-DDTHH:mm wall clock, the same for every link. Required only when email_delivery_mode is schedule. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/payment-links/batch" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Idempotency-Key: <unique-key>" \
-H "Content-Type: application/json" \
-d '{"invoice_id":"8f14e45f-ceea-4a5b-9d2c-167ce7de1a10","installment_id":"a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70","payments":[{"amount_minor":50000},{"amount_minor":33500,"payment_due_date":"2026-08-15"}]}'Request body example
{
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"payments": [
{
"amount_minor": 50000
},
{
"amount_minor": 33500,
"payment_due_date": "2026-08-15"
}
]
}Success response (201)
{
"data": [
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "active",
"provider": "stripe",
"mode": "live",
"amount_minor": 50000,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": "https://app.getfinero.com/pay/EXAMPLEtokenEXAMPLEtokenEXAMPLEtokenEXAMPL",
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": null,
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
},
{
"id": "a3c1e7d2-7b1f-4f0e-9d7a-2c5b8e4f6a19",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "active",
"provider": "stripe",
"mode": "live",
"amount_minor": 33500,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": "https://app.getfinero.com/pay/EXAMPLEtokenEXAMPLEtokenEXAMPLEtokenEXAMPL",
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": null,
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
}
]
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | The created payment links, in the order the payments were given. |
| data[].id | uuid | yes | Payment link id. |
| data[].invoice_id | uuid | yes | Invoice the link collects. |
| data[].installment_id | uuid | null | yes | Installment the link collects. An installment may carry SEVERAL live links (0280) — their amounts together may not exceed the installment's `collection_outstanding`, which the database enforces under a row lock. What Finero already collected is ALREADY netted off that figure; subtracting it a second time under-states the room. |
| data[].status | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | yes | Lifecycle status. active = payable now; processing = a payment is being confirmed; paid = settled; inactive = deactivated or superseded; needs_replacement = the provider-side link stopped being usable (expired or removed at the provider), so a new link must be created; action_required = frozen pending a human decision — Finero will not act on it automatically. A payment whose provider report does not match the request is frozen in processing, not here. |
| data[].provider | "stripe" | "wix" | yes | Payment provider hosting the checkout. |
| data[].mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| data[].amount_minor | integer | yes | Amount the link collects. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].currency | string | yes | Link currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].payment_due_date | date | null | yes | Business due date owned by this payment link (YYYY-MM-DD). It is snapshotted at creation and does not change when the source installment changes. Null only for legacy links whose source had no date. |
| data[].url | string | null | yes | Hosted customer-facing pay-page URL. Treat as a capability: anyone with the URL can open the checkout. |
| data[].paid_at | date-time | null | yes | When the link was paid (ISO 8601 UTC). |
| data[].paid_amount_minor | integer | null | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].paid_currency | string | null | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].deactivated_at | date-time | null | yes | When the link was deactivated (ISO 8601 UTC). |
| data[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| data[].updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| data[].incident.reason | "amount_mismatch" | "currency_mismatch" | "mode_mismatch" | "attempt_request_mismatch" | "attempt_tenant_mismatch" | "attempt_not_found" | "duplicate_settlement" | yes | Why Finero refused to record the payment. duplicate_settlement = the link was already paid and a second payment arrived; the others = what the provider reported does not match what this link asked for. |
| data[].incident.provider | "stripe" | "wix" | yes | Provider that reported the payment. |
| data[].incident.reported_amount_minor | integer | yes | Amount the provider reported. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].incident.reported_currency | string | yes | Currency the provider reported. ISO 4217 alphabetic code, e.g. "USD". |
| data[].incident.recorded_at | date-time | yes | When Finero recorded the incident (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | idempotency_key_required | This operation requires the Idempotency-Key header (1–255 characters). → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 409 | idempotency_conflict | The Idempotency-Key was already used with a DIFFERENT request body. Use a fresh key for a new request. → fix the request |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 422 | amount_exceeds_available | The installment is collectible, but the amount asked for is more than is still available on it. The available amount is the installment's collection_outstanding minus the amounts of its live payment links; what Finero already collected is netted off collection_outstanding and must not be subtracted twice. Ask for that amount or less, or omit amount_minor to take exactly what is left. → fix the request |
| 422 | currency_not_supported | The selected payment provider cannot collect the invoice's currency (single-currency providers can only charge in their store currency). Use a provider that supports it, or collect the invoice outside Finero. → fix the request |
| 422 | installment_not_collectible | The invoice or installment target cannot be collected right now — it is already paid, excluded from collections, has no outstanding balance, its invoice is not collection-ready, or its invoice is no longer present in your ERP. GET /v1/invoices/{invoiceId} reports the invoice's collection_status and each installment's collectible flag. → fix the request |
| 422 | integration_unavailable | The tenant has no connected payments integration able to mint this link, or the one selected is not ready. Connect or repair a payment provider in the Finero app, then retry. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payment-links/{paymentLinkId}/deactivateDeactivate a payment link
Immediately blocks the Finero payment link and queues closure of corresponding provider checkouts. Provider closure is reported separately and retried if unavailable. Naturally idempotent: deactivating an already-inactive link succeeds without change. A paid or processing link cannot be deactivated — refused as `conflict` over HTTP and `link_not_withdrawable` through MCP; repeating the call will not change the answer.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| paymentLinkId (path) | uuid | yes | Payment link id. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/payment-links/<paymentLinkId>/deactivate" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"status": "inactive",
"provider": "stripe",
"mode": "live",
"amount_minor": 125000,
"currency": "USD",
"payment_due_date": "2026-08-15",
"url": null,
"paid_at": null,
"paid_amount_minor": null,
"paid_currency": null,
"deactivated_at": "2026-07-14T07:10:00+00:00",
"created_at": "2026-07-10T06:31:00+00:00",
"updated_at": "2026-07-10T06:31:00+00:00",
"incident": null
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Payment link id. |
| invoice_id | uuid | yes | Invoice the link collects. |
| installment_id | uuid | null | yes | Installment the link collects. An installment may carry SEVERAL live links (0280) — their amounts together may not exceed the installment's `collection_outstanding`, which the database enforces under a row lock. What Finero already collected is ALREADY netted off that figure; subtracting it a second time under-states the room. |
| status | "active" | "processing" | "paid" | "inactive" | "needs_replacement" | "action_required" | yes | Lifecycle status. active = payable now; processing = a payment is being confirmed; paid = settled; inactive = deactivated or superseded; needs_replacement = the provider-side link stopped being usable (expired or removed at the provider), so a new link must be created; action_required = frozen pending a human decision — Finero will not act on it automatically. A payment whose provider report does not match the request is frozen in processing, not here. |
| provider | "stripe" | "wix" | yes | Payment provider hosting the checkout. |
| mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| amount_minor | integer | yes | Amount the link collects. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| currency | string | yes | Link currency. ISO 4217 alphabetic code, e.g. "USD". |
| payment_due_date | date | null | yes | Business due date owned by this payment link (YYYY-MM-DD). It is snapshotted at creation and does not change when the source installment changes. Null only for legacy links whose source had no date. |
| url | string | null | yes | Hosted customer-facing pay-page URL. Treat as a capability: anyone with the URL can open the checkout. |
| paid_at | date-time | null | yes | When the link was paid (ISO 8601 UTC). |
| paid_amount_minor | integer | null | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| paid_currency | string | null | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| deactivated_at | date-time | null | yes | When the link was deactivated (ISO 8601 UTC). |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| incident.reason | "amount_mismatch" | "currency_mismatch" | "mode_mismatch" | "attempt_request_mismatch" | "attempt_tenant_mismatch" | "attempt_not_found" | "duplicate_settlement" | yes | Why Finero refused to record the payment. duplicate_settlement = the link was already paid and a second payment arrived; the others = what the provider reported does not match what this link asked for. |
| incident.provider | "stripe" | "wix" | yes | Provider that reported the payment. |
| incident.reported_amount_minor | integer | yes | Amount the provider reported. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| incident.reported_currency | string | yes | Currency the provider reported. ISO 4217 alphabetic code, e.g. "USD". |
| incident.recorded_at | date-time | yes | When Finero recorded the incident (ISO 8601 UTC). |
| provider_closure_status | "not_required" | "pending" | "failed" | "succeeded" | no | Provider checkout closure outcome; failed closures are retried. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Scheduler
/v1/scheduled-emailsList scheduled payment-link emails
Lists active schedules by send time; completed/cancelled items are removed. Emails use workspace reply-to/format at send time. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. THIS list is ordered by `scheduled_for` — when the email is due to send — not by creation time, because it is a send queue. Unlike every other list here, that value is MUTABLE: rescheduling through `PATCH /v1/scheduled-emails/{scheduledEmailId}` moves a row within the order. A row rescheduled across your cursor between pages can therefore be missed or returned twice. If you need an exact set, re-walk from the first page, or filter by `status` and reconcile on `id`. Default desc (latest send time first). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| status (query) | "scheduled" | "processing" | "failed" | no | Filter by scheduler state. |
| payment_link_id (query) | uuid | no | Filter by payment link. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/scheduled-emails" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "22222222-2222-4222-8222-222222222222",
"payment_link_id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"scheduled_for": "2026-08-14T06:00:00+00:00",
"timezone": "Asia/Jerusalem",
"status": "scheduled",
"attempt_count": 0,
"next_attempt_at": null,
"last_error_code": null,
"created_at": "2026-08-10T09:00:00+00:00",
"updated_at": "2026-08-10T09:00:00+00:00"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Active scheduled emails. |
| data[].id | uuid | yes | Scheduled email id. |
| data[].payment_link_id | uuid | yes | Payment link this email will deliver. |
| data[].scheduled_for | date-time | yes | Resolved send instant (ISO 8601 UTC). |
| data[].timezone | string | yes | Tenant timezone snapshot used for scheduling. |
| data[].status | "scheduled" | "processing" | "failed" | yes | Current scheduler state. Successful deliveries leave this active queue. |
| data[].attempt_count | integer | yes | Dispatch attempts so far. |
| data[].next_attempt_at | date-time | null | yes | Next bounded retry instant (ISO 8601 UTC). |
| data[].last_error_code | string | null | yes | Categorical failure code; never a provider response or recipient detail. |
| data[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| data[].updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/scheduled-emails/{scheduledEmailId}Reschedule a payment-link email
Changes the tenant-local send time of an email whose status is `scheduled` or `failed`. A `processing` email cannot be edited while its provider outcome may be in flight.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| scheduledEmailId (path) | uuid | yes | Scheduled email id. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| scheduled_local | string | yes | New tenant-local YYYY-MM-DDTHH:mm send time. |
Example request
curl -X PATCH "https://api.getfinero.com/functions/v1/api/v1/scheduled-emails/<scheduledEmailId>" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scheduled_local":"2026-08-15T09:30"}'Request body example
{
"scheduled_local": "2026-08-15T09:30"
}Success response (200)
{
"id": "22222222-2222-4222-8222-222222222222",
"payment_link_id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"scheduled_for": "2026-08-14T06:00:00+00:00",
"timezone": "Asia/Jerusalem",
"status": "scheduled",
"attempt_count": 0,
"next_attempt_at": null,
"last_error_code": null,
"created_at": "2026-08-10T09:00:00+00:00",
"updated_at": "2026-08-10T09:00:00+00:00"
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Scheduled email id. |
| payment_link_id | uuid | yes | Payment link this email will deliver. |
| scheduled_for | date-time | yes | Resolved send instant (ISO 8601 UTC). |
| timezone | string | yes | Tenant timezone snapshot used for scheduling. |
| status | "scheduled" | "processing" | "failed" | yes | Current scheduler state. Successful deliveries leave this active queue. |
| attempt_count | integer | yes | Dispatch attempts so far. |
| next_attempt_at | date-time | null | yes | Next bounded retry instant (ISO 8601 UTC). |
| last_error_code | string | null | yes | Categorical failure code; never a provider response or recipient detail. |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| updated_at | date-time | yes | Last update time (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/scheduled-emails/{scheduledEmailId}Delete a scheduled payment-link email
Cancels an email whose status is `scheduled` or `failed`: the customer will NOT be emailed about this payment link (its email mode becomes suppress); the link itself stays payable. A `processing` email cannot be cancelled because the provider outcome may already be in flight. A repeat answers `not_found` (already gone).
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| scheduledEmailId (path) | uuid | yes | Scheduled email id. |
Example request
curl -X DELETE "https://api.getfinero.com/functions/v1/api/v1/scheduled-emails/<scheduledEmailId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"id": "22222222-2222-4222-8222-222222222222",
"deleted": true
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Deleted scheduled email id. |
| deleted | boolean | yes | True when the schedule was deleted. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Payments
/v1/paymentsList confirmed payments
Lists confirmed payments, newest first by creation time. A payment appears here only after the payment provider verifies settlement; payments cannot be created through the API. Each payment also reports whether it has reached the ERP as a receipt (`erp_push_status` and the other `erp_*` fields), so money Finero holds but the ERP has not recorded is visible without a second call. There is no `erp_push_status` query filter — read the field on each returned payment. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| invoice_id (query) | uuid | no | Filter by invoice. |
| customer_id (query) | uuid | no | Only payments on invoices belonging to this customer (`customer.id`). Resolved through the invoice — a payment carries no customer of its own — so 'everything this customer paid' is one call, not one per invoice. |
| payment_link_id (query) | uuid | no | Filter by payment link. |
| provider (query) | "stripe" | "wix" | no | Filter by provider. |
| confirmed_since (query) | date-time | no | Only payments confirmed at or after this ISO 8601 timestamp. |
| confirmed_before (query) | date-time | no | Only payments confirmed at or before this ISO 8601 timestamp. With `confirmed_since`, one period is one bounded call — 'collected this month against last' is two calls, not a walk. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/payments" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"payment_link_id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"provider": "stripe",
"provider_payment_id": "pi_ExampleOnly000000000000",
"amount_minor": 125000,
"currency": "USD",
"mode": "live",
"status": "confirmed",
"provider_paid_at": "2026-07-11T14:02:11+00:00",
"confirmed_at": "2026-07-11T14:02:14+00:00",
"erp_push_status": "posted",
"erp_receipt_number": "FIN-8ZQ4N2VP7KMX",
"erp_external_receipt_id": "300100123456789",
"erp_pushed_at": "2026-07-11T14:03:02+00:00",
"erp_push_error_category": null
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Confirmed payments, newest first by creation time. |
| data[].id | uuid | yes | Payment id. |
| data[].invoice_id | uuid | yes | Invoice the payment settles. |
| data[].installment_id | uuid | null | yes | Installment the payment settles, or null for an invoice-level payment. |
| data[].payment_link_id | uuid | yes | The payment link the payer used. |
| data[].provider | "stripe" | "wix" | yes | Payment provider. |
| data[].provider_payment_id | string | yes | The provider's payment/transaction identifier. |
| data[].amount_minor | integer | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| data[].currency | string | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| data[].mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| data[].status | "confirmed" | yes | Payments appear here only once verified/confirmed by the provider. |
| data[].provider_paid_at | date-time | null | yes | Provider-reported payment time (ISO 8601 UTC). |
| data[].confirmed_at | date-time | yes | When Finero confirmed the settlement (ISO 8601 UTC). |
| data[].erp_push_status | "held" | "pending" | "processing" | "posted" | "failed" | null | yes | Whether this payment has reached the ERP as a receipt. `held` = the connection's payment_push_mode is manual and no admin has released this receipt yet — nothing is wrong, and POST /v1/payments/erp-push releases it. `posted` = the ERP accepted it, and Finero's part ends there: what the ERP does with the receipt afterwards (allocating it, or reversing it) is the ERP's business. `failed` means it did not reach the ERP — retried automatically only while the cause is the ERP being unreachable. `null` when there is no push at all: a payment on a Finero-created invoice (there is no ERP to report to), a tenant not syncing payments to an ERP, or a payment that settled before ERP payment sync was switched on — enabling it never posts history backwards. NOT TO BE CONFUSED WITH an invoice's `erp_push_state`: that one rolls up every push on the invoice and reports the worst, while this is THIS payment's own. |
| data[].erp_receipt_number | string | null | yes | The receipt identifier Finero sends to the ERP (`FIN-…`), derived deterministically from the payment id. This is the value that identifies the receipt inside the ERP. |
| data[].erp_external_receipt_id | string | null | yes | The ERP's own identifier for the receipt, once it has created one. |
| data[].erp_pushed_at | date-time | null | yes | When the ERP first accepted the receipt (ISO 8601 UTC). |
| data[].erp_push_error_category | string | null | yes | Coarse reason the push has not gone through, when it has not. Cleared by the database the moment a push succeeds, so it is never a stale error beside a `posted` status. Provider error text is deliberately not exposed. |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payments/{paymentId}Get a payment
Returns one confirmed payment by id, including the provider reference, the settlement timestamps, and whether the payment has reached the ERP as a receipt.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| paymentId (path) | uuid | yes | Payment id. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/payments/<paymentId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"payment_link_id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"provider": "stripe",
"provider_payment_id": "pi_ExampleOnly000000000000",
"amount_minor": 125000,
"currency": "USD",
"mode": "live",
"status": "confirmed",
"provider_paid_at": "2026-07-11T14:02:11+00:00",
"confirmed_at": "2026-07-11T14:02:14+00:00",
"erp_push_status": "posted",
"erp_receipt_number": "FIN-8ZQ4N2VP7KMX",
"erp_external_receipt_id": "300100123456789",
"erp_pushed_at": "2026-07-11T14:03:02+00:00",
"erp_push_error_category": null
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Payment id. |
| invoice_id | uuid | yes | Invoice the payment settles. |
| installment_id | uuid | null | yes | Installment the payment settles, or null for an invoice-level payment. |
| payment_link_id | uuid | yes | The payment link the payer used. |
| provider | "stripe" | "wix" | yes | Payment provider. |
| provider_payment_id | string | yes | The provider's payment/transaction identifier. |
| amount_minor | integer | yes | Settled amount. Integer amount in the currency's MINOR unit (e.g. cents), as defined by the PAYMENT PROVIDER's own table, which is not always ISO 4217 — the two disagree on ISK and MGA among others, so converting with an ISO exponent misprices those. Invoice and installment amounts are exact decimal strings and are the safe basis for arithmetic and display. |
| currency | string | yes | Settled currency. ISO 4217 alphabetic code, e.g. "USD". |
| mode | "test" | "live" | yes | Provider environment. `test` means a TEST processor handled this money: it is not revenue and must never be reported as cash collected, however real the amount looks. `live` is real money. |
| status | "confirmed" | yes | Payments appear here only once verified/confirmed by the provider. |
| provider_paid_at | date-time | null | yes | Provider-reported payment time (ISO 8601 UTC). |
| confirmed_at | date-time | yes | When Finero confirmed the settlement (ISO 8601 UTC). |
| erp_push_status | "held" | "pending" | "processing" | "posted" | "failed" | null | yes | Whether this payment has reached the ERP as a receipt. `held` = the connection's payment_push_mode is manual and no admin has released this receipt yet — nothing is wrong, and POST /v1/payments/erp-push releases it. `posted` = the ERP accepted it, and Finero's part ends there: what the ERP does with the receipt afterwards (allocating it, or reversing it) is the ERP's business. `failed` means it did not reach the ERP — retried automatically only while the cause is the ERP being unreachable. `null` when there is no push at all: a payment on a Finero-created invoice (there is no ERP to report to), a tenant not syncing payments to an ERP, or a payment that settled before ERP payment sync was switched on — enabling it never posts history backwards. NOT TO BE CONFUSED WITH an invoice's `erp_push_state`: that one rolls up every push on the invoice and reports the worst, while this is THIS payment's own. |
| erp_receipt_number | string | null | yes | The receipt identifier Finero sends to the ERP (`FIN-…`), derived deterministically from the payment id. This is the value that identifies the receipt inside the ERP. |
| erp_external_receipt_id | string | null | yes | The ERP's own identifier for the receipt, once it has created one. |
| erp_pushed_at | date-time | null | yes | When the ERP first accepted the receipt (ISO 8601 UTC). |
| erp_push_error_category | string | null | yes | Coarse reason the push has not gone through, when it has not. Cleared by the database the moment a push succeeds, so it is never a stale error beside a `posted` status. Provider error text is deliberately not exposed. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/payments/erp-pushPush held receipts to the ERP
Releases payments whose receipt is HELD (the connection's payment_push_mode is manual) so they are pushed to the ERP. One request releases up to 100 payments; the ERP still receives ONE receipt per payment, one call each, so a failure on one never affects the others and a payment already in the ERP is never posted twice. The answer is a verdict per payment id — ACCEPTANCE, not the ERP's outcome, which arrives afterwards: read each payment's `erp_push_status` (`posted` on success, `failed` with `erp_push_error_category` otherwise). `released` — it was held and is now being pushed. `already_pushed` — the ERP has it. `not_held` — nothing to release: it is queued, in flight, failed (retry is a separate admin action in the app), or has no receipt at all (a Finero-created invoice, payment sync switched off, or a payment before payment sync was enabled). `not_found` — not a payment in this workspace. Safe to repeat: a second request for the same ids answers `already_pushed` or `not_held`.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| payment_ids | array of string | yes | Payment ids (from GET /v1/payments) to release. Duplicates are answered once. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/payments/erp-push" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"payment_ids":["7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928"]}'Request body example
{
"payment_ids": [
"7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928"
]
}Success response (202)
{
"results": [
{
"payment_id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
"verdict": "released"
}
]
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| results | array of object | yes | One verdict per distinct payment id asked for. |
| results[].payment_id | uuid | yes | The payment this verdict is about. |
| results[].verdict | "released" | "already_pushed" | "not_held" | "not_found" | yes | `released` is now being pushed; `already_pushed` the ERP has it; `not_held` nothing to release; `not_found` not a payment in this workspace. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Integrations
/v1/payment-integrationsList the payment processors that collect for this workspace
Which payment service provider Finero charges through, and how it is configured — environment, account and checkout methods. CONFIGURATION ONLY: credentials, credential masks, provider metadata, publishable keys and provider account ids are deliberately never returned. Read `environment` before reporting any collected figure: a test integration takes no real money.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/payment-integrations" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "aa11bb22-cc33-4d44-8e55-ff6677889900",
"provider": "stripe",
"status": "connected",
"environment": "test",
"account_name": "Acme Industries",
"checkout_payment_methods": "stripe_dashboard",
"last_verified_at": "2026-07-14T06:59:31+00:00",
"connected_at": "2026-06-02T10:00:00+00:00"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Payment processors, newest first by creation time. |
| data[].id | string | yes | Integration id. |
| data[].provider | string | yes | Which processor collects the money — the payment service provider Finero charges through. |
| data[].status | "connected" | "error" | "disconnected" | yes | Whether Finero can currently charge through it. |
| data[].environment | "test" | "live" | null | yes | TEST or LIVE. Read this before reporting any collected figure: a test integration takes no real money, and a number sourced from one is not revenue. |
| data[].account_name | string | null | yes | The processor account's own name, as the provider reports it. |
| data[].checkout_payment_methods | string | null | yes | Which methods the hosted checkout offers: card_only (cards only, plus Apple Pay and Google Pay where the customer's device supports them), or whatever the provider dashboard is configured to allow. Null for a provider without that setting (Wix). |
| data[].last_verified_at | string | null | yes | When Finero last confirmed the credentials work. |
| data[].connected_at | string | yes | When the integration was created. |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
ERP sync
/v1/erp-connectionsList ERP connections
Lists the tenant's ERP connections (safe status metadata only — never credentials, hosts, or configuration). Connecting a new ERP and configuring what it syncs are managed inside the Finero app; starting a pull and changing the pull cadence can be done here.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/erp-connections" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"provider": "oracle_fusion",
"pull_mode": "rest_api",
"payment_push_transport": "rest",
"payment_push_mode": "auto",
"status": "connected",
"last_sync_at": "2026-07-14T05:00:04+00:00",
"last_successful_sync_at": "2026-07-14T05:00:04+00:00",
"auto_sync_enabled": true,
"auto_sync_interval_minutes": 60,
"pull_invoice_date_from": "2024-01-01",
"created_at": "2026-06-20T11:00:00+00:00"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | ERP connections. |
| data[].id | uuid | yes | ERP connection id — use as connection_id when starting a sync run. |
| data[].provider | "oracle_fusion" | yes | ERP system this connection syncs from. |
| data[].pull_mode | "rest_api" | "publisher_report" | yes | How invoices are read from the ERP. `rest_api` reads the receivables REST resource with its payment schedules; `publisher_report` reads invoice headers from an Analytics Publisher report, so every invoice from that connection is collected as a whole — installments and contact names are never present. Both methods sync incrementally (changes since the last sync) and run a complete pass at least once a day, which is when an invoice that vanished from the ERP or from the report is marked no longer present. Read-only: the method is chosen when a workspace admin connects the ERP. |
| data[].payment_push_transport | "rest" | "soap" | yes | HOW a payment's receipt is pushed to the ERP. `rest` — the receivables REST resource. `soap` — the ERP's receipt SOAP services, offered only on a `publisher_report` connection, which then uses its report's SOAP user for everything and holds no OAuth client. Either way an accepted receipt reads `erp_push_status: posted`. Read-only: the transport is chosen when a workspace admin connects the ERP. |
| data[].payment_push_mode | "auto" | "manual" | yes | WHEN a payment collected through Finero is pushed to the ERP as a receipt. `auto` — right after the payment settles, without anyone acting. `manual` — the receipt is created at settlement but HELD until a workspace admin releases it, one or many at a time (POST /v1/payments/erp-push); a held payment reads `erp_push_status: held`. Releasing pushes each receipt on its own, so one failure never affects another. Switching from manual to auto releases everything held on the connection. Set with POST /v1/erp-connections/{connectionId}/payment-push-mode. |
| data[].status | "connected" | "requires_reauthentication" | "temporarily_unavailable" | "disconnected" | yes | Connection health, and therefore whether the invoice data you are reading is CURRENT. `connected` means pulls are working. `requires_reauthentication` means the credential is missing, expired or revoked: scheduled pulls pause until a workspace admin reconnects it in the Finero app, so nothing new arrives and every figure is only as fresh as `last_successful_sync_at` - say so when you report numbers. `temporarily_unavailable` is the ERP being unreachable and usually clears itself. `disconnected` means the connection is switched off. In any state but `connected`, compare `last_successful_sync_at` against `last_sync_at`: a LATER `last_sync_at` means runs are still happening and not completing. |
| data[].last_sync_at | date-time | null | yes | When a sync run last finished, whatever its outcome. Use last_successful_sync_at to judge freshness (ISO 8601 UTC). |
| data[].last_successful_sync_at | date-time | null | yes | When a sync run last completed its walk of the ERP: a `success` run, or a `partial` one that only skipped invoices (its `error_summary` is null). Freshness of the imported data (ISO 8601 UTC). |
| data[].auto_sync_enabled | boolean | yes | Whether scheduled auto-sync is on. False is the workspace's "off" choice - manual pulls still work, and `auto_sync_interval_minutes` keeps the cadence that applies when it is turned back on. True still requires connected health and a stored credential; reconnection restores the retained schedule after a missing-credential failure. |
| data[].auto_sync_interval_minutes | integer | yes | How often Finero pulls this connection automatically, in minutes. A workspace admin chooses it: 10 to 1440 (24 hours). Read it together with `auto_sync_enabled` - when that is false there is no automatic pull at all and this value is simply the setting kept for when it is turned back on. It is a FLOOR rather than an appointment: a connection is pulled on the first scheduled check after this much time has passed, typically within a minute of it. |
| data[].pull_invoice_date_from | date | null | yes | Oldest invoice date this connection imports, inclusive. null means no limit. This is why an invoice that exists in the ERP may be absent here: it is dated before the limit. Changing it never deletes invoices already imported (YYYY-MM-DD). |
| data[].created_at | date-time | yes | Creation time (ISO 8601 UTC). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/erp-connections/{connectionId}/syncStart a pull from the ERP
Starts an invoice pull for this connection and returns immediately with the id of the run it reserved — the pull itself continues in the background. Poll that run until `status` is terminal. Only ONE run can be in flight per connection: if a scheduled or app-triggered pull is already running, this is REFUSED as a conflict rather than queueing a second one — list the connection's runs to find the one in flight and poll that. A `running` answer means the run was RESERVED and dispatched, NOT that the connection authenticated: a connection whose credential has expired accepts this call and the run fails seconds later. Check the connection's `status`, and poll the run to a terminal status before reporting success. Partial runs preserve progress; unresolved rows or schedules block completion. ERP read rate limits receive bounded retries within the pull deadline. This does not change the connection's schedule; the automatic cadence continues unchanged alongside it.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| connectionId (path) | uuid | yes | ERP connection id (from GET /v1/erp-connections). |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/erp-connections/<connectionId>/sync" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (202)
{
"sync_run_id": "3f2e1d0c-9b8a-4756-8493-21f0e9d8c7b6",
"status": "running"
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| sync_run_id | uuid | yes | The run that was reserved — poll GET /v1/sync-runs/{syncRunId}. |
| status | "running" | yes | Always `running`: the run is reserved and the pull has been dispatched. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 422 | connection_disconnected | This ERP connection has been disconnected, so Finero holds no credential for it and a pull cannot be started. A workspace administrator reconnects the ERP in the Finero app; retrying will not change the answer. → fix the request |
| 422 | sync_disabled | Invoice sync is switched off for this ERP connection, so there is nothing to pull. A workspace administrator turns it back on in the Finero app; retrying will not change the answer. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/erp-connections/{connectionId}/sync-scheduleSet the automatic pull cadence
Sets how often Finero pulls this connection automatically, and whether it does so at all. Returns the updated connection. `interval_minutes` is a FLOOR, not an appointment: the connection is pulled on the first scheduled check after that much time has passed, typically within a minute of it. Setting `enabled` to false stops automatic pulls and KEEPS the interval, so turning it back on resumes the same cadence — which is why `interval_minutes` is required either way. Manual pulls (POST .../sync) work regardless.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| connectionId (path) | uuid | yes | ERP connection id (from GET /v1/erp-connections). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| interval_minutes | integer | yes | Minutes between automatic pulls: 10 (the shortest cadence Finero commits to) to 1440 (24 hours). Required even when `enabled` is false — it is the cadence kept for when automatic syncing is switched back on. |
| enabled | boolean | yes | Whether automatic pulls run at all. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/erp-connections/<connectionId>/sync-schedule" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"interval_minutes":30,"enabled":true}'Request body example
{
"interval_minutes": 30,
"enabled": true
}Success response (200)
{
"id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"provider": "oracle_fusion",
"pull_mode": "rest_api",
"payment_push_transport": "rest",
"payment_push_mode": "auto",
"status": "connected",
"last_sync_at": "2026-07-14T05:00:04+00:00",
"last_successful_sync_at": "2026-07-14T05:00:04+00:00",
"auto_sync_enabled": true,
"auto_sync_interval_minutes": 30,
"pull_invoice_date_from": "2024-01-01",
"created_at": "2026-06-20T11:00:00+00:00"
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | ERP connection id — use as connection_id when starting a sync run. |
| provider | "oracle_fusion" | yes | ERP system this connection syncs from. |
| pull_mode | "rest_api" | "publisher_report" | yes | How invoices are read from the ERP. `rest_api` reads the receivables REST resource with its payment schedules; `publisher_report` reads invoice headers from an Analytics Publisher report, so every invoice from that connection is collected as a whole — installments and contact names are never present. Both methods sync incrementally (changes since the last sync) and run a complete pass at least once a day, which is when an invoice that vanished from the ERP or from the report is marked no longer present. Read-only: the method is chosen when a workspace admin connects the ERP. |
| payment_push_transport | "rest" | "soap" | yes | HOW a payment's receipt is pushed to the ERP. `rest` — the receivables REST resource. `soap` — the ERP's receipt SOAP services, offered only on a `publisher_report` connection, which then uses its report's SOAP user for everything and holds no OAuth client. Either way an accepted receipt reads `erp_push_status: posted`. Read-only: the transport is chosen when a workspace admin connects the ERP. |
| payment_push_mode | "auto" | "manual" | yes | WHEN a payment collected through Finero is pushed to the ERP as a receipt. `auto` — right after the payment settles, without anyone acting. `manual` — the receipt is created at settlement but HELD until a workspace admin releases it, one or many at a time (POST /v1/payments/erp-push); a held payment reads `erp_push_status: held`. Releasing pushes each receipt on its own, so one failure never affects another. Switching from manual to auto releases everything held on the connection. Set with POST /v1/erp-connections/{connectionId}/payment-push-mode. |
| status | "connected" | "requires_reauthentication" | "temporarily_unavailable" | "disconnected" | yes | Connection health, and therefore whether the invoice data you are reading is CURRENT. `connected` means pulls are working. `requires_reauthentication` means the credential is missing, expired or revoked: scheduled pulls pause until a workspace admin reconnects it in the Finero app, so nothing new arrives and every figure is only as fresh as `last_successful_sync_at` - say so when you report numbers. `temporarily_unavailable` is the ERP being unreachable and usually clears itself. `disconnected` means the connection is switched off. In any state but `connected`, compare `last_successful_sync_at` against `last_sync_at`: a LATER `last_sync_at` means runs are still happening and not completing. |
| last_sync_at | date-time | null | yes | When a sync run last finished, whatever its outcome. Use last_successful_sync_at to judge freshness (ISO 8601 UTC). |
| last_successful_sync_at | date-time | null | yes | When a sync run last completed its walk of the ERP: a `success` run, or a `partial` one that only skipped invoices (its `error_summary` is null). Freshness of the imported data (ISO 8601 UTC). |
| auto_sync_enabled | boolean | yes | Whether scheduled auto-sync is on. False is the workspace's "off" choice - manual pulls still work, and `auto_sync_interval_minutes` keeps the cadence that applies when it is turned back on. True still requires connected health and a stored credential; reconnection restores the retained schedule after a missing-credential failure. |
| auto_sync_interval_minutes | integer | yes | How often Finero pulls this connection automatically, in minutes. A workspace admin chooses it: 10 to 1440 (24 hours). Read it together with `auto_sync_enabled` - when that is false there is no automatic pull at all and this value is simply the setting kept for when it is turned back on. It is a FLOOR rather than an appointment: a connection is pulled on the first scheduled check after this much time has passed, typically within a minute of it. |
| pull_invoice_date_from | date | null | yes | Oldest invoice date this connection imports, inclusive. null means no limit. This is why an invoice that exists in the ERP may be absent here: it is dated before the limit. Changing it never deletes invoices already imported (YYYY-MM-DD). |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/erp-connections/{connectionId}/payment-push-modeSet when payments are pushed to the ERP
Sets whether a payment collected through Finero is pushed to the ERP right after it settles (`auto`, the default) or held until a workspace admin releases it (`manual`, via POST /v1/payments/erp-push). Returns the updated connection. Switching from manual to auto releases every payment currently held on the connection, in the same call. Switching from auto to manual changes nothing already queued — a push on its way still lands. This is the same setting a workspace admin changes in the Finero app; both go through one rule.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| connectionId (path) | uuid | yes | ERP connection id (from GET /v1/erp-connections). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| payment_push_mode | "auto" | "manual" | yes | `auto` pushes each payment as it settles; `manual` holds it for an admin's release. |
Example request
curl -X POST "https://api.getfinero.com/functions/v1/api/v1/erp-connections/<connectionId>/payment-push-mode" \
-H "Authorization: Bearer $FINERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"payment_push_mode":"manual"}'Request body example
{
"payment_push_mode": "manual"
}Success response (200)
{
"id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"provider": "oracle_fusion",
"pull_mode": "rest_api",
"payment_push_transport": "rest",
"payment_push_mode": "manual",
"status": "connected",
"last_sync_at": "2026-07-14T05:00:04+00:00",
"last_successful_sync_at": "2026-07-14T05:00:04+00:00",
"auto_sync_enabled": true,
"auto_sync_interval_minutes": 60,
"pull_invoice_date_from": "2024-01-01",
"created_at": "2026-06-20T11:00:00+00:00"
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | ERP connection id — use as connection_id when starting a sync run. |
| provider | "oracle_fusion" | yes | ERP system this connection syncs from. |
| pull_mode | "rest_api" | "publisher_report" | yes | How invoices are read from the ERP. `rest_api` reads the receivables REST resource with its payment schedules; `publisher_report` reads invoice headers from an Analytics Publisher report, so every invoice from that connection is collected as a whole — installments and contact names are never present. Both methods sync incrementally (changes since the last sync) and run a complete pass at least once a day, which is when an invoice that vanished from the ERP or from the report is marked no longer present. Read-only: the method is chosen when a workspace admin connects the ERP. |
| payment_push_transport | "rest" | "soap" | yes | HOW a payment's receipt is pushed to the ERP. `rest` — the receivables REST resource. `soap` — the ERP's receipt SOAP services, offered only on a `publisher_report` connection, which then uses its report's SOAP user for everything and holds no OAuth client. Either way an accepted receipt reads `erp_push_status: posted`. Read-only: the transport is chosen when a workspace admin connects the ERP. |
| payment_push_mode | "auto" | "manual" | yes | WHEN a payment collected through Finero is pushed to the ERP as a receipt. `auto` — right after the payment settles, without anyone acting. `manual` — the receipt is created at settlement but HELD until a workspace admin releases it, one or many at a time (POST /v1/payments/erp-push); a held payment reads `erp_push_status: held`. Releasing pushes each receipt on its own, so one failure never affects another. Switching from manual to auto releases everything held on the connection. Set with POST /v1/erp-connections/{connectionId}/payment-push-mode. |
| status | "connected" | "requires_reauthentication" | "temporarily_unavailable" | "disconnected" | yes | Connection health, and therefore whether the invoice data you are reading is CURRENT. `connected` means pulls are working. `requires_reauthentication` means the credential is missing, expired or revoked: scheduled pulls pause until a workspace admin reconnects it in the Finero app, so nothing new arrives and every figure is only as fresh as `last_successful_sync_at` - say so when you report numbers. `temporarily_unavailable` is the ERP being unreachable and usually clears itself. `disconnected` means the connection is switched off. In any state but `connected`, compare `last_successful_sync_at` against `last_sync_at`: a LATER `last_sync_at` means runs are still happening and not completing. |
| last_sync_at | date-time | null | yes | When a sync run last finished, whatever its outcome. Use last_successful_sync_at to judge freshness (ISO 8601 UTC). |
| last_successful_sync_at | date-time | null | yes | When a sync run last completed its walk of the ERP: a `success` run, or a `partial` one that only skipped invoices (its `error_summary` is null). Freshness of the imported data (ISO 8601 UTC). |
| auto_sync_enabled | boolean | yes | Whether scheduled auto-sync is on. False is the workspace's "off" choice - manual pulls still work, and `auto_sync_interval_minutes` keeps the cadence that applies when it is turned back on. True still requires connected health and a stored credential; reconnection restores the retained schedule after a missing-credential failure. |
| auto_sync_interval_minutes | integer | yes | How often Finero pulls this connection automatically, in minutes. A workspace admin chooses it: 10 to 1440 (24 hours). Read it together with `auto_sync_enabled` - when that is false there is no automatic pull at all and this value is simply the setting kept for when it is turned back on. It is a FLOOR rather than an appointment: a connection is pulled on the first scheduled check after this much time has passed, typically within a minute of it. |
| pull_invoice_date_from | date | null | yes | Oldest invoice date this connection imports, inclusive. null means no limit. This is why an invoice that exists in the ERP may be absent here: it is dated before the limit. Changing it never deletes invoices already imported (YYYY-MM-DD). |
| created_at | date-time | yes | Creation time (ISO 8601 UTC). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 409 | conflict | State conflict — e.g. live payment links already cover the installment, or a concurrent identical request is in flight. → retryable |
| 413 | payload_too_large | The request body exceeds the 64 KiB limit. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/sync-runsList sync runs
Lists ERP sync runs, newest first by creation time. Keeps the newest five hundred finished runs per connection, plus any running; older runs are removed daily. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| connection_id (query) | uuid | no | Filter by ERP connection. |
| status (query) | "running" | "success" | "partial" | "failed" | no | Filter by run status. A run that skipped any invoice is `partial`. |
| direction (query) | "pull" | "push" | no | Filter by sync direction. Only `pull` matches new runs; `push` matches historical runs recorded before outbound sync was removed. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/sync-runs" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "3f2e1d0c-9b8a-4756-8493-21f0e9d8c7b6",
"connection_id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"direction": "pull",
"trigger": "manual",
"status": "partial",
"started_at": "2026-07-14T05:00:04+00:00",
"finished_at": "2026-07-14T05:00:41+00:00",
"items_processed": 36,
"items_changed": 35,
"items_failed": 0,
"items_skipped": 1,
"skipped_invoices": [
{
"external_invoice_id": "300000123456789",
"invoice_number": "INV-1042",
"missing": [
"currency"
]
}
],
"error_summary": null
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Sync runs, newest first by creation time. |
| data[].id | uuid | yes | Sync run id — poll this run until status is terminal. |
| data[].connection_id | uuid | yes | The ERP connection that ran. |
| data[].direction | "pull" | "push" | yes | Sync direction. Every new run is `pull` — Finero no longer writes invoices back to an ERP. `push` remains in the enum because historical runs carry it. |
| data[].trigger | "manual" | "scheduled" | "api" | yes | How the run started: `manual` (a person, in the Finero app), `scheduled` (the connection's own cadence), or `api` (POST /v1/erp-connections/{connectionId}/sync). Until 2026-08-11 API-started runs were recorded as `manual`; they are `api` now, because reporting a machine-started run as a human one misattributes it. |
| data[].status | "running" | "success" | "partial" | "failed" | yes | running is non-terminal; success/partial/failed are terminal. `partial` means the run ended with some invoices processed and some not: what WAS processed is applied and trustworthy. `error_summary` says why the rest was not — a failure on some items, or a pause at a per-run limit that the next sync resumes from; null means it only skipped invoices (`items_skipped`). |
| data[].started_at | date-time | yes | Run start time (ISO 8601 UTC). |
| data[].finished_at | date-time | null | yes | Run finish time (null while running) (ISO 8601 UTC). |
| data[].items_processed | integer | yes | Invoices durably processed by this run, including correctness-preserving boundary rechecks. This is operational work, not the number of ERP changes; use `items_changed` for that. Judge completion by `status` and freshness by the connection's `last_successful_sync_at`. |
| data[].items_changed | integer | null | yes | New invoices or invoices whose persisted ERP `source_updated_at` changed in this run. Zero is valid when positive `items_processed` only rechecks an inclusive cursor boundary. Null means the historical count is unknown. |
| data[].items_failed | integer | yes | Invoices this run read but could not process. |
| data[].items_skipped | integer | yes | Invoices NOT imported because the ERP sent them without a required value (named in `skipped_invoices`). Not a failure: the rest was imported, and the run is `partial` (`success` before 2026-10-02). |
| data[].skipped_invoices | array of object | yes | The first 20 skipped invoices, so they can be found and fixed in the ERP. |
| data[].skipped_invoices[].external_invoice_id | string | yes | The ERP's own id for the invoice. |
| data[].skipped_invoices[].invoice_number | string | null | yes | Null when the number is itself what is missing. |
| data[].skipped_invoices[].missing | array of string | yes | Which values were missing. `invoice_status` counts only while the connection collects invoice status. |
| data[].error_summary | string | null | yes | Categorical failure summary (never raw provider payloads). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/sync-runs/{syncRunId}Get a sync run
Returns one ERP sync run (read-only operational visibility). Poll it after starting a pull, to follow that pull to completion. A run removed from the retained history answers `not_found`, like an unknown id.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| syncRunId (path) | uuid | yes | Sync run id. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/sync-runs/<syncRunId>" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"id": "3f2e1d0c-9b8a-4756-8493-21f0e9d8c7b6",
"connection_id": "1a2b3c4d-5e6f-4a80-91b2-c3d4e5f60718",
"direction": "pull",
"trigger": "manual",
"status": "partial",
"started_at": "2026-07-14T05:00:04+00:00",
"finished_at": "2026-07-14T05:00:41+00:00",
"items_processed": 36,
"items_changed": 35,
"items_failed": 0,
"items_skipped": 1,
"skipped_invoices": [
{
"external_invoice_id": "300000123456789",
"invoice_number": "INV-1042",
"missing": [
"currency"
]
}
],
"error_summary": null
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | uuid | yes | Sync run id — poll this run until status is terminal. |
| connection_id | uuid | yes | The ERP connection that ran. |
| direction | "pull" | "push" | yes | Sync direction. Every new run is `pull` — Finero no longer writes invoices back to an ERP. `push` remains in the enum because historical runs carry it. |
| trigger | "manual" | "scheduled" | "api" | yes | How the run started: `manual` (a person, in the Finero app), `scheduled` (the connection's own cadence), or `api` (POST /v1/erp-connections/{connectionId}/sync). Until 2026-08-11 API-started runs were recorded as `manual`; they are `api` now, because reporting a machine-started run as a human one misattributes it. |
| status | "running" | "success" | "partial" | "failed" | yes | running is non-terminal; success/partial/failed are terminal. `partial` means the run ended with some invoices processed and some not: what WAS processed is applied and trustworthy. `error_summary` says why the rest was not — a failure on some items, or a pause at a per-run limit that the next sync resumes from; null means it only skipped invoices (`items_skipped`). |
| started_at | date-time | yes | Run start time (ISO 8601 UTC). |
| finished_at | date-time | null | yes | Run finish time (null while running) (ISO 8601 UTC). |
| items_processed | integer | yes | Invoices durably processed by this run, including correctness-preserving boundary rechecks. This is operational work, not the number of ERP changes; use `items_changed` for that. Judge completion by `status` and freshness by the connection's `last_successful_sync_at`. |
| items_changed | integer | null | yes | New invoices or invoices whose persisted ERP `source_updated_at` changed in this run. Zero is valid when positive `items_processed` only rechecks an inclusive cursor boundary. Null means the historical count is unknown. |
| items_failed | integer | yes | Invoices this run read but could not process. |
| items_skipped | integer | yes | Invoices NOT imported because the ERP sent them without a required value (named in `skipped_invoices`). Not a failure: the rest was imported, and the run is `partial` (`success` before 2026-10-02). |
| skipped_invoices | array of object | yes | The first 20 skipped invoices, so they can be found and fixed in the ERP. |
| skipped_invoices[].external_invoice_id | string | yes | The ERP's own id for the invoice. |
| skipped_invoices[].invoice_number | string | null | yes | Null when the number is itself what is missing. |
| skipped_invoices[].missing | array of string | yes | Which values were missing. `invoice_status` counts only while the connection collects invoice status. |
| error_summary | string | null | yes | Categorical failure summary (never raw provider payloads). |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
Workflows
/v1/workflowsList automation workflows
Every workflow Finero has, what each one does, what causes it to run, and which are switched on for this workspace — plus what every execution reason code means. For payment-link emails it also reports whether new links send immediately or before their link-owned due date, including the tenant-local send time. Read this to understand how the automations relate to each other: creating a payment link is what sends the customer the payment email, so the same email goes out whether the link was created by the automation, by hand in Finero, or through this API. The set is closed and small, so this operation is not paginated.
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/workflows" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"workflows": [
{
"workflow_type": "payment_link_automation",
"name": "Payment Link Automation",
"audience": "none",
"enabled": true,
"configured": true,
"does": "Gives each NEW invoice — one that reaches Finero while this workflow is on — one try once it is ready for collection (a draft waits until your ERP completes it): a hosted payment link for every installment Finero can collect on, through the workspace's connected payment processor. Invoices already in Finero when it is switched on are never touched, and an invoice is never tried twice. It contacts nobody itself — the customer learns about the link only if the Email Notification workflow is on.",
"never_does": [
"Send anything to a customer. Creating the link and emailing it are two workflows.",
"Charge a card or move money. The customer pays through the link when they choose to.",
"Write anything back to your ERP.",
"Create a second link for an installment that already has a live one.",
"Look at an invoice again after its one try — a link that fails, or is later withdrawn or closed, is replaced by a person.",
"Touch invoices that were already in Finero when it was switched on.",
"Decide which invoices are collectible — that follows from your ERP data."
],
"triggers": [
{
"key": null,
"title": "When a new invoice reaches Finero",
"enabled": true,
"fired_by": [
"an ERP sync bringing in an invoice Finero did not have — tried right away by a sync someone starts by hand, otherwise within 15 minutes",
"an invoice being created in Finero"
],
"effect": "One payment link per collectible installment, created through the connected processor.",
"delivery_timing": null
}
],
"causes": [
{
"workflow_type": "email_notification",
"trigger": "payment_link_created",
"when": "Always, when that trigger is on. Creating the link is what sends the email — so the SAME email goes out for a link created by hand in Finero or through the public API. It is a consequence of the link existing, not a step this automation performs."
}
],
"operator_controls": [
"Switch the whole workflow on or off. Switch it on after your first ERP sync: every invoice a sync brings in while it is on counts as new.",
"Which payment processor it uses — whichever one is connected."
],
"automatic": [
"Live payment links can never add up to more than an installment owes, enforced by the database, so a duplicate cannot be created even if two runs overlap.",
"Each new invoice is tried once, whatever the outcome; the outcome is in its history. A try cut short by an outage is not repeated.",
"A new invoice that is not ready yet waits, and gets its try once a later sync shows it ready; one that never becomes ready (paid or cancelled first) is never tried.",
"An invoice held by an open payment-arrangement task waits for the task to be dismissed, then gets its try.",
"One invoice failing never stops the rest of the batch."
],
"timing": "Immediately after a sync someone starts by hand, and otherwise within 15 minutes of the invoice arriving.",
"history": "Every try is recorded — succeeded, skipped and failed, each with a reason code — and is readable through the workflow-executions endpoint, which keeps the latest 500 per workspace (a few more for a moment when tries are written at once).",
"updated_at": "2026-07-05T17:56:44.261124+00:00"
},
{
"workflow_type": "payment_arrangement",
"name": "Payment Arrangement",
"audience": "internal",
"enabled": false,
"configured": false,
"does": "Opens a task when an invoice newly matches a configured ERP field rule: notifies an assigned workspace member, and keeps Auto Generate off that invoice until the task is dismissed.",
"never_does": [
"Does not notify existing matches when enabled.",
"Does not create or replace links without a person's instruction.",
"Does not withdraw a task when the invoice later stops matching or the workflow is turned off."
],
"triggers": [
{
"key": null,
"title": "Invoice starts matching",
"enabled": false,
"fired_by": [
"A committed ERP sync changes a known nonmatch to a match, or imports a new matching invoice after activation."
],
"effect": "Opens one task for the invoice (one ever) and sends one notice on each configured channel. An invoice that already has a payment link gets a task that is already configured, and no notice.",
"delivery_timing": null
}
],
"causes": [],
"operator_controls": [
"Choose the ERP connection, invoice field, equals value, one workspace member and email, Slack or both.",
"Enable or disable the workflow."
],
"automatic": [
"Repeated syncs do not duplicate tasks or notices.",
"The invoice's first payment link, however it is created, configures the task.",
"Notices stop while the workflow is off."
],
"timing": "The task commits with the ERP change; the notice is sent within seconds and retried per channel.",
"history": "The task records when it was configured or dismissed; it writes no workflow execution rows.",
"updated_at": null
},
{
"workflow_type": "email_notification",
"name": "Email Notification",
"audience": "customer",
"enabled": true,
"configured": true,
"does": "Emails the customer at the bill-to address on the invoice. Two triggers, switched on and off independently.",
"never_does": [
"Email anyone other than the address on the invoice. The recipient is never chosen by a caller.",
"Send the same notification twice for one payment link — the database refuses a second one.",
"Run on its own. It only ever reacts to something else happening."
],
"triggers": [
{
"key": "payment_link_created",
"title": "After a payment link is created",
"enabled": true,
"fired_by": [
"Payment Link Automation creating a link",
"someone creating a payment link by hand in Finero",
"a payment link created through the public API"
],
"effect": "The customer is emailed the link so they can pay online.",
"delivery_timing": {
"mode": "before_due_date",
"days_before": 7,
"local_time": "14:30",
"timezone_source": "tenant",
"past_due_behavior": "send_immediately"
}
},
{
"key": "invoice_paid",
"title": "After an invoice is paid",
"enabled": false,
"fired_by": [
"a payment being confirmed by the payment processor — every path that can confirm one fires this, so the email does not depend on any single one of them working"
],
"effect": "The customer is emailed confirmation that their payment was received.",
"delivery_timing": null
}
],
"causes": [],
"operator_controls": [
"Switch each trigger on or off independently.",
"Send new payment-link emails immediately or a chosen number of days before the link-owned due date at a tenant-local time.",
"Set the reply-to address.",
"Choose the sending mailbox by connecting a Google mailbox; with none connected, Finero sends."
],
"automatic": [
"At most one email per trigger, payment link and recipient — enforced by the database, which is what makes it safe for several settlement paths to overlap as safety nets.",
"A notification that was never attempted — because a read failed, say — is picked up by a recovery sweep within about 15 minutes.",
"The amount in the email is the amount that link collects, never the invoice total."
],
"timing": "Payment-link emails send immediately by default, or at the configured time in the workspace timezone a chosen number of days before the link-owned due date. The default time is 09:00, and a calculated time in the past sends immediately. Payment confirmations send as soon as payment is confirmed.",
"history": "Email attempts are recorded in Finero's notification history, which this API does not expose. NO workflow-execution row is written for an email — so an absent execution never means an email was not sent.",
"updated_at": "2026-08-12T16:36:09.323261+00:00"
},
{
"workflow_type": "internal_notification",
"name": "Internal Notification",
"audience": "internal",
"enabled": false,
"configured": false,
"does": "Emails the workspace's own team at a configured address. Never reaches a customer.",
"never_does": [
"Email a customer. The recipient is a setting, not the invoice's address.",
"Stop working when a connected Google mailbox does. It is always sent from Finero, deliberately, so alerts still arrive on the day that mailbox is disconnected or rate limited."
],
"triggers": [
{
"key": "invoice_paid",
"title": "After an invoice is paid",
"enabled": false,
"fired_by": [
"a payment being confirmed by the payment processor — the same moment that fires the customer-facing confirmation, and independent of it"
],
"effect": "The team is emailed that a payment was confirmed. Sent whether or not the payment has reached the ERP yet.",
"delivery_timing": null
}
],
"causes": [],
"operator_controls": [
"Switch it on or off.",
"Set the address your team is notified at. The address itself is not readable through this API."
],
"automatic": [
"Sent from Finero always, even with a Google mailbox connected.",
"Recovered by its own sweep within about 15 minutes if it was never attempted."
],
"timing": "Sent when a payment is confirmed. A missed one is recovered within about 15 minutes.",
"history": "Recorded in Finero's notification history, which this API does not expose. No workflow-execution row is written.",
"updated_at": null
}
],
"block_reasons": [
{
"code": "closed",
"label": "Closed",
"meaning": "Your ERP reports this installment as closed."
},
{
"code": "collection_mode_sync_pending",
"label": "Waiting for collection-mode synchronization",
"meaning": "The collection setting changed, and a full ERP sync must finish before new payment links can be created."
},
{
"code": "excluded",
"label": "Excluded from collections",
"meaning": "Someone marked this installment as not to be chased."
},
{
"code": "excluded_from_collections",
"label": "Excluded from collections",
"meaning": "Someone marked this installment as not to be chased."
},
{
"code": "installment_excluded_from_collections",
"label": "Excluded from collections",
"meaning": "Someone marked this installment as not to be chased."
},
{
"code": "installment_missing_due_date",
"label": "Missing due date",
"meaning": "Without a due date Finero cannot schedule collection."
},
{
"code": "installment_removed_from_source",
"label": "Removed from your ERP",
"meaning": "Your ERP no longer lists this installment, so Finero stopped chasing it. Nothing here needs doing unless its disappearance was a mistake."
},
{
"code": "installment_status_not_open",
"label": "Closed",
"meaning": "Your ERP reports this installment as closed."
},
{
"code": "installment_zero_or_negative_balance",
"label": "Zero outstanding",
"meaning": "Nothing is left to collect on it."
},
{
"code": "installments_missing",
"label": "No payment schedule in your ERP",
"meaning": "Your ERP reports the invoice as complete but sent no installments for it."
},
{
"code": "installments_original_mismatch",
"label": "Installment totals do not match invoice",
"meaning": "The installments do not add up to the invoice total, so the amounts cannot be trusted."
},
{
"code": "invalid_amount",
"label": "Invalid amount",
"meaning": "The amount could not be read from the ERP."
},
{
"code": "invalid_installment",
"label": "Invalid installment data",
"meaning": "The installment is missing information Finero needs."
},
{
"code": "invoice_cancelled",
"label": "Invoice cancelled in the ERP",
"meaning": "Your ERP cancelled this invoice, so Finero stopped chasing it. Reinstate it there if that was wrong."
},
{
"code": "invoice_frozen",
"label": "Invoice frozen in the ERP",
"meaning": "The invoice is locked in the ERP. The money is still owed — unfreeze it there to collect."
},
{
"code": "invoice_invalid_currency",
"label": "Unusable currency code",
"meaning": "Your ERP sent a currency Finero cannot use. It has to be a three-letter code such as USD — fix it there and the next sync picks it up."
},
{
"code": "invoice_missing_amount",
"label": "No amount in the ERP yet",
"meaning": "The ERP has no total or balance for this invoice yet, usually because it is still a draft. The next sync picks the amount up once the ERP has one."
},
{
"code": "invoice_missing_due_date",
"label": "No invoice due date",
"meaning": "The ERP invoice has no due date, so invoice-only collection cannot create a payment link."
},
{
"code": "invoice_missing_recipient_email",
"label": "No billing email",
"meaning": "The invoice has no billing address in your ERP, so there is nobody to send a payment link to. Add one and it will be picked up on the next sync."
},
{
"code": "invoice_not_approved",
"label": "Invoice not finalized in the ERP",
"meaning": "The invoice is still a draft."
},
{
"code": "invoice_not_collectible",
"label": "Invoice not collectible in the ERP",
"meaning": "The connector says this invoice cannot be collected, without a more specific reason."
},
{
"code": "invoice_not_ready",
"label": "Invoice not collection-ready",
"meaning": "Something on the invoice blocks every installment on it."
},
{
"code": "invoice_settled",
"label": "Paid in full",
"meaning": "It was collected in Finero and nothing is left."
},
{
"code": "invoice_status_sync_pending",
"label": "Waiting for refreshed ERP invoice status",
"meaning": "Invoice status collection was enabled, and an ERP sync must refresh the status before collection can continue."
},
{
"code": "invoice_unreadable_balance",
"label": "Invoice balance could not be read",
"meaning": "The ERP sent a balance Finero could not parse, so it will not act on it."
},
{
"code": "invoice_validation_issues",
"label": "Invoice data could not be validated",
"meaning": "Something on the invoice did not survive Finero's checks — most often an amount or a currency it could not read. The invoice detail names the field."
},
{
"code": "invoice_zero_balance",
"label": "Nothing outstanding on the invoice",
"meaning": "The invoice balance is already zero."
},
{
"code": "missing_currency",
"label": "Missing currency",
"meaning": "Without a currency Finero cannot ask for an amount."
},
{
"code": "missing_due_date",
"label": "Missing due date",
"meaning": "Without a due date Finero cannot schedule collection."
},
{
"code": "missing_recipient_email",
"label": "No billing email",
"meaning": "The invoice has no billing address in your ERP, so there is nobody to send a payment link to. Add one and it will be picked up on the next sync."
},
{
"code": "no_collectible_installment",
"label": "No collectible installment",
"meaning": "The invoice itself is fine, but not one of its installments can be charged right now. Each installment shows its own reason."
},
{
"code": "non_positive_outstanding",
"label": "Invoice total is not positive",
"meaning": "This invoice was created in Finero with a total of zero or less, so there is nothing to collect. Create it again with the amount owed."
},
{
"code": "not_collectible",
"label": "Not collectible",
"meaning": "The ERP flagged this installment as not collectible."
},
{
"code": "paid",
"label": "Paid",
"meaning": "It is already paid."
},
{
"code": "settled_in_finero",
"label": "Paid in full",
"meaning": "It was collected in Finero and nothing is left."
},
{
"code": "validation_issues",
"label": "Invoice data could not be validated",
"meaning": "Something on the invoice did not survive Finero's checks — most often an amount or a currency it could not read. The invoice detail names the field."
},
{
"code": "zero_outstanding",
"label": "Zero outstanding",
"meaning": "Nothing is left to collect on it."
}
],
"reason_codes": [
{
"code": "payment_arrangement_required",
"meaning": "Auto Generate skips this invoice: it has a payment-arrangement task, so a person sets up its payment links.",
"fault": "none",
"resolution": "Create the payment links in Finero or with your connected AI assistant, or dismiss the task to give the invoice its Auto Generate try."
},
{
"code": "workflow_disabled",
"meaning": "The workflow is switched off for this workspace, so nothing was attempted.",
"fault": "configuration",
"resolution": "A workspace administrator switches it on in Finero, under Workflows."
},
{
"code": "invoice_not_ready",
"meaning": "The invoice is not marked ready to collect. Readiness comes from your ERP data, not from a Finero setting.",
"fault": "data",
"resolution": "The invoice had its one automatic try. Once the ERP shows a collectible balance, create its payment link in Finero."
},
{
"code": "no_collectible_installment",
"meaning": "The invoice is ready, but not one installment on it can be charged — each is already paid, excluded from collections, or has nothing outstanding.",
"fault": "data",
"resolution": "Read the installment's own block reason on the invoice. After correcting the ERP, create the payment link in Finero."
},
{
"code": "missing_payment_integration",
"meaning": "No payment processor is connected, so there is nothing to create a link with.",
"fault": "configuration",
"resolution": "An administrator connects a payment processor in Finero, then creates the invoice's payment link; the automation tries each invoice once."
},
{
"code": "integration_unavailable",
"meaning": "A payment processor is connected but Finero could not use it — the credential was rejected, or the processor could not be reached.",
"fault": "provider",
"resolution": "If the credential was rejected, an administrator reconnects the processor. Then create the invoice's payment link in Finero; the automation tries each invoice once."
},
{
"code": "invalid_invoice_state",
"meaning": "The invoice or installment carries an amount Finero cannot read as money, or a balance below the smallest amount the payment processor can charge.",
"fault": "data",
"resolution": "Correct the amount in the ERP, then create the payment link in Finero."
},
{
"code": "payment_link_already_exists",
"meaning": "A live payment link already exists for that installment, so a second was not created.",
"fault": "none",
"resolution": "Nothing is wrong — this is the duplicate protection working. The existing link is the one to use."
},
{
"code": "amount_changed",
"meaning": "The amount owed changed after the link was created, so the old link no longer matches the invoice.",
"fault": "data",
"resolution": "Nothing to do. Finero revalidates before a customer pays, so a stale amount is never charged."
},
{
"code": "currency_changed",
"meaning": "The invoice is now billed in a different currency than the one the payment link was created in, so that link no longer matches the invoice.",
"fault": "data",
"resolution": "Nothing to do. Finero revalidates before a customer pays, so the wrong currency is never charged."
},
{
"code": "currency_not_supported",
"meaning": "The connected payment processor cannot charge the invoice's currency.",
"fault": "configuration",
"resolution": "Connect a processor that supports that currency and create the link in Finero, or collect those invoices outside Finero."
},
{
"code": "feature_unavailable",
"meaning": "The workspace's plan does not include payment links.",
"fault": "configuration",
"resolution": "Change the plan. This is not a fault — retrying will keep producing the same answer."
},
{
"code": "ineligible",
"meaning": "The installment cannot be collected on: nothing is outstanding, it is excluded, or a payment landed while the link was being created.",
"fault": "data",
"resolution": "Usually nothing — an already-collected installment is the ordinary case. Otherwise the block reason on the installment says which."
},
{
"code": "temporary_provider_failure",
"meaning": "The payment processor failed in a way that is worth retrying.",
"fault": "provider",
"resolution": "Create the payment link in Finero once the processor is reachable; the automation tries each invoice once."
},
{
"code": "unknown_provider_result",
"meaning": "Finero contacted the email provider and could not learn whether the message went out. Belongs to the email path, not to automation runs.",
"fault": "provider",
"resolution": "It is deliberately not retried automatically where a retry could email someone twice, and the delivery keeps its place so nothing else sends in its stead. If the customer confirms it never arrived, send it again by hand from the invoice."
},
{
"code": "internal_error",
"meaning": "Finero failed, and the cause is not something in your workspace.",
"fault": "finero",
"resolution": "Report it to Finero support with the execution's timestamp."
}
]
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| workflows | array of object | yes | Every workflow Finero has, with this workspace's switches merged in. |
| workflows[].workflow_type | "payment_link_automation" | "payment_arrangement" | "email_notification" | "internal_notification" | yes | Stable identifier. |
| workflows[].name | string | yes | Display name in Finero. |
| workflows[].audience | "customer" | "internal" | "none" | yes | Who this workflow contacts directly. `none` means it contacts nobody — its effects reach people only through the workflows it fires. |
| workflows[].enabled | boolean | yes | Whether the workflow is on for this workspace. |
| workflows[].configured | boolean | yes | Whether anyone has ever set this workflow up. False with `enabled` false means never set up, which is a different answer from deliberately switched off. |
| workflows[].does | string | yes | What the workflow does. |
| workflows[].never_does | array of string | yes | Limits worth stating, because they are what people assume wrongly. |
| workflows[].triggers | array of object | yes | Its triggers, and whether each is on here. |
| workflows[].triggers[].key | string | null | yes | The switch an administrator toggles, or null when the workflow has no per-trigger switch and its own on/off is the switch. |
| workflows[].triggers[].title | string | yes | How the switch reads in Finero. |
| workflows[].triggers[].enabled | boolean | yes | Whether this trigger is on for this workspace. |
| workflows[].triggers[].fired_by | array of string | yes | What causes this trigger to fire — including things a person does, not only automated ones. |
| workflows[].triggers[].effect | string | yes | What actually happens when it fires. |
| workflows[].triggers[].delivery_timing.mode | "immediate" | "before_due_date" | yes | When a new payment-link email is planned. |
| workflows[].triggers[].delivery_timing.days_before | integer | null | yes | Calendar days before the link-owned payment due date, or null. |
| workflows[].triggers[].delivery_timing.local_time | string | null | yes | Tenant-local 24-hour HH:mm send time, or null for immediate delivery. Defaults to 09:00 when older settings omit it. |
| workflows[].triggers[].delivery_timing.timezone_source | "tenant" | null | yes | The timezone is inherited from the tenant; clients never supply it. |
| workflows[].triggers[].delivery_timing.past_due_behavior | "send_immediately" | yes | What happens when the calculated instant has already passed. |
| workflows[].causes | array of object | yes | Workflows this one fires. This is the relationship between automations: creating a payment link is what sends the customer the email. |
| workflows[].causes[].workflow_type | "payment_link_automation" | "payment_arrangement" | "email_notification" | "internal_notification" | yes | The workflow this one fires. |
| workflows[].causes[].trigger | string | yes | Which of its triggers. |
| workflows[].causes[].when | string | yes | Under what conditions, and why. |
| workflows[].operator_controls | array of string | yes | What a workspace administrator decides. |
| workflows[].automatic | array of string | yes | What Finero does on its own, without being asked. |
| workflows[].timing | string | yes | When it runs, and how late a missed one can be. |
| workflows[].history | string | yes | Where the record of what it did lives, and whether this API exposes it. Read this before concluding from an empty execution list that nothing happened. |
| workflows[].updated_at | date-time | null | yes | When the configuration was last changed (ISO 8601 UTC). |
| block_reasons | array of object | yes | Every code an installment's `block_reasons` can carry, and what each means. A DIFFERENT vocabulary from `reason_codes` below: these say why one installment cannot be collected, those say why an automation run did not do something. |
| block_reasons[].code | string | yes | The value that appears in an installment's `block_reasons`. |
| block_reasons[].label | string | yes | How it reads on screen in Finero. |
| block_reasons[].meaning | string | yes | What it means, and the next move. |
| reason_codes | array of object | yes | Every reason code an execution can carry, and what each means. Flat, because a code is not owned by one workflow. |
| reason_codes[].code | string | yes | The value that appears as an execution's `reason_code`. |
| reason_codes[].meaning | string | yes | What it means, in plain words. |
| reason_codes[].fault | "none" | "configuration" | "data" | "provider" | "finero" | yes | Where the problem is: `configuration` a Finero setting, `data` the ERP data, `provider` the payment or email provider, `finero` a Finero fault, `none` not a fault at all. |
| reason_codes[].resolution | string | yes | What to do about it, and by whom. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |
/v1/workflow-executionsList workflow executions
Audit trail of Payment Link Automation runs (what ran, what was skipped, why), with stable reason codes; `GET /v1/workflows` explains each. Holds each workspace's newest five hundred runs (a few more for a moment when runs are written at once); older ones are removed as new ones are written. Email workflows record nothing here: an absent execution never means an email was not sent. Payment Arrangement tasks are listed by `GET /v1/tasks`. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| limit (query) | integer | no | Page size, 1–100. Default 25. |
| cursor (query) | string | no | Opaque pagination cursor from a previous response's pagination.next_cursor. |
| order (query) | "asc" | "desc" | no | Sort direction. Ordering is ALWAYS by when the record was CREATED in Finero, never by when it changed — `updated_since` narrows the set but does not reorder it, so the most recently updated row is not necessarily first. Creation time is immutable, which is what lets the cursor stay stable: ordering by a value that changes would move a row mid-walk and make a page skip or repeat it. Default desc (newest first by creation time). |
| include_total (query) | boolean | no | Return pagination.total_count — the number of rows matching the filters, ignoring paging. It is the SAME number on every page, including alongside a cursor. Off by default: an exact count scans the whole filtered set, so asking on each page pays repeatedly for an answer that does not change — ask on the first request and keep it. |
| workflow_type (query) | "payment_link_automation" | "payment_arrangement" | "email_notification" | "internal_notification" | no | Filter by workflow type. |
| status (query) | "pending" | "processing" | "succeeded" | "skipped" | "failed" | no | Filter by outcome. |
| invoice_id (query) | uuid | no | Filter by invoice. |
Example request
curl "https://api.getfinero.com/functions/v1/api/v1/workflow-executions" \
-H "Authorization: Bearer $FINERO_API_KEY"Success response (200)
{
"data": [
{
"id": "6c5d4e3f-2a1b-4c9d-8e7f-0a1b2c3d4e5f",
"workflow_type": "payment_link_automation",
"invoice_id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
"installment_id": "a3c9d2e1-55b4-4c8e-9f01-2b3c4d5e6f70",
"payment_link_id": "0d9c8b7a-6e5f-4d3c-b2a1-908f7e6d5c4b",
"trigger_reason": "invoice_synced",
"status": "succeeded",
"reason_code": null,
"reason_message": null,
"started_at": "2026-07-10T06:31:00+00:00",
"finished_at": "2026-07-10T06:31:01+00:00"
}
],
"pagination": {
"next_cursor": null,
"has_more": false,
"limit": 25
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| data | array of object | yes | Workflow executions, newest first by creation time. |
| data[].id | uuid | yes | Workflow execution id. |
| data[].workflow_type | "payment_link_automation" | "payment_arrangement" | "email_notification" | "internal_notification" | yes | Which automation ran. |
| data[].invoice_id | uuid | null | yes | Related invoice. |
| data[].installment_id | uuid | null | yes | Related installment. |
| data[].payment_link_id | uuid | null | yes | Related payment link. |
| data[].trigger_reason | string | yes | What triggered the run. |
| data[].status | "pending" | "processing" | "succeeded" | "skipped" | "failed" | yes | Outcome of this execution. |
| data[].reason_code | string | null | yes | Categorical reason for a skip/failure (stable machine-readable code). the workflows catalog returns what every code means, where the problem is, and what to do about it — never guess from the name. |
| data[].reason_message | string | null | yes | Human-readable reason. |
| data[].started_at | date-time | yes | Execution start (ISO 8601 UTC). |
| data[].finished_at | date-time | null | yes | Execution finish (ISO 8601 UTC). |
| pagination.next_cursor | string | null | yes | Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results. |
| pagination.has_more | boolean | yes | Whether another page exists. |
| pagination.limit | integer | yes | The page size that was applied. |
| pagination.total_count | integer | no | Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true. |
Errors
| Status | Code | When it happens · what to do |
|---|---|---|
| 400 | invalid_cursor | The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. → fix the request |
| 400 | validation_failed | The request failed schema validation. See error.details for field messages. → fix the request |
| 401 | invalid_credentials | The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request |
| 401 | missing_credentials | No Authorization: Bearer header was sent. → fix the request |
| 401 | revoked_credentials | The presented key was revoked. Create a new key in Settings → API. → fix the request |
| 403 | feature_unavailable | The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request |
| 403 | permission_denied | The key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request |
| 404 | not_found | No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request |
| 429 | rate_limit_exceeded | Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal. → retryable |
| 500 | internal_error | Unexpected server error. Safe to retry with the same Idempotency-Key. → retryable |