Skip to main content

Operations workflow

Operations are the core transactional primitive in FinSync. The application models an operation as one business event made up of one or more transactions. That distinction matters for integrations:
  • An operation is the full movement, such as a payout, collection, FX trade, or manual adjustment.
  • Each transaction inside the operation is an individual credit or debit that affects one account.

Accounting meaning of credit and debit

The app wiki makes an important point: credit and debit do not mean the same business outcome for every client type. Use this rule when you validate transaction types and when you explain balances back to downstream systems.

Preflight lookups

Before creating operations, fetch the catalog data that drives payload construction:
  • GET /api/operation-types — includes operation_form_config (company operation-type form config: kind up_down, exchange, or other)
  • POST /api/operation-types when the company is missing a category (for example type=budget). Optional operation_form_config is validated and persisted when present; omit it to keep today’s null/default.
  • PATCH /api/operation-types/{id} — name, type, and/or operation_form_config (JSON null clears defaults; omit leaves them unchanged)
  • DELETE /api/operation-types/{id} — soft-delete (deleted_at)
  • GET /api/transaction-types
These endpoints let you resolve:
  • which operation_type_id corresponds to up, down, other, exchange, or budget
  • which transaction_type_id should be used for regular movements versus commissions
  • optional dashboard operation_form_config on each type
operation_form_config uses the same parseOperationFormConfig helper as Configuración > Catálogo > Tipos de operación. kind must match type (up/down → up_down, exchange → exchange, other → other). Invalid objects return 400. Soft-deleted types are hidden from GET and return 404 on PATCH/DELETE.

List and monitor operations

Use GET /api/operations to review existing operations, inspect their transactions, and monitor status transitions.
Useful filters include:
  • dateFrom
  • dateTo
  • page
  • pageSize
  • operationType with values such as up, down, other, exchange, budget, interest
  • status with values pending, completed, or adjusted
When page and pageSize are omitted, the endpoint returns a simpler response shape. When pagination is supplied, it also returns pagination metadata.

Choose the right creation payload

Use POST /api/operations with the payload shape that matches the workflow you want to automate. The public API currently supports four main creation patterns:
  • UP and DOWN: paired internal/external movements between one internal account and one external client
  • OTHER: one or many custom transactions, including same-currency transfers between two internal cajas
  • EXCHANGE: FX operations in either internal mode or external client mode. Requires different currencies; do not use EXCHANGE for same-currency moves between two internal cajas.
  • BUDGET: centros de resultados. Requires operation_type_id (type=budget), cost_center_id, and amount. direction defaults to expense. payment_source / payment_source_mode defaults to internal (caja) and then also requires internal_account_id. Expense + external credits payer_client_id (type=1, not the center client) and does not debit caja. Do not invent an income operation type.
The application UI also teaches additional business modes for how money can move. In the API, those scenarios map to the payload families above rather than to a separate mode flag for UP and DOWN. The current reference includes examples for:
  • UP and DOWN operations
  • OTHER operations, including two internal legs
  • Internal exchange operations
  • External exchange operations

Optional date

Pass date on POST /api/operations for UP, DOWN, OTHER, EXCHANGE, and BUDGET. It is written to Operation.date and each created Transaction.created_at / updated_at (metrics bucket by t.created_at) the same way as the dashboard with-transactions path.
  • ISO timestamp (2024-03-15T18:30:00.000Z) or date-only (2024-03-15)
  • Omit it to use NOW()
  • Garbage or non-parseable values return 400
Use your company’s operation_type_id from GET /api/operation-types (up / down). The IDs above are examples only.

Same-currency internal transfer (OTHER)

Public UP/DOWN is internal↔external only. For a same-currency movement between two internal cajas (for example Caja USD → Banco), use OTHER with two legs:
  • debit the source caja account
  • credit the destination caja account
  • each type=0 client requires account_id
  • both legs may be internal; this is already supported and is not a new operation type
Resolve operation_type_id (other) and transaction_type_id (debit / credit) from GET /api/operation-types and GET /api/transaction-types. The operation is created pending; approve it when the movement should apply to balances.

Budget operations (centros de resultados)

Resolve operation_type_id with type=budget from GET /api/operation-types and IDs from GET /api/cost-centers. If the company has no type=budget row, create one with POST /api/operation-types (name + type, seed default name Centros de resultados). If it has no centers, create one with POST /api/cost-centers. See Cost centers workflow. Expense is the default so existing callers do not change. Same planner as the dashboard Gastos form (buildBudgetTransactionPlan). Office expense = expense + caja (payment_source_mode=internal, default). Personal expense = expense + CC (payment_source_mode=external + payer_client_id). Both stay type=budget with cost_center_id and budget_direction=expense. External credits that person (company owes them) and does not debit caja. payer_client_id must be type=1 and not the center client. Income without a payer inverts caja/center legs. Income with payer_client_id is credit caja + debit the paying client; net_balance adds an extra credit on that payer so their cuenta corriente nets to 0. The caja credit is kept. The center is attribution (cost_center_id + budget_direction) — no center-client legs. Expense from caja (default, existing callers):
Expense from a third-party cuenta corriente (no caja):
Income with optional paying client:
Optional fields: direction (default expense), payment_source / payment_source_mode (default internal; external is expense-only), date, currency (must match the caja on internal; defaults to the center currency on expense+external), payer_client_id (required on expense+external; optional on income), internal_account_id (required on internal), net_balance (defaults from the center), description, external_transaction_id, debit_transaction_type_id, credit_transaction_type_id. Regular credit/debit types default the same way as the dashboard. The create is pending; approve or cancel with the existing endpoints. GET /api/operations?operationType=budget already lists them.

Idempotent creates with external_transaction_id

Pass an optional external_transaction_id (your own payment or transfer reference) when creating an operation. FinSync stores it on the operation’s primary transaction and rejects duplicates with 409 Conflict, including the existing operation_id and status (pending or completed). Cancelled operations (adjusted) release the id so you can reuse it. Example UP or DOWN payload:
If the first request succeeded but the client timed out, retrying with the same external_transaction_id returns:

Understand statuses

The wiki defines three transaction lifecycle states that also drive the approval endpoints:
  • pending: created, but not yet applied as a finalized balance movement
  • completed: approved and applied
  • adjusted: cancelled (while still pending) or contraasentado after completion
A completed transaction is immutable: amounts, accounts, and legs stay. It is never flipped back to pending.

Approve or cancel

Use these endpoints only when the operation is still pending:
  • POST /api/operations/{id}/approve
  • DELETE /api/operations/{id}
Approval marks the operation transactions as completed. Cancellation marks them as adjusted. Both endpoints require the operation’s transactions to still be pending.

Revert a completed operation (contraasiento)

Use POST /api/operations/{id}/revert when the operation is fully completed and you need to undo the balance effect without losing the original rows. This is the same ledger path as dashboard Revertir operación: each completed transaction gets an inverse contraasiento on the same operation, then the original is marked adjusted. Balances invert because the new legs are inserted as adjusted (the existing update_account_from_transaction insert path), not because completed rows are edited or unapproved.
A successful response includes the original operation_id, counterpart_operation_id (same id — dashboard posts inverse legs on the same operation), the new counterpart_transaction_ids, and the original adjusted_transaction_ids. The endpoint returns 409 when the operation is not fully completed, already reverted/adjusted, or cheque-linked (the dashboard hides revert in those cases). Missing operations or another company’s id return 404. There is no bulk revert; call once per operation (company mutation budget is 720 requests / 60 seconds).

Suggested integration pattern

  1. Fetch operation-types and transaction-types so you can build payloads from company-specific IDs instead of hardcoding assumptions.
  2. Create the operation with POST /api/operations, using a stable external_transaction_id when you need safe retries.
  3. Persist the returned operation_id and created transaction IDs (on 409, use the operation_id from the error body).
  4. List operations with GET /api/operations to confirm the new operation, inspect its transaction set, and validate pending vs completed state.
  5. Approve with POST /api/operations/{id}/approve or cancel with DELETE /api/operations/{id} only after your business checks pass.
  6. After approval, undo with POST /api/operations/{id}/revert (contraasiento). Do not expect a completed→pending unapprove.