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
creditordebitthat 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— includesoperation_form_config(company operation-type form config:kindup_down,exchange, orother)POST /api/operation-typeswhen the company is missing a category (for exampletype=budget). Optionaloperation_form_configis validated and persisted when present; omit it to keep today’s null/default.PATCH /api/operation-types/{id}—name,type, and/oroperation_form_config(JSONnullclears defaults; omit leaves them unchanged)DELETE /api/operation-types/{id}— soft-delete (deleted_at)GET /api/transaction-types
- which
operation_type_idcorresponds toup,down,other,exchange, orbudget - which
transaction_type_idshould be used for regular movements versus commissions - optional dashboard
operation_form_configon 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
UseGET /api/operations to review existing operations, inspect their transactions, and monitor status transitions.
dateFromdateTopagepageSizeoperationTypewith values such asup,down,other,exchange,budget,intereststatuswith valuespending,completed, oradjusted
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
UsePOST /api/operations with the payload shape that matches the workflow you want to automate.
The public API currently supports four main creation patterns:
UPandDOWN: paired internal/external movements between one internal account and one external clientOTHER: one or many custom transactions, including same-currency transfers between two internal cajasEXCHANGE: 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. Requiresoperation_type_id(type=budget),cost_center_id, andamount.directiondefaults toexpense.payment_source/payment_source_modedefaults tointernal(caja) and then also requiresinternal_account_id. Expense +externalcreditspayer_client_id(type=1, not the center client) and does not debit caja. Do not invent anincomeoperation type.
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
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=
0client requiresaccount_id - both legs may be internal; this is already supported and is not a new operation type
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)
Resolveoperation_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):
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:
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 movementcompleted: approved and appliedadjusted: cancelled (while still pending) or contraasentado after completion
pending.
Approve or cancel
Use these endpoints only when the operation is still pending:POST /api/operations/{id}/approveDELETE /api/operations/{id}
completed. Cancellation marks them as adjusted.
Both endpoints require the operation’s transactions to still be pending.
Revert a completed operation (contraasiento)
UsePOST /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.
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
- Fetch
operation-typesandtransaction-typesso you can build payloads from company-specific IDs instead of hardcoding assumptions. - Create the operation with
POST /api/operations, using a stableexternal_transaction_idwhen you need safe retries. - Persist the returned
operation_idand created transaction IDs (on 409, use theoperation_idfrom the error body). - List operations with
GET /api/operationsto confirm the new operation, inspect its transaction set, and validatependingvscompletedstate. - Approve with
POST /api/operations/{id}/approveor cancel withDELETE /api/operations/{id}only after your business checks pass. - After approval, undo with
POST /api/operations/{id}/revert(contraasiento). Do not expect a completed→pending unapprove.