openapi: 3.1.0
info:
  title: Fibady Public API
  version: 1.0.0
  description: >-
    ## Overview


    The public API of Fibady loads the accounting, invoicing and banking data of the companies of a Fibady account in
    batches, and reads back what was loaded. This is version 1: the base URL is `https://public-api.fibady.com/v1`, and
    every path is under `/v1`.


    Requests and responses are JSON in UTF-8, except the token request, which is a form. The error messages of the
    resource operations follow `Accept-Language` (English by default), and every response emitted by the API carries
    `X-Request-Id` (see Errors and Retries and rate limits).


    Start with the Quickstart. The reference of every operation follows this guide, grouped by resource.


    ## Quickstart


    Seven steps in POSIX `sh` (they also work in bash, dash and zsh), with `curl` and `jq`. The client secret never goes
    on the command line: it is typed without echo and handed to `curl` on its standard input (`curl -K -`), so it shows
    up neither in `ps` nor in the shell history.


    Paste each block whole. In the block that reads the secret, the read is its last line: a shell without bracketed
    paste cannot take a line pasted after it as the secret.


    **1. Create a client.** In the Fibady app, go to **Settings → Organization → API and integrations**
    (`/settings/organization/api-integrations`), create an API client limited to a test company and choose its scopes;
    this guide needs `companies:read`, `accounting:read`, `accounting:write` and `ingestion:read`. Copy its client id
    (`fapi_…`) and its secret. The API must also be the source of the test company's journal entries or invoices, and
    neither of them may come from an ERP: otherwise its accounts cannot be written through the API (`catalogs`, in step
    3, says so). The app does not offer that choice yet: once the client exists, ask Fibady to set the API as that
    source.


    **2. Get an access token.** Replace `fapi_…` with your client id and paste the block. It defines `fibady_token`,
    which requests a token with HTTP Basic authentication (`client_secret_basic`) and caches it until `exp − 60 s`, and
    `fibady`, which calls the API with it and prints the response body. On an error status, `fibady` prints the status
    and the body on standard error and fails; after a 401 it also drops the cached token. The last line of the block
    asks for the client secret and requests the first token.


    ```sh

    FIBADY_CLIENT_ID='fapi_…'

    FIBADY_API='https://public-api.fibady.com'

    FIBADY_TOKEN_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/fibady-public-api-token-$(printf '%s|%s' "$FIBADY_API"
    "$FIBADY_CLIENT_ID" | cksum | cut -d' ' -f1).json"


    fibady_token() {
      now=$(date +%s)
      exp=$(jq -r '.exp // 0' "$FIBADY_TOKEN_CACHE" 2>/dev/null || echo 0)
      if [ "$exp" -gt $((now + 60)) ]; then
        jq -r '.access_token' "$FIBADY_TOKEN_CACHE"; return
      fi
      resp=$(printf 'user = "%s:%s"\n' "$FIBADY_CLIENT_ID" "$FIBADY_CLIENT_SECRET" \
        | curl -sS -K - -d grant_type=client_credentials "$FIBADY_API/v1/oauth/token")
      token=$(printf '%s' "$resp" | jq -r '.access_token // empty')
      [ -n "$token" ] || { printf '%s\n' "$resp" >&2; return 1; }
      ( umask 077; mkdir -p "$(dirname "$FIBADY_TOKEN_CACHE")"
        printf '%s' "$resp" | jq --argjson now "$now" '{access_token, exp: ($now + .expires_in)}' \
          > "$FIBADY_TOKEN_CACHE" )
      printf '%s\n' "$token"
    }


    fibady() {
      token=$(fibady_token) || return 1
      method=$1; route=$2; shift 2
      out=$(printf 'header = "Authorization: Bearer %s"\n' "$token" \
        | curl -sS -K - -X "$method" -w '\n%{http_code}' "$@" "$FIBADY_API$route") || return 1
      code=$(printf '%s\n' "$out" | tail -n 1)
      body=$(printf '%s\n' "$out" | sed '$d')
      [ "$code" != 401 ] || rm -f "$FIBADY_TOKEN_CACHE"
      [ "$code" -lt 400 ] || { printf 'HTTP %s: %s\n' "$code" "$body" >&2; return 1; }
      printf '%s\n' "$body"
    }


    printf 'FIBADY_CLIENT_SECRET: '; stty -echo; IFS= read -r FIBADY_CLIENT_SECRET; stty echo; printf '\n'; fibady_token
    >/dev/null && echo 'Token ready.'

    ```


    **3. Find the company.** `GET /v1/companies` lists the companies the client can access; `id` is the `companyId` of
    every other path, and `catalogs.accounts.writable` must be `true`. If the client can access several companies, set
    `FIBADY_COMPANY` to the `id` of the test one.


    ```sh

    fibady GET /v1/companies | jq '.items[] | {id, name, accounts: .catalogs.accounts}'

    FIBADY_COMPANY=$(fibady GET /v1/companies | jq -r '.items[0].id // empty'); echo "$FIBADY_COMPANY"

    ```


    **4. Validate a batch.** With `dryRun=true` the batch is validated exactly like a real write and nothing is written:
    each account comes back `valid`, with its warnings, or the response is a 422 `validation_failed` whose `details` say
    what to fix. Accounts are identified by `code`, and a `code` that already exists in the company, whatever loaded it,
    is adopted: the account takes the `name` sent and from then on belongs to the API. Run the quickstart on a test
    company only.


    ```sh

    FIBADY_ACCOUNTS='{"items": [
      {"code": "7000001", "name": "Sales of goods"},
      {"code": "5720001", "name": "Main bank account"}
    ]}'

    fibady POST "/v1/companies/$FIBADY_COMPANY/accounts/batch?dryRun=true" \
      -H 'Content-Type: application/json' --data-binary "$FIBADY_ACCOUNTS" | jq .
    ```


    **5. Load it.** The same batch, without `dryRun`, and with an `Idempotency-Key`. The response is the run: `runId`,
    `status` (`succeeded`, or `partial` when some record is `unprojectable`), `counts` and one `results` entry per
    account. If the response is lost, or is a 5xx, paste the block again: it keeps `FIBADY_KEY`, and with the same key a
    write that already finished returns its stored response, with `Idempotent-Replayed: true`. Run `unset FIBADY_KEY`
    before loading a different batch.


    ```sh

    FIBADY_KEY=${FIBADY_KEY:-accounts-$(date +%s)}

    FIBADY_RESULT=$(fibady POST "/v1/companies/$FIBADY_COMPANY/accounts/batch" \
      -H 'Content-Type: application/json' -H "Idempotency-Key: $FIBADY_KEY" \
      --data-binary "$FIBADY_ACCOUNTS")
    printf '%s\n' "$FIBADY_RESULT" | jq .

    FIBADY_RUN=$(printf '%s' "$FIBADY_RESULT" | jq -r '.runId // empty'); echo "$FIBADY_RUN"

    ```


    **6. Follow the run.** Poll `GET /v1/companies/{companyId}/runs/{runId}` with backoff until `status` is `succeeded`,
    `partial` or `failed`; any other value means poll again. If a call fails, `fibady` prints why and the loop stops:
    paste the block again to resume.


    ```sh

    delay=1; run=

    while [ -n "$FIBADY_RUN" ]; do
      run=$(fibady GET "/v1/companies/$FIBADY_COMPANY/runs/$FIBADY_RUN") || break
      run_status=$(printf '%s' "$run" | jq -r '.status')
      case "$run_status" in succeeded|partial|failed) break ;; esac
      sleep "$delay"; [ "$delay" -ge 30 ] || delay=$((delay * 2))
    done

    printf '%s\n' "$run" | jq '{status, counts, reportsVisibility, postProcessing}'

    ```


    **7. Check the reports and read the data back.** `reportsVisibility` says whether what this batch changed is already
    in the reports (`visible`) or not yet (`pending`, which may last: see Runs and report visibility). Accounts have no
    `GET` by identifier, so read them back with the listing `GET /v1/companies/{companyId}/accounts`, which returns the
    whole chart of accounts sorted by `code`: the block follows `nextCursor` to the last page. The other entities have
    `GET /v1/companies/{companyId}/{entity}/{externalId}`, which says whether one record is `projected` in Fibady or
    still `pending`.


    ```sh

    page=$(fibady GET "/v1/companies/$FIBADY_COMPANY/accounts")

    while [ -n "$page" ]; do
      printf '%s\n' "$page" | jq '.items[] | select(.code == "7000001" or .code == "5720001")'
      cursor=$(printf '%s' "$page" | jq -r '.nextCursor // empty')
      [ -n "$cursor" ] || break
      page=$(fibady GET "/v1/companies/$FIBADY_COMPANY/accounts" -G --data-urlencode "cursor=$cursor")
    done

    ```


    ## Authentication


    OAuth 2.0 client credentials (RFC 6749 §4.4). Request an access token with `POST /v1/oauth/token` on the same host
    you call, authenticating the client with `client_secret_basic` or `client_secret_post`, and send it as
    `Authorization: Bearer <token>` on every other request.


    Cache the token and reuse it until `exp − 60 s`, where `exp` is the time you received it plus `expires_in` (600
    seconds); then request a new one. Do not request a token per call: a client may request at most 60 tokens per hour.


    <!-- ReDoc-Inject: <security-definitions> -->


    Errors of the token endpoint use the RFC 6749 body `{ "error": "<code>", "error_description": "<text>" }`: branch on
    `error`.


    | Group | `error` | Status | Retryable | Meaning |

    |---|---|---|---|---|

    | Validation | `invalid_request` | 400 | No | Malformed token request: wrong `Content-Type`, body too large, missing
    `grant_type`, both client authentication methods, or a `client_id` that does not match. |

    | Validation | `unsupported_grant_type` | 400 | No | `grant_type` is not `client_credentials`. |

    | Credentials and tokens | `invalid_client` | 401 | No | Unknown, inactive or revoked client, wrong secret, or
    missing or malformed client authentication. |

    | Access | `origin_forbidden` | 403 | No | The request did not come through the public API endpoint. |

    | Rate limits and quota | `rate_limited` | 429 | Yes | The client exceeded its tokens per hour. Wait at least
    `Retry-After`, and cache the token instead of requesting one per call. |

    | Server | `server_error` | 500 | Yes | Unexpected failure. Retry with backoff. |

    | Server | `temporarily_unavailable` | 503 | Yes | The public API is not available for the client's account, or a
    dependency failed. Wait at least `Retry-After`; if it persists for more than 5 attempts in a row, it means the same
    as `public_api_disabled`. |


    ## Sandbox and environments


    There is a single server, production, and no separate sandbox. The sandbox of version 1 is `dryRun=true` on a test
    company: create a company for testing in your Fibady account and an API client limited to it.


    A `dryRun` runs the same checks as the real write, with the same warnings, and writes nothing: no run, no `runId`
    and no daily quota, although it counts for the rate limit. The `Idempotency-Key` is ignored with `dryRun=true`.


    ## Load order and currency


    Load the records that others refer to first. A record that refers to one that is not loaded yet is rejected with a
    422:


    - accounts → analytic values → partners → journal entries and invoices → invoice payments;

    - bank accounts → bank movements.


    Journal entry amounts are always in the company's currency. Invoices and bank movements may be in another currency:
    load the `closing` exchange rates in the Fibady app before them. Without a rate, the record is loaded with the
    warning `fx_rate_missing`, and its amount counts unconverted in the reports. Once the rate is loaded, the record is
    converted automatically only if it is dated in the month the rate is loaded or later, or in the three months before
    it, or if the Fibady account uses its own exchange rates: an older record may stay unconverted, and sending it again
    unchanged does not convert it. So load the rates before a historical load. Loading exchange rates through the API
    (`/v1/exchange-rates`) is planned for version 1.1.


    ## Idempotency


    Send an `Idempotency-Key` header on every write. The same key with the same body returns the stored response instead
    of writing again, with `Idempotent-Replayed: true`. The same key with a different body, or on a different operation,
    is a 422 `idempotency_key_reused`; while the first request is still running, a 409 `idempotency_key_in_progress`
    (wait at least `Retry-After` and send it again with the same key).


    A batch is not atomic. Its validation is: a 422 writes nothing. Its load is not: each record is applied on its own
    and identified by its `externalId` (by its `code`, for accounts), and a record that did not change comes back
    `unchanged`. So sending the same batch again after a 5xx or a timeout is safe and converges, with or without a key.


    A deletion is idempotent too: repeating it without a key returns `not_found` for the records already deleted.


    ## Runs and report visibility


    Every real write creates a run, identified by `runId`. Its `status` goes from `in_progress` to `succeeded`,
    `partial` (some records are `unprojectable`) or `failed`. Poll `GET /v1/companies/{companyId}/runs/{runId}` with
    backoff until `status` is one of those three; any other value means poll again.


    `reportsVisibility: visible` means that what THIS batch changed is already in the reports; an `unchanged` record
    changed nothing in this batch, so its visibility is that of the run that wrote it. To retry a batch, send it again
    with the same `Idempotency-Key`: the retry keeps what the previous attempt dispatched.


    `pending` may last. The reports of a large company are refreshed as a whole, not by month, and a run of invoices or
    bank movements may stay `pending` for a long time. That is not a failure of the load: do not wait for `visible` to
    consider a batch loaded. `postProcessing` is `done` once the run is in the import history.


    ## Retries and rate limits


    A 429 or a 503 from the API carries `Retry-After` (seconds) and `X-Request-Id`: wait at least `Retry-After` before
    retrying, and back off further if it repeats. A 429 without `Retry-After` (the gateway throttle does not send it):
    wait at least 1 second, with exponential backoff.


    A 409 `batch_in_progress` (the company's load lock is taken, normally by another batch of the company) or
    `idempotency_key_in_progress` (a request with the same `Idempotency-Key` is still running) is retryable too: that
    response wrote nothing and carries `Retry-After`. Wait at least `Retry-After` and send the same request again, with
    the same `Idempotency-Key` if it had one.


    Some errors do not come from the API and are outside this contract: a 429 or a 5xx without `X-Request-Id` and
    without the error bodies below (edge rate limit, gateway throttling, exhausted concurrency or the 30-second gateway
    timeout). Retry them with exponential backoff and jitter.


    A 401 `api_token_invalid` means the token expired or was revoked: request a new one and retry once.


    The only retryable 4xx responses are a 429, a 409 `batch_in_progress` or `idempotency_key_in_progress`, and a 401
    `api_token_invalid` (once, with a new token). Every other 4xx is final, including a 409 with any other code: fix the
    request before sending it again.


    **Exception: a 503 `public_api_disabled`.** The public API is not enabled for the account (invitation-only preview)
    or has been switched off in an emergency. Do not retry immediately or in a loop at the pace of `Retry-After`, even
    though the response carries it: back off exponentially, with at most 5 minutes between attempts, or stop and contact
    Fibady quoting `X-Request-Id`. A 503 `temporarily_unavailable` from the token endpoint that persists for more than 5
    attempts in a row means the same.


    ## Errors


    Every response emitted by the API carries `X-Request-Id`; quote it when you contact Fibady through your usual
    channel. Resource errors use `{ "error": "<message>", "code": "<code>" }`: branch on `code`, never on `error`, whose
    text follows `Accept-Language` (English by default). A 422 `validation_failed` adds `details`, the list of problems;
    some 409 add a `details` object whose keys depend on the `code`. The token endpoint uses the RFC 6749 body instead
    (see Authentication).


    The codes of the resource operations, by group. Retryable means that sending the same request again, after
    `Retry-After` or with backoff, may succeed; otherwise fix the request, the credential or the account first.
    `details[].code` of a 422 and `results[].code` of a record are not error codes: see Forward compatibility.


    ### Credentials and tokens


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `api_token_missing` | 401 | No | No `Authorization: Bearer` header. |

    | `api_token_invalid` | 401 | Yes | Unknown, expired or revoked access token, or an inactive client. Request a new
    token and retry once. |


    ### Access


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `auth_invalid_or_expired_license` | 402 | No | The account's license is not valid. |

    | `api_origin_forbidden` | 403 | No | The request did not come through the public API endpoint. |

    | `api_insufficient_scope` | 403 | No | The client lacks the scope the operation needs. The response carries
    `WWW-Authenticate`. |

    | `api_company_not_allowed` | 403 | No | The client cannot access the company, whether it exists or not. |

    | `api_company_param_conflict` | 403 | No | A `companyId` in the query or in the body does not match the one in the
    path: the company is always the one of the path. |

    | `public_api_disabled` | 503 | No | The public API is not enabled for the account (invitation-only preview) or has
    been switched off in an emergency. Do not retry immediately or in a loop at the pace of `Retry-After`, even though
    the response carries it: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
    quoting `X-Request-Id`. A 503 `temporarily_unavailable` from the token endpoint that persists for more than 5
    attempts in a row means the same. |


    ### Validation


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `api_invalid_query` | 400 | No | An unknown, repeated or invalid query parameter. |

    | `api_invalid_json` | 400 | No | The request body is not valid JSON. |

    | `api_resource_not_found` | 404 | No | There is no such record or run for the company. The same response for an id
    that does not exist, belongs to another company or source, or is malformed. |

    | `api_payload_too_large` | 413 | No | The request body is larger than the maximum size (see Limits). |

    | `api_batch_too_large` | 413 | No | The batch has more items than the operation accepts (see Limits). |

    | `validation_failed` | 422 | No | Some item of the batch is invalid and nothing was written or deleted: `details`
    lists the problems. |

    | `analytic_dimension_code_reserved` | 409 | No | The code of some dimension of the batch is already taken by a
    dimension that did not come from the API; `details.dimensions` lists them. Nothing was written. |


    ### Idempotency


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `idempotency_key_reused` | 422 | No | The `Idempotency-Key` was already used with a different body or operation. |

    | `idempotency_key_in_progress` | 409 | Yes | A request with the same `Idempotency-Key` is still running; this one
    wrote nothing. Wait at least `Retry-After` and send it again with the same key. |

    | `batch_in_progress` | 409 | Yes | Another batch of the company is running; this one wrote nothing. Wait at least
    `Retry-After` and send the same request again. |


    ### Sources


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `source_not_owned` | 409 | No | The company's data for this entity is not fed by the API: `details.reason` is
    `source_not_assigned` (it has no source) or `source_not_api` (its source is not the API). Nothing was written. |

    | `erp_api_mix_not_supported` | 409 | No | Of the company's journal entries and invoices, one is fed by the API and
    the other by an ERP connector. Nothing was written. |


    ### Deletes


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `delete_confirmation_required` | 409 | No | The batch would delete a large share of the company's records and
    `expectedDeletes` was not sent: send it again with `expectedDeletes`. Nothing was deleted. |


    ### Rate limits and quota


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `api_rate_limited` | 429 | Yes | Too many requests for the account or the client. Wait at least `Retry-After`. |

    | `api_quota_exceeded` | 429 | Yes | The account's daily write quota is used up. Wait at least `Retry-After`. |

    | `api_concurrent_requests_exceeded` | 429 | Yes | Too many writes of the account in flight. Wait at least
    `Retry-After`. |


    ### Server


    | Code | Status | Retryable | Meaning |

    |---|---|---|---|

    | `internal_server_error` | 500 | Yes | Unexpected failure. Retry with backoff: a write may be partially applied,
    and sending it again is safe. |

    | `api_temporarily_unavailable` | 503 | Yes | A dependency failed, or there was not enough time left to start a
    write. Wait at least `Retry-After`. |


    ## Forward compatibility


    Ignore the properties you do not recognize, in every response, errors included.


    Every enum of a response, except the three of `sources[]` below, may gain new values within `/v1`, but never loses
    or renames one (the descriptions of the fields call this «closed per version»). When a value you do not know
    arrives, apply the rule of its class, which the description of each field repeats:


    - `results[].code` of a record: Treat an unknown value as `other`.

    - `details[].code` of a 422: Treat an unknown value as a generic validation error: the record was not written.

    - `outcome` of a write or a deletion: Treat an unknown value as an unconfirmed result: check the record before
    retrying it.

    - `entity` and `operation` of a run: An unknown value is an entity or operation this client does not know: ignore
    the run.

    - lifecycle (`status`, `reportsVisibility`, `postProcessing` and any `state`): An unknown value is NOT terminal:
    poll again.

    - `results[].warnings[]`: Treat an unknown value as a generic warning: it does not block the record.

    - `errorClass` of a run: Treat an unknown value as a generic error, the same as `internal`.

    - `catalogs.*.reason` of a company: Show it as is; do not branch on it.


    No value is ever renamed or retired. Exception: `sources[].type`, `sources[].kinds` and `sources[].reason` of a
    company are fixed, and never gain values within `/v1`.


    ## Deletes and listings


    Journal entries, invoices and bank movements are deleted with `POST …/batch-delete`, by the `externalId` they were
    written with; the other entities cannot be deleted through the API (the payments of an invoice go away with it).
    Nothing is deleted by absence: a record you stop sending stays in Fibady.


    - Confirm first: run the deletion with `dryRun=true`. When the company has at least 10 live records of the entity
    written through the API and the batch would delete more than 30% of them, send `expectedDeletes` with the number of
    items of the batch; without it the response is a 409 `delete_confirmation_required` and nothing is deleted.

    - After a 5xx or a timeout, send the same deletion again BEFORE sending any of those records through `…/batch`: a
    repeated deletion completes it.

    - The replay of a deletion (the same `Idempotency-Key`) describes what happened then, with `reportsVisibility` and
    `postProcessing` up to date.

    - Deleting a record dated in a month up to `lastClosedMonth` is not blocked: it carries the warning
    `date_in_closed_period` and recalculates the budget variance and the forecast of that month.

    - The run of a deletion of invoices or bank movements may stay `pending` even when the data is already recalculated,
    and so may one of journal entries when the reports of the company are not refreshed by month.


    Listings of records (`GET` of `…/analytic-values`, `…/partners`, `…/journal-entries`, `…/invoices`,
    `…/invoice-payments`, `…/bank-accounts` and `…/bank-movements`) return only the records written through the API, one
    summary per record, in an internal order:


    - Paginate with `cursor` until `nextCursor` is `null`: a page may be shorter than `limit`, even empty, with a
    non-null `nextCursor`.

    - For an incremental sync, pass `updatedSince`. Every page of a pass returns the same `asOf`: start the next pass
    with `updatedSince` set to it. A record may appear in two passes: deduplicate by `externalId`.

    - Deleted records are not listed, and neither are the payments of a deleted invoice until the invoice and its
    payments are sent again.

    - `updatedSince` needs a time zone: send `Z`, or encode the `+` of a positive offset as `%2B`.


    The listing of accounts (`GET …/accounts`) is not one of them: it returns every live account of the company's chart
    of accounts, whatever loaded it (manual entry, a CSV import, an ERP connector or the API), sorted by `code`, without
    `updatedSince` or `asOf`. Paginate it with `cursor` until `nextCursor` is `null`; a page is shorter than `limit`
    only when it is the last one.


    ## External ids


    `externalId` is your identifier of a record: unique within a batch and scoped to the company of the path, so the
    same `externalId` in two companies identifies two records. Percent-encode it in the path of `GET
    …/{entity}/{externalId}`.


    An `externalId` that contains `/` cannot be read with `GET …/{entity}/{externalId}`, even percent-encoded: the
    response is a 404. Such a record can still be written, deleted and found in the listing.


    ## Limits


    These limits may go up within `/v1`, never down:


    - Request body: at most 1 MiB (`api_payload_too_large`).

    - Batch size (`api_batch_too_large`): 500 records of accounts, analytic values, partners or bank accounts; 200
    journal entries or invoices, with at most 2000 lines in the whole batch; 500 bank movements; 500 invoice payments;
    500 items per `batch-delete`.

    - Page size (`limit`, whose default is the maximum): 100 companies; 500 accounts; 500 records in the other listings.

    - Access tokens: at most 60 per client and hour, each valid for 600 seconds.


    The request rate, the daily write quota and the writes in flight of an account are not published as figures: when
    one is reached, the response is a 429 with `Retry-After` (see Retries and rate limits).


    ## Versioning and changelog


    Within `/v1` changes are additive only. Response objects may gain new properties: ignore the ones you do not
    recognize.


    A breaking change goes to a new version under `/v2`. The `/v1` operations it replaces announce it with the
    `Deprecation` and `Sunset` response headers, and both versions run side by side for at least 6 months before those
    `/v1` operations are retired.


    The changelog lists the versions of this document, newest first; `info.version` is the newest.


    ### 1.0.0 (2026-10-25)


    - First version, in private preview: the API is enabled only for invited accounts.

    - Access tokens (OAuth 2.0 client credentials), companies and runs.

    - Batch writes and reads of accounts, analytic values, journal entries, partners, invoices, invoice payments, bank
    accounts and bank movements, and batch deletes of journal entries, invoices and bank movements.


    New operations: `issueAccessToken`, `getOpenApiDocument`, `listCompanies`, `getRun`, `listAccounts`,
    `upsertAccounts`, `listAnalyticValues`, `getAnalyticValue`, `upsertAnalyticValues`, `listJournalEntries`,
    `getJournalEntry`, `upsertJournalEntries`, `deleteJournalEntries`, `listPartners`, `getPartner`, `upsertPartners`,
    `listInvoices`, `getInvoice`, `upsertInvoices`, `deleteInvoices`, `listInvoicePayments`, `getInvoicePayment`,
    `upsertInvoicePayments`, `listBankAccounts`, `getBankAccount`, `upsertBankAccounts`, `listBankMovements`,
    `getBankMovement`, `upsertBankMovements`, `deleteBankMovements`.
servers:
  - url: https://public-api.fibady.com
    description: Production.
tags:
  - name: Access tokens
    description: >-
      Every other operation except `GET /v1/openapi.json` needs an access token. Request one with `POST /v1/oauth/token`
      (OAuth 2.0 client credentials) and send it as `Authorization: Bearer <token>`.


      A token lasts 600 seconds and a client may request at most 60 tokens per hour: cache it and reuse it until `exp −
      60 s`.
  - name: Companies
    description: >-
      The companies the client can access. The `id` of each one is the `companyId` of every other path.


      `catalogs` says whether each catalog could be written through the API when the request was served. It is a
      snapshot: if the company's sources change afterwards, the response of the batch is the one that counts.
  - name: Runs
    description: >-
      Every real write batch creates a run. Its `status` goes from `in_progress` to `succeeded`, `partial` or `failed`.


      Poll `GET /v1/companies/{companyId}/runs/{runId}` with backoff until `status` is terminal (`succeeded`, `partial`
      or `failed`); any other value, including one this client does not know, means poll again. `reportsVisibility`
      tells when what the batch changed is in the reports.
  - name: Accounts
    description: >-
      The company's chart of accounts: list it and load it in batches, identified by `code`. Accounts have no `GET` by
      identifier: read them back with the listing.


      A classification hint that is not sent keeps the stored one, and the accounts a batch leaves unclassified get the
      company's code-prefix rules applied.
  - name: Analytic values
    description: >-
      Analytic values (cost centers, projects and the like) belong to dimensions named by the client and shared by all
      the companies of the account.


      `api:` is the reserved prefix of the dimensions written through the API: in Fibady a dimension created by a batch
      carries the code `api:<dimension>`, and if a dimension that did not come from the API already has that code, the
      batch is a 409 `analytic_dimension_code_reserved`.
  - name: Journal entries
    description: >-
      The journal entries of the company, identified by `externalId`. Every upsert replaces the WHOLE entry: its lines
      are identified by their position in `lines`, and the entry must balance.


      Its accounts may come from any source, but its partners and analytic values must have been loaded through the API
      first (see Load order and currency). `entryType` sets the typology of the whole entry: omitted keeps the stored
      one, and `null` turns it back into an ordinary entry.


      Within a line and dimension, analytic percentages add up to 100 at most and are never rescaled.
      `reportsVisibility` turns `visible` when the months the batch touched are in the reports. Entries are deleted with
      `batch-delete`.
  - name: Partners
    description: >-
      The customers and suppliers of the company, identified by `externalId`. Every upsert writes the whole partner: an
      optional field that is not sent, or is `null`, is cleared, and so are the currency, the external reference and the
      invoiced and outstanding totals that Fibady keeps for the partner.


      Partners are never deleted through the API; send `active: false` to deactivate one. A partner that was deleted in
      Fibady is restored when a batch writes it again.
  - name: Invoices
    description: >-
      Issued and received invoices and their refunds, identified by `externalId` and upserted whole: an optional field
      that is not sent is cleared, and lines are identified by their position. An invoice loaded any other way (an ERP
      connector, a CSV import, manual entry) is never matched or changed.


      The payment status of an invoice is derived, never accepted: `paid` invoices are paid and every other `status` is
      unpaid. The payment date comes from its invoice payments.


      An invoice in a currency other than the company's needs a `closing` exchange rate in Fibady; without it, it is
      loaded with the warning `fx_rate_missing`. `reportsVisibility` may stay `pending` until the reports of the whole
      company are rebuilt. Invoices are deleted with `batch-delete`, with their lines and their payments.
  - name: Invoice payments
    description: >-
      Each record is one payment applied to one invoice loaded through the API: a payment that settles several invoices
      is sent as one record per invoice, with the part applied to each one in `dedicatedAmount`. The payment date of a
      `paid` invoice is the latest `paymentDate` of its payments.


      Payments are not deleted through the API: they go away with their invoice, and come back when the invoice and its
      payments are sent again. The receivables aging and the payment-days series may take up to 5 minutes to show a new
      payment.
  - name: Bank accounts
    description: >-
      The bank accounts, cash accounts and cards of the company. Once an account exists its `name` never changes: the
      one sent is ignored.


      `openingBalance` and `openingBalanceDate` replace the stored ones when sent and are kept when omitted. Set the
      opening balance date before the first movement you send.
  - name: Bank movements
    description: >-
      The movements of the bank accounts loaded through the API, identified by `externalId`. There is no `currency`
      field: a movement is stored in the currency of its account, and keeps the account it was first loaded into.


      `amount` carries the direction in its sign: positive is money in, negative is money out. After the batch, the
      company's bank rules classify the new movements (see `postProcessing`).


      A movement of an account in a currency other than the company's needs a `closing` exchange rate in Fibady; without
      it, it is loaded with the warning `fx_rate_missing`. Movements are deleted with `batch-delete`.
  - name: OpenAPI document
    description: >-
      This document, in OpenAPI 3.1. It needs no credentials: download it to browse the contract offline or to generate
      a client.
x-tagGroups:
  - name: Getting started
    tags:
      - Access tokens
      - Companies
      - Runs
  - name: Accounting
    tags:
      - Accounts
      - Analytic values
      - Journal entries
  - name: Invoicing
    tags:
      - Partners
      - Invoices
      - Invoice payments
  - name: Banking
    tags:
      - Bank accounts
      - Bank movements
  - name: Reference
    tags:
      - OpenAPI document
paths:
  /v1/companies:
    get:
      operationId: listCompanies
      summary: List companies
      description: >-
        The companies of the client's allowlist that still exist in its account, sorted by `id`, with whether each kind
        of data could be loaded through the API. Deleted companies are skipped, so a page may be shorter than `limit`.
        Paginate with `cursor` until `nextCursor` is `null`.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Companies
      responses:
        '200':
          description: One page of companies.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiCompanyPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 100,
            or a malformed `cursor`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: '`api_origin_forbidden`: the request did not come through the public API endpoint.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - companies:read
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 100. Defaults to 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - name: cursor
          in: query
          required: false
          description: Opaque `nextCursor` from the previous page.
          schema:
            type: string
            maxLength: 200
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies'
  /v1/companies/{companyId}/accounts:
    get:
      operationId: listAccounts
      summary: List accounts
      description: >-
        The live accounts of the company's chart of accounts, whatever loaded them (manual entry, a CSV import, an ERP
        connector or the API), so that an integration can map its own account codes before loading journal entries.
        Deleted accounts are not listed. `code` comes exactly as stored.


        Accounts are sorted by `code` with the server's collation; do not rely on where spaces or punctuation sort.
        Paginate with `cursor` until `nextCursor` is `null`: following the cursors never returns the same `code` twice
        and never skips an account that kept its code throughout, even if the chart changes in between.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Accounts
      responses:
        '200':
          description: One page of accounts.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiAccountPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            or a malformed `cursor`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose chart of accounts is listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: Opaque `nextCursor` from the previous page of the same company.
          schema:
            type: string
            maxLength: 2100
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/accounts'
  /v1/companies/{companyId}/accounts/batch:
    post:
      operationId: upsertAccounts
      summary: Upsert accounts
      description: >-
        Upserts up to 500 accounts of the company's chart of accounts. An account is identified by its `code`. A code
        that already exists in the company, whatever loaded it (a CSV import, manual entry), is ADOPTED: the account
        keeps its identifier, its classification and its journal entries, takes the `name` sent, and from then on
        belongs to the API. A code that does not exist creates the account. Accounts are never deleted or deactivated
        through this operation: sending a new code for an existing account creates another one and leaves the previous
        one in place. Accounts of the chart that the batch does not name are not touched.


        A classification hint that is not sent, or is `null`, keeps the stored one; a hint cannot be cleared. Accounts
        of the batch that end up unclassified get the company's code-prefix rules applied.


        Allowed only when the company's journal entries or invoices are fed by the API and neither comes from an ERP
        connector; otherwise 409.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. In `results`, `externalId` is the account `code`. A record is `unprojectable` with code `other`
        when its code matches more than one stored account that differ only in leading or trailing spaces.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Accounts
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's accounting is not
            fed by the API; `details.reason` is `source_not_assigned` (neither journal entries nor invoices have a
            source) or `source_not_api` (they have one, and none is the API). `erp_api_mix_not_supported`: one of the
            two is fed by the API and the other by an ERP connector; `details` is empty. `batch_in_progress`: another
            batch of the company is loading; no `details`. `idempotency_key_in_progress`: a write with the same
            `Idempotency-Key` is still running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 records).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose chart of accounts is written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The accounts. A `code` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      code:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Account code: the identity of the account in the company. No control characters and no leading
                          or trailing whitespace.
                      name:
                        type: string
                        minLength: 1
                        pattern: \S
                        description: Account name. Stored as sent; it cannot be empty or blank.
                      classificationHint:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          Free text that helps classify the account. Omitted or `null` keeps the stored hint. It cannot
                          be empty or blank.
                      flowTypeHint:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          Free text that helps classify the account's cash flow. Omitted or `null` keeps the stored
                          hint. It cannot be empty or blank.
                    required:
                      - code
                      - name
              required:
                - items
            examples:
              chart_of_accounts:
                summary: Four accounts of the chart of accounts
                value:
                  items:
                    - code: '7000001'
                      name: Sales of goods
                      classificationHint: Revenue from sales of goods
                    - code: '6210001'
                      name: Office rent
                    - code: '4000001'
                      name: Suppliers
                    - code: '5720001'
                      name: Main bank account
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/accounts/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-accounts-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "code": "7000001",
                      "name": "Sales of goods",
                      "classificationHint": "Revenue from sales of goods"
                    },
                    {
                      "code": "6210001",
                      "name": "Office rent"
                    },
                    {
                      "code": "4000001",
                      "name": "Suppliers"
                    },
                    {
                      "code": "5720001",
                      "name": "Main bank account"
                    }
                  ]
                }'
  /v1/companies/{companyId}/analytic-values:
    get:
      operationId: listAnalyticValues
      summary: List analytic values
      description: >-
        The analytic values written to this company with `POST /v1/companies/{companyId}/analytic-values/batch`, one
        summary per record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It
        returns nothing else of the record. Use it to reconcile what Fibady holds with your own system. Any client of
        the account with access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: a analytic value that another source loaded (manual entry, a
        CSV import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Analytic values
      responses:
        '200':
          description: One page of analytic values.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose analytic values are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/analytic-values'
  /v1/companies/{companyId}/analytic-values/{externalId}:
    get:
      operationId: getAnalyticValue
      summary: Get analytic value state
      description: >-
        Whether an analytic value written to this company with `POST /v1/companies/{companyId}/analytic-values/batch` is
        in Fibady, looked up by the `externalId` it was written with: `projected`, with its `internalId`, or `pending`.
        It returns nothing else of the record. Use it to confirm a record after a write whose result you could not read,
        before sending it again. Any client of the account with access to the company can read it, not only the one that
        wrote it.


        Only records written through the API are found: an analytic value that another source loaded (manual entry, a
        CSV import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not
        exist, belongs to another company or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Analytic values
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such analytic value written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record from
            another source or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the analytic value was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/analytic-values/example-region-north'
  /v1/companies/{companyId}/analytic-values/batch:
    post:
      operationId: upsertAnalyticValues
      summary: Upsert analytic values
      description: >-
        Upserts up to 500 analytic values of the company. A value is identified by its `externalId`, which is unique
        within the company ACROSS all dimensions and is scoped to the company of the path: the same `externalId` sent to
        two companies creates two different values, and a batch never changes the values of another company. A value
        never moves to another dimension: sending an `externalId` that already is a value of a different dimension is a
        422 (`duplicate_external_id`); to move a value, send it with a new `externalId`.


        `dimension` is the name of the dimension the value belongs to, chosen by the client (for example `region` or
        `project`). It is case-sensitive. Dimensions are shared by all the companies of the account and are created by
        the first batch that names them, up to 5 per account; a batch that would need a sixth one is a 422
        (`analytic_dimension_limit_reached`) on every record that uses it. Dimensions cannot be renamed or deleted
        through the API, and a batch never changes a dimension that already exists. A dimension name whose code is
        already taken in the account by a dimension that did not come from the API cannot be used: 409
        `analytic_dimension_code_reserved`.


        Every upsert writes the whole value: `name` not sent, or `null`, is the `externalId`, and `active` not sent, or
        `null`, is `true`. Values are never deleted through this operation; send `active: false` to deactivate one.


        Allowed only when the company's journal entries or invoices are fed by the API and neither comes from an ERP
        connector; otherwise 409.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. A record is `unprojectable` with code `other` when its dimension could not be created at load time,
        because another company of the account filled the limit or took its code in the meantime; the rest of the batch
        is loaded. A batch of analytic values changes nothing in the reports, so `reportsVisibility` is `visible`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated, and no dimension is created.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Analytic values
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's accounting is not
            fed by the API; `details.reason` is `source_not_assigned` (neither journal entries nor invoices have a
            source) or `source_not_api` (they have one, and none is the API). `erp_api_mix_not_supported`: one of the
            two is fed by the API and the other by an ERP connector; `details` is empty.
            `analytic_dimension_code_reserved`: the code of some dimension of the batch is already taken in the account
            by a dimension that did not come from the API; `details.dimensions` lists those dimension names, as sent.
            `batch_in_progress`: another batch of the company is loading; no `details`. `idempotency_key_in_progress`: a
            write with the same `Idempotency-Key` is still running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 records).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose analytic values are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The analytic values. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the value: its identity within the company, in every dimension. No control
                          characters and no leading or trailing whitespace.
                      dimension:
                        type: string
                        minLength: 1
                        maxLength: 64
                        description: >-
                          Name of the dimension the value belongs to. Case-sensitive. No control characters and no
                          leading or trailing whitespace.
                      name:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          Name of the value. Stored as sent; it cannot be empty or blank. Omitted or `null` is the
                          `externalId`.
                      active:
                        type:
                          - boolean
                          - 'null'
                        default: true
                        description: '`false` deactivates the value. Omitted or `null` is `true`.'
                    required:
                      - externalId
                      - dimension
              required:
                - items
            examples:
              regions:
                summary: Two values of the dimension Region
                value:
                  items:
                    - externalId: example-region-north
                      dimension: Region
                      name: North
                    - externalId: example-region-south
                      dimension: Region
                      name: South
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/analytic-values/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-analytic-values-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-region-north",
                      "dimension": "Region",
                      "name": "North"
                    },
                    {
                      "externalId": "example-region-south",
                      "dimension": "Region",
                      "name": "South"
                    }
                  ]
                }'
  /v1/companies/{companyId}/bank-accounts:
    get:
      operationId: listBankAccounts
      summary: List bank accounts
      description: >-
        The bank accounts written to this company with `POST /v1/companies/{companyId}/bank-accounts/batch`, one summary
        per record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It returns
        nothing else of the record. Use it to reconcile what Fibady holds with your own system. Any client of the
        account with access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: a bank account that another source loaded (manual entry, a CSV
        import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Bank accounts
      responses:
        '200':
          description: One page of bank accounts.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query parameter
            that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose bank accounts are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-accounts'
  /v1/companies/{companyId}/bank-accounts/{externalId}:
    get:
      operationId: getBankAccount
      summary: Get bank account state
      description: >-
        Whether a bank account written to this company with `POST /v1/companies/{companyId}/bank-accounts/batch` is in
        Fibady, looked up by the `externalId` it was written with: `projected`, with its `internalId`, or `pending`. It
        returns nothing else of the record. Use it to confirm a record after a write whose result you could not read,
        before sending it again. Any client of the account with access to the company can read it, not only the one that
        wrote it.


        Only records written through the API are found: a bank account that another source loaded (manual entry, a CSV
        import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Bank accounts
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query parameter
            that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such bank account written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record from
            another source or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the bank account was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-accounts/example-bank-account-0001'
  /v1/companies/{companyId}/bank-accounts/batch:
    post:
      operationId: upsertBankAccounts
      summary: Upsert bank accounts
      description: >-
        Upserts up to 500 money accounts of the company: bank accounts, cash accounts and cards. An account is
        identified by its `externalId`, which is scoped to the company of the path: the same `externalId` sent to two
        companies creates two different accounts, and a batch never changes the accounts of another company. An account
        that did not come from the API (created in Fibady, imported from a file, synced from an ERP connector) is never
        matched or changed, even with the same name: sending it creates another account.


        Account names are unique within a company. When the name is already taken by another account, also by one
        earlier in the same batch, the new account is created as `<name> (api)`; the record still comes back as
        `created`. If no free name is found, that record comes back as `unprojectable` with
        `bank_account_name_exhausted` and the rest of the batch is loaded.


        On an account that already exists: `name` is never changed, the one sent is ignored; `currency` cannot change, a
        different one is a 422 with `invalid_currency`; `openingBalance` and `openingBalanceDate` replace the stored
        ones when sent and are kept when omitted; `archived: true` closes the account, `false` reopens it and omitting
        it leaves it as it is. `type`, `iban`, `accountNumber` and `bic` are set when the account is created and are not
        changed by a later batch. The record comes back as `updated` whenever its content differs from the one last
        sent, even if nothing changed in Fibady. Accounts are never deleted through this operation.


        Allowed only when the company's bank data is fed by the API; otherwise 409. It does not depend on where journal
        entries and invoices come from: bank data fed by the API can coexist with an ERP connector.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. A batch of bank accounts changes nothing in the reports, so `reportsVisibility` is `visible`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Bank accounts
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the query or
            in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's bank data is not
            fed by the API; `details.reason` is `source_not_assigned` (the company has no bank source) or
            `source_not_api` (it has one, and none is the API). `batch_in_progress`: another batch of the company is
            loading; no `details`. `idempotency_key_in_progress`: a write with the same `Idempotency-Key` is still
            running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 records).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose bank accounts are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The accounts. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the account: its identity within the company. No control characters and no
                          leading or trailing whitespace.
                      name:
                        type: string
                        minLength: 1
                        pattern: \S
                        description: >-
                          Account name, as shown in Fibady. Leading and trailing whitespace is removed before storing
                          it; it cannot be empty or blank. Set when the account is created: on an existing account the
                          name sent is ignored.
                      currency:
                        type: string
                        pattern: ^[A-Z]{3}$
                        description: >-
                          Currency of the account: three uppercase letters (ISO 4217, `EUR`). It cannot change once the
                          account exists.
                      type:
                        type: string
                        description: >-
                          Kind of account: a bank account, cash or a card. Any other value is a 422. Set when the
                          account is created.
                        enum:
                          - bank
                          - cash
                          - card
                      iban:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          IBAN of the account. Stored as sent, without validating its format. When both `iban` and
                          `accountNumber` are sent, the IBAN is the one stored. Set when the account is created.
                      accountNumber:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Account number, for accounts without an IBAN. Stored as sent. Set when the account is created.
                      bic:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          BIC or SWIFT code. Stored as sent, without validating its format. Set when the account is
                          created.
                      openingBalance:
                        type:
                          - string
                          - 'null'
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: >-
                          Opening balance, as a decimal string in the currency of the account: up to 13 integer digits
                          and up to 2 decimals, with a dot (`"1500.25"`, `"-320.00"`). A JSON number is a 422. On a new
                          account, omitted or `null` is `0`; on an existing one, omitted or `null` keeps the stored
                          balance.
                      openingBalanceDate:
                        type:
                          - string
                          - 'null'
                        format: date
                        description: >-
                          Date of the opening balance, `YYYY-MM-DD`. Requires `openingBalance`. Omitted or `null` leaves
                          the account without a date when it is created, and keeps the stored one afterwards.
                      archived:
                        type:
                          - boolean
                          - 'null'
                        description: >-
                          `true` closes the account and `false` reopens it. Omitted or `null` changes nothing: a new
                          account is created open, and an existing one keeps its state.
                    required:
                      - externalId
                      - name
                      - currency
                      - type
              required:
                - items
            examples:
              current_account:
                summary: A current account with its opening balance
                value:
                  items:
                    - externalId: example-bank-account-0001
                      name: Main account
                      currency: EUR
                      type: bank
                      iban: ES9121000418450200051332
                      openingBalance: '15000.00'
                      openingBalanceDate: '2026-01-01'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-accounts/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-bank-accounts-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-bank-account-0001",
                      "name": "Main account",
                      "currency": "EUR",
                      "type": "bank",
                      "iban": "ES9121000418450200051332",
                      "openingBalance": "15000.00",
                      "openingBalanceDate": "2026-01-01"
                    }
                  ]
                }'
  /v1/companies/{companyId}/bank-movements:
    get:
      operationId: listBankMovements
      summary: List bank movements
      description: >-
        The bank movements written to this company with `POST /v1/companies/{companyId}/bank-movements/batch`, one
        summary per record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It
        returns nothing else of the record. Use it to reconcile what Fibady holds with your own system. Any client of
        the account with access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: a bank movement that another source loaded (manual entry, a CSV
        import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Bank movements
      responses:
        '200':
          description: One page of bank movements.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query parameter
            that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose bank movements are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-movements'
  /v1/companies/{companyId}/bank-movements/{externalId}:
    get:
      operationId: getBankMovement
      summary: Get bank movement state
      description: >-
        Whether a bank movement written to this company with `POST /v1/companies/{companyId}/bank-movements/batch` is in
        Fibady, looked up by the `externalId` it was written with: `projected`, with its `internalId`, or `pending`. It
        returns nothing else of the record. Use it to confirm a record after a write whose result you could not read,
        before sending it again. Any client of the account with access to the company can read it, not only the one that
        wrote it.


        Only records written through the API are found: a bank movement that another source loaded (manual entry, a CSV
        import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company, belongs to a record of another kind (an invoice's `externalId` asked for as a bank
        movement) or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Ask with the `externalId` exactly as it was written, with no prefix: the response returns it the same way.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Bank movements
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query parameter
            that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such bank movement written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record of another
            kind or from another source, or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the bank movement was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-movements/example-movement-0001'
  /v1/companies/{companyId}/bank-movements/batch:
    post:
      operationId: upsertBankMovements
      summary: Upsert bank movements
      description: >-
        Upserts up to 500 bank movements of the company. A movement is identified by its `externalId`, which is scoped
        to the company of the path, not to the account: the same `externalId` sent to two companies creates two
        different movements. `results[].externalId` is your identifier as sent. Every upsert replaces the WHOLE
        movement: send it complete each time; an optional field that is omitted is cleared.


        `bankAccountExternalId` must be a bank account loaded through `bank-accounts/batch` into this same company; any
        other account, created in Fibady, imported from a file or synced from an ERP connector, is a 422
        (`unknown_reference`). Closed accounts are accepted. If the account is removed between the validation and the
        load, that movement comes back `unprojectable` with code `missing_parent` and the rest of the batch is loaded. A
        movement keeps the account it was first loaded into: sending it again with another `bankAccountExternalId` is a
        422 (`bank_movement_account_change`).


        There is no `currency` field: every movement is stored in the currency of its account, and sending `currency` is
        a 422 (`unknown_field`). `amount` carries the direction in its sign: positive is money in, negative is money
        out. Movements from the API never match or change movements that came from a file or from Fibady, even with the
        same identifier.


        After the batch, the company's bank rules classify its new movements, also by `counterpartyName` and
        `counterpartyAccount`. That runs after the response (see `postProcessing`) and does not reclassify a movement
        that is already classified.


        Warnings, which do not block the movement: `date_in_closed_period` (its date, or its stored date, falls in a
        closed month), `date_far_future` (more than 24 months ahead), `fx_rate_missing` (its account is in a currency
        other than the company's and there is no closing exchange rate in Fibady to convert it, on any date, directly or
        through USD: until that rate is loaded in Fibady the amount counts unconverted in the reports and is flagged in
        the company's data status. Once the rate is loaded, the movement is converted automatically if it is dated in
        the month the rate is loaded or later, or in the three months before it, or if the Fibady account uses its own
        exchange rates; an older movement may stay unconverted, and sending it again unchanged does not convert it,
        since it comes back `unchanged`) and `before_opening_balance_date` (its date is on or before the opening balance
        date of its account: the opening balance already includes it, so balances would count it twice; set the opening
        balance date before the first movement you send).


        Allowed only when the company's bank data is fed by the API; otherwise 409 `source_not_owned`. It does not
        depend on where journal entries and invoices come from.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. `reportsVisibility` may stay `pending` for a long time, and that is not a failure of the load: the
        reports of a large company are refreshed as a whole, not by month. Do not wait for `visible` to consider a batch
        loaded.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated, with the same warnings, and nothing is written.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Bank movements
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client cannot
            access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the query or
            in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's bank data is not
            fed by the API; `details.reason` is `source_not_assigned` (the company has no bank source) or
            `source_not_api` (it has one, and none is the API). `batch_in_progress`: another batch of the company is
            loading; no `details`. `idempotency_key_in_progress`: a write with the same `Idempotency-Key` is still
            running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 movements).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose bank movements are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The movements. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the movement: its identity within the company, not within the account. No
                          control characters and no leading or trailing whitespace.
                      bankAccountExternalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          `externalId` of a bank account loaded through `bank-accounts/batch` into this company. It
                          cannot change once the movement exists.
                      date:
                        type: string
                        format: date
                        description: Date of the movement, `YYYY-MM-DD`.
                      amount:
                        type: string
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: >-
                          Amount in the currency of the account, as a decimal string with sign: positive is money in,
                          negative is money out. Up to 13 integer digits and up to 2 decimals, with a dot (`"1500.25"`,
                          `"-320.00"`). A JSON number is a 422.
                      description:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Concept of the movement. It cannot be empty or blank; omit it or send `null` to leave it out.
                      counterpartyName:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          Name of the other party of the movement, as the bank shows it. Bank rules can match it. It
                          cannot be empty or blank; omit it or send `null` to leave it out.
                      counterpartyAccount:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: >-
                          Account of the other party (an IBAN or an account number), stored as sent. Bank rules can
                          match it. It cannot be empty or blank; omit it or send `null` to leave it out.
                    required:
                      - externalId
                      - bankAccountExternalId
                      - date
                      - amount
              required:
                - items
            examples:
              payment_and_collection:
                summary: A payment out and a collection in
                value:
                  items:
                    - externalId: example-movement-0001
                      bankAccountExternalId: example-bank-account-0001
                      date: '2026-10-01'
                      amount: '-800.00'
                      description: Office rent, September
                      counterpartyName: Example Supplies GmbH
                      counterpartyAccount: DE89370400440532013000
                    - externalId: example-movement-0002
                      bankAccountExternalId: example-bank-account-0001
                      date: '2026-10-05'
                      amount: '1210.00'
                      description: Invoice 2026-0001
                      counterpartyName: Example Customer
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-movements/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-bank-movements-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-movement-0001",
                      "bankAccountExternalId": "example-bank-account-0001",
                      "date": "2026-10-01",
                      "amount": "-800.00",
                      "description": "Office rent, September",
                      "counterpartyName": "Example Supplies GmbH",
                      "counterpartyAccount": "DE89370400440532013000"
                    },
                    {
                      "externalId": "example-movement-0002",
                      "bankAccountExternalId": "example-bank-account-0001",
                      "date": "2026-10-05",
                      "amount": "1210.00",
                      "description": "Invoice 2026-0001",
                      "counterpartyName": "Example Customer"
                    }
                  ]
                }'
  /v1/companies/{companyId}/bank-movements/batch-delete:
    post:
      operationId: deleteBankMovements
      summary: Delete bank movements
      description: >-
        Deletes up to 500 bank movements of the company, by the `externalId` they were written with through
        `bank-movements/batch`. Only movements written through the API into the company of the path are deleted; a
        movement from any other source (imported from a file, created in Fibady, synced from a connector) is never
        touched, even with the same identifier.


        Each requested `externalId` comes back `deleted`, or `not_found` when the company has no live movement with that
        `externalId` written through the API (it never existed, it was already deleted, its bank account was deleted in
        Fibady, or it belongs to another company): nothing tells those cases apart. A movement that was accepted but
        never written (`unprojectable`) is deleted too, with `internalId: null`. After a deletion, `GET` of that
        `externalId` is a 404, and repeating the deletion returns `not_found`.


        Confirmation: when the company has at least 10 live bank movements written through the API and the batch would
        delete more than 30% of them, send `expectedDeletes` with the number of items of the batch; without it the
        response is 409 `delete_confirmation_required`, with `details.candidateCount` (movements that would be deleted)
        and `details.universeSize` (live movements), and nothing is deleted. When sent, `expectedDeletes` must be the
        number of items of the batch, even if no confirmation is needed; otherwise the response is 422
        (`expected_deletes_mismatch`). Use `dryRun=true` to see the outcome first: it answers the same 409.


        Deleting a record dated in a month up to `lastClosedMonth` is not blocked: it carries the warning
        `date_in_closed_period` and recalculates the budget variance and the forecast of that month.


        Allowed only when the company's bank data is fed by the API; otherwise 409 `source_not_owned`. It does not
        depend on where journal entries and invoices come from.


        Validation is atomic: if any item is invalid, the response is 422 and nothing is deleted. The deletion itself is
        not atomic: after a 5xx or a timeout, send the same deletion again BEFORE sending any of those movements through
        `bank-movements/batch`; a repeated deletion completes it. `reportsVisibility` may stay `pending` for a long
        time, and that is not a failure of the deletion: the reports of a large company are refreshed as a whole, not by
        month. Do not wait for `visible` to consider a deletion done.


        Sending a deleted `externalId` again through `bank-movements/batch` creates the movement again (`created`),
        always with a new `internalId`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of deleting again; a key already
        used with another body, or with `bank-movements/batch`, is a 422 (`idempotency_key_reused`). With `dryRun=true`
        the batch is only checked, with the same warnings, and nothing is deleted.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Bank movements
      responses:
        '200':
          description: The movements were deleted or, with `dryRun=true`, checked.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier deletion with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PublicApiDeleteBatchResult'
                  - $ref: '#/components/schemas/PublicApiDeleteDryRunResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `banking:delete`, which `banking:write` does not include; carries `WWW-Authenticate`),
            `api_company_not_allowed` (the client cannot access the company, whether it exists or not) or
            `api_company_param_conflict` (a `companyId` in the query or in the body that does not match the path: the
            company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            Nothing was deleted. `source_not_owned`: the company's bank data is not fed by the API; `details.reason` is
            `source_not_assigned` (the company has no bank source) or `source_not_api` (it has one, and none is the
            API). `delete_confirmation_required`: the batch would delete a large share of the company's bank movements
            and `expectedDeletes` was not sent; `details.candidateCount` is the number of movements that would be
            deleted and `details.universeSize` the number of live movements; send the batch again with
            `expectedDeletes`. `batch_in_progress`: another batch of the company is running; no `details`.
            `idempotency_key_in_progress`: a request with the same `Idempotency-Key` is still running; no `details`. The
            last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 items).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some item is invalid, `details` says where, and nothing was deleted.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body or operation; no
            `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: >-
            `internal_server_error`: unexpected failure. The deletion may be partially applied; send the same deletion
            again.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was deleted) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the deletion). After `api_temporarily_unavailable` the
            deletion may be partially applied: send the same deletion again after `Retry-After` seconds. Do not retry
            `public_api_disabled` at that pace: back off exponentially, with at most 5 minutes between attempts, or stop
            and contact Fibady quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - banking:delete
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose bank movements are deleted. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` checks the batch and deletes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the deletion idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The bank movements to delete. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the movement, as sent to `bank-movements/batch`. No control characters and
                          no leading or trailing whitespace.
                    required:
                      - externalId
                expectedDeletes:
                  type:
                    - integer
                    - 'null'
                  description: >-
                    The number of items of the batch, to confirm a large deletion. Omitted or `null` means no
                    confirmation. When sent, it must be exactly the number of items.
              required:
                - items
            examples:
              one_movement:
                summary: One bank movement, confirming how many are deleted
                value:
                  items:
                    - externalId: example-movement-0001
                  expectedDeletes: 1
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/bank-movements/batch-delete' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-bank-movements-batch-delete-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-movement-0001"
                    }
                  ],
                  "expectedDeletes": 1
                }'
  /v1/companies/{companyId}/invoice-payments:
    get:
      operationId: listInvoicePayments
      summary: List invoice payments
      description: >-
        The invoice payments written to this company with `POST /v1/companies/{companyId}/invoice-payments/batch`, one
        summary per record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It
        returns nothing else of the record. Use it to reconcile what Fibady holds with your own system. Any client of
        the account with access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: an invoice payment that another source loaded (manual entry, a
        CSV import, an ERP connector) is not. Deleted records are not listed either, and neither is a payment that went
        away with its invoice (the invoice was deleted, through the API or in Fibady): if the invoice is sent again,
        send its payments again and they are listed again, with the same `internalId`. A page may come out shorter, or
        empty, because of those payments.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Invoice payments
      responses:
        '200':
          description: One page of invoice payments.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose invoice payments are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoice-payments'
  /v1/companies/{companyId}/invoice-payments/{externalId}:
    get:
      operationId: getInvoicePayment
      summary: Get invoice payment state
      description: >-
        Whether an invoice payment written to this company with `POST /v1/companies/{companyId}/invoice-payments/batch`
        is in Fibady, looked up by the `externalId` it was written with: `projected`, with its `internalId`, or
        `pending`. It returns nothing else of the record: no amount and no date. Use it to confirm a record after a
        write whose result you could not read, before sending it again. Any client of the account with access to the
        company can read it, not only the one that wrote it.


        A payment goes away with its invoice: once the invoice is deleted, the payment gives 404, the same as one that
        never existed, until the invoice is loaded again and the payment is sent again.


        Only records written through the API are found: a payment that another source loaded (an ERP connector, a CSV
        import, manual entry) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company, belongs to a record of another kind (an invoice's `externalId` asked for as a
        payment) or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Invoice payments
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such invoice payment written through the API for this company, or it
            went away with its deleted invoice. The same response for an `externalId` that does not exist, belongs to
            another company, belongs to a record of another kind or from another source, or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the invoice payment was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoice-payments/example-payment-0001%3Aexample-invoice-2026-0001'
  /v1/companies/{companyId}/invoice-payments/batch:
    post:
      operationId: upsertInvoicePayments
      summary: Upsert invoice payments
      description: >-
        Upserts up to 500 payment applications of the company. Each record is ONE payment applied to ONE invoice: a
        payment that settles several invoices is sent as one record per invoice, each with its own `externalId` (for
        example `<payment>:<invoice>`), repeating the total `amount` of the payment and giving in `dedicatedAmount` the
        part applied to that invoice. A record is identified by its `externalId`, which is scoped to the company of the
        path.


        The invoice must be one loaded through `invoices/batch` for this same company, in an earlier request: an invoice
        loaded any other way, of another company, not loaded yet or deleted is a 422 `unknown_reference`, and a `draft`
        or `cancelled` invoice is a 422 `invoice_not_payable`. A payment already loaded cannot move to another invoice:
        that is a 422 `invoice_payment_invoice_change`. Payments cannot be deleted through the API: they go away with
        their invoice. If the invoice is deleted and later sent again, send its payments again to restore them; they
        come back with the same `internalId`.


        `dedicatedAmount` is in the currency of the invoice. When the payment has the currency of the invoice,
        `dedicatedAmount` cannot be greater than `amount`; with another currency they are not compared. Overpaying an
        invoice is accepted.


        The payment status of an invoice is still the one its `status` declares; the payment date of a `paid` invoice is
        derived from its payments (the latest `paymentDate`), in the same request. A batch of payments changes nothing
        in the materialized reports, so `reportsVisibility` is normally `visible`; the receivables aging and the
        payment-days series may take up to 5 minutes to show a new payment.


        Allowed only when the company's invoices are fed by the API; otherwise 409. Validation is atomic: if any record
        is invalid, the response is 422 and nothing is written. The load itself is not atomic; after a 5xx or a timeout,
        send the same batch again: records already loaded come back as `unchanged`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Invoice payments
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's invoices are not
            fed by the API; `details.reason` is `source_not_assigned` (the company has no invoice source) or
            `source_not_api` (it has one, and none is the API). `batch_in_progress`: another batch of the company is
            loading; no `details`. `idempotency_key_in_progress`: a write with the same `Idempotency-Key` is still
            running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 records).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose invoice payments are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: >-
                    The payment applications, one per payment and invoice. An `externalId` may appear only once in the
                    batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of this application of a payment to an invoice: its identity within the
                          company. No control characters and no leading or trailing whitespace.
                      invoiceExternalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          `externalId` of the invoice, an invoice loaded through `invoices/batch` for this same company.
                          Any other invoice is a 422 `unknown_reference`.
                      paymentDate:
                        type: string
                        format: date
                        description: >-
                          Date of the payment, `YYYY-MM-DD`. A date-time is a 422. A date in a closed month or more than
                          24 months ahead is loaded with a warning.
                      amount:
                        type: string
                        pattern: ^\d{1,13}(\.\d{1,2})?$
                        description: >-
                          Total amount of the payment, in `currency`, as a decimal string greater than zero: up to 13
                          integer digits and up to 2 decimals, with a dot (`"1500.25"`). No sign. A JSON number is a
                          422.
                      currency:
                        type: string
                        pattern: ^[A-Z]{3}$
                        description: Currency of the payment, three uppercase letters (ISO 4217, `EUR`).
                      dedicatedAmount:
                        type: string
                        pattern: ^\d{1,13}(\.\d{1,2})?$
                        description: >-
                          Part of the payment applied to this invoice, in the currency of the INVOICE, with the format
                          of `amount` (greater than zero). With the currency of the invoice, it cannot be greater than
                          `amount`.
                    required:
                      - externalId
                      - invoiceExternalId
                      - paymentDate
                      - amount
                      - currency
                      - dedicatedAmount
              required:
                - items
            examples:
              full_payment:
                summary: The full payment of an invoice
                value:
                  items:
                    - externalId: example-payment-0001:example-invoice-2026-0001
                      invoiceExternalId: example-invoice-2026-0001
                      paymentDate: '2026-10-05'
                      amount: '1210.00'
                      currency: EUR
                      dedicatedAmount: '1210.00'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoice-payments/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-invoice-payments-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-payment-0001:example-invoice-2026-0001",
                      "invoiceExternalId": "example-invoice-2026-0001",
                      "paymentDate": "2026-10-05",
                      "amount": "1210.00",
                      "currency": "EUR",
                      "dedicatedAmount": "1210.00"
                    }
                  ]
                }'
  /v1/companies/{companyId}/invoices:
    get:
      operationId: listInvoices
      summary: List invoices
      description: >-
        The invoices written to this company with `POST /v1/companies/{companyId}/invoices/batch`, one summary per
        record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It returns nothing
        else of the record. Use it to reconcile what Fibady holds with your own system. Any client of the account with
        access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: an invoice that another source loaded (manual entry, a CSV
        import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Invoices
      responses:
        '200':
          description: One page of invoices.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose invoices are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoices'
  /v1/companies/{companyId}/invoices/{externalId}:
    get:
      operationId: getInvoice
      summary: Get invoice state
      description: >-
        Whether an invoice written to this company with `POST /v1/companies/{companyId}/invoices/batch` is in Fibady,
        looked up by the `externalId` it was written with: `projected`, with its `internalId`, or `pending`. It returns
        nothing else of the record. Use it to confirm a record after a write whose result you could not read, before
        sending it again. Any client of the account with access to the company can read it, not only the one that wrote
        it.


        Only records written through the API are found: an invoice that another source loaded (manual entry, a CSV
        import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company, belongs to a record of another kind (a journal entry's `externalId` asked for as an
        invoice) or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Ask with the `externalId` exactly as it was written, with no prefix: the response returns it the same way.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Invoices
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such invoice written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record of another
            kind or from another source, or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the invoice was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoices/example-invoice-2026-0001'
  /v1/companies/{companyId}/invoices/batch:
    post:
      operationId: upsertInvoices
      summary: Upsert invoices
      description: >-
        Upserts up to 200 invoices of the company, with up to 2,000 lines in the whole batch. An invoice is identified
        by its `externalId`, which is scoped to the company of the path. An invoice loaded by anything other than the
        API (an ERP connector, a CSV import, manual entry) is never matched or changed, even with the same
        `invoiceNumber` and `invoiceType`: sending it creates another invoice. Sending the `externalId` of an invoice
        that was deleted in Fibady restores it.


        Every upsert replaces the whole invoice: an optional field that is not sent, or is `null`, is cleared, and the
        lines are replaced by position (line `i` of the batch is line `i` of the invoice; lines beyond the last one sent
        are removed). Reordering or removing lines changes their identity. An empty or blank string is not a way to
        clear a field: it is a 422.


        An invoice in a currency other than the company's is loaded with the warning `fx_rate_missing` when there is no
        closing exchange rate in Fibady to convert it, on any date, directly or through USD. It does not block the
        invoice: until that rate is loaded in Fibady its amounts count unconverted in the reports and are flagged in the
        company's data status. Once the rate is loaded, the invoice is converted automatically if it is dated in the
        month the rate is loaded or later, or in the three months before it, or if the Fibady account uses its own
        exchange rates. An older invoice may stay unconverted, and sending it again unchanged does not convert it, since
        it comes back `unchanged`.


        The payment status of an invoice is derived, never accepted: `paid` invoices are paid and every other `status`
        is unpaid. The payment date is not part of the invoice: until payments are loaded, a `paid` invoice has no
        payment date. Header amounts are not checked against the sum of the lines. The sign of a refund is given by
        `invoiceType`; positive amounts are recommended.


        Allowed only when the company's invoices are fed by the API; otherwise 409. Validation is atomic: if any record
        is invalid, the response is 422 and nothing is written. The load itself is not atomic; after a 5xx or a timeout,
        send the same batch again: records already loaded come back as `unchanged`, and their payment status is derived
        again.


        `reportsVisibility` may stay `pending` after an invoice batch, until the reports of the whole company are
        rebuilt, which in a large company may not happen soon. It is not a load failure: do not wait for `visible` to
        consider a batch loaded.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Invoices
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's invoices are not
            fed by the API; `details.reason` is `source_not_assigned` (the company has no invoice source) or
            `source_not_api` (it has one, and none is the API). `batch_in_progress`: another batch of the company is
            loading; no `details`. `idempotency_key_in_progress`: a write with the same `Idempotency-Key` is still
            running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: >-
            `api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 200 invoices, or
            more than 2,000 lines in the whole batch).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose invoices are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 200
                  description: >-
                    The invoices. An `externalId` may appear only once in the batch. The lines of all the invoices of a
                    batch add up to 2,000 at most.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the invoice: its identity within the company. No control characters and no
                          leading or trailing whitespace. Send it as is: Fibady never shows it with a prefix.
                      invoiceType:
                        type: string
                        description: >-
                          Kind of invoice: issued to a customer, received from a supplier, or a refund of either. Any
                          other value is a 422.
                        enum:
                          - customer_invoice
                          - supplier_invoice
                          - customer_refund
                          - supplier_refund
                      invoiceNumber:
                        type: string
                        minLength: 1
                        maxLength: 255
                        pattern: \S
                        description: >-
                          The number of the invoice, as shown on the document. No control characters. It does not need
                          to be unique.
                      invoiceDate:
                        type: string
                        format: date
                        description: >-
                          Date of the invoice, `YYYY-MM-DD`. A date-time is a 422. A date in a closed month or more than
                          24 months ahead is loaded with a warning.
                      dueDate:
                        type:
                          - string
                          - 'null'
                        format: date
                        description: Due date, `YYYY-MM-DD`.
                      partnerExternalId:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        maxLength: 255
                        description: >-
                          `externalId` of the customer or supplier, a partner loaded through `partners/batch` for this
                          same company. A partner loaded any other way, or for another company, is a 422
                          `unknown_reference`. Omitted or `null`: no partner. The invoice keeps its own currency; it
                          takes nothing from the partner.
                      reference:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Free reference of the invoice (an order number, for example).
                      currency:
                        type: string
                        pattern: ^[A-Z]{3}$
                        description: >-
                          Currency of the invoice, three uppercase letters (ISO 4217, `EUR`). Amounts are in this
                          currency.
                      untaxedAmount:
                        type: string
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: >-
                          Amount before taxes, as a decimal string: up to 13 integer digits and up to 2 decimals, with a
                          dot (`"1500.25"`). A JSON number is a 422.
                      taxAmount:
                        type: string
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: Tax amount, with the format of `untaxedAmount`.
                      totalAmount:
                        type: string
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: Total amount, with the format of `untaxedAmount`.
                      withholdingTaxAmount:
                        type:
                          - string
                          - 'null'
                        pattern: ^-?\d{1,13}(\.\d{1,2})?$
                        description: Withholding tax amount, with the format of `untaxedAmount`.
                      status:
                        type: string
                        description: >-
                          Status of the document. `paid` makes the invoice paid; any other status makes it unpaid.
                          `draft` and `cancelled` invoices do not count in the receivables and payables reports. Any
                          other value is a 422.
                        enum:
                          - draft
                          - posted
                          - paid
                          - cancelled
                      journal:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Code of the journal of the invoice. The first time a code is used in the company, a journal
                          with that code is created; it can be renamed in Fibady and the name is kept. The same code
                          used by a journal entry is the same journal. Omitted or `null`: no journal. Send it every
                          time.
                      lines:
                        type: array
                        minItems: 1
                        maxItems: 200
                        description: The lines of the invoice, in order. They replace the stored ones by position.
                        items:
                          type: object
                          additionalProperties: false
                          properties:
                            description:
                              type:
                                - string
                                - 'null'
                              minLength: 1
                              pattern: \S
                              description: Description of the line.
                            quantity:
                              type:
                                - string
                                - 'null'
                              pattern: ^-?\d{1,9}(\.\d{1,6})?$
                              description: >-
                                Quantity, as a decimal string: up to 9 integer digits and up to 6 decimals. Omitted or
                                `null` is `"1"`.
                            unitPrice:
                              type: string
                              pattern: ^-?\d{1,11}(\.\d{1,4})?$
                              description: 'Unit price, as a decimal string: up to 11 integer digits and up to 4 decimals.'
                            untaxedAmount:
                              type: string
                              pattern: ^-?\d{1,13}(\.\d{1,2})?$
                              description: Amount of the line before taxes, with the format of the header amounts.
                            taxAmount:
                              type: string
                              pattern: ^-?\d{1,13}(\.\d{1,2})?$
                              description: Tax amount of the line.
                            totalAmount:
                              type: string
                              pattern: ^-?\d{1,13}(\.\d{1,2})?$
                              description: Total amount of the line.
                            taxRate:
                              type:
                                - string
                                - 'null'
                              pattern: ^\d{1,2}(\.\d{1,4})?$
                              description: 'Tax rate as a fraction, not a percentage: `"0.21"` is 21 %. Up to 4 decimals.'
                            accountCode:
                              type:
                                - string
                                - 'null'
                              minLength: 1
                              maxLength: 255
                              description: >-
                                Code of an account of the company's chart of accounts, loaded in any way. An unknown
                                code is a 422 `unknown_reference`; accounts are never created from an invoice.
                          required:
                            - unitPrice
                            - untaxedAmount
                            - taxAmount
                            - totalAmount
                    required:
                      - externalId
                      - invoiceType
                      - invoiceNumber
                      - invoiceDate
                      - currency
                      - untaxedAmount
                      - taxAmount
                      - totalAmount
                      - status
                      - lines
              required:
                - items
            examples:
              customer_invoice:
                summary: An invoice issued to a customer
                value:
                  items:
                    - externalId: example-invoice-2026-0001
                      invoiceType: customer_invoice
                      invoiceNumber: 2026-0001
                      invoiceDate: '2026-09-30'
                      dueDate: '2026-10-30'
                      partnerExternalId: example-partner-0001
                      currency: EUR
                      untaxedAmount: '1000.00'
                      taxAmount: '210.00'
                      totalAmount: '1210.00'
                      status: posted
                      journal: SALES
                      lines:
                        - description: Product A, September deliveries
                          quantity: '10'
                          unitPrice: '100.00'
                          untaxedAmount: '1000.00'
                          taxAmount: '210.00'
                          totalAmount: '1210.00'
                          taxRate: '0.21'
                          accountCode: '7000001'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoices/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-invoices-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-invoice-2026-0001",
                      "invoiceType": "customer_invoice",
                      "invoiceNumber": "2026-0001",
                      "invoiceDate": "2026-09-30",
                      "dueDate": "2026-10-30",
                      "partnerExternalId": "example-partner-0001",
                      "currency": "EUR",
                      "untaxedAmount": "1000.00",
                      "taxAmount": "210.00",
                      "totalAmount": "1210.00",
                      "status": "posted",
                      "journal": "SALES",
                      "lines": [
                        {
                          "description": "Product A, September deliveries",
                          "quantity": "10",
                          "unitPrice": "100.00",
                          "untaxedAmount": "1000.00",
                          "taxAmount": "210.00",
                          "totalAmount": "1210.00",
                          "taxRate": "0.21",
                          "accountCode": "7000001"
                        }
                      ]
                    }
                  ]
                }'
  /v1/companies/{companyId}/invoices/batch-delete:
    post:
      operationId: deleteInvoices
      summary: Delete invoices
      description: >-
        Deletes up to 500 invoices of the company, by the `externalId` they were written with through `invoices/batch`.
        An invoice is deleted with its lines, their analytic allocations and its payments. Only invoices written through
        the API into the company of the path are deleted; an invoice from any other source is never touched, even with
        the same `invoiceNumber`.


        Each requested `externalId` comes back `deleted`, or `not_found` when the company has no live invoice with that
        `externalId` written through the API (it never existed, it was already deleted, or it belongs to another
        company): nothing tells those cases apart. An invoice that was accepted but never written (`unprojectable`) is
        deleted too, with `internalId: null`. After a deletion, `GET` of that `externalId` is a 404, and repeating the
        deletion returns `not_found`.


        Confirmation: when the company has at least 10 live invoices written through the API and the batch would delete
        more than 30% of them, send `expectedDeletes` with the number of items of the batch; without it the response is
        409 `delete_confirmation_required`, with `details.candidateCount` (invoices that would be deleted) and
        `details.universeSize` (live invoices), and nothing is deleted. When sent, `expectedDeletes` must be the number
        of items of the batch, even if no confirmation is needed; otherwise the response is 422
        (`expected_deletes_mismatch`). Use `dryRun=true` to see the outcome first: it answers the same 409.


        Deleting a record dated in a month up to `lastClosedMonth` is not blocked: it carries the warning
        `date_in_closed_period` and recalculates the budget variance and the forecast of that month. The date is the
        stored `invoiceDate` of the invoice.


        Allowed only when the company's invoices are fed by the API; otherwise 409 `source_not_owned`.


        Validation is atomic: if any item is invalid, the response is 422 and nothing is deleted. The deletion itself is
        not atomic: after a 5xx or a timeout, send the same deletion again BEFORE sending any of those invoices through
        `invoices/batch`; a repeated deletion completes it. `reportsVisibility` may stay `pending` after an invoice
        deletion, until the reports of the whole company are rebuilt, which in a large company may not happen soon. It
        is not a deletion failure: do not wait for `visible` to consider the invoices deleted.


        Sending a deleted `externalId` again through `invoices/batch` creates the invoice again (`created`), with the
        same `internalId` while the deleted one has not been purged. Its payments do not come back with it: send them
        again through `invoice-payments/batch`, after the invoice, and they come back `created` with the same
        `internalId`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of deleting again; a key already
        used with another body, or with `invoices/batch`, is a 422 (`idempotency_key_reused`). With `dryRun=true` the
        batch is only checked, with the same warnings, and nothing is deleted.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Invoices
      responses:
        '200':
          description: The invoices were deleted or, with `dryRun=true`, checked.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier deletion with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PublicApiDeleteBatchResult'
                  - $ref: '#/components/schemas/PublicApiDeleteDryRunResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:delete`, which `invoicing:write` does not include; carries `WWW-Authenticate`),
            `api_company_not_allowed` (the client cannot access the company, whether it exists or not) or
            `api_company_param_conflict` (a `companyId` in the query or in the body that does not match the path: the
            company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            Nothing was deleted. `source_not_owned`: the company's invoices are not fed by the API; `details.reason` is
            `source_not_assigned` (the company has no invoice source) or `source_not_api` (it has one, and none is the
            API). `delete_confirmation_required`: the batch would delete a large share of the company's invoices and
            `expectedDeletes` was not sent; `details.candidateCount` is the number of invoices that would be deleted and
            `details.universeSize` the number of live invoices; send the batch again with `expectedDeletes`.
            `batch_in_progress`: another batch of the company is running; no `details`. `idempotency_key_in_progress`: a
            request with the same `Idempotency-Key` is still running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 items).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some item is invalid, `details` says where, and nothing was deleted.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body or operation; no
            `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: >-
            `internal_server_error`: unexpected failure. The deletion may be partially applied; send the same deletion
            again.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was deleted) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the deletion). After `api_temporarily_unavailable` the
            deletion may be partially applied: send the same deletion again after `Retry-After` seconds. Do not retry
            `public_api_disabled` at that pace: back off exponentially, with at most 5 minutes between attempts, or stop
            and contact Fibady quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:delete
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose invoices are deleted. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` checks the batch and deletes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the deletion idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The invoices to delete. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the invoice, as sent to `invoices/batch`. No control characters and no
                          leading or trailing whitespace.
                    required:
                      - externalId
                expectedDeletes:
                  type:
                    - integer
                    - 'null'
                  description: >-
                    The number of items of the batch, to confirm a large deletion. Omitted or `null` means no
                    confirmation. When sent, it must be exactly the number of items.
              required:
                - items
            examples:
              one_invoice:
                summary: One invoice, confirming how many are deleted
                value:
                  items:
                    - externalId: example-invoice-2026-0001
                  expectedDeletes: 1
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/invoices/batch-delete' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-invoices-batch-delete-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-invoice-2026-0001"
                    }
                  ],
                  "expectedDeletes": 1
                }'
  /v1/companies/{companyId}/journal-entries:
    get:
      operationId: listJournalEntries
      summary: List journal entries
      description: >-
        The journal entries written to this company with `POST /v1/companies/{companyId}/journal-entries/batch`, one
        summary per record: its `externalId`, `projected` (with its `internalId`) or `pending`, and `updatedAt`. It
        returns nothing else of the record. Use it to reconcile what Fibady holds with your own system. Any client of
        the account with access to the company can list them, not only the one that wrote them.


        Only records written through the API are listed: a journal entry that another source loaded (manual entry, a CSV
        import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Journal entries
      responses:
        '200':
          description: One page of journal entries.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose journal entries are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/journal-entries'
  /v1/companies/{companyId}/journal-entries/{externalId}:
    get:
      operationId: getJournalEntry
      summary: Get journal entry state
      description: >-
        Whether a journal entry written to this company with `POST /v1/companies/{companyId}/journal-entries/batch` is
        in Fibady, looked up by the `externalId` it was written with: `projected`, with its `internalId`, or `pending`.
        It returns nothing else of the record. Use it to confirm a record after a write whose result you could not read,
        before sending it again. Any client of the account with access to the company can read it, not only the one that
        wrote it.


        Only records written through the API are found: a journal entry that another source loaded (manual entry, a CSV
        import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company, belongs to a record of another kind (an invoice's `externalId` asked for as a
        journal entry) or is malformed gives the same 404.


        Percent-encode the `externalId` in the path.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Journal entries
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such journal entry written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record of another
            kind or from another source, or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the journal entry was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/journal-entries/example-journal-entry-0001'
  /v1/companies/{companyId}/journal-entries/batch:
    post:
      operationId: upsertJournalEntries
      summary: Upsert journal entries
      description: >-
        Upserts up to 200 journal entries of the company, with up to 2000 lines in the whole batch (the sum of `lines`
        of all its entries; more is a 413). An entry is identified by its `externalId`, scoped to the company of the
        path. Every upsert replaces the WHOLE entry: send it complete each time.


        Lines are identified by their POSITION in `lines`, never by any numbering of yours. Sending fewer lines than
        before removes the extra ones, with their analytic allocations; reordering or trimming lines changes their
        identity, and an allocation stays with its position. Each line goes to the debit or to the credit side, never
        both; the entry must balance (a difference of up to 0.01 is accepted) and cannot be all zeros.


        `accountCode` is the code of an account of the company, whatever its origin; an unknown code, or one that
        matches several stored codes only after trimming spaces, is a 422 (`unknown_reference`). Accounts are never
        created by this operation. `partnerExternalId` must be a partner loaded through `partners/batch` into this same
        company, and `analytics[].value` an analytic value loaded through `analytic-values/batch` into the dimension
        named in `analytics[].dimension`; anything else is a 422 (`unknown_reference`). If one of them is removed
        between the validation and the load, that entry comes back `unprojectable` with code `missing_parent` and the
        rest of the batch is loaded.


        `entryType`: omitted keeps the stored typology (a new entry is born ordinary); a value re-types the WHOLE entry
        when it differs from the stored one; `null` turns it back into an ordinary entry, the only way to undo a wrong
        typology. Re-sending an identical entry does not re-type it: if a user changed its typology in Fibady
        afterwards, that decision stands until the entry changes. When the typology cannot be applied, because a user
        pinned the entry as the starting opening or an omission rule claims it, the entry is loaded with the warning
        `entry_type_not_applied`.


        `journal` is the code of the journal of the entry. Its first use creates one journal with that code and name in
        the company, shared by journal entries and invoices; renaming it in Fibady is kept. An entry sent without
        `journal` is left without a journal: send it every time.


        `analytics` on a line replaces ALL the analytic allocations of that line, in every dimension. Omitted or `null`
        keeps the stored ones; `[]` removes them. Within a line and dimension the percentages add up to 100 at most and
        are never rescaled; on a `tag` dimension each one is 100.


        `currency` is optional and, when sent, must be the company's currency: amounts are always in the company's
        currency. An entry dated before the company's history start is a 422 (`before_history_start`). Warnings, which
        do not block the entry: `date_in_closed_period` (its date, or its stored date, falls in a closed month),
        `date_far_future` (more than 24 months ahead) and `entry_type_not_applied`.


        Allowed only when the company's journal entries are fed by the API; otherwise 409 `source_not_owned`.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. `reportsVisibility` tells when the months the batch touched are in the reports; it may stay
        `pending` for a while, and that is not a failure of the load: when the batch leaves a month without entries,
        while the company has no reports built yet (no classified chart of accounts or no exchange rates in its scope),
        or until its refresh runs. Months more than 24 months ahead do not count. Do not wait for `visible` to consider
        a batch loaded.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated, with the same warnings, and nothing is written (no journal is created
        either).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Journal entries
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's journal entries
            are not fed by the API; `details.reason` is `source_not_assigned` (they have no source) or `source_not_api`
            (their source is not the API). `batch_in_progress`: another batch of the company is loading; no `details`.
            `idempotency_key_in_progress`: a write with the same `Idempotency-Key` is still running; no `details`. The
            last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: >-
            `api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 200 entries, or
            more than 2000 lines in the whole batch).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose journal entries are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 200
                  description: >-
                    The journal entries. An `externalId` may appear only once in the batch, and the batch carries up to
                    2000 lines in total.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the entry: its identity within the company. No control characters and no
                          leading or trailing whitespace.
                      date:
                        type: string
                        format: date
                        description: Accounting date of the entry, `YYYY-MM-DD`.
                      description:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Description of the entry. It cannot be empty or blank; omit it or send `null` to leave it out.
                      entryType:
                        type:
                          - string
                          - 'null'
                        enum:
                          - opening
                          - closing
                          - regularization
                          - other
                          - null
                        description: >-
                          Year-end typology. Omitted keeps the stored one (a new entry is ordinary); a value re-types
                          the whole entry; `null` makes it ordinary again.
                      journal:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Code of the journal. No control characters and no leading or trailing whitespace. Omitted or
                          `null` leaves the entry without a journal.
                      currency:
                        type:
                          - string
                          - 'null'
                        pattern: ^[A-Z]{3}$
                        description: >-
                          Currency of the amounts. Optional; when sent it must be the company's currency (three
                          uppercase letters).
                      lines:
                        type: array
                        minItems: 2
                        maxItems: 500
                        description: The lines of the entry, identified by their position.
                        items:
                          type: object
                          additionalProperties: false
                          properties:
                            accountCode:
                              type: string
                              minLength: 1
                              maxLength: 255
                              description: Code of an account of the company, of any origin.
                            description:
                              type:
                                - string
                                - 'null'
                              minLength: 1
                              pattern: \S
                              description: >-
                                Description of the line. It cannot be empty or blank; omitted or `null`, the line shows
                                the description of the entry.
                            debit:
                              type:
                                - string
                                - 'null'
                              pattern: ^\d{1,13}(\.\d{1,2})?$
                              description: >-
                                Debit amount, as a decimal string without sign: up to 13 integer digits and up to 2
                                decimals, with a dot (`"1500.25"`). A JSON number is a 422. Omitted or `null` is `0`.
                            credit:
                              type:
                                - string
                                - 'null'
                              pattern: ^\d{1,13}(\.\d{1,2})?$
                              description: Credit amount, with the same format as `debit`. Omitted or `null` is `0`.
                            partnerExternalId:
                              type:
                                - string
                                - 'null'
                              minLength: 1
                              maxLength: 255
                              description: >-
                                `externalId` of a partner loaded through `partners/batch` into this company. Omitted or
                                `null` leaves the line without a partner.
                            dueDate:
                              type:
                                - string
                                - 'null'
                              format: date
                              description: Due date of the line, `YYYY-MM-DD`.
                            analytics:
                              type:
                                - array
                                - 'null'
                              description: >-
                                Analytic allocations of the line. They replace all the stored ones of the line, in every
                                dimension; omitted or `null` keeps them, `[]` removes them.
                              items:
                                type: object
                                additionalProperties: false
                                properties:
                                  dimension:
                                    type: string
                                    minLength: 1
                                    maxLength: 64
                                    description: Name of the dimension, as sent to `analytic-values/batch`.
                                  value:
                                    type: string
                                    minLength: 1
                                    maxLength: 255
                                    description: '`externalId` of an analytic value of that dimension.'
                                  percentage:
                                    type:
                                      - string
                                      - 'null'
                                    pattern: ^\d{1,3}(\.\d{1,3})?$
                                    description: >-
                                      Share of the line, greater than 0 and up to 100, with up to 3 decimals
                                      (`"33.333"`). Omitted or `null` is `100`. Within a line and dimension the shares
                                      add up to 100 at most; on a `tag` dimension each one is 100.
                                required:
                                  - dimension
                                  - value
                          required:
                            - accountCode
                    required:
                      - externalId
                      - date
                      - lines
              required:
                - items
            examples:
              rent_and_payment:
                summary: A supplier's rent, split by region, and its payment
                value:
                  items:
                    - externalId: example-journal-entry-0001
                      date: '2026-09-30'
                      description: Office rent, September
                      journal: PURCHASES
                      lines:
                        - accountCode: '6210001'
                          debit: '800.00'
                          analytics:
                            - dimension: Region
                              value: example-region-north
                              percentage: '60'
                            - dimension: Region
                              value: example-region-south
                              percentage: '40'
                        - accountCode: '4000001'
                          credit: '800.00'
                          partnerExternalId: example-partner-0002
                          dueDate: '2026-10-10'
                    - externalId: example-journal-entry-0002
                      date: '2026-10-01'
                      description: Payment of the office rent, September
                      journal: BANK
                      lines:
                        - accountCode: '4000001'
                          debit: '800.00'
                          partnerExternalId: example-partner-0002
                        - accountCode: '5720001'
                          credit: '800.00'
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/journal-entries/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-journal-entries-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-journal-entry-0001",
                      "date": "2026-09-30",
                      "description": "Office rent, September",
                      "journal": "PURCHASES",
                      "lines": [
                        {
                          "accountCode": "6210001",
                          "debit": "800.00",
                          "analytics": [
                            {
                              "dimension": "Region",
                              "value": "example-region-north",
                              "percentage": "60"
                            },
                            {
                              "dimension": "Region",
                              "value": "example-region-south",
                              "percentage": "40"
                            }
                          ]
                        },
                        {
                          "accountCode": "4000001",
                          "credit": "800.00",
                          "partnerExternalId": "example-partner-0002",
                          "dueDate": "2026-10-10"
                        }
                      ]
                    },
                    {
                      "externalId": "example-journal-entry-0002",
                      "date": "2026-10-01",
                      "description": "Payment of the office rent, September",
                      "journal": "BANK",
                      "lines": [
                        {
                          "accountCode": "4000001",
                          "debit": "800.00",
                          "partnerExternalId": "example-partner-0002"
                        },
                        {
                          "accountCode": "5720001",
                          "credit": "800.00"
                        }
                      ]
                    }
                  ]
                }'
  /v1/companies/{companyId}/journal-entries/batch-delete:
    post:
      operationId: deleteJournalEntries
      summary: Delete journal entries
      description: >-
        Deletes up to 500 journal entries of the company, by the `externalId` they were written with through
        `journal-entries/batch`. An entry is deleted WHOLE: its header, all its lines and their analytic allocations.
        Only entries written through the API into the company of the path are deleted; an entry from any other source is
        never touched.


        Each requested `externalId` comes back `deleted`, or `not_found` when the company has no live entry with that
        `externalId` written through the API (it never existed, it was already deleted, or it belongs to another
        company): nothing tells those cases apart. An entry that was accepted but never written (`unprojectable`) is
        deleted too, with `internalId: null`. After a deletion, `GET` of that `externalId` is a 404, and repeating the
        deletion returns `not_found`.


        Confirmation: when the company has at least 10 live journal entries written through the API and the batch would
        delete more than 30% of them, send `expectedDeletes` with the number of items of the batch; without it the
        response is 409 `delete_confirmation_required`, with `details.candidateCount` (entries that would be deleted)
        and `details.universeSize` (live entries), and nothing is deleted. When sent, `expectedDeletes` must be the
        number of items of the batch, even if no confirmation is needed; otherwise the response is 422
        (`expected_deletes_mismatch`). Use `dryRun=true` to see the outcome first: it answers the same 409.


        Deleting a record dated in a month up to `lastClosedMonth` is not blocked: it carries the warning
        `date_in_closed_period` and recalculates the budget variance and the forecast of that month.


        Allowed only when the company's journal entries are fed by the API; otherwise 409 `source_not_owned`.


        Validation is atomic: if any item is invalid, the response is 422 and nothing is deleted. The deletion itself is
        not atomic: after a 5xx or a timeout, send the same deletion again BEFORE sending any of those entries through
        `journal-entries/batch`; a repeated deletion completes it. `reportsVisibility` tells when the months of the
        deleted entries are out of the reports; it may stay `pending` for a while, and that is not a failure of the
        deletion. Months more than 24 months ahead do not count.


        Sending a deleted `externalId` again through `journal-entries/batch` creates the entry again (`created`), with
        the same `internalId` while the deleted one has not been purged.


        Send an `Idempotency-Key` to make a retry return the stored response instead of deleting again; a key already
        used with another body, or with `journal-entries/batch`, is a 422 (`idempotency_key_reused`). With `dryRun=true`
        the batch is only checked, with the same warnings, and nothing is deleted.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Journal entries
      responses:
        '200':
          description: The entries were deleted or, with `dryRun=true`, checked.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier deletion with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PublicApiDeleteBatchResult'
                  - $ref: '#/components/schemas/PublicApiDeleteDryRunResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `accounting:delete`, which `accounting:write` does not include; carries
            `WWW-Authenticate`), `api_company_not_allowed` (the client cannot access the company, whether it exists or
            not) or `api_company_param_conflict` (a `companyId` in the query or in the body that does not match the
            path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            Nothing was deleted. `source_not_owned`: the company's journal entries are not fed by the API;
            `details.reason` is `source_not_assigned` (they have no source) or `source_not_api` (their source is not the
            API). `delete_confirmation_required`: the batch would delete a large share of the company's journal entries
            and `expectedDeletes` was not sent; `details.candidateCount` is the number of entries that would be deleted
            and `details.universeSize` the number of live entries; send the batch again with `expectedDeletes`.
            `batch_in_progress`: another batch of the company is running; no `details`. `idempotency_key_in_progress`: a
            request with the same `Idempotency-Key` is still running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 items).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some item is invalid, `details` says where, and nothing was deleted.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body or operation; no
            `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: >-
            `internal_server_error`: unexpected failure. The deletion may be partially applied; send the same deletion
            again.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was deleted) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the deletion). After `api_temporarily_unavailable` the
            deletion may be partially applied: send the same deletion again after `Retry-After` seconds. Do not retry
            `public_api_disabled` at that pace: back off exponentially, with at most 5 minutes between attempts, or stop
            and contact Fibady quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - accounting:delete
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose journal entries are deleted. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` checks the batch and deletes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the deletion idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The journal entries to delete. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the entry, as sent to `journal-entries/batch`. No control characters and no
                          leading or trailing whitespace.
                    required:
                      - externalId
                expectedDeletes:
                  type:
                    - integer
                    - 'null'
                  description: >-
                    The number of items of the batch, to confirm a large deletion. Omitted or `null` means no
                    confirmation. When sent, it must be exactly the number of items.
              required:
                - items
            examples:
              one_entry:
                summary: One journal entry, confirming how many are deleted
                value:
                  items:
                    - externalId: example-journal-entry-0002
                  expectedDeletes: 1
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/journal-entries/batch-delete' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-journal-entries-batch-delete-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-journal-entry-0002"
                    }
                  ],
                  "expectedDeletes": 1
                }'
  /v1/companies/{companyId}/partners:
    get:
      operationId: listPartners
      summary: List partners
      description: >-
        The partners (customers and suppliers) written to this company with `POST
        /v1/companies/{companyId}/partners/batch`, one summary per record: its `externalId`, `projected` (with its
        `internalId`) or `pending`, and `updatedAt`. It returns nothing else of the record. Use it to reconcile what
        Fibady holds with your own system. Any client of the account with access to the company can list them, not only
        the one that wrote them.


        Only records written through the API are listed: a partner that another source loaded (manual entry, a CSV
        import, an ERP connector) is not. Deleted records are not listed either.


        Records come in an internal order, not by `externalId`. Paginate with `cursor` until `nextCursor` is `null`; a
        page may be shorter than `limit`, even empty, with a non-null `nextCursor`.


        For an incremental sync, pass `updatedSince` to get only the records changed since then. Every page of a pass
        returns the same `asOf`: start the next pass with `updatedSince` set to it, and no change in between is missed
        (a record may appear in two passes). `updatedSince` needs a time zone offset: send `Z`, or percent-encode the
        `+` of a positive offset as `%2B` (an unencoded `+` reads as a space and gives 400).


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Partners
      responses:
        '200':
          description: One page of partners.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordPage'
        '400':
          description: >-
            `api_invalid_query`: an unknown or repeated query parameter, a `limit` that is not an integer from 1 to 500,
            an `updatedSince` that is not an RFC 3339 date-time with a time zone offset, or a `cursor` that is
            malformed, comes from another list or company, or is sent with another `updatedSince`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose partners are listed. It must be one the client can access.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Page size, 1 to 500. Defaults to 500.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 500
        - name: cursor
          in: query
          required: false
          description: >-
            Opaque `nextCursor` from the previous page of the same list and company. With it, `updatedSince` may be
            omitted; if sent, it must be the value the pass started with.
          schema:
            type: string
            maxLength: 2048
        - name: updatedSince
          in: query
          required: false
          description: >-
            Only records changed at or after this instant: an RFC 3339 date-time with a time zone offset (`Z` or
            `±HH:MM`) and up to 6 fractional digits, such as `2026-10-10T08:15:30.123456Z`. Percent-encode `+` as `%2B`.
            A future instant gives an empty last page.
          schema:
            type: string
            format: date-time
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/partners'
  /v1/companies/{companyId}/partners/{externalId}:
    get:
      operationId: getPartner
      summary: Get partner state
      description: >-
        Whether a partner (a customer or a supplier) written to this company with `POST
        /v1/companies/{companyId}/partners/batch` is in Fibady, looked up by the `externalId` it was written with:
        `projected`, with its `internalId`, or `pending`. It returns nothing else of the record. Use it to confirm a
        record after a write whose result you could not read, before sending it again. Any client of the account with
        access to the company can read it, not only the one that wrote it.


        Only records written through the API are found: a partner that another source loaded (manual entry, a CSV
        import, an ERP connector) gives 404 even if it carries the same identifier. An `externalId` that does not exist,
        belongs to another company or is malformed gives the same 404. The same `externalId` in two companies are two
        different partners.


        Percent-encode the `externalId` in the path.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Partners
      responses:
        '200':
          description: The state of the record.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRecordStatus'
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such partner written through the API for this company. The same
            response for an `externalId` that does not exist, belongs to another company, belongs to a record from
            another source or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the record was written to. It must be one the client can access.
          schema:
            type: string
        - name: externalId
          in: path
          required: true
          description: >-
            The `externalId` the partner was written with, percent-encoded: 1 to 255 characters, without control
            characters and without leading or trailing whitespace.
          schema:
            type: string
            minLength: 1
            maxLength: 255
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/partners/example-partner-0001'
  /v1/companies/{companyId}/partners/batch:
    post:
      operationId: upsertPartners
      summary: Upsert partners
      description: >-
        Upserts up to 500 partners of the company: its customers, its suppliers, or both. A partner is identified by its
        `externalId`, which is scoped to the company of the path: the same `externalId` sent to two companies creates
        two different partners, and a batch never changes the partners of another company. A partner loaded by anything
        other than the API (an ERP connector, a CSV import, manual entry) is never matched or changed, even with the
        same tax identifier or name: sending it creates another partner.


        Every upsert writes the whole partner: an optional field that is not sent, or is `null`, is cleared. An empty or
        blank string is not a way to clear a field: it is a 422. Partners are never deleted through this operation; send
        `active: false` to deactivate one. A partner that was deleted in Fibady is restored when a batch writes it
        again.


        Allowed only when the company's journal entries or invoices are fed by the API and neither comes from an ERP
        connector; otherwise 409.


        Validation is atomic: if any record is invalid, the response is 422 and nothing is written. The load itself is
        not atomic; after a 5xx or a timeout, send the same batch again: records already loaded come back as
        `unchanged`. A batch of partners changes nothing in the reports, so `reportsVisibility` is `visible`.


        Send an `Idempotency-Key` to make a retry return the stored response instead of loading again. With
        `dryRun=true` the batch is only validated.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Error messages follow
        `Accept-Language` (English by default).
      tags:
        - Partners
      responses:
        '200':
          description: The batch was loaded or, with `dryRun=true`, validated.
          headers:
            Idempotent-Replayed:
              description: >-
                Present, with `true`, only when the response is the replay of an earlier write with the same
                `Idempotency-Key` and the same body. The body of a replay is the stored one, except `reportsVisibility`
                and `postProcessing`, which are computed again from the run.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiBatchResult'
        '400':
          description: >-
            `api_invalid_query` (a query parameter other than `dryRun`, a repeated one, or a `dryRun` that is not `true`
            or `false`) or `api_invalid_json` (the body is not valid JSON).
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `invoicing:write`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` in the
            query or in the body that does not match the path: the company is always the one of the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '409':
          description: >-
            The batch cannot be loaded now, and nothing was written. `source_not_owned`: the company's accounting is not
            fed by the API; `details.reason` is `source_not_assigned` (neither journal entries nor invoices have a
            source) or `source_not_api` (they have one, and none is the API). `erp_api_mix_not_supported`: one of the
            two is fed by the API and the other by an ERP connector; `details` is empty. `batch_in_progress`: another
            batch of the company is loading; no `details`. `idempotency_key_in_progress`: a write with the same
            `Idempotency-Key` is still running; no `details`. The last two carry `Retry-After`.
          headers:
            Retry-After:
              description: Only with `batch_in_progress` and `idempotency_key_in_progress`.
              schema:
                type: integer
                minimum: 1
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiErrorWithDetails'
              examples:
                batch_in_progress:
                  $ref: '#/components/examples/PublicApiBatchInProgress'
        '413':
          description: '`api_payload_too_large` (the body is larger than 1 MiB) or `api_batch_too_large` (more than 500 records).'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '422':
          description: >-
            `validation_failed`: some record is invalid, `details` says where, and nothing was written.
            `idempotency_key_reused`: the `Idempotency-Key` was already used with a different body; no `details`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiValidationError'
              examples:
                validation_failed:
                  $ref: '#/components/examples/PublicApiValidationFailed'
        '429':
          description: >-
            `api_rate_limited` (too many requests for the account or the client), `api_quota_exceeded` (the account's
            daily write quota is used up) or `api_concurrent_requests_exceeded` (too many writes of the account in
            flight). Retry after `Retry-After` seconds.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. The batch may be partially loaded; send it again.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off; nothing was written) or `api_temporarily_unavailable` (a dependency
            failed, or there was not enough time left to start the load). After `api_temporarily_unavailable` the batch
            may be partially loaded: send it again after `Retry-After` seconds. Do not retry `public_api_disabled` at
            that pace: back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady
            quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - invoicing:write
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company whose partners are written. It must be one the client can access.
          schema:
            type: string
        - name: dryRun
          in: query
          required: false
          description: >-
            `true` validates the batch and writes nothing: no run, no quota. Only `true` or `false`; any other value, or
            any other query parameter, is a 400.
          schema:
            type: boolean
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Makes the write idempotent: the same key with the same body returns the stored response, with
            `Idempotent-Replayed: true`. 1 to 255 printable ASCII characters, no spaces. Ignored with `dryRun=true`.
          schema:
            type: string
            pattern: ^[\x21-\x7E]{1,255}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  description: The partners. An `externalId` may appear only once in the batch.
                  items:
                    type: object
                    additionalProperties: false
                    properties:
                      externalId:
                        type: string
                        minLength: 1
                        maxLength: 255
                        description: >-
                          Your identifier of the partner: its identity within the company. No control characters and no
                          leading or trailing whitespace.
                      type:
                        type: string
                        description: Whether the partner is a customer (`client`), a supplier or both. Any other value is a 422.
                        enum:
                          - client
                          - supplier
                          - both
                      name:
                        type: string
                        minLength: 1
                        pattern: \S
                        description: Partner name. Stored as sent; it cannot be empty or blank.
                      taxId:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Tax identifier (VAT number, NIF, CUIT). Stored as sent, without validating its format.
                      isCompany:
                        type:
                          - boolean
                          - 'null'
                        default: false
                        description: '`true` for a legal entity, `false` for an individual. Omitted or `null` is `false`.'
                      email:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Contact email. Stored as sent, without validating its format.
                      phone:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Contact phone. Stored as sent.
                      address:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Postal address, as a single line.
                      country:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Country. Free text, stored as sent.
                      city:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: City. Free text, stored as sent.
                      accountNumber:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Bank account of the partner (an IBAN, for example). Stored as sent.
                      activity:
                        type:
                          - string
                          - 'null'
                        minLength: 1
                        pattern: \S
                        description: Business activity of the partner. Free text.
                      paymentTermsDays:
                        type:
                          - integer
                          - 'null'
                        minimum: 0
                        maximum: 3650
                        description: Payment terms, in days. An integer; `0` is payment on receipt.
                      active:
                        type:
                          - boolean
                          - 'null'
                        default: true
                        description: '`false` deactivates the partner. Omitted or `null` is `true`.'
                    required:
                      - externalId
                      - type
                      - name
              required:
                - items
            examples:
              customer_and_supplier:
                summary: A customer and a supplier
                value:
                  items:
                    - externalId: example-partner-0001
                      type: client
                      name: Example Customer
                      taxId: 99999999R
                      isCompany: false
                      email: billing@example.com
                      country: ES
                      paymentTermsDays: 30
                    - externalId: example-partner-0002
                      type: supplier
                      name: Example Supplies GmbH
                      isCompany: true
                      email: invoices@example.com
                      country: DE
                      accountNumber: DE89370400440532013000
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/partners/batch' \
                -H 'Content-Type: application/json' \
                -H 'Idempotency-Key: example-partners-batch-0001' \
                --data-binary '{
                  "items": [
                    {
                      "externalId": "example-partner-0001",
                      "type": "client",
                      "name": "Example Customer",
                      "taxId": "99999999R",
                      "isCompany": false,
                      "email": "billing@example.com",
                      "country": "ES",
                      "paymentTermsDays": 30
                    },
                    {
                      "externalId": "example-partner-0002",
                      "type": "supplier",
                      "name": "Example Supplies GmbH",
                      "isCompany": true,
                      "email": "invoices@example.com",
                      "country": "DE",
                      "accountNumber": "DE89370400440532013000"
                    }
                  ]
                }'
  /v1/companies/{companyId}/runs/{runId}:
    get:
      operationId: getRun
      summary: Get a run
      description: >-
        The run of a write to this company: its status, its per-record results once it has finished, whether what it
        changed is already in the reports (`reportsVisibility`) and whether it is already in the import history
        (`postProcessing`). Any client of the account with access to the company can read it, not only the one that
        created it. Poll it with backoff until `status` is `succeeded`, `partial` or `failed`; any other value,
        including one this client does not know, means poll again.


        A `runId` that does not exist, belongs to another company or is malformed gives the same 404.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. Error messages follow `Accept-Language` (English by default).
      tags:
        - Runs
      responses:
        '200':
          description: The run.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiRun'
              examples:
                in_progress:
                  summary: A run still loading
                  value:
                    runId: 01K8EXAMP0RVN0000000000001
                    entity: journal-entries
                    operation: upsert
                    status: in_progress
                    startedAt: '2026-10-05T09:30:00.000Z'
                    finishedAt: null
                    errorClass: null
                    reportsVisibility: pending
                    postProcessing: pending
                    counts: null
                    results: null
                succeeded:
                  summary: A finished run
                  value:
                    runId: 01K8EXAMP0RVN0000000000001
                    entity: journal-entries
                    operation: upsert
                    status: succeeded
                    startedAt: '2026-10-05T09:30:00.000Z'
                    finishedAt: '2026-10-05T09:30:02.000Z'
                    errorClass: null
                    reportsVisibility: visible
                    postProcessing: done
                    counts:
                      created: 2
                      updated: 0
                      unchanged: 0
                      unprojectable: 0
                    results:
                      - index: 0
                        externalId: example-journal-entry-0001
                        outcome: created
                        internalId: 00000000-0000-4000-8000-000000000001
                      - index: 1
                        externalId: example-journal-entry-0002
                        outcome: created
                        internalId: 00000000-0000-4000-8000-000000000002
        '400':
          description: '`api_invalid_query`: the operation takes no query parameters, and one was sent.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '401':
          description: >-
            `api_token_missing` (no `Authorization: Bearer` header) or `api_token_invalid` (unknown, expired or revoked
            access credential, or an inactive client). Carries `WWW-Authenticate`. Request a new token.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '402':
          description: '`auth_invalid_or_expired_license`: the account''s license is not valid.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '403':
          description: >-
            `api_origin_forbidden` (the request did not come through the public API endpoint), `api_insufficient_scope`
            (the client lacks `ingestion:read`; carries `WWW-Authenticate`), `api_company_not_allowed` (the client
            cannot access the company, whether it exists or not) or `api_company_param_conflict` (a `companyId` query
            parameter that does not match the path).
          headers:
            WWW-Authenticate:
              description: Only with `api_insufficient_scope`.
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '404':
          description: >-
            `api_resource_not_found`: there is no such run for this company. The same response for a `runId` that does
            not exist, belongs to another company or is malformed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '429':
          description: '`api_rate_limited`: too many requests for the account or the client. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
              examples:
                api_rate_limited:
                  $ref: '#/components/examples/PublicApiRateLimited'
        '500':
          description: '`internal_server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is not enabled for the client's account, during the invitation-only
            preview, or has been switched off) or `api_temporarily_unavailable` (a dependency failed). Retry
            `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry `public_api_disabled` at that pace:
            back off exponentially, with at most 5 minutes between attempts, or stop and contact Fibady quoting
            `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security:
        - oauth2:
            - ingestion:read
      parameters:
        - name: companyId
          in: path
          required: true
          description: Company the run belongs to. It must be one the client can access.
          schema:
            type: string
        - name: runId
          in: path
          required: true
          description: The `runId` returned by the write that created the run.
          schema:
            type: string
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_ACCESS_TOKEN:?set it to the access_token of POST /v1/oauth/token, or to the output of
            fibady_token after the Quickstart}" &&

            printf 'header = "Authorization: Bearer %s"\n' "$FIBADY_ACCESS_TOKEN" \
              | curl -sS -K - 'https://public-api.fibady.com/v1/companies/01K8EXAMP0C0MPANY000000001/runs/01K8EXAMP0RVN0000000000001'
  /v1/oauth/token:
    post:
      operationId: issueAccessToken
      summary: Issue an access token
      description: >-
        Follows RFC 6749 §4.4. The body is `application/x-www-form-urlencoded` (at most 8 KiB), NOT JSON, and errors use
        the RFC's `error` / `error_description` fields rather than the product's `{error, code}` envelope.


        The client authenticates with `client_secret_basic` (the `Authorization: Basic` header, RFC 6749 §2.3.1) or
        `client_secret_post` (`client_id` + `client_secret` in the body), never both. During a rotation the previous
        secret is accepted until its expiry.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 429 and the
        503 carry `Retry-After`. At most 60 tokens per client and hour; a token lasts 10 minutes, so request a new one
        when it expires instead of one per call.
      tags:
        - Access tokens
      responses:
        '200':
          description: Access token issued.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenResponse'
              examples:
                access_token:
                  summary: A token for a client with three scopes
                  value:
                    access_token: fpt_example-not-a-real-token
                    token_type: Bearer
                    expires_in: 600
                    scope: companies:read accounting:read accounting:write
        '400':
          description: >-
            `invalid_request` (wrong `Content-Type`, body over 8 KiB, missing `grant_type`, both authentication methods,
            mismatched `client_id`) or `unsupported_grant_type`.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
        '401':
          description: >-
            `invalid_client`: unknown, inactive or revoked client, wrong secret, no client authentication, or a
            malformed `Authorization: Basic` header. When the client tried Basic the response carries `WWW-Authenticate:
            Basic realm="fibady-public-api"`.
          headers:
            WWW-Authenticate:
              schema:
                type: string
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
        '403':
          description: '`origin_forbidden`: the request did not come through the public API endpoint.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
        '429':
          description: '`rate_limited`: the client exceeded its tokens per hour. Retry after `Retry-After` seconds.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
        '500':
          description: '`server_error`: unexpected failure. Safe to retry with backoff.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
        '503':
          description: >-
            `temporarily_unavailable`: the public API is not available for the client's account, or a dependency failed.
            Wait at least `Retry-After` seconds and retry; if it persists for more than 5 attempts in a row, the API is
            not enabled for the account: back off exponentially, with at most 5 minutes between attempts, or stop and
            contact Fibady quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenError'
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                grant_type:
                  type: string
                  enum:
                    - client_credentials
                client_id:
                  type: string
                  description: >-
                    `client_secret_post` only. With `Authorization: Basic` it may be omitted; if sent it must match the
                    header's.
                client_secret:
                  type: string
                  description: '`client_secret_post` only. Mutually exclusive with `Authorization: Basic`.'
              required:
                - grant_type
      x-codeSamples:
        - lang: Shell
          label: curl
          source: >-
            : "${FIBADY_CLIENT_SECRET:?set it to your client secret, read without echo as in step 2 of the Quickstart}"
            &&

            printf 'user = "%s:%s"\n' 'fapi_ExampleClientId0000000' "$FIBADY_CLIENT_SECRET" \
              | curl -sS -K - -d grant_type=client_credentials 'https://public-api.fibady.com/v1/oauth/token'
  /v1/openapi.json:
    get:
      operationId: getOpenApiDocument
      summary: Get the OpenAPI document
      description: >-
        The machine-readable contract of every `/v1` operation: this same document. It needs no credentials and is
        served whenever the public API is enabled in the environment, whether or not your account is admitted yet.


        Every response carries `X-Request-Id`, `Cache-Control: no-store` and `Pragma: no-cache`. Only the 503 carries
        `Retry-After`.
      tags:
        - OpenAPI document
      responses:
        '200':
          description: The OpenAPI document.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiOpenApiDocument'
        '403':
          description: '`api_origin_forbidden`: the request did not come through the public API endpoint.'
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
        '503':
          description: >-
            `public_api_disabled` (the public API is switched off in this environment) or `api_temporarily_unavailable`
            (a dependency failed). Retry `api_temporarily_unavailable` after `Retry-After` seconds. Do not retry
            `public_api_disabled` at that pace: back off exponentially, with at most 5 minutes between attempts, or stop
            and contact Fibady quoting `X-Request-Id`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseWithCode'
      security: []
      x-codeSamples:
        - lang: Shell
          label: curl
          source: curl -sS 'https://public-api.fibady.com/v1/openapi.json'
components:
  schemas:
    ErrorResponseWithCode:
      type: object
      description: >-
        Error of a resource operation. Branch on `code`, never on `error`: `error` is a human-readable message in the
        language of `Accept-Language` (English by default).
      required:
        - error
        - code
      properties:
        error:
          type: string
        code:
          type: string
          description: 'Machine-readable error code: branch on it. The description of each response lists the codes it can carry.'
    PublicApiErrorWithDetails:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
          description: >-
            Structured context of the error. Its keys depend on `code` and are listed in the description of each
            response; absent for codes that carry no context.
    PublicApiCompanySource:
      type: object
      description: Whether the client could load this kind of data for the company through the public API.
      properties:
        type:
          type: string
          enum:
            - journal_entries
            - invoices
            - bank
        kinds:
          type: array
          description: >-
            Kinds of the sources assigned to this data type, without duplicates and sorted. `api` is a source fed by the
            public API.
          items:
            type: string
            enum:
              - api
              - connector
              - erp
              - manual
        writable:
          type: boolean
          description: >-
            `true` when an `api` source is assigned and the client has the write scope of the type (`accounting:write`,
            `invoicing:write` or `banking:write`).
        reason:
          type:
            - string
            - 'null'
          description: >-
            Why it is not writable, the first that applies: no source assigned, no `api` source, or the client lacks the
            write scope. `null` when `writable`.
          enum:
            - source_not_assigned
            - source_not_api
            - missing_scope
            - null
      required:
        - type
        - kinds
        - writable
        - reason
    PublicApiCompanyCatalog:
      type: object
      description: >-
        Whether the client could write one catalog of the company through the public API. `reason` is `null` when
        `writable`; otherwise it is the first that applies: `source_not_assigned` (no source is assigned to the data the
        catalog depends on), `source_not_api` (a source is assigned, and none is an `api` source),
        `erp_api_mix_not_supported` (journal entries and invoices: one is fed by the API and the other by an ERP
        connector) or `missing_scope` (the client lacks the write scope of the catalog).
      properties:
        writable:
          type: boolean
          description: >-
            `true` when a batch of this catalog would pass the source check and the client has the write scope of the
            catalog. It says nothing about the validation of the records, concurrent batches or rate limits.
        reason:
          type:
            - string
            - 'null'
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. Show it as is; do not branch on
            it.
          enum:
            - source_not_assigned
            - source_not_api
            - erp_api_mix_not_supported
            - missing_scope
            - null
          x-extensible-enum:
            - source_not_assigned
            - source_not_api
            - erp_api_mix_not_supported
            - missing_scope
            - null
      required:
        - writable
        - reason
    PublicApiCompany:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        currency:
          type: string
          description: Functional currency of the company (ISO 4217).
        lastClosedMonth:
          type:
            - string
            - 'null'
          pattern: ^\d{4}-\d{2}$
          description: Last closed month (`YYYY-MM`), or `null` if none.
        sources:
          type: array
          description: One entry per data type, always the three, in this order.
          items:
            $ref: '#/components/schemas/PublicApiCompanySource'
        catalogs:
          type: object
          description: >-
            Whether the client could write each catalog of the company through the public API, always the four keys.
            `accounts`, `partners` and `analyticValues` depend on the sources of journal entries and invoices: at least
            one of the two must be an `api` source, and neither may be fed by an ERP connector. `bankAccounts` depends
            on the `bank` source alone, like the `bank` entry of `sources`. The write scopes are `accounting:write`
            (`accounts` and `analyticValues`), `invoicing:write` (`partners`) and `banking:write` (`bankAccounts`). A
            `reason` about the sources is the `code` (`erp_api_mix_not_supported`) or the `details.reason`
            (`source_not_assigned`, `source_not_api`) of the 409 a batch of that catalog would get; a client without the
            write scope gets the 403 `api_insufficient_scope` first. It reflects the sources assigned when the request
            is served: if they change afterwards, the response of the batch is the one that counts.
          properties:
            accounts:
              $ref: '#/components/schemas/PublicApiCompanyCatalog'
            partners:
              $ref: '#/components/schemas/PublicApiCompanyCatalog'
            analyticValues:
              $ref: '#/components/schemas/PublicApiCompanyCatalog'
            bankAccounts:
              $ref: '#/components/schemas/PublicApiCompanyCatalog'
          required:
            - accounts
            - partners
            - analyticValues
            - bankAccounts
      required:
        - id
        - name
        - currency
        - lastClosedMonth
        - sources
        - catalogs
    PublicApiCompanyPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PublicApiCompany'
        nextCursor:
          type:
            - string
            - 'null'
          description: >-
            Opaque cursor for the next page, or `null` on the last one. A page may be shorter than `limit`, even empty,
            with a non-null `nextCursor`.
      required:
        - items
        - nextCursor
    PublicApiAccount:
      type: object
      description: One live account of the company's chart of accounts, as stored.
      properties:
        code:
          type: string
          description: >-
            Account code, exactly as stored: it is not trimmed, so it may carry leading or trailing spaces when another
            source loaded it that way. Unique among the live accounts of the company.
        name:
          type: string
        origin:
          type: string
          description: >-
            The source that wrote the account last: `manual`, `csv`, `api`, the name of a connector (`odoo`, `holded`)
            or another value. An open string, not an enum: new values appear with new sources.
        internalId:
          type: string
          description: Fibady identifier of the account.
        classificationHint:
          type:
            - string
            - 'null'
          description: Free text that helps classify the account, or `null` if none.
        flowTypeHint:
          type:
            - string
            - 'null'
          description: Free text that helps classify the account's cash flow, or `null` if none.
      required:
        - code
        - name
        - origin
        - internalId
        - classificationHint
        - flowTypeHint
    PublicApiAccountPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PublicApiAccount'
        nextCursor:
          type:
            - string
            - 'null'
          description: >-
            Opaque cursor for the next page, or `null` on the last one. A page is shorter than `limit` only when it is
            the last one.
      required:
        - items
        - nextCursor
    PublicApiRun:
      type: object
      description: >-
        State of one write batch (a run). `status` goes from `in_progress` to `succeeded`, `partial` (some records are
        `unprojectable`) or `failed`. `counts` and `results` are those of the write response and are `null` while the
        run is `in_progress` and when it `failed`. `finishedAt` is `null` while `in_progress`. `errorClass` is `null`
        unless the run `failed`, and is never a message.


        `operation` tells which write created the run. An `upsert` run counts `created`, `updated`, `unchanged` and
        `unprojectable`. A `delete` run (a `batch-delete`) counts `deleted` and `notFound`, carries the four upsert
        counts at 0, and its `results` say `deleted` or `not_found`; the 200 of the `batch-delete` itself publishes only
        `deleted` and `notFound`.


        `reportsVisibility` and `postProcessing` are computed on every request. `reportsVisibility: visible` means that
        what THIS batch changed is already in the reports; an `unchanged` record changed nothing in this batch, so its
        visibility is that of the run that wrote it. `pending` may last. `postProcessing` is `done` once the run is in
        the import history.
      properties:
        runId:
          type: string
          description: Identifier of the run, as returned by the write that created it.
        entity:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is an entity
            or operation this client does not know: ignore the run.
          enum:
            - accounts
            - analytic-values
            - partners
            - bank-accounts
            - journal-entries
            - invoices
            - bank-movements
            - invoice-payments
          x-extensible-enum:
            - accounts
            - analytic-values
            - partners
            - bank-accounts
            - journal-entries
            - invoices
            - bank-movements
            - invoice-payments
        operation:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is an entity
            or operation this client does not know: ignore the run.
          enum:
            - upsert
            - delete
          x-extensible-enum:
            - upsert
            - delete
        status:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - in_progress
            - succeeded
            - partial
            - failed
          x-extensible-enum:
            - in_progress
            - succeeded
            - partial
            - failed
        startedAt:
          type: string
          format: date-time
          description: When the current attempt started. A retry with the same `Idempotency-Key` restarts it.
        finishedAt:
          type:
            - string
            - 'null'
          format: date-time
          description: When the run finished, or `null` while it is `in_progress`.
        errorClass:
          type:
            - string
            - 'null'
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as a
            generic error, the same as `internal`.
          enum:
            - timeout
            - unavailable
            - internal
            - stale_run
            - null
          x-extensible-enum:
            - timeout
            - unavailable
            - internal
            - stale_run
            - null
        reportsVisibility:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - pending
            - visible
          x-extensible-enum:
            - pending
            - visible
        postProcessing:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - pending
            - done
            - failed
          x-extensible-enum:
            - pending
            - done
            - failed
        counts:
          type:
            - object
            - 'null'
          description: >-
            Records of the batch by outcome, or `null` while `in_progress` and when `failed`. Every run carries the four
            upsert counts; a `delete` run carries them at 0, plus `deleted` and `notFound`, which an `upsert` run does
            not carry.
          properties:
            created:
              type: integer
              minimum: 0
            updated:
              type: integer
              minimum: 0
            unchanged:
              type: integer
              minimum: 0
            unprojectable:
              type: integer
              minimum: 0
            deleted:
              type: integer
              minimum: 0
            notFound:
              type: integer
              minimum: 0
          required:
            - created
            - updated
            - unchanged
            - unprojectable
        results:
          type:
            - array
            - 'null'
          description: One entry per record of the batch, in request order, or `null` while `in_progress` and when `failed`.
          items:
            type: object
            properties:
              index:
                type: integer
                minimum: 0
                description: Position of the record in the request.
              externalId:
                type: string
                description: Identity of the record, as sent in the request.
              outcome:
                type: string
                description: >-
                  Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as
                  an unconfirmed result: check the record before retrying it.
                enum:
                  - created
                  - updated
                  - unchanged
                  - unprojectable
                  - deleted
                  - not_found
                x-extensible-enum:
                  - created
                  - updated
                  - unchanged
                  - unprojectable
                  - deleted
                  - not_found
              internalId:
                type:
                  - string
                  - 'null'
                description: >-
                  Fibady identifier of the record. `null` only for an `unprojectable` record that was never written and,
                  in a `delete` run, for a `not_found` record and for a `deleted` one that had been accepted but never
                  written.
              code:
                type: string
                description: >-
                  Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as
                  `other`.
                enum:
                  - missing_account
                  - missing_bank_account
                  - missing_date
                  - bank_account_unlinked
                  - missing_source_id
                  - bank_account_name_exhausted
                  - missing_parent
                  - other
                x-extensible-enum:
                  - missing_account
                  - missing_bank_account
                  - missing_date
                  - bank_account_unlinked
                  - missing_source_id
                  - bank_account_name_exhausted
                  - missing_parent
                  - other
              warnings:
                type: array
                description: Present only when the record has warnings. They do not block it.
                items:
                  type: string
                  description: >-
                    Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value
                    as a generic warning: it does not block the record.
                  enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
                  x-extensible-enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
            required:
              - index
              - externalId
              - outcome
              - internalId
      required:
        - runId
        - entity
        - operation
        - status
        - startedAt
        - finishedAt
        - errorClass
        - reportsVisibility
        - postProcessing
        - counts
        - results
    PublicApiRecordStatus:
      type: object
      description: >-
        State of one record written through the API, looked up by the `externalId` it was written with. `state` is
        `projected` when the record has a Fibady identifier (`internalId`), and `pending` when it was received but never
        applied: it came back `unprojectable` in its batch (the reason is in `results[].code` of that run), or its batch
        did not finish. A `pending` record becomes `projected` when a later write of the same `externalId` applies it.


        `projected` says the record exists, not that the last version sent is the one applied: a later write of the same
        record that came back `unprojectable` leaves it `projected`, with the same `internalId`.
      properties:
        externalId:
          type: string
          description: The identifier the record was written with.
        state:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - projected
            - pending
          x-extensible-enum:
            - projected
            - pending
        internalId:
          type:
            - string
            - 'null'
          description: Fibady identifier of the record, or `null` while it is `pending`.
      required:
        - externalId
        - state
        - internalId
    PublicApiRecordListItem:
      description: >-
        One record of a list of what was written through the API: its state, as `GET …/{externalId}` returns it, plus
        `updatedAt`.
      allOf:
        - $ref: '#/components/schemas/PublicApiRecordStatus'
        - type: object
          properties:
            updatedAt:
              type: string
              format: date-time
              description: >-
                Last time a write through the API changed the record in Fibady (its content, or when it was applied), in
                UTC with microseconds. It does not move when the record is edited inside Fibady.
          required:
            - updatedAt
    PublicApiRecordPage:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PublicApiRecordListItem'
        nextCursor:
          type:
            - string
            - 'null'
          description: >-
            Opaque cursor for the next page, or `null` on the last one. A page may be shorter than `limit`, even empty,
            with a non-null `nextCursor`: keep following it.
        asOf:
          type: string
          format: date-time
          description: >-
            The same in every page of a pass, in UTC with microseconds. Start the next pass with `updatedSince` set to
            this value: a record changed after it is listed again, never missed.
      required:
        - items
        - nextCursor
        - asOf
    PublicApiValidationError:
      type: object
      description: >-
        A 422 of a write. With `validation_failed`, `details` lists up to 100 problems, sorted by item index and then by
        pointer, and NOTHING of the batch was written: fix them and send the whole batch again. With
        `idempotency_key_reused` there is no `details`: the `Idempotency-Key` was already used with a different body.
      properties:
        error:
          type: string
          description: Human-readable message, in the language of `Accept-Language`.
        code:
          type: string
          description: '`validation_failed` or `idempotency_key_reused`.'
        details:
          type: array
          maxItems: 100
          description: Only with `validation_failed`.
          items:
            type: object
            properties:
              pointer:
                type: string
                description: >-
                  JSON Pointer (RFC 6901) to the offending value in the request body, such as `/items/3/name`. Empty for
                  a problem that is not in the body (a malformed `Idempotency-Key`).
              code:
                type: string
                description: >-
                  Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as
                  a generic validation error: the record was not written.
                enum:
                  - duplicate_external_id
                  - currency_not_functional
                  - unbalanced_entry
                  - before_history_start
                  - invalid_amount
                  - invalid_date
                  - invalid_enum
                  - required_field
                  - unknown_field
                  - invalid_currency
                  - unknown_reference
                  - line_count_out_of_range
                  - bank_movement_account_change
                  - analytic_allocation_over_100
                  - analytic_dimension_limit_reached
                  - invalid_external_id
                  - invalid_idempotency_key
                  - invoice_not_payable
                  - invoice_payment_invoice_change
                  - expected_deletes_mismatch
                x-extensible-enum:
                  - duplicate_external_id
                  - currency_not_functional
                  - unbalanced_entry
                  - before_history_start
                  - invalid_amount
                  - invalid_date
                  - invalid_enum
                  - required_field
                  - unknown_field
                  - invalid_currency
                  - unknown_reference
                  - line_count_out_of_range
                  - bank_movement_account_change
                  - analytic_allocation_over_100
                  - analytic_dimension_limit_reached
                  - invalid_external_id
                  - invalid_idempotency_key
                  - invoice_not_payable
                  - invoice_payment_invoice_change
                  - expected_deletes_mismatch
            required:
              - pointer
              - code
      required:
        - error
        - code
    PublicApiBatchResult:
      description: >-
        The 200 of a write batch, in one of two shapes.


        A real write returns the run: `runId`, `status` (`succeeded`, or `partial` when some records are
        `unprojectable`), `counts` and one `results` entry per record, in request order. `reportsVisibility` says
        whether what THIS batch changed is already in the reports; `pending` may last, and `GET
        /v1/companies/{companyId}/runs/{runId}` tells when it turns `visible`. An `unchanged` record changed nothing in
        this batch, so its visibility is that of the run that wrote it. `postProcessing` is `pending` until the run
        reaches the import history.


        With `dryRun=true` nothing is written and there is no run: the response is `dryRun: true` and one `valid` entry
        per record.
      oneOf:
        - type: object
          description: A real write. The batch was loaded and has a run.
          properties:
            runId:
              type: string
              description: Identifier of the run. Use it with `GET /v1/companies/{companyId}/runs/{runId}`.
            status:
              type: string
              description: >-
                Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
                terminal: poll again.
              enum:
                - in_progress
                - succeeded
                - partial
                - failed
              x-extensible-enum:
                - in_progress
                - succeeded
                - partial
                - failed
            counts:
              type: object
              description: Records of the batch by outcome.
              properties:
                created:
                  type: integer
                  minimum: 0
                updated:
                  type: integer
                  minimum: 0
                unchanged:
                  type: integer
                  minimum: 0
                unprojectable:
                  type: integer
                  minimum: 0
              required:
                - created
                - updated
                - unchanged
                - unprojectable
            results:
              type: array
              description: One entry per record of the batch, in request order.
              items:
                type: object
                properties:
                  index:
                    type: integer
                    minimum: 0
                    description: Position of the record in the request.
                  externalId:
                    type: string
                    description: Identity of the record, as sent in the request.
                  outcome:
                    type: string
                    description: >-
                      Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown
                      value as an unconfirmed result: check the record before retrying it.
                    enum:
                      - created
                      - updated
                      - unchanged
                      - unprojectable
                    x-extensible-enum:
                      - created
                      - updated
                      - unchanged
                      - unprojectable
                  internalId:
                    type:
                      - string
                      - 'null'
                    description: Fibady identifier of the record. `null` only for an `unprojectable` record that was never written.
                  code:
                    type: string
                    description: >-
                      Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown
                      value as `other`.
                    enum:
                      - missing_account
                      - missing_bank_account
                      - missing_date
                      - bank_account_unlinked
                      - missing_source_id
                      - bank_account_name_exhausted
                      - missing_parent
                      - other
                    x-extensible-enum:
                      - missing_account
                      - missing_bank_account
                      - missing_date
                      - bank_account_unlinked
                      - missing_source_id
                      - bank_account_name_exhausted
                      - missing_parent
                      - other
                  warnings:
                    type: array
                    description: Present only when the record has warnings. They do not block it.
                    items:
                      type: string
                      description: >-
                        Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown
                        value as a generic warning: it does not block the record.
                      enum:
                        - date_in_closed_period
                        - date_far_future
                        - fx_rate_missing
                        - entry_type_not_applied
                        - before_opening_balance_date
                      x-extensible-enum:
                        - date_in_closed_period
                        - date_far_future
                        - fx_rate_missing
                        - entry_type_not_applied
                        - before_opening_balance_date
                required:
                  - index
                  - externalId
                  - outcome
                  - internalId
            reportsVisibility:
              type: string
              description: >-
                Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
                terminal: poll again.
              enum:
                - pending
                - visible
              x-extensible-enum:
                - pending
                - visible
            postProcessing:
              type: string
              description: >-
                Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
                terminal: poll again.
              enum:
                - pending
                - done
                - failed
              x-extensible-enum:
                - pending
                - done
                - failed
          required:
            - runId
            - status
            - counts
            - results
            - reportsVisibility
            - postProcessing
        - type: object
          description: A `dryRun=true` validation. Nothing was written and there is no run.
          properties:
            dryRun:
              type: boolean
              description: Always `true`.
              enum:
                - true
            results:
              type: array
              description: One entry per record of the batch, in request order.
              items:
                type: object
                properties:
                  index:
                    type: integer
                    minimum: 0
                    description: Position of the record in the request.
                  externalId:
                    type: string
                    description: Identity of the record, as sent in the request.
                  outcome:
                    type: string
                    description: >-
                      Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown
                      value as an unconfirmed result: check the record before retrying it.
                    enum:
                      - valid
                    x-extensible-enum:
                      - valid
                  warnings:
                    type: array
                    description: Present only when the record has warnings. They do not block it.
                    items:
                      type: string
                      description: >-
                        Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown
                        value as a generic warning: it does not block the record.
                      enum:
                        - date_in_closed_period
                        - date_far_future
                        - fx_rate_missing
                        - entry_type_not_applied
                        - before_opening_balance_date
                      x-extensible-enum:
                        - date_in_closed_period
                        - date_far_future
                        - fx_rate_missing
                        - entry_type_not_applied
                        - before_opening_balance_date
                required:
                  - index
                  - externalId
                  - outcome
          required:
            - dryRun
            - results
    PublicApiDeleteBatchResult:
      type: object
      description: >-
        The 200 of a real `batch-delete`: the run of the deletion. `status` is `succeeded`, `counts` says how many
        records were deleted and how many were not found, and there is one `results` entry per requested `externalId`,
        in request order. `reportsVisibility` says whether what THIS deletion changed is already out of the reports;
        `pending` may last, and `GET /v1/companies/{companyId}/runs/{runId}` tells when it turns `visible`.
        `postProcessing` is `pending` until the run reaches the import history.
      properties:
        runId:
          type: string
          description: Identifier of the run. Use it with `GET /v1/companies/{companyId}/runs/{runId}`.
        status:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - in_progress
            - succeeded
            - partial
            - failed
          x-extensible-enum:
            - in_progress
            - succeeded
            - partial
            - failed
        counts:
          type: object
          description: 'Requested records by outcome: `deleted` and `notFound`. May gain new keys in `/v1`.'
          properties:
            deleted:
              type: integer
              minimum: 0
            notFound:
              type: integer
              minimum: 0
          required:
            - deleted
            - notFound
        results:
          type: array
          description: One entry per requested `externalId`, in request order.
          items:
            type: object
            properties:
              index:
                type: integer
                minimum: 0
                description: Position of the record in the request.
              externalId:
                type: string
                description: Identity of the record, as sent in the request.
              outcome:
                type: string
                description: >-
                  Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as
                  an unconfirmed result: check the record before retrying it.
                enum:
                  - deleted
                  - not_found
                x-extensible-enum:
                  - deleted
                  - not_found
              internalId:
                type:
                  - string
                  - 'null'
                description: >-
                  Fibady identifier of the deleted record. `null` for a `not_found` record, and for a `deleted` one that
                  had been accepted but never written.
              warnings:
                type: array
                description: Present only when the record has warnings. They do not block it.
                items:
                  type: string
                  description: >-
                    Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value
                    as a generic warning: it does not block the record.
                  enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
                  x-extensible-enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
            required:
              - index
              - externalId
              - outcome
              - internalId
        reportsVisibility:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - pending
            - visible
          x-extensible-enum:
            - pending
            - visible
        postProcessing:
          type: string
          description: >-
            Closed per version; may gain new values in `/v1`, never lose or rename them. An unknown value is NOT
            terminal: poll again.
          enum:
            - pending
            - done
            - failed
          x-extensible-enum:
            - pending
            - done
            - failed
      required:
        - runId
        - status
        - counts
        - results
        - reportsVisibility
        - postProcessing
    PublicApiDeleteDryRunResult:
      type: object
      description: >-
        The 200 of a `batch-delete` with `dryRun=true`: what the deletion would do. Nothing was deleted and there is no
        run. One `results` entry per requested `externalId`, in request order.
      properties:
        dryRun:
          type: boolean
          description: Always `true`.
          enum:
            - true
        results:
          type: array
          description: One entry per requested `externalId`, in request order.
          items:
            type: object
            properties:
              index:
                type: integer
                minimum: 0
                description: Position of the record in the request.
              externalId:
                type: string
                description: Identity of the record, as sent in the request.
              outcome:
                type: string
                description: >-
                  Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value as
                  an unconfirmed result: check the record before retrying it.
                enum:
                  - would_delete
                  - not_found
                x-extensible-enum:
                  - would_delete
                  - not_found
              internalId:
                type:
                  - string
                  - 'null'
                description: >-
                  Fibady identifier of the record that would be deleted. `null` for a `not_found` record, and for one
                  that was accepted but never written.
              warnings:
                type: array
                description: Present only when the record has warnings. They do not block it.
                items:
                  type: string
                  description: >-
                    Closed per version; may gain new values in `/v1`, never lose or rename them. Treat an unknown value
                    as a generic warning: it does not block the record.
                  enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
                  x-extensible-enum:
                    - date_in_closed_period
                    - date_far_future
                    - fx_rate_missing
                    - entry_type_not_applied
                    - before_opening_balance_date
            required:
              - index
              - externalId
              - outcome
              - internalId
      required:
        - dryRun
        - results
    PublicApiTokenResponse:
      type: object
      description: Access token of the public API (RFC 6749 §5.1). No refresh token.
      properties:
        access_token:
          type: string
          description: 'Opaque bearer token (`fpt_…`). Send it as `Authorization: Bearer`.'
        token_type:
          type: string
          enum:
            - Bearer
        expires_in:
          type: integer
          description: Lifetime in seconds (600).
        scope:
          type: string
          description: Space-separated scopes of the client.
      required:
        - access_token
        - token_type
        - expires_in
        - scope
    PublicApiTokenError:
      type: object
      description: RFC 6749 §5.2 error. `error_description` is a fixed English text per `error`; it never echoes the request.
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - invalid_client
            - unsupported_grant_type
            - origin_forbidden
            - rate_limited
            - server_error
            - temporarily_unavailable
        error_description:
          type: string
      required:
        - error
        - error_description
    PublicApiOpenApiDocument:
      type: object
      description: The OpenAPI 3.1 document of the public API, this same document.
      properties:
        openapi:
          type: string
          description: OpenAPI version (`3.1.x`).
        info:
          type: object
        paths:
          type: object
      required:
        - openapi
        - info
        - paths
  examples:
    PublicApiRateLimited:
      summary: Too many requests
      value:
        error: Too many requests. Wait for the indicated time and try again.
        code: api_rate_limited
    PublicApiValidationFailed:
      summary: A record with a field the API does not accept
      value:
        error: The request is not valid and nothing was written. Check the details, fix the listed fields and send it again.
        code: validation_failed
        details:
          - pointer: /items/0/note
            code: unknown_field
    PublicApiBatchInProgress:
      summary: Another batch of the company is loading
      value:
        error: Another batch is being loaded for this company. Wait for the indicated time and send yours again.
        code: batch_in_progress
  headers:
    XRequestId:
      description: >-
        Identifier of the request, on every response of the API, errors included: quote it when you contact Fibady. A
        429 or 5xx without it did not come from the API but from the edge.
      schema:
        type: string
    RetryAfter:
      description: >-
        How many seconds to wait, at least, before sending the request again. Except on a 503 `public_api_disabled`: the
        API is not enabled for the account, so do not retry at this pace; back off exponentially, with at most 5 minutes
        between attempts, or stop and contact Fibady.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    oauth2:
      type: oauth2
      description: >-
        Public API access token (`fpt_…`) from `POST /v1/oauth/token` (client_credentials), sent as `Authorization:
        Bearer`.
      flows:
        clientCredentials:
          tokenUrl: https://public-api.fibady.com/v1/oauth/token
          scopes:
            companies:read: List the companies the client can access.
            ingestion:read: Read data loads.
            accounting:read: Read accounting data.
            accounting:write: Write accounting data.
            accounting:delete: Delete accounting data.
            invoicing:read: Read invoicing data.
            invoicing:write: Write invoicing data.
            invoicing:delete: Delete invoicing data.
            banking:read: Read banking data.
            banking:write: Write banking data.
            banking:delete: Delete banking data.
