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/api

Your 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

  1. In Finero, open Settings → API & agents and choose Connect an agent. Give it a name.
  2. Copy the three values shown. The client secret appears once — Finero does not store it and cannot show it again.
  3. In Claude: Settings → Connectors → Add custom connector. Paste the server URL, then open Advanced settings and paste the client ID and secret.
  4. Approve the connection when Finero asks. You will be shown which workspace it is for and exactly what it may do. This happens once per person.
https://api.getfinero.com/functions/v1/mcp

What an assistant can do: fifteen tools, 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), list_payment_links and get_payment_link (the links you have issued, and the page a customer would pay at), and — for an assistant connected to an admin only — create_payment_link and deactivate_payment_link.

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 connected to an admin can issue one for an installment — for the whole amount, or for part of it — 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. If you have the payment-link email automation switched on, creating a link sends your standard email, exactly as it does when you create one yourself — the assistant cannot change a word of it. An assistant connected to a member, rather than an admin, is refused by the database and told to use an admin account.

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 back to your ERP, change members, integrations or API keys, or delete anything. Those tools do not exist, and every write path in the database refuses an agent session outside the two payment-link 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 — readonly reads every resource in this workspace, admin adds the two writes. 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 and deactivating payment links. An Admin key does not get write access to other resources — invoices, installments, payments, connections, sync runs, 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 for an installment (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"
  }
}
CodeStatusRetry?Meaning
missing_credentials401fix requestNo Authorization: Bearer header was sent.
invalid_credentials401fix requestThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists.
revoked_credentials401fix requestThe presented key was revoked. Create a new key in Settings → API.
feature_unavailable403fix requestThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement.
permission_denied403fix requestThe key's permission does not allow this operation (readonly keys cannot call admin-only operations).
not_found404fix requestNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones.
validation_failed400fix requestThe request failed schema validation. See error.details for field messages.
conflict409retryState conflict — e.g. an active payment link already exists for the installment, or a concurrent identical request is in flight.
idempotency_key_required400fix requestThis operation requires the Idempotency-Key header (1–255 characters).
idempotency_conflict409fix requestThe Idempotency-Key was already used with a DIFFERENT request body. Use a fresh key for a new request.
rate_limit_exceeded429retryToo many requests. Honor the Retry-After header (seconds) before retrying.
payload_too_large413fix requestThe request body exceeds the 64 KiB limit.
method_not_allowed405fix requestThe path exists but not with this HTTP method.
installment_not_collectible422fix requestThe installment cannot be collected right now — it is already paid, disputed, 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.
sync_disabled422fix requestInvoice 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.
integration_unavailable422fix requestThe 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_supported422fix requestThe 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_error500retryUnexpected 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.

API context

GET
/v1/me
Admin or Read-only key
getApiContext

Identify 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

FieldTypeRequiredDescription
tenant_iduuidyesThe tenant this API key belongs to. All data is scoped to it.
tenant_namestringyesTenant display name.
api_key.iduuidyesAPI key id.
api_key.namestringyesKey name given at creation.
api_key.permission"admin" | "readonly"yesadmin can call every operation; readonly only side-effect-free ones.
api_key.created_atdate-timeyesKey creation time (ISO 8601 UTC).
api_key.last_used_atdate-time | nullyesPrevious use of this key (ISO 8601 UTC).

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable

Invoices

GET
/v1/invoices
Admin or Read-only key
listInvoices

List invoices

Lists the tenant's synced invoices, newest first by creation time. Cursor-paginated: pass ?limit= (1–100) and follow pagination.next_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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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"noReturn 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)stringnoFilter by ISO 4217 currency code.
invoice_number (query)stringnoExact-match filter on the ERP invoice number.
connection_id (query)uuidnoFilter by ERP connection.
updated_since (query)date-timenoOnly invoices updated at or after this ISO 8601 timestamp. 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)uuidnoOnly 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)"pending" | "processing" | "posted" | "applied" | "failed" | "reversed"noOnly invoices whose payment write-back to the ERP is in this state. `failed` and `reversed` are the ones to watch: under both, 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.
due_before (query)datenoOnly 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. A PAID invoice has no `collection_due_date` either, so a date-filtered call reports `amount_collected` as 0 in every currency: these filters answer what is OWED, never what was collected.
due_after (query)datenoOnly 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. Invoices with no `collection_due_date` are excluded from EVERY date-filtered call, and one has none precisely when nothing on it is collectible — which is also when it can still owe money. Date bands never sum to the whole book; check a total against a call with no date filter. A PAID invoice has no `collection_due_date` either, so a date-filtered call reports `amount_collected` as 0 in every currency: these filters answer what is OWED, never what was collected.

Example request

curl "https://api.getfinero.com/functions/v1/api/v1/invoices" \
  -H "Authorization: Bearer $FINERO_API_KEY"

Success response (200)

{
  "data": [
    {
      "id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
      "invoice_number": "INV-10421",
      "currency": "USD",
      "total_amount": "1250.0000",
      "erp_invoice_open_balance": "1250.0000",
      "invoice_date": "2026-07-01",
      "due_date": "2026-08-01",
      "last_synced_at": "2026-07-10T06:30:00+00:00",
      "collection_status": "open",
      "collection_outstanding": "1250.0000",
      "amount_collected": "0.0000",
      "erp_push_state": null,
      "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": "[email protected]"
      },
      "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

FieldTypeRequiredDescription
dataarray of objectyesInvoices, newest first by creation time.
data[].iduuidyesInvoice id (stable Finero identifier).
data[].invoice_numberstring | nullyesHuman-readable local or ERP invoice number.
data[].currencystring | nullyesInvoice currency. ISO 4217 alphabetic code, e.g. "USD".
data[].total_amountstring | nullyesInvoice total. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
data[].erp_invoice_open_balancestring | nullyesOutstanding balance across the invoice, as YOUR ERP last reported it. It does NOT drop when Finero collects: settlement is reported to the ERP as a receipt, and this figure only moves once the ERP has processed it and the next sync reads it back. So an invoice can read `collection_status: "paid"` with `collection_outstanding: "0.0000"` and still show a balance here — that is the normal window, not a discrepancy. `erp_push_state` says where in that window the invoice is: `pending`/`processing` is in flight, `posted`/`applied` means the ERP has it and the balance will follow; `failed` means it never arrived, and `reversed` means it arrived and the ERP later took it back out. Under either of the last two this figure stays stale until somebody acts. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
data[].invoice_datedate | nullyesInvoice date (YYYY-MM-DD).
data[].due_datedate | nullyesInvoice due date (YYYY-MM-DD).
data[].collection_due_datedate | nullyesTHE DATE TO AGE THIS INVOICE BY — the earliest due date among the installments Finero can currently collect on. Use this for overdue lists and aging buckets, NOT `due_date` above: an ERP commonly carries dates on the payment schedule and leaves the invoice-level field empty, so `due_date` is null on most real data and aging by it silently omits most of the book. Null when nothing is collectible or the ERP dated none of it. Filter with `due_before` / `due_after`.
data[].last_synced_atdate-time | nullyesWhen 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 move it too (recording a payment, marking an invoice no longer present), so a row can look freshly updated while the ERP has not been heard from in weeks. 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_outstandingstringyesWhat Finero still has to collect on this invoice, as an exact decimal string. THIS IS THE FIELD THAT ACCOUNTS FOR PAYMENTS FINERO HAS ALREADY TAKEN, including before your ERP has been told about them. `erp_invoice_open_balance` above is the ERP's last word and goes stale the moment Finero collects - on a real invoice it read 1398.63 while this read 0.0000, because two installments had been paid through Finero and the push back to the ERP had failed. Neither number is wrong; they answer different questions. Use `erp_invoice_open_balance` when comparing with what your ERP shows, and this to answer "what is left to collect".
data[].amount_collectedstringyesHow 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 0.0000.
data[].erp_push_state"pending" | "processing" | "posted" | "applied" | "failed" | "reversed" | nullyesHow far the money Finero collected has got into your ERP, or null when Finero has collected nothing on this invoice. `pending` means the push is queued and `processing` that it is in flight - under both, the ERP has not been told yet. `applied` means the ERP accepted the receipt AND allocated it; `posted` means accepted but not yet allocated - the ERP owns that decision. `failed` and `reversed` are why an `erp_invoice_open_balance` can stay stale, and they are NOT the same event: `failed` means the receipt never reached the ERP, `reversed` means it did and the ERP undid it - reversed, stopped, or returned unpaid. Finero retries a `failed` push every 15 minutes for 24 hours when the ERP was simply unreachable; any other cause, and every `reversed` row, 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"yesANSWER "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 installment has a balance outstanding. An invoice where Finero collected part and the remainder is disputed, excluded or otherwise blocked reads `inactive`, not `paid` — so `paid` can never hide money that is still owed. `open` means at least one installment on it 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'S OWN FIGURES LAG THIS FIELD, which has three consequences. ONE: `erp_invoice_open_balance` above zero on a `paid` invoice is that lag, your ERP not having applied the receipt yet, or a push that failed - NOT a contradiction and NOT a reason to doubt the status. Report both the paid status and the ERP lag, and read `erp_push_state` to tell ordinary lag from a push that will stay unreflected until someone acts. TWO: an invoice finalised with a balance still reads `inactive` when nothing on it can be collected - every installment disputed, 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: `erp_invoice_open_balance` still carries it, as does each installment's `erp_installment_outstanding_amount` on the invoice detail. THREE: DO NOT SUM `erp_invoice_open_balance` FOR RECEIVABLES - it still counts money Finero has already collected but not yet pushed back, indefinitely so when a push has failed. 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. Other `inactive` causes: your ERP shows no balance outstanding, has not finalised the invoice, no longer has the invoice at all (Finero stops collecting an invoice a complete sync no longer finds, and resumes if a later sync finds it again), or has not generated a payment schedule - every way Finero collects is attached to an installment, so with none there is nothing to collect against however much is owed.
data[].customer.iduuid | nullyesSTABLE 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.numberstring | nullyesBill-to customer number from the ERP.
data[].customer.namestring | nullyesBill-to customer name — the ACCOUNT (for example a company). Distinct from contact_name, which is a person.
data[].customer.contact_namestring | nullyesThe 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.emailstring | nullyesBilling email the payment-link email is sent to.
data[].origin"local" | "erp"yesProvenance / system of record: local = the invoice exists only in Finero; erp = an ERP connection owns (or will own) the record.
data[].connection_iduuid | nullyesThe exact ERP connection associated with this invoice.
data[].created_atdate-timeyesCreation time (ISO 8601 UTC).
data[].updated_atdate-timeyesLast update time (ISO 8601 UTC).
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/invoices/{invoiceId}
Admin or Read-only key
getInvoice

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

FieldTypeRequiredDescription
invoiceId (path)uuidyesInvoice id.

Example request

curl "https://api.getfinero.com/functions/v1/api/v1/invoices/<invoiceId>" \
  -H "Authorization: Bearer $FINERO_API_KEY"

Success response (200)

{
  "id": "8f14e45f-ceea-4a5b-9d2c-167ce7de1a10",
  "invoice_number": "INV-10421",
  "currency": "USD",
  "total_amount": "1250.0000",
  "erp_invoice_open_balance": "1250.0000",
  "invoice_date": "2026-07-01",
  "due_date": "2026-08-01",
  "last_synced_at": "2026-07-10T06:30:00+00:00",
  "collection_status": "open",
  "collection_outstanding": "1250.0000",
  "amount_collected": "0.0000",
  "erp_push_state": null,
  "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": "[email protected]"
  },
  "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.0000",
      "erp_installment_outstanding_amount": "1250.0000",
      "disputed_amount": "0.0000",
      "due_date": "2026-08-01",
      "source_status": "OPEN",
      "collection_status": "open",
      "collection_outstanding": "1250.0000",
      "amount_collected": "0.0000",
      "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

FieldTypeRequiredDescription
iduuidyesInvoice id (stable Finero identifier).
invoice_numberstring | nullyesHuman-readable local or ERP invoice number.
currencystring | nullyesInvoice currency. ISO 4217 alphabetic code, e.g. "USD".
total_amountstring | nullyesInvoice total. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
erp_invoice_open_balancestring | nullyesOutstanding balance across the invoice, as YOUR ERP last reported it. It does NOT drop when Finero collects: settlement is reported to the ERP as a receipt, and this figure only moves once the ERP has processed it and the next sync reads it back. So an invoice can read `collection_status: "paid"` with `collection_outstanding: "0.0000"` and still show a balance here — that is the normal window, not a discrepancy. `erp_push_state` says where in that window the invoice is: `pending`/`processing` is in flight, `posted`/`applied` means the ERP has it and the balance will follow; `failed` means it never arrived, and `reversed` means it arrived and the ERP later took it back out. Under either of the last two this figure stays stale until somebody acts. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
invoice_datedate | nullyesInvoice date (YYYY-MM-DD).
due_datedate | nullyesInvoice due date (YYYY-MM-DD).
collection_due_datedate | nullyesTHE DATE TO AGE THIS INVOICE BY — the earliest due date among the installments Finero can currently collect on. Use this for overdue lists and aging buckets, NOT `due_date` above: an ERP commonly carries dates on the payment schedule and leaves the invoice-level field empty, so `due_date` is null on most real data and aging by it silently omits most of the book. Null when nothing is collectible or the ERP dated none of it. Filter with `due_before` / `due_after`.
last_synced_atdate-time | nullyesWhen 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 move it too (recording a payment, marking an invoice no longer present), so a row can look freshly updated while the ERP has not been heard from in weeks. 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_outstandingstringyesWhat Finero still has to collect on this invoice, as an exact decimal string. THIS IS THE FIELD THAT ACCOUNTS FOR PAYMENTS FINERO HAS ALREADY TAKEN, including before your ERP has been told about them. `erp_invoice_open_balance` above is the ERP's last word and goes stale the moment Finero collects - on a real invoice it read 1398.63 while this read 0.0000, because two installments had been paid through Finero and the push back to the ERP had failed. Neither number is wrong; they answer different questions. Use `erp_invoice_open_balance` when comparing with what your ERP shows, and this to answer "what is left to collect".
amount_collectedstringyesHow 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 0.0000.
erp_push_state"pending" | "processing" | "posted" | "applied" | "failed" | "reversed" | nullyesHow far the money Finero collected has got into your ERP, or null when Finero has collected nothing on this invoice. `pending` means the push is queued and `processing` that it is in flight - under both, the ERP has not been told yet. `applied` means the ERP accepted the receipt AND allocated it; `posted` means accepted but not yet allocated - the ERP owns that decision. `failed` and `reversed` are why an `erp_invoice_open_balance` can stay stale, and they are NOT the same event: `failed` means the receipt never reached the ERP, `reversed` means it did and the ERP undid it - reversed, stopped, or returned unpaid. Finero retries a `failed` push every 15 minutes for 24 hours when the ERP was simply unreachable; any other cause, and every `reversed` row, 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"yesANSWER "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 installment has a balance outstanding. An invoice where Finero collected part and the remainder is disputed, excluded or otherwise blocked reads `inactive`, not `paid` — so `paid` can never hide money that is still owed. `open` means at least one installment on it 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'S OWN FIGURES LAG THIS FIELD, which has three consequences. ONE: `erp_invoice_open_balance` above zero on a `paid` invoice is that lag, your ERP not having applied the receipt yet, or a push that failed - NOT a contradiction and NOT a reason to doubt the status. Report both the paid status and the ERP lag, and read `erp_push_state` to tell ordinary lag from a push that will stay unreflected until someone acts. TWO: an invoice finalised with a balance still reads `inactive` when nothing on it can be collected - every installment disputed, 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: `erp_invoice_open_balance` still carries it, as does each installment's `erp_installment_outstanding_amount` on the invoice detail. THREE: DO NOT SUM `erp_invoice_open_balance` FOR RECEIVABLES - it still counts money Finero has already collected but not yet pushed back, indefinitely so when a push has failed. 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. Other `inactive` causes: your ERP shows no balance outstanding, has not finalised the invoice, no longer has the invoice at all (Finero stops collecting an invoice a complete sync no longer finds, and resumes if a later sync finds it again), or has not generated a payment schedule - every way Finero collects is attached to an installment, so with none there is nothing to collect against however much is owed.
customer.iduuid | nullyesSTABLE 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.numberstring | nullyesBill-to customer number from the ERP.
customer.namestring | nullyesBill-to customer name — the ACCOUNT (for example a company). Distinct from contact_name, which is a person.
customer.contact_namestring | nullyesThe 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.emailstring | nullyesBilling email the payment-link email is sent to.
origin"local" | "erp"yesProvenance / system of record: local = the invoice exists only in Finero; erp = an ERP connection owns (or will own) the record.
connection_iduuid | nullyesThe exact ERP connection associated with this invoice.
created_atdate-timeyesCreation time (ISO 8601 UTC).
updated_atdate-timeyesLast update time (ISO 8601 UTC).
installmentsarray of objectyesThe invoice's installments, ordered by sequence.
installments[].iduuidyesInstallment id — use as installment_id when creating a payment link.
installments[].sequenceintegeryes1-based installment sequence within the invoice.
installments[].original_amountstringyesOriginal installment amount. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
installments[].erp_installment_outstanding_amountstringyesYour ERP's outstanding figure for THIS INSTALLMENT — its own share, not the invoice total. The invoice-level equivalent is `erp_invoice_open_balance`: the same kind of number one level up, which an invoice's installments sum to. Like every `erp_` field it is your ERP's last word, so it does NOT account for money Finero has collected but not yet written back — it overstates what is owed until the push lands, and stays that way indefinitely if the push failed. `collection_outstanding` beside it is Finero's own figure and already accounts for that; prefer it for anything you are going to act on. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
installments[].disputed_amountstringyesAmount your ERP records as under dispute. Any value above zero blocks collection entirely — no payment link is created for this installment. A dispute does not reduce what is owed, so outstanding_amount is unchanged by it. Clearing a resolved dispute is usually a manual step in the ERP; until it is cleared this stays above zero and the installment stays blocked. Decimal amount as a string (exact, 4 dp) — never parse as float for arithmetic.
installments[].due_datedate | nullyesInstallment due date (YYYY-MM-DD).
installments[].source_statusstringyesRaw installment status from the ERP. EMPTY when there is no provider installment behind this row: an invoice your ERP has finalised without generating a payment schedule gets one installment carrying the whole amount, and it has no status of its own to report. Never parse this to decide whether money is owed - `collection_status` answers that in Finero's own vocabulary, for every connector.
installments[].collection_status"paid" | "open" | "inactive"yesWhat 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 - disputed, 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; `erp_installment_outstanding_amount` still carries it, and the invoice explains why in plain words.
installments[].collection_outstandingstringyesWhat is still outstanding on this installment, as an exact decimal string. Accounts for Finero payments the ERP has not been told about yet, which `erp_installment_outstanding_amount` above cannot - that is the ERP's own figure. It is what is OWED, not what is collectible: it stays non-zero when `collectible` is false, and it does not subtract `disputed_amount`. Check `collectible` and `block_reasons` before treating it as money you can ask for.
installments[].amount_collectedstringyesHow 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 0.0000.
installments[].collectiblebooleanyesWhether Finero considers this installment collectible right now. False once there is nothing left to collect, and also for installments blocked by a dispute, an exclusion, a non-final invoice, a missing billing email, or an invoice that is no longer present in your ERP.
installments[].block_reasonsarray of stringyesWHY 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. 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_collectionsbooleanyesManually excluded from collections.
installments[].created_atdate-timeyesCreation time (ISO 8601 UTC).
installments[].updated_atdate-timeyesLast update time (ISO 8601 UTC).

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/invoices/summary
Admin or Read-only key
summarizeInvoices

Count invoices by collection status, and total the money

Answers 'how many, and how much' in ONE request. Returns counts per collection_status plus outstanding and collected money per currency, over the same filters listInvoices accepts. 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 across currencies. Exact at any workspace size: the counts come from an indexed column, so nothing is capped and nothing is sampled. Every filter listInvoices accepts works here too — pair `due_after` with `due_before` and one call answers a whole aging band.

Parameters

FieldTypeRequiredDescription
collection_status (query)"paid" | "open" | "inactive"noCount 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)stringnoFilter by ISO 4217 currency code.
connection_id (query)uuidnoFilter by ERP connection.
updated_since (query)date-timenoOnly invoices updated at or after this ISO 8601 timestamp. 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)uuidnoOnly 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)"pending" | "processing" | "posted" | "applied" | "failed" | "reversed"noOnly count invoices whose payment write-back to the ERP is in this state. `erp_push_state=failed` and `erp_push_state=reversed` each answer part of 'how much money has Finero collected that the ERP does not know about?' in one call.
due_before (query)datenoOnly 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. A PAID invoice has no `collection_due_date` either, so a date-filtered call reports `amount_collected` as 0 in every currency: these filters answer what is OWED, never what was collected.
due_after (query)datenoOnly 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. Invoices with no `collection_due_date` are excluded from EVERY date-filtered call, and one has none precisely when nothing on it is collectible — which is also when it can still owe money. Date bands never sum to the whole book; check a total against a call with no date filter. A PAID invoice has no `collection_due_date` either, so a date-filtered call reports `amount_collected` as 0 in every currency: these filters answer what is OWED, never what was collected.

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.5000",
      "amount_collected": "20450.0000"
    }
  ]
}

Response fields

FieldTypeRequiredDescription
totalintegeryesInvoices matching the filters. An exact count, never an estimate — and the number to trust for 'how many are there'.
by_collection_status.paidintegeryesInvoices with nothing left to collect that Finero collected on. See `collection_status` on the invoice for the full rule.
by_collection_status.openintegeryesInvoices with at least one installment Finero can collect. See `collection_status` on the invoice for the full rule.
by_collection_status.inactiveintegeryesInvoices 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_collectintegeryesThe subset of `inactive` with nothing left to collect — settled outside Finero, cancelled, or empty. `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 zero), not the ERP's balance, so it stays correct while the ERP lags.
by_currencyarray of objectyesMoney 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.
by_currency[].currencystringyesISO 4217 code.
by_currency[].collection_outstandingstringyesWhat is still outstanding after Finero's own collections, in this currency. Decimal string, 4 dp. Use this rather than summing open_balance, which is the ERP's last word and goes stale the moment Finero collects. CAUTION: this is what is OWED, not what is collectible — it includes invoices Finero is blocked from collecting (see an installment's block_reasons) and does not subtract a disputed portion. 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_collectedstringyesWhat Finero has collected, in this currency. Decimal string, 4 dp.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable

Payment links

Payments

GET
/v1/payments
Admin or Read-only key
listPayments

List 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 follow pagination.next_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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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)uuidnoFilter by invoice.
payment_link_id (query)uuidnoFilter by payment link.
provider (query)"stripe" | "wix"noFilter by provider.
confirmed_since (query)date-timenoOnly payments confirmed at or after this ISO 8601 timestamp.

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

FieldTypeRequiredDescription
dataarray of objectyesConfirmed payments, newest first by creation time.
data[].iduuidyesPayment id.
data[].invoice_iduuidyesInvoice the payment settles.
data[].installment_iduuidyesInstallment the payment settles.
data[].payment_link_iduuidyesThe payment link the payer used.
data[].provider"stripe" | "wix"yesPayment provider.
data[].provider_payment_idstringyesThe provider's payment/transaction identifier.
data[].amount_minorintegeryesSettled 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[].currencystringyesSettled currency. ISO 4217 alphabetic code, e.g. "USD".
data[].mode"test" | "live"yesProvider 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"yesPayments appear here only once verified/confirmed by the provider.
data[].provider_paid_atdate-time | nullyesProvider-reported payment time (ISO 8601 UTC).
data[].confirmed_atdate-timeyesWhen Finero confirmed the settlement (ISO 8601 UTC).
data[].erp_push_status"pending" | "processing" | "posted" | "applied" | "failed" | "reversed" | nullyesWhether this payment has reached the ERP as a receipt. `posted` = the ERP accepted it; `applied` = the ERP also allocated it against the receivable — the ERP owns allocation, so `posted` is a normal steady state and not a fault. `reversed` means it DID reach the ERP and was undone there - reversed, stopped, or returned unpaid - which no retry can repair. `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_numberstring | nullyesThe 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_idstring | nullyesThe ERP's own identifier for the receipt, once it has created one.
data[].erp_pushed_atdate-time | nullyesWhen the ERP first accepted the receipt (ISO 8601 UTC).
data[].erp_push_error_categorystring | nullyesCoarse 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`/`applied` status. Provider error text is deliberately not exposed.
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/payments/{paymentId}
Admin or Read-only key
getPayment

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

FieldTypeRequiredDescription
paymentId (path)uuidyesPayment 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

FieldTypeRequiredDescription
iduuidyesPayment id.
invoice_iduuidyesInvoice the payment settles.
installment_iduuidyesInstallment the payment settles.
payment_link_iduuidyesThe payment link the payer used.
provider"stripe" | "wix"yesPayment provider.
provider_payment_idstringyesThe provider's payment/transaction identifier.
amount_minorintegeryesSettled 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.
currencystringyesSettled currency. ISO 4217 alphabetic code, e.g. "USD".
mode"test" | "live"yesProvider 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"yesPayments appear here only once verified/confirmed by the provider.
provider_paid_atdate-time | nullyesProvider-reported payment time (ISO 8601 UTC).
confirmed_atdate-timeyesWhen Finero confirmed the settlement (ISO 8601 UTC).
erp_push_status"pending" | "processing" | "posted" | "applied" | "failed" | "reversed" | nullyesWhether this payment has reached the ERP as a receipt. `posted` = the ERP accepted it; `applied` = the ERP also allocated it against the receivable — the ERP owns allocation, so `posted` is a normal steady state and not a fault. `reversed` means it DID reach the ERP and was undone there - reversed, stopped, or returned unpaid - which no retry can repair. `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_numberstring | nullyesThe 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_idstring | nullyesThe ERP's own identifier for the receipt, once it has created one.
erp_pushed_atdate-time | nullyesWhen the ERP first accepted the receipt (ISO 8601 UTC).
erp_push_error_categorystring | nullyesCoarse 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`/`applied` status. Provider error text is deliberately not exposed.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable

Integrations

GET
/v1/payment-integrations
Admin or Read-only key
listPaymentIntegrations

List 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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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

FieldTypeRequiredDescription
dataarray of objectyesPayment processors, newest first by creation time.
data[].idstringyesIntegration id.
data[].providerstringyesWhich processor collects the money — the payment service provider Finero charges through.
data[].status"connected" | "error" | "disconnected"yesWhether Finero can currently charge through it.
data[].environment"test" | "live" | nullyesTEST 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_namestring | nullyesThe processor account's own name, as the provider reports it.
data[].checkout_payment_methodsstring | nullyesWhich methods the hosted checkout offers: card_only, or whatever the provider dashboard is configured to allow.
data[].last_verified_atstring | nullyesWhen Finero last confirmed the credentials work.
data[].connected_atstringyesWhen the integration was created.
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable

ERP sync

GET
/v1/erp-connections
Admin or Read-only key
listErpConnections

List 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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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",
      "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

FieldTypeRequiredDescription
dataarray of objectyesERP connections.
data[].iduuidyesERP connection id — use as connection_id when starting a sync run.
data[].provider"oracle_fusion"yesERP system this connection syncs from.
data[].status"connected" | "requires_reauthentication" | "temporarily_unavailable" | "disconnected"yesConnection health, and therefore whether the invoice data you are reading is CURRENT. `connected` means pulls are working. `requires_reauthentication` means the credential has expired or been revoked: every pull fails 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 failing.
data[].last_sync_atdate-time | nullyesWhen a sync run last finished, whatever its outcome. Use last_successful_sync_at to judge freshness (ISO 8601 UTC).
data[].last_successful_sync_atdate-time | nullyesWhen a sync run last finished successfully (ISO 8601 UTC).
data[].auto_sync_enabledbooleanyesWhether 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.
data[].auto_sync_interval_minutesintegeryesHow 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_fromdate | nullyesOldest 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_atdate-timeyesCreation time (ISO 8601 UTC).
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
POST
/v1/erp-connections/{connectionId}/sync
Admin key required
Has side effectsstartErpSync

Start 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 that anything refreshed. This does not change the connection's schedule; the automatic cadence continues unchanged alongside it.

Parameters

FieldTypeRequiredDescription
connectionId (path)uuidyesERP 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

FieldTypeRequiredDescription
sync_run_iduuidyesThe run that was reserved — poll GET /v1/sync-runs/{syncRunId}.
status"running"yesAlways `running`: the run is reserved and the pull has been dispatched.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
409conflictState conflict — e.g. an active payment link already exists for the installment, or a concurrent identical request is in flight. → retryable
413payload_too_largeThe request body exceeds the 64 KiB limit. → fix the request
422sync_disabledInvoice 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
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
POST
/v1/erp-connections/{connectionId}/sync-schedule
Admin key required
Has side effectssetErpSyncSchedule

Set 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

FieldTypeRequiredDescription
connectionId (path)uuidyesERP connection id (from GET /v1/erp-connections).

Request body

FieldTypeRequiredDescription
interval_minutesintegeryesMinutes 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.
enabledbooleanyesWhether 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",
  "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

FieldTypeRequiredDescription
iduuidyesERP connection id — use as connection_id when starting a sync run.
provider"oracle_fusion"yesERP system this connection syncs from.
status"connected" | "requires_reauthentication" | "temporarily_unavailable" | "disconnected"yesConnection health, and therefore whether the invoice data you are reading is CURRENT. `connected` means pulls are working. `requires_reauthentication` means the credential has expired or been revoked: every pull fails 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 failing.
last_sync_atdate-time | nullyesWhen a sync run last finished, whatever its outcome. Use last_successful_sync_at to judge freshness (ISO 8601 UTC).
last_successful_sync_atdate-time | nullyesWhen a sync run last finished successfully (ISO 8601 UTC).
auto_sync_enabledbooleanyesWhether 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.
auto_sync_interval_minutesintegeryesHow 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_fromdate | nullyesOldest 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_atdate-timeyesCreation time (ISO 8601 UTC).

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
409conflictState conflict — e.g. an active payment link already exists for the installment, or a concurrent identical request is in flight. → retryable
413payload_too_largeThe request body exceeds the 64 KiB limit. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/sync-runs
Admin or Read-only key
listSyncRuns

List sync runs

Lists ERP sync runs, newest first by creation time. Cursor-paginated: pass ?limit= (1–100) and follow pagination.next_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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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)uuidnoFilter by ERP connection.
status (query)"running" | "success" | "partial" | "failed"noFilter by run status.
direction (query)"pull" | "push"noFilter 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": "success",
      "started_at": "2026-07-14T05:00:04+00:00",
      "finished_at": "2026-07-14T05:00:41+00:00",
      "items_processed": 36,
      "items_failed": 0,
      "error_summary": null
    }
  ],
  "pagination": {
    "next_cursor": null,
    "has_more": false,
    "limit": 25
  }
}

Response fields

FieldTypeRequiredDescription
dataarray of objectyesSync runs, newest first by creation time.
data[].iduuidyesSync run id — poll this run until status is terminal.
data[].connection_iduuidyesThe ERP connection that ran.
data[].direction"pull" | "push"yesSync 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"yesHow 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"yesrunning 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, and `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.
data[].started_atdate-timeyesRun start time (ISO 8601 UTC).
data[].finished_atdate-time | nullyesRun finish time (null while running) (ISO 8601 UTC).
data[].items_processedintegeryesInvoices this run processed. A pull is INCREMENTAL: it reads only invoices your ERP reports changed since the last successful run, so a small number - even 1 - on a large workspace is the normal, healthy result of a quiet hour, NOT a partial read. A run that processes the whole book is the one that had no saved position to resume from: the first pull, or recovery after certain failures. Judge a run by `status`; judge freshness by the connection's `last_successful_sync_at` - never by whether this number looks big enough.
data[].items_failedintegeryesInvoices this run read but could not process.
data[].error_summarystring | nullyesCategorical failure summary (never raw provider payloads).
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/sync-runs/{syncRunId}
Admin or Read-only key
getSyncRun

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.

Parameters

FieldTypeRequiredDescription
syncRunId (path)uuidyesSync 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": "success",
  "started_at": "2026-07-14T05:00:04+00:00",
  "finished_at": "2026-07-14T05:00:41+00:00",
  "items_processed": 36,
  "items_failed": 0,
  "error_summary": null
}

Response fields

FieldTypeRequiredDescription
iduuidyesSync run id — poll this run until status is terminal.
connection_iduuidyesThe ERP connection that ran.
direction"pull" | "push"yesSync 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"yesHow 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"yesrunning 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, and `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.
started_atdate-timeyesRun start time (ISO 8601 UTC).
finished_atdate-time | nullyesRun finish time (null while running) (ISO 8601 UTC).
items_processedintegeryesInvoices this run processed. A pull is INCREMENTAL: it reads only invoices your ERP reports changed since the last successful run, so a small number - even 1 - on a large workspace is the normal, healthy result of a quiet hour, NOT a partial read. A run that processes the whole book is the one that had no saved position to resume from: the first pull, or recovery after certain failures. Judge a run by `status`; judge freshness by the connection's `last_successful_sync_at` - never by whether this number looks big enough.
items_failedintegeryesInvoices this run read but could not process.
error_summarystring | nullyesCategorical failure summary (never raw provider payloads).

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable

Workflows

GET
/v1/workflows
Admin or Read-only key
listWorkflows

List 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. 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": "Creates a hosted payment link for every installment Finero can currently collect on, through the workspace's connected payment processor. It contacts nobody itself — the customer learns about the link only if the Email Notification workflow is switched 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.",
        "Decide which invoices are collectible — that follows from your ERP data."
      ],
      "triggers": [
        {
          "key": null,
          "title": "When an invoice is ready to collect",
          "enabled": true,
          "fired_by": [
            "a scheduled sweep, every 15 minutes, over the invoices that are ready",
            "an ERP sync finishing — including one someone starts by hand in Finero",
            "an invoice being created in Finero"
          ],
          "effect": "One payment link per collectible installment, created through the connected processor."
        }
      ],
      "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.",
        "Which payment processor it uses — whichever one is connected."
      ],
      "automatic": [
        "One live payment link per installment, enforced by the database, so a duplicate cannot be created even if two runs overlap.",
        "An invoice skipped for a missing processor or an unsupported currency is backed off for 24 hours rather than retried on every sweep.",
        "One invoice failing never stops the rest of the batch."
      ],
      "timing": "Immediately during a sync, and otherwise on a sweep every 15 minutes. Nothing waits longer than that.",
      "history": "Every run is recorded — succeeded, skipped and failed, each with a reason code — and is readable through the workflow-executions endpoint.",
      "updated_at": "2026-07-05T17:56:44.261124+00:00"
    },
    {
      "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."
        },
        {
          "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."
        }
      ],
      "causes": [],
      "operator_controls": [
        "Switch each trigger on or off independently.",
        "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": "Sent as soon as the thing that fires it happens. A missed one is recovered within about 15 minutes.",
      "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."
        }
      ],
      "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": "disputed",
      "label": "Disputed",
      "meaning": "Part or all of the amount is under dispute in the ERP."
    },
    {
      "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": "has_dispute",
      "label": "Disputed",
      "meaning": "Part or all of the amount is under dispute in the ERP."
    },
    {
      "code": "installment_disputed",
      "label": "Disputed",
      "meaning": "Part or all of the amount is under dispute in the ERP."
    },
    {
      "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_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_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_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": "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": "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": "Nothing to change in Finero. The invoice becomes eligible once the ERP shows a collectible balance, and the next sync picks it up."
    },
    {
      "code": "no_collectible_installment",
      "meaning": "The invoice is ready, but not one installment on it can be charged — each is already paid, disputed, excluded from collections, or has nothing outstanding.",
      "fault": "data",
      "resolution": "Read the installment's own block reason on the invoice. Resolving a dispute or correcting the ERP clears it."
    },
    {
      "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. Until then the invoice is backed off for 24 hours rather than retried on every sweep."
    },
    {
      "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 it was briefly unreachable, the next sweep succeeds and nothing needs doing. If the credential was rejected, an administrator reconnects the processor in Finero."
    },
    {
      "code": "invalid_invoice_state",
      "meaning": "The installment carries an amount Finero cannot read as money.",
      "fault": "data",
      "resolution": "Correct the amount in the ERP; retrying cannot fix it."
    },
    {
      "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, or collect those invoices outside Finero. The invoice is backed off for 24 hours rather than retried on every sweep."
    },
    {
      "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 disputed or 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": "Nothing to do. The next sweep tries again."
    },
    {
      "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

FieldTypeRequiredDescription
workflowsarray of objectyesEvery workflow Finero has, with this workspace's switches merged in.
workflows[].workflow_type"payment_link_automation" | "email_notification" | "internal_notification"yesStable identifier.
workflows[].namestringyesDisplay name in Finero.
workflows[].audience"customer" | "internal" | "none"yesWho this workflow contacts directly. `none` means it contacts nobody — its effects reach people only through the workflows it fires.
workflows[].enabledbooleanyesWhether the workflow is on for this workspace.
workflows[].configuredbooleanyesWhether 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[].doesstringyesWhat the workflow does.
workflows[].never_doesarray of stringyesLimits worth stating, because they are what people assume wrongly.
workflows[].triggersarray of objectyesIts triggers, and whether each is on here.
workflows[].triggers[].keystring | nullyesThe switch an administrator toggles, or null when the workflow has no per-trigger switch and its own on/off is the switch.
workflows[].triggers[].titlestringyesHow the switch reads in Finero.
workflows[].triggers[].enabledbooleanyesWhether this trigger is on for this workspace.
workflows[].triggers[].fired_byarray of stringyesWhat causes this trigger to fire — including things a person does, not only automated ones.
workflows[].triggers[].effectstringyesWhat actually happens when it fires.
workflows[].causesarray of objectyesWorkflows 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" | "email_notification" | "internal_notification"yesThe workflow this one fires.
workflows[].causes[].triggerstringyesWhich of its triggers.
workflows[].causes[].whenstringyesUnder what conditions, and why.
workflows[].operator_controlsarray of stringyesWhat a workspace administrator decides.
workflows[].automaticarray of stringyesWhat Finero does on its own, without being asked.
workflows[].timingstringyesWhen it runs, and how late a missed one can be.
workflows[].historystringyesWhere 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_atdate-time | nullyesWhen the configuration was last changed (ISO 8601 UTC).
block_reasonsarray of objectyesEvery 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[].codestringyesThe value that appears in an installment's `block_reasons`.
block_reasons[].labelstringyesHow it reads on screen in Finero.
block_reasons[].meaningstringyesWhat it means, and the next move.
reason_codesarray of objectyesEvery reason code an execution can carry, and what each means. Flat, because a code is not owned by one workflow.
reason_codes[].codestringyesThe value that appears as an execution's `reason_code`.
reason_codes[].meaningstringyesWhat it means, in plain words.
reason_codes[].fault"none" | "configuration" | "data" | "provider" | "finero"yesWhere 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[].resolutionstringyesWhat to do about it, and by whom.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable
GET
/v1/workflow-executions
Admin or Read-only key
listWorkflowExecutions

List workflow executions

Audit trail of the tenant's Payment Link Automation runs: what ran, what was skipped, and why — with stable machine-readable reason codes. Call `GET /v1/workflows` for what each reason code means. NOTE: only Payment Link Automation records executions here. Email workflows do not, so an absent execution never means an email was not sent. Cursor-paginated: pass ?limit= (1–100) and follow pagination.next_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

FieldTypeRequiredDescription
limit (query)integernoPage size, 1–100. Default 25.
cursor (query)stringnoOpaque pagination cursor from a previous response's pagination.next_cursor.
order (query)"asc" | "desc"noSort 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)booleannoReturn 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" | "email_notification" | "internal_notification"noFilter by workflow type.
status (query)"pending" | "processing" | "succeeded" | "skipped" | "failed"noFilter by outcome.
invoice_id (query)uuidnoFilter 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

FieldTypeRequiredDescription
dataarray of objectyesWorkflow executions, newest first by creation time.
data[].iduuidyesWorkflow execution id.
data[].workflow_type"payment_link_automation" | "email_notification" | "internal_notification"yesWhich automation ran.
data[].invoice_iduuid | nullyesRelated invoice.
data[].installment_iduuid | nullyesRelated installment.
data[].payment_link_iduuid | nullyesRelated payment link.
data[].trigger_reasonstringyesWhat triggered the run.
data[].status"pending" | "processing" | "succeeded" | "skipped" | "failed"yesOutcome of this execution.
data[].reason_codestring | nullyesCategorical 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_messagestring | nullyesHuman-readable reason.
data[].started_atdate-timeyesExecution start (ISO 8601 UTC).
data[].finished_atdate-time | nullyesExecution finish (ISO 8601 UTC).
pagination.next_cursorstring | nullyesOpaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
pagination.has_morebooleanyesWhether another page exists.
pagination.limitintegeryesThe page size that was applied.
pagination.total_countintegernoTotal rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

Errors

StatusCodeWhen it happens · what to do
400validation_failedThe request failed schema validation. See error.details for field messages. → fix the request
401invalid_credentialsThe credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. → fix the request
401missing_credentialsNo Authorization: Bearer header was sent. → fix the request
401revoked_credentialsThe presented key was revoked. Create a new key in Settings → API. → fix the request
403feature_unavailableThe tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. → fix the request
403permission_deniedThe key's permission does not allow this operation (readonly keys cannot call admin-only operations). → fix the request
404not_foundNo such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones. → fix the request
429rate_limit_exceededToo many requests. Honor the Retry-After header (seconds) before retrying. → retryable
500internal_errorUnexpected server error. Safe to retry with the same Idempotency-Key. → retryable