# Bulk delete financial accounts Source: https://docs.synci.io/api-reference/accounts/bulk-delete-financial-accounts /openapi/v1.json post /finance/accounts/bulk-delete Deletes multiple financial accounts in one request. Returns a per-account result indicating success or failure, so a partial failure does not fail the whole request. Deleting an account also deletes all of its associated data, like balances, holdings, transactions, and transfer links. # Delete financial account Source: https://docs.synci.io/api-reference/accounts/delete-financial-account /openapi/v1.json delete /finance/accounts/{accountId} Deleting a financial account will also delete all associated data, like balances, transactions, and transfer links. # Get account balance entry Source: https://docs.synci.io/api-reference/accounts/get-account-balance-entry /openapi/v1.json get /finance/accounts/{accountId}/balances/{balanceId} # Get account holding Source: https://docs.synci.io/api-reference/accounts/get-account-holding /openapi/v1.json get /finance/accounts/{accountId}/holdings/{holdingId} # Get enrichment backfill status Source: https://docs.synci.io/api-reference/accounts/get-enrichment-backfill-status /openapi/v1.json get /finance/accounts/{accountId}/enrichment-status Progress of the one-off enrichment pass over transactions that were already stored when enrichment was switched on for this account. Counts cover every backfill this account has run, not only the latest, and only transactions the backfill itself sent: enrichment of newly synced transactions is not reported here. # Get financial account Source: https://docs.synci.io/api-reference/accounts/get-financial-account /openapi/v1.json get /finance/accounts/{accountId} Returns a single financial account, including its configuration, health, and attached rules. # List account holdings Source: https://docs.synci.io/api-reference/accounts/list-account-holdings /openapi/v1.json get /finance/accounts/{accountId}/holdings Returns the current holdings (positions) snapshot for an investment- category financial account. Holdings are replaced on every sync, so the list reflects the user's current portfolio — not history. Only SnapTrade-fed accounts (crypto / brokerage) populate holdings; bank-fed accounts will always return an empty list. Filter by `symbol`, `asset_class`, `currency`, or `integrator`. Sort by `symbol`, `market_value`, `quantity`, `synced_at`, `created_at`, or `id`. Default sort is largest-by-market-value first. # List all account balance entries Source: https://docs.synci.io/api-reference/accounts/list-all-account-balance-entries /openapi/v1.json get /finance/accounts/{accountId}/balances Returns a paginated list of balance entries recorded for the account. # List all financial accounts Source: https://docs.synci.io/api-reference/accounts/list-all-financial-accounts /openapi/v1.json get /finance/accounts Returns a paginated list of the authenticated user's financial accounts. # Reset financial account config to defaults Source: https://docs.synci.io/api-reference/accounts/reset-financial-account-config-to-defaults /openapi/v1.json delete /finance/accounts/{accountId}/reset-config Removes the account's custom configuration so the default configuration applies. # Sync financial account data Source: https://docs.synci.io/api-reference/accounts/sync-financial-account-data /openapi/v1.json post /finance/accounts/{accountId}/sync Initiates a sync of the account's data from the provider. Syncing is processed asynchronously. Manual syncs are rate limited; a 429 response indicates the requested data was synced recently. # Update financial account Source: https://docs.synci.io/api-reference/accounts/update-financial-account /openapi/v1.json put /finance/accounts/{accountId} # Choose the accounts to keep Source: https://docs.synci.io/api-reference/connections/choose-the-accounts-to-keep /openapi/v1.json put /finance/connections/{connectionId}/setup/accounts Keeps the given accounts enabled and disables every other account the provider found. Only possible once account discovery has finished (`accounts_last_synced_at` is set). While setup is pending, the first sync waits for `POST /setup/complete`. After setup is complete, the selection can be revisited and newly enabled accounts start syncing immediately if their settings allow automatic sync. # Complete the setup Source: https://docs.synci.io/api-reference/connections/complete-the-setup /openapi/v1.json post /finance/connections/{connectionId}/setup/complete Marks the connection's setup as finished and starts the first transaction sync of every account that remains, using whatever account settings have been saved by then. Safe to call again: a connection whose setup is already complete is returned unchanged and nothing is started twice. # Create a new financial connection Source: https://docs.synci.io/api-reference/connections/create-a-new-financial-connection /openapi/v1.json post /finance/connections Creates the connection and returns an authorization URL (`auth_url`). Send the user there to authorize with the bank. Once authorized, Synci discovers the connection's accounts and starts their first balance and transaction sync, reaching back `max_historical_days` (any value up to the institution's maximum). Two kinds of connection cannot start here. US banks (Array) authorize through an embedded SDK rather than a URL: the endpoint answers `403` with `code: "provider_requires_synci_surface"`, and the connection is made in the Synci Dashboard or the connection portal. Managed users connect in the portal too: with a managed user's token the endpoint answers `403` with `code: "managed_connection_requires_portal"`, and you mint a portal session instead (`POST /managed/v1/users/{id}/portal-sessions`). Accounts discovered on a platform app's connection are created with `sync_auto` off and nothing is fetched until you configure them. Send the account config (`sync_start_date` plus `sync_auto: true`) or call `POST /finance/accounts/{id}/sync` to start. `sync_start_date` is the oldest date the backfill reaches; history before it is never fetched. # Delete financial connection Source: https://docs.synci.io/api-reference/connections/delete-financial-connection /openapi/v1.json delete /finance/connections/{connectionId} Deleting a financial connection will also delete all associated data, like financial accounts, balances, transactions, and transfer links. # Disable financial connection Source: https://docs.synci.io/api-reference/connections/disable-financial-connection /openapi/v1.json put /finance/connections/{connectionId}/disable Disables the connection so its accounts stop syncing. Use the reauthorize endpoint to re-enable it. # Get financial connection Source: https://docs.synci.io/api-reference/connections/get-financial-connection /openapi/v1.json get /finance/connections/{connectionId} # List all financial connections Source: https://docs.synci.io/api-reference/connections/list-all-financial-connections /openapi/v1.json get /finance/connections Returns a paginated list of the authenticated user's financial connections. # Reconfirm the consent of a financial connection Source: https://docs.synci.io/api-reference/connections/reconfirm-the-consent-of-a-financial-connection /openapi/v1.json put /finance/connections/{connectionId}/reauthorize Starts a fresh authorization with the bank for an existing connection and returns an authorization URL (`auth_url`). Send the user there; once they have authorized, the connection keeps its id, accounts and history, and syncing resumes. Any accounts the bank newly reports are discovered as on creation. Managed users renew in the connection portal instead: with a managed user's token the endpoint answers `403` with `code: "managed_connection_requires_portal"`. Mint a portal session with `connection_id` (`POST /managed/v1/users/{userId}/portal-sessions`); a `connection.reauthorized` webhook follows the renewal. # Create a new worksheet (tab) in a spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/create-a-new-worksheet-tab-in-a-spreadsheet /openapi/v1.json post /destinations/google-sheets/connections/{connectionId}/sheets/{sheetId}/tabs Optionally assigns the new tab to the transactions, current balances, or balance history sync slot and writes the corresponding header row. # Create new Google Sheets connection Source: https://docs.synci.io/api-reference/google-sheets/create-new-google-sheets-connection /openapi/v1.json post /destinations/google-sheets/connections Synci generates an authorization URL (auth_url). Follow it to authorize Synci to manage spreadsheets on your behalf. Once authorized, you can register destination spreadsheets and link them to financial accounts. # Delete a destination spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/delete-a-destination-spreadsheet /openapi/v1.json delete /destinations/google-sheets/connections/{connectionId}/sheets/{sheetId} Removes the Synci record and its transfer links. The spreadsheet itself remains in the user's Google Drive. # Delete Google Sheets connection Source: https://docs.synci.io/api-reference/google-sheets/delete-google-sheets-connection /openapi/v1.json delete /destinations/google-sheets/connections/{connectionId} Deleting a connection also deletes its registered destination sheets and their transfer links. The spreadsheets themselves remain in the user's Google Drive. # Get a destination spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/get-a-destination-spreadsheet /openapi/v1.json get /destinations/google-sheets/connections/{connectionId}/sheets/{sheetId} Returns a single destination spreadsheet, including its tab assignments, transfer links, and health status. # Get Google Sheets connection Source: https://docs.synci.io/api-reference/google-sheets/get-google-sheets-connection /openapi/v1.json get /destinations/google-sheets/connections/{connectionId} Returns a single Google Sheets connection, including its registered spreadsheets and health status. # List all Google Sheets connections Source: https://docs.synci.io/api-reference/google-sheets/list-all-google-sheets-connections /openapi/v1.json get /destinations/google-sheets/connections Returns the Google Sheets connections that belong to the authenticated user. Connections that never completed Google authorization are not included. # List destination spreadsheets for a connection Source: https://docs.synci.io/api-reference/google-sheets/list-destination-spreadsheets-for-a-connection /openapi/v1.json get /destinations/google-sheets/connections/{connectionId}/sheets Returns the spreadsheets registered as sync destinations on this connection. # List the worksheets (tabs) of a spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/list-the-worksheets-tabs-of-a-spreadsheet /openapi/v1.json get /destinations/google-sheets/connections/{connectionId}/sheets/{sheetId}/tabs Returns the worksheet tabs of the spreadsheet, with their identifiers (gid) and names. # Refresh Google Sheets connection Source: https://docs.synci.io/api-reference/google-sheets/refresh-google-sheets-connection /openapi/v1.json post /destinations/google-sheets/connections/{connectionId}/refresh Re-validates the stored Google credentials and refreshes the connected account's email. Rate limited to once per minute per connection. # Register a destination spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/register-a-destination-spreadsheet /openapi/v1.json post /destinations/google-sheets/connections/{connectionId}/sheets Either creates a new Synci-managed spreadsheet (mode=create) or attaches an existing spreadsheet the user selected (mode=attach). # Update a destination spreadsheet Source: https://docs.synci.io/api-reference/google-sheets/update-a-destination-spreadsheet /openapi/v1.json put /destinations/google-sheets/connections/{connectionId}/sheets/{sheetId} Update the spreadsheet's display name, tab assignments, or column configuration. When the column configuration changes, previously synced rows are reformatted to the new layout asynchronously. # Update Google Sheets connection Source: https://docs.synci.io/api-reference/google-sheets/update-google-sheets-connection /openapi/v1.json put /destinations/google-sheets/connections/{connectionId} Update the connection's display name, or enable/disable syncing through it. # Change the CSV mapping Source: https://docs.synci.io/api-reference/imports/change-the-csv-mapping /openapi/v1.json put /finance/accounts/{accountId}/imports/{importId}/mapping Re-reads a CSV batch under review with a different mapping and returns a new preview; the rows endpoint then serves the re-read rows. The mapping must name a date column and an amount column (or debit and credit columns), and no column may feed two fields. A `counterparty_account` column is accepted and ignored. # Commit an import Source: https://docs.synci.io/api-reference/imports/commit-an-import /openapi/v1.json post /finance/accounts/{accountId}/imports/{importId}/commit Writes the new rows in the background: rules, enrichment, counterparty pairing, destinations and webhooks run exactly as for a provider sync, with `origin = IMPORT`. A closing balance in the file is recorded as a balance checkpoint. Returns `202` with the batch `COMMITTING`; poll the batch until it is `COMPLETED` or `FAILED`. # Discard an import Source: https://docs.synci.io/api-reference/imports/discard-an-import /openapi/v1.json delete /finance/accounts/{accountId}/imports/{importId} Deletes a batch still under review together with its uploaded file. # Get an import Source: https://docs.synci.io/api-reference/imports/get-an-import /openapi/v1.json get /finance/accounts/{accountId}/imports/{importId} With the preview while the batch is `PENDING_REVIEW`; poll it while `COMMITTING` or `UNDOING`. # List an import's rows Source: https://docs.synci.io/api-reference/imports/list-an-imports-rows /openapi/v1.json get /finance/accounts/{accountId}/imports/{importId}/rows Every row of the file in file order, paginated (`page[size]` 10 to 100, default 25), with its fate (`NEW`, `DUPLICATE` or `INVALID`) and the reason, the raw cells for a CSV, and the fields read with the batch's current mapping: after a mapping change the next request reflects it. Available in every status but `EXPIRED`, whose rows are gone with the file (a `409`); after the commit the rows are the record of what was decided. # List imports Source: https://docs.synci.io/api-reference/imports/list-imports /openapi/v1.json get /finance/accounts/{accountId}/imports The account's import history, newest first, without previews. # Undo an import Source: https://docs.synci.io/api-reference/imports/undo-an-import /openapi/v1.json post /finance/accounts/{accountId}/imports/{importId}/undo Removes the rows a `COMPLETED` batch wrote, taking them back from every destination that supports deletion, in the background. Returns `202` with the batch `UNDOING`; poll it until it is `UNDONE`. An undo that `FAILED` (a destination would not give a row back) can be started again; it continues where it stopped. # Upload a statement file Source: https://docs.synci.io/api-reference/imports/upload-a-statement-file /openapi/v1.json post /finance/accounts/{accountId}/imports Detects the format (camt.053, OFX/QFX or CSV), parses the file, checks every row against the account for duplicates and returns the batch in `PENDING_REVIEW` with a preview. A CSV is read with the account's saved mapping when it has one, otherwise with a proposed mapping and a confidence per column; adjust it with the mapping endpoint. Nothing is written until the batch is committed. The same file uploaded twice into one account is a `409` with `code: duplicate_import_file` and the earlier batch's id. Files over 10 MB or 20,000 rows, and files Synci cannot read, are a `422`. # Get supported countries Source: https://docs.synci.io/api-reference/institutions/get-supported-countries /openapi/v1.json get /finance/institutions/countries Get a list of countries supported by Synci's financial integrators. # Get supported institution Source: https://docs.synci.io/api-reference/institutions/get-supported-institution /openapi/v1.json get /finance/institutions/{institutionId} Get a supported institution. Use the institution IDs when creating new financial connections. # List all supported institutions Source: https://docs.synci.io/api-reference/institutions/list-all-supported-institutions /openapi/v1.json get /finance/institutions Get a list of all supported institutions. Use the institution IDs when creating new financial connections. # Add a rate period Source: https://docs.synci.io/api-reference/loans/add-a-rate-period /openapi/v1.json post /finance/accounts/{accountId}/loan/rate-periods A new annual rate from a date onward. The date must be on or after the loan's last accrual date; interest from that date is recomputed. Settles a pending payment-change question dated on or before it. # Create a manual account Source: https://docs.synci.io/api-reference/loans/create-a-manual-account /openapi/v1.json post /finance/accounts/manual Creates an account Synci does not sync from a provider, under the user's "Manual accounts" connection. A `LOAN`: give it today's balance, the rate and the fee, and either the bank account that pays it (its matched payments then post the payment, interest and fee rows and keep the balance current) or a fixed payment schedule. The account carries an `OPENING` row dated `balance_date`. A `BANK` account (`cash_account_type` CACC, SVGS or CARD): a checking, savings or card account the user fills from statement files through the import endpoints. Its signed `balance` today is optional; when absent the balance stays unknown until the first import with a closing balance or a checkpoint. An optional `institution_name` (the bank or lender, as the user wants it shown) groups manual accounts in the dashboard; it is also editable through the account update endpoint. The plan feature `manual_accounts_max` caps how many a user may hold (Basic one, Pro and above unlimited). One more than that is a 403 with `feature: manual_accounts_max`, `current` and `limit`. # Derived loan balance on a date Source: https://docs.synci.io/api-reference/loans/derived-loan-balance-on-a-date /openapi/v1.json get /finance/accounts/{accountId}/loan/balance The balance Synci derives for the end of a day, from the anchor and the rows posted since (or before). Lets a client preview what a checkpoint on that date would adjust by. # Dismiss a payment change Source: https://docs.synci.io/api-reference/loans/dismiss-a-payment-change /openapi/v1.json post /finance/accounts/{accountId}/loan/dismiss-payment-change The user says the changed payment amount was not a rate change: it becomes the regular amount and the prompt clears. # Remove a rate period Source: https://docs.synci.io/api-reference/loans/remove-a-rate-period /openapi/v1.json delete /finance/accounts/{accountId}/loan/rate-periods/{effectiveFrom} Only the latest rate period can be removed, and only while no interest has accrued at it. # Suggest loan payments Source: https://docs.synci.io/api-reference/loans/suggest-loan-payments /openapi/v1.json get /finance/accounts/{accountId}/loan/candidate-payments Outflows on the loan's paying account over the last 120 days, in the loan's currency, that recur at a monthly cadence within a small amount tolerance, most frequent first, at most 30. A heuristic to preselect examples in the setup wizard; nothing is stored. # Update loan terms Source: https://docs.synci.io/api-reference/loans/update-loan-terms /openapi/v1.json put /finance/accounts/{accountId}/loan Everything on the `loan` object except the rate, which has its own endpoint. Changing the fee or the day count applies to payments from now on. Setting a paying account turns schedule mode off. # Verify the balance Source: https://docs.synci.io/api-reference/loans/verify-the-balance /openapi/v1.json post /finance/accounts/{accountId}/loan/checkpoints The balance the bank shows on a date. When it differs from the balance Synci derived, an `ADJUSTMENT` row for the difference is posted on that date and the anchor moves there; rows before it are never rewritten. # Create Lunch Money account Source: https://docs.synci.io/api-reference/lunch-money/create-lunch-money-account /openapi/v1.json post /destinations/lunch-money/connections/{connectionId}/accounts Creates a manual account in Lunch Money and stores it in Synci, so it can be used as a transfer link destination right away. The starting balance is in currency units with Synci's sign convention (negative when money is owed); Synci converts it to Lunch Money's. Leave it at 0 when Synci will sync the account's history, or that history is counted twice. A rejection by Lunch Money, such as a duplicate name, is returned as a 422 with its message. # Create new Lunch Money connection Source: https://docs.synci.io/api-reference/lunch-money/create-new-lunch-money-connection /openapi/v1.json post /destinations/lunch-money/connections Connects a Lunch Money account using a Lunch Money API access token. Synci validates the token, then imports the budget details and accounts. # Delete Lunch Money account Source: https://docs.synci.io/api-reference/lunch-money/delete-lunch-money-account /openapi/v1.json delete /destinations/lunch-money/connections/{connectionId}/accounts/{accountId} Removes the account from Synci along with its transfer links. The account itself is not affected in Lunch Money. # Delete Lunch Money connection Source: https://docs.synci.io/api-reference/lunch-money/delete-lunch-money-connection /openapi/v1.json delete /destinations/lunch-money/connections/{connectionId} Deleting a Lunch Money connection will also delete all associated data, like accounts and their transfer links. # Get Lunch Money account Source: https://docs.synci.io/api-reference/lunch-money/get-lunch-money-account /openapi/v1.json get /destinations/lunch-money/connections/{connectionId}/accounts/{accountId} Returns a single Lunch Money account, including its transfer links and health status. # Get Lunch Money connection Source: https://docs.synci.io/api-reference/lunch-money/get-lunch-money-connection /openapi/v1.json get /destinations/lunch-money/connections/{connectionId} Returns a single Lunch Money connection, including its accounts and health status. # List all Lunch Money accounts Source: https://docs.synci.io/api-reference/lunch-money/list-all-lunch-money-accounts /openapi/v1.json get /destinations/lunch-money/connections/{connectionId}/accounts Returns the Lunch Money accounts synced from this connection. # List all Lunch Money connections Source: https://docs.synci.io/api-reference/lunch-money/list-all-lunch-money-connections /openapi/v1.json get /destinations/lunch-money/connections Returns the Lunch Money connections that belong to the authenticated user. # Refresh Lunch Money connection Source: https://docs.synci.io/api-reference/lunch-money/refresh-lunch-money-connection /openapi/v1.json post /destinations/lunch-money/connections/{connectionId}/refresh Triggers a background refresh of the connection's budget metadata and accounts. Use this when you've added or renamed manual accounts in Lunch Money and want Synci to pick up the changes without waiting for the next scheduled sync. Rate limited to once per minute per connection. # Update Lunch Money connection Source: https://docs.synci.io/api-reference/lunch-money/update-lunch-money-connection /openapi/v1.json put /destinations/lunch-money/connections/{connectionId} Enable or disable the connection, or supply a new access token to rotate credentials. New tokens are validated with Lunch Money before the connection's details and accounts are refreshed. # Derived balance on a date Source: https://docs.synci.io/api-reference/manual-accounts/derived-balance-on-a-date /openapi/v1.json get /finance/accounts/{accountId}/balance The balance Synci derives for a manual account at the end of a day: the last verified balance (the anchor) plus the booked rows since, or minus the rows after the date for a day before the anchor. Signed like the account balance. Null for a manual bank account whose balance is not known yet. Lets a client preview what a checkpoint on that date would adjust by. Serves manual loans too. # Verify the balance Source: https://docs.synci.io/api-reference/manual-accounts/verify-the-balance /openapi/v1.json post /finance/accounts/{accountId}/balance-checkpoints The balance the bank shows at the end of a date, signed. When the account already had a verified balance and the derived balance on that date differs, an `ADJUSTMENT` row for the difference is posted on that date; rows before it are never rewritten. The date becomes the new anchor. A first checkpoint on an account without a known balance simply sets it. Serves manual loans too (a loan's balance is negative). # Create new SimpleFIN connection Source: https://docs.synci.io/api-reference/simplefin/create-new-simplefin-connection /openapi/v1.json post /destinations/simplefin/connections Creates a SimpleFIN destination and returns a one-time setup token. Paste the token into the consumer app's "Link bank account with SimpleFIN" flow; it exchanges the token for pull credentials and then syncs the exposed accounts on demand. `app` selects which app this connection is presented as (Actual Budget by default); it does not change behavior. Third-party applications receive `setup_token: null`: the user copies the token from Synci. # Delete SimpleFIN connection Source: https://docs.synci.io/api-reference/simplefin/delete-simplefin-connection /openapi/v1.json delete /destinations/simplefin/connections/{connectionId} Deleting a connection invalidates its setup token and pull credentials immediately; the consumer.s next sync for the linked accounts will fail with a credential error. # Get SimpleFIN connection Source: https://docs.synci.io/api-reference/simplefin/get-simplefin-connection /openapi/v1.json get /destinations/simplefin/connections/{connectionId} Returns a single SimpleFIN connection, including its setup token (while unclaimed) and health status. The setup token is only returned to the connection's owner on a first-party surface; third-party applications always receive `setup_token: null`. # List all SimpleFIN connections Source: https://docs.synci.io/api-reference/simplefin/list-all-simplefin-connections /openapi/v1.json get /destinations/simplefin/connections Returns the SimpleFIN connections that belong to the authenticated user. # Update SimpleFIN connection Source: https://docs.synci.io/api-reference/simplefin/update-simplefin-connection /openapi/v1.json put /destinations/simplefin/connections/{connectionId} Updates the connection's name, app, enabled state, or exposed accounts. Account changes take effect on the consumer's next pull. # Bulk delete transactions Source: https://docs.synci.io/api-reference/transactions/bulk-delete-transactions /openapi/v1.json post /finance/transactions/bulk-delete Deletes multiple transactions in one request. Returns a per-transaction result indicating success or failure. # Create a new transaction Source: https://docs.synci.io/api-reference/transactions/create-a-new-transaction /openapi/v1.json post /finance/transactions For most users, this endpoint is not needed. Transactions are normally synced automatically once a financial connection is established. However, if you want to manually push transactions from some other source that is not integrated with Synci, you can use this endpoint. # Delete transaction Source: https://docs.synci.io/api-reference/transactions/delete-transaction /openapi/v1.json delete /finance/transactions/{transactionId} # Get transaction Source: https://docs.synci.io/api-reference/transactions/get-transaction /openapi/v1.json get /finance/transactions/{transactionId} # List all transactions Source: https://docs.synci.io/api-reference/transactions/list-all-transactions /openapi/v1.json get /finance/transactions To improve efficiency and developer experience, most empty transaction fields are omitted from the response. Fields that are marked as required in the API specification are always included, even if they are empty. # Pairing candidates Source: https://docs.synci.io/api-reference/transactions/pairing-candidates /openapi/v1.json get /finance/transactions/{transactionId}/pairing-candidates Transactions on the counterparty account that could be the other side of this one: opposite sign, not paired with anything else, booked within 30 days. Closest booking date first. Empty when the transaction has no counterparty account. Pair one by sending its id as `counterparty_transaction_id` to the update endpoint. # Update transaction Source: https://docs.synci.io/api-reference/transactions/update-transaction /openapi/v1.json put /finance/transactions/{transactionId} For most users, this endpoint is not needed. Transactions are normally synced (and updated) automatically once a financial connection is established. We do not recommend editing transactions manually unless you have a good reason, as this can lead to inconsistencies in your data. # Create new YNAB connection Source: https://docs.synci.io/api-reference/ynab/create-new-ynab-connection /openapi/v1.json post /destinations/ynab/connections When creating a new YNAB connection, Synci will generate an authorization URL (auth_url). You should follow the authorization URL to activate the connection. Once the connection is authorized, Synci will automatically fetch available budgets and budget accounts. # Create YNAB budget account Source: https://docs.synci.io/api-reference/ynab/create-ynab-budget-account /openapi/v1.json post /destinations/ynab/connections/{connectionId}/budgets/{budgetId}/accounts Creates the account in YNAB and stores it in Synci, so it can be used as a transfer link destination right away. The starting balance is in currency units, negative for a liability, and becomes YNAB's "Starting * Balance" transaction: leave it at 0 when Synci will sync the account's history, or that history is counted twice. YNAB rejects a name already used in the budget; its message is returned as a 422. # Delete YNAB budget Source: https://docs.synci.io/api-reference/ynab/delete-ynab-budget /openapi/v1.json delete /destinations/ynab/connections/{connectionId}/budgets/{budgetId} Deleting a YNAB budget will also delete all associated data, like budget accounts. # Delete YNAB budget account Source: https://docs.synci.io/api-reference/ynab/delete-ynab-budget-account /openapi/v1.json delete /destinations/ynab/connections/{connectionId}/budgets/{budgetId}/accounts/{budgetAccountId} Removes the account from Synci along with its transfer links. The account itself is not affected in YNAB. # Delete YNAB connection Source: https://docs.synci.io/api-reference/ynab/delete-ynab-connection /openapi/v1.json delete /destinations/ynab/connections/{connectionId} Deleting a YNAB connection will also delete all associated data, like budgets and budget accounts. # Get YNAB budget Source: https://docs.synci.io/api-reference/ynab/get-ynab-budget /openapi/v1.json get /destinations/ynab/connections/{connectionId}/budgets/{budgetId} Returns a single YNAB budget, including its accounts. # Get YNAB budget account Source: https://docs.synci.io/api-reference/ynab/get-ynab-budget-account /openapi/v1.json get /destinations/ynab/connections/{connectionId}/budgets/{budgetId}/accounts/{budgetAccountId} Returns a single YNAB budget account, including its transfer links, rules, and health status. # Get YNAB connection Source: https://docs.synci.io/api-reference/ynab/get-ynab-connection /openapi/v1.json get /destinations/ynab/connections/{connectionId} Returns a single YNAB connection, including its budgets and budget accounts. # List all YNAB budget accounts Source: https://docs.synci.io/api-reference/ynab/list-all-ynab-budget-accounts /openapi/v1.json get /destinations/ynab/connections/{connectionId}/budgets/{budgetId}/accounts Returns the accounts synced from this YNAB budget. # List all YNAB budgets Source: https://docs.synci.io/api-reference/ynab/list-all-ynab-budgets /openapi/v1.json get /destinations/ynab/connections/{connectionId}/budgets Returns the YNAB budgets synced from this connection. # List all YNAB connections Source: https://docs.synci.io/api-reference/ynab/list-all-ynab-connections /openapi/v1.json get /destinations/ynab/connections Returns the YNAB connections that belong to the authenticated user. Connections that never completed YNAB authorization are not included. # Refresh YNAB connection Source: https://docs.synci.io/api-reference/ynab/refresh-ynab-connection /openapi/v1.json post /destinations/ynab/connections/{connectionId}/refresh Triggers a background refresh of the connection's budgets and budget accounts. Use this when you've added or renamed budgets/accounts in YNAB and want Synci to pick up the changes without waiting for the next scheduled sync. Rate limited to once per minute per connection. # Update YNAB budget Source: https://docs.synci.io/api-reference/ynab/update-ynab-budget /openapi/v1.json put /destinations/ynab/connections/{connectionId}/budgets/{budgetId} Enable or disable syncing to this budget. # Update YNAB connection Source: https://docs.synci.io/api-reference/ynab/update-ynab-connection /openapi/v1.json put /destinations/ynab/connections/{connectionId} Update the connection's display name, or enable/disable syncing through it. # Authentication Source: https://docs.synci.io/guides/authentication Personal access tokens, OAuth tokens, and the scopes that govern what each may do. Every API request is authenticated with a Bearer token in the `Authorization` header: ```http theme={"dark"} GET /api/v1/finance/accounts HTTP/1.1 Host: api.synci.io Authorization: Bearer YOUR_ACCESS_TOKEN Accept: application/json ``` There are two kinds of tokens: * **Personal access tokens**: for your own scripts and server-to-server calls against your own Synci account. No OAuth flow needed. * **OAuth access tokens**: for apps that other Synci users authorize. Obtained through the OAuth 2.0 authorization code flow with PKCE. See [Building OAuth apps](/guides/oauth). Both token types are sent the same way and are subject to the same scope checks. ## Personal access tokens Create a token in the Synci dashboard under **Developers → API tokens**: **1. Create the token** Click **Create API token** and give it a descriptive name (for example, "Reporting script"). **2. Restrict it, optionally** Restrict the token to specific **scopes** (see the table below) and specific **accounts**. A token with no scope restriction has full access to your account. **3. Copy it immediately** For security the token is displayed only once. If you lose it, delete the token and create a new one. Personal access tokens are valid for **1 year**. You can revoke a token at any time from the same page. > [!NOTE] > > **Migrating a legacy token?** Tokens issued in the older `{id}|{secret}` format keep working for a limited deprecation window but are being retired. Regenerate the token in **Developers → API tokens** and update your integration. ## OAuth access tokens If you are building something other Synci users will connect to, register an OAuth app under **Developers → Apps**. Users approve your app on a consent screen, choose which accounts to share, and your app receives: * An **access token**, valid for **1 hour**. * A **refresh token**, valid for **30 days**, which rotates on every use. The full flow, including PKCE, token exchange, and refresh, is covered in [Building OAuth apps](/guides/oauth). If your users will never hold a Synci account, a **Managed app** mints tokens for them server-to-server instead, with no consent screen. See [Managed apps](/guides/managed-apps). ## Scopes Scopes control what a token may do. Personal access tokens can optionally be restricted to a set of scopes; OAuth apps must request them and the user must approve them. | Scope | Grants | | - | - | | `profile:read` | Name, email, and profile details | | `accounts:read` | View connected financial accounts and balances | | `accounts:write` | Update, sync, and remove financial accounts, and change their settings | | `transactions:read` | View transactions | | `transactions:write` | Create, update, and delete transactions | | `sensitive:read` | Full account identifiers: IBANs, account and card numbers, and account holder names. Only meaningful together with `accounts:read` or `transactions:read`. | | `financial-connections:read` | View bank connections and their health | | `financial-connections:write` | Manage bank connections (reauthorize, disable, remove) | | `transfer-links:read` | View transfer links and transfer history | | `transfer-links:write` | Manage transfer links, retry and undo transfers | | `rules:read` | View rules and rule activity | | `rules:write` | Manage rules | | `webhooks:read` | View webhook endpoints | | `webhooks:write` | Manage webhook endpoints and rotate signing secrets | | `destinations:read` | View destination connections (YNAB, Lunch Money, Google Sheets) | | `destinations:write` | Manage destination connections | > [!TIP] > > Request the least you need. Users are more likely to approve an app that asks only for what it needs, and app reviews go faster. ### Sensitive identifiers are redacted by default For third-party apps, sensitive identifiers (IBAN, BBAN, account and card numbers, account holder names) are **always redacted** unless the user granted `sensitive:read`. This is enforced server-side and cannot be switched off by the caller. `sensitive:read` on its own grants nothing; it un-redacts fields on data you can already see through `accounts:read` or `transactions:read`. ### Account selection Some scopes are account-scoped. When a user authorizes an OAuth app with account-scoped permissions, they pick exactly which accounts the app may access. Personal access tokens can be restricted to specific accounts the same way when you create them. ## How authorization failures look | Response | Meaning | | - | - | | `401 Unauthorized` | Missing, expired, or revoked token. | | `403 Forbidden` | The token is valid but lacks a scope this endpoint requires. | | `404 Not Found` | The resource exists but was not granted to this token (for example, an account the user did not select). Synci returns `404` rather than `403` here so tokens cannot probe for the existence of data they were not granted. | # Errors Source: https://docs.synci.io/guides/errors The error response format, the status codes the API uses, and how to handle them. The Synci API uses conventional HTTP status codes. Codes in the `2xx` range mean success, `4xx` means something was wrong with the request, and `5xx` means something went wrong on Synci's side. ## Error response format Errors return a JSON body with a human-readable `message`: ```json theme={"dark"} { "message": "Not found" } ``` Validation failures (`422`) additionally include an `errors` map with per-field messages: ```json theme={"dark"} { "message": "The url field must be a valid URL.", "errors": { "url": [ "The url field must be a valid URL." ] } } ``` ## Status codes | Code | Meaning | | - | - | | `200 OK` | The request succeeded. | | `201 Created` | The resource was created. | | `204 No Content` | The request succeeded and there is nothing to return (typically deletes). | | `400 Bad Request` | The request was malformed. | | `401 Unauthorized` | Missing, expired, or revoked token. | | `403 Forbidden` | The token is valid but lacks a required scope, or the action is not available on your plan. | | `404 Not Found` | The resource does not exist, **or** it was not granted to this token. Synci deliberately returns `404` instead of `403` for ungranted resources so tokens cannot probe for data they were not given. | | `409 Conflict` | The request cannot be performed in the resource's current state (for example, syncing an account that is inactive at the bank). | | `422 Unprocessable Entity` | Validation failed; see the `errors` map. | | `429 Too Many Requests` | You hit a rate limit; see [Rate limits](/guides/rate-limits). The `message` explains which limit and, where applicable, when you can retry. | | `500 Internal Server Error` | Something went wrong on our side. Safe to retry with backoff; contact support if it persists. | ## OAuth errors The OAuth endpoints (`/oauth/authorize`, `/oauth/token`) return standard OAuth 2.0 error codes: | Error | Cause | | - | - | | `invalid_client` | Wrong or missing client credentials, or the token request was sent as JSON instead of `application/x-www-form-urlencoded`. | | `invalid_grant` | Expired or already-used authorization code, mismatched `code_verifier`, or a rotated/revoked refresh token. | | `invalid_scope` | A requested scope is not registered for your app. | | `access_denied` | The user declined the authorization request (returned on the redirect). | ## Handling errors * **Retry `429` and `5xx`** with exponential backoff. Honor the `Retry-After` header when present. * **Do not retry `4xx`** (other than `429`) without fixing the request. * **Treat `401` on a previously working OAuth token** as "access token expired": refresh and retry once. If the refresh also fails, the user has likely revoked your app and needs to authorize again. # Managed apps Source: https://docs.synci.io/guides/managed-apps Offer bank connections inside your own product to people who have no Synci account, through the Managed Users API and the hosted connection portal. A **Managed app** lets you offer bank connections inside your own product to people who do not have a Synci account and never will. You register each of your users with Synci through a server-to-server API, send them to a hosted portal to connect their bank, and then read their financial data with a token scoped to that one person. You pay for their connections out of your own Synci plan. They never see Synci branding beyond the portal, never create a password, and never get a bill. > [!NOTE] > > If your users already have (or would happily create) their own Synci accounts, you want a [Standard OAuth app](/guides/oauth) instead: it is simpler, and they pay for their own connections. ## How it fits together ```mermaid theme={"dark"} sequenceDiagram autonumber participant User as Your user participant App as Your app participant Synci participant Bank App ->> Synci: POST /managed/v1/users (email) Synci -->> App: user_id + access & refresh tokens App ->> Synci: POST /managed/v1/users/{user_id}/portal-sessions Synci -->> App: single-use portal URL (10 min) App ->> User: Send them to that URL User ->> Synci: Portal opens, already signed in User ->> Bank: Authorizes their bank Bank ->> Synci: Consent result Synci ->> User: Back to your return URL Synci -->> App: Webhook: connection.activated Synci -->> App: Webhook: connection.backfill.completed App ->> Synci: GET /api/v1/finance/accounts (Bearer) Synci -->> App: That user's accounts, balances, transactions ``` Two separate surfaces are involved, and it is worth keeping them straight: | Surface | Base URL | Authentication | What it is for | | - | - | - | - | | **Managed Users API** | `https://api.synci.io/managed/v1` | HTTP Basic (`client_id`:`client_secret`) | Managing your users (register, list, update, delete, mint tokens, mint portal links) and your app's webhook endpoints | | **Data API** | `https://api.synci.io/api/v1` | Bearer token for one user | Reading and managing that user's financial data | The Managed Users API has its own reference: [Managed Users API](/api-reference/managed/introduction). ## Create the app In the Synci dashboard, go to **Developers → Apps → Create app** and choose **Managed users** as the app type. > [!WARNING] > > The choice is permanent. It decides who is billed for every connection the app creates, so it cannot be changed afterwards. If you pick wrong, create a new app. You get a **client ID** and a **client secret** (shown once, prefixed `oac_`). Both are used as HTTP Basic credentials on the Managed Users API. Store the secret on your server; it is never safe in a browser or a mobile binary. A Managed app has **no redirect URIs**; the field is rejected. Nobody is sent through an OAuth consent screen, so there is nowhere to redirect to. What you configure instead is a **portal return URL** (see [App settings](#app-settings)). ### What a Managed app may request A Managed app is restricted to bank-data scopes. These eight and no others: `profile:read` · `accounts:read` · `accounts:write` · `transactions:read` · `transactions:write` · `sensitive:read` · `financial-connections:read` · `financial-connections:write` Rules, transfer links, destinations and webhooks are deliberately out of reach: those are consumer-product features that belong to a Synci account, and your users do not have one. See the scope table in [Authentication](/guides/authentication#scopes). ## Connect a user, end to end **1. Authenticate to the Managed Users API** Standard HTTP Basic. Your client ID is the username, your client secret is the password. ```bash theme={"dark"} curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ https://api.synci.io/managed/v1/users \ -H "Accept: application/json" ``` | Response | Meaning | | - | - | | `401` `invalid_client` | Bad or missing credentials, or the app is not a Managed app. Deliberately indistinguishable, so this endpoint cannot be used to discover which client IDs exist. | | `403` `client_suspended` | The app has been suspended by Synci. | Rate limits are **300 requests/minute per client** and 600/minute per source address. Errors carry `error` and `message` keys. **2. Register the user** ```bash Request theme={"dark"} curl -X POST https://api.synci.io/managed/v1/users \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"email":"alice@example.com","external_user_id":"acct-9001"}' ``` ```json Response (201 Created) theme={"dark"} { "user_id": "9f1c2e64-1f3a-4c77-9b21-8a4c5d6e7f80", "external_user_id": "acct-9001", "email": "alice@example.com", "email_verified": false, "onboarded_at": null, "limits": { "financial_connections": null }, "created_at": "2026-08-28T09:12:00+00:00", "token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOi...", "refresh_token": "def50200..." } ``` | Field | Rules | | - | - | | `email` | **Required.** Valid address, max 255. | | `external_user_id` | Optional, max 255. Your own identifier for this person, and the easiest way to reconcile later. | | `name` | Optional, max 255. Defaults to a title-cased version of the email local part. | | `connection_limit` | Optional integer, 0 to 20. A per-user cap on financial connections. It caps **all** connection types, not just bank. | **`201 Created`** returns the user *and* their first token pair, unwrapped at the top level. `user_id` is an opaque UUID with no relationship to Synci's internal identifiers, and it is scoped to your app. It is the id you use everywhere else. Asking for another developer's user returns `404`, identical to asking for one that does not exist. Registration is atomic: if the token cannot be issued, the user is not created. | Status | `error` | When | | - | - | - | | `409` | `email_already_registered` | You already registered that address. Uniqueness is **per app**: another developer can register the same person, and an address belonging to a Synci account can still be registered. | | `409` | `external_user_id_taken` | You already used that `external_user_id`. | | `403` | `testing_limit_reached` | An unreviewed app hit the registration cap. The response includes a `limit` key. Counted across **all** your unreviewed apps, not per app. | | `402` | `subscription_required` | Your app is live but you have no Synci plan. Your users' connections draw on your plan's connection quantity, so there is nothing for them to draw on. | **3. Send them to the portal** Your user connects their bank in Synci's hosted **connection portal**. You do not build this, and you never touch their bank credentials. Mint a single-use link: ```bash Request theme={"dark"} curl -X POST https://api.synci.io/managed/v1/users/USER_ID/portal-sessions \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"return_url":"https://yourapp.com/banks/done"}' ``` ```json Response theme={"dark"} { "data": { "url": "https://app.synci.io/portal/session?token=...&app_client_id=...", "expires_at": "2026-08-28T09:22:00+00:00" } } ``` Send the user to `url`. They arrive already signed in, with no password, no code and no Synci account. ![The Synci connection portal showing the app's name, a bank picker, and the user's connected accounts.](https://mintcdn.com/synci/6HoxVjb_wmNxVtbF/images/connection-portal.png?fit=max\&auto=format\&n=6HoxVjb_wmNxVtbF\&q=85\&s=1b451430b4f44fda7403f9afad204033) The Synci connection portal, as your user sees it. Your app's name and logo appear here once it is approved; until then the portal tells the user it has not been reviewed yet and shows an initial avatar instead. > [!WARNING] > > **The link lives 10 minutes and is strictly single-use.** Mint it at the moment you are about to redirect, not in advance. A replayed link is dead. **`return_url` is optional.** Provide it to override the app default for this one visit, which is useful when you know which of your screens they left from. Omit it to use the app's configured `portal_return_url`. **Skip the bank picker, or renew a connection.** Two optional fields make the portal start a bank flow by itself after a short "‹App› uses Synci" interlude: | Field | Effect | | - | - | | `institution_id` | Starts a **new** connection at the institution the user already picked in your app. Ids come from `GET /finance/institutions`. | | `connection_id` | Renews one of this user's **existing** connections (an expiring consent, or a bank asking them to sign in again). It keeps its id, accounts and history, and `connection.reauthorized` follows. | The two are mutually exclusive. With either one, a successful bank flow returns the user to your return URL with `status=completed` and `connection=`, even when the bank finishes in its own app and opens the return in another browser tab. Do not rely on that redirect alone: the [`connection.activated` webhook](#webhooks) arrives whether or not the browser comes back. After the bank, the portal asks the user which accounts to keep. If you enable [field mapping](#app-settings), it then lets them choose how payee, description and date are filled, with a preview of their own transactions. **An expired link degrades gracefully.** The app id rides along in the URL, so a user who opens a stale link is offered an emailed sign-in code by the portal instead of hitting a dead end. Nothing for you to build; see [When a link expires](#when-a-link-expires). **4. Read their data** Use the access token from registration. It is an ordinary Bearer token against the normal API: ```bash theme={"dark"} curl "https://api.synci.io/api/v1/finance/accounts" \ -H "Authorization: Bearer THEIR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` The token reaches **only that user's data**, and only within your app's scopes. Everything outside those scopes returns `403`; another user's data returns `404`. Unlike a Standard app (where the user picks which accounts to share on a consent screen), a Managed app's token sees **every account that user connects**, including ones connected later. There is no account picker in this lane; you registered them, and the grant is implicit in that. Recent transactions arrive first and older history follows over the next minutes or hours, depending on the bank. To know when it is all in, subscribe to `connection.backfill.completed` (or `account.backfill.completed` per account) rather than polling. See [Knowing when history is in](/guides/webhooks#knowing-when-history-is-in). **Sensitive identifiers (IBAN, account and card numbers, account holder names) are redacted** unless your app holds `sensitive:read`. This is enforced server-side and cannot be switched off by the caller. **5. Mint fresh tokens as needed** Access tokens last **1 hour**, refresh tokens **30 days**. Refresh normally, or mint a new pair at any time: ```bash Managed Users API theme={"dark"} curl -X POST https://api.synci.io/managed/v1/users/USER_ID/tokens \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" ``` ```bash OAuth token endpoint theme={"dark"} curl -X POST https://api.synci.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=managed_user \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d user_id=USER_ID ``` A `user_id` that is not one of your users returns `400` with `"error": "invalid_request"`; the specific reason is in the `hint` field, not `error_description`. ## When a link expires Portal links live 10 minutes, and people do not always click them in 10 minutes. Rather than showing a dead end, the portal offers to email that user a six-digit sign-in code, scoped to your app. They enter it and land in the same place the link would have taken them. **There is nothing to build and nothing to call.** The portal owns this entirely: it decides when to offer a code, sends it, and handles the retries. Your part is already done when you mint the link, because the app id travels in the URL and is what lets the portal scope the code to your app rather than to the address in general. Two things follow from it that are worth knowing: * **A user who lets a link expire is not stuck.** You do not need to detect it or mint a replacement, though minting a fresh link is always safe if your own UI is bringing them back anyway. * **`email_verified` flips to `true` when someone redeems a code**, and only then. A link you minted never sets it: you vouching for an address is not evidence that the person controls that mailbox, and the field would be worthless if it said otherwise. Read it as "has proven they can read this inbox", not as "we checked the address is valid". ## Managing users | Endpoint | What it does | | - | - | | `GET /managed/v1/users` | List your users. Filter by `external_user_id`, `email`, `per_page` (default 50, max 200). | | `GET /managed/v1/users/{user_id}` | One user, including `connections_count` (all connection types). | | `PUT` or `PATCH /managed/v1/users/{user_id}` | Update `external_user_id` or `connection_limit`. Email cannot be changed. | | `DELETE /managed/v1/users/{user_id}` | `204`. Hard-deletes them, cascades their connections, accounts and transactions, revokes every token, and frees their email address for re-registration. | Request and response schemas for each are in the [Managed Users API reference](/api-reference/managed/introduction). ## See every connection at once Rather than polling each user, list every connection your app pays for. **This is not bank-only:** your users' brokerage and crypto-exchange connections come back here too, and the `provider` filter is how you narrow to one kind. ```http theme={"dark"} GET /managed/v1/connections ``` Filters: `user_id`, `external_user_id`, `status`, `provider`, `expiring_within_days`, `per_page`. > [!TIP] > > For changes, [webhooks](#webhooks) are the better signal: they tell you the moment a connection activates, nears expiry or starts failing. Use this listing to reconcile, or to build your first picture of the fleet. Under PSD2 a bank consent lapses every 90 to 180 days, and `?expiring_within_days=14` lists exactly who needs to re-authorize soon. ## Webhooks A Managed app registers webhook endpoints that belong to **the app**, not to any one user. One endpoint receives events for every user the app manages, and each delivery names the user it is about. Your users own no endpoints of their own. Deliveries use the same signatures, retries and envelope as every other Synci webhook, so the [Webhooks guide](/guides/webhooks) applies in full: verify `X-Synci-Signature`, acknowledge fast, deduplicate on `event_id`. ### Register an endpoint Register through the Managed Users API, or in the dashboard under **Developers → Apps → your app → Webhooks**. ```bash Request theme={"dark"} curl -X POST https://api.synci.io/managed/v1/webhooks \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/webhooks/synci", "events": ["connection.activated", "connection.backfill.completed", "connection.expiring", "transactions.created"] }' ``` ```json Response (201 Created) theme={"dark"} { "message": "Webhook endpoint created.", "data": { "id": 123, "url": "https://yourapp.com/webhooks/synci", "events": ["connection.activated", "connection.backfill.completed", "connection.expiring", "transactions.created"], "enabled": true, "secret": "whsec_..." } } ``` The signing `secret` is returned once, here and on rotation. Things that differ from user-owned endpoints: * **Up to 3 endpoints per app**, enough for production, staging and one spare during a migration. A fourth returns `403` with `webhook_endpoint_limit_reached`. * **An approved app needs a Synci plan** before it registers endpoints (`402` with `subscription_required`). Apps in testing can register them to develop against. * **No account filter and no payload version.** An endpoint covers every user, so `financial_accounts` is refused, and deliveries always use the current (v2) payload shape. * **Sensitive identifiers** (IBAN, BBAN, account holder names) are omitted unless your app is approved for `sensitive:read`. `omit_sensitive_identifiers` strips them even when it is. ### Events Every event in the [Webhooks guide](/guides/webhooks#event-types) is available, except the legacy `bank_account.*` aliases, plus one that only a Managed app receives. The ones a Managed app usually builds on: | Event | Use it to | | - | - | | `connection.activated` | Learn that a portal flow worked, without relying on the browser coming back | | `connection.backfill.completed` | Know that a new connection's transaction history has arrived | | `connection.expiring` / `connection.expired` | Prompt the user to renew, if you have turned off Synci's emails | | `connection.failing` / `connection.recovered` | Warn the user, then clear the warning | | `connection.disabled` / `connection.deleted` | Stop showing the connection; you are no longer billed once it is deleted | | `transactions.created` / `transactions.updated` | Pull new or changed transactions as they arrive | | `portal.user.verified` | **Managed apps only.** The user redeemed their first emailed sign-in code, so the address you registered is real and reachable. `data` carries `verified_at`. | ### Which user an event is about App-owned deliveries carry a top-level `managed_user` block next to the usual envelope: ```json theme={"dark"} { "event_type": "connection.activated", "event_id": "evt_0197f2a4c3de7a1b9f04d2e6b8c11a2f", "webhook_endpoint_id": 123, "created_at": "2026-10-17T10:00:00+00:00", "managed_user": { "id": "9f1c2e64-1f3a-4c77-9b21-8a4c5d6e7f80", "external_user_id": "acct-9001" }, "data": { "financial_connection": { "id": 916407839591829, "status": "CONNECTED" }, "activated_at": "2026-10-17T09:59:58+00:00" } } ``` `managed_user.id` is the same `user_id` the Managed Users API returns, and `external_user_id` is your own identifier from registration, so you can route the event without a lookup. ### Manage, test and debug | Endpoint | What it does | | - | - | | `GET /managed/v1/webhooks` | List your endpoints. | | `GET /managed/v1/webhooks/{id}` | One endpoint, with its health. | | `PATCH /managed/v1/webhooks/{id}` | Change `url`, `events`, `enabled`, `description` or `omit_sensitive_identifiers`. | | `DELETE /managed/v1/webhooks/{id}` | Stop deliveries immediately. | | `POST /managed/v1/webhooks/{id}/rotate-secret` | New signing secret, returned once; the old one stops being used at once. | | `POST /managed/v1/webhooks/{id}/test` | Send a signed sample of one event: `{"event": "connection.activated"}`. Sent once, without retries, limited to 5 per minute. | | `GET /managed/v1/webhooks/events` | The delivery log: one entry per attempt, with the outcome. Add `include=payload` to see what was sent. | > [!NOTE] > > Test deliveries carry `managed_user.id` `00000000-0000-4000-8000-000000000000`, an id no real user has, so code that looks the user up fails loudly instead of matching a record by accident. ## App settings All Managed-only, all in **Developers → Apps → your app**, and all take effect **immediately**: none are review-gated. | Setting | Default | What it does | | - | - | - | | **Portal return URL** | none | Where "Continue to ‹App›" sends a user who has finished in the portal. No button is shown if unset. | | **Field mapping** | off | Lets users choose how payee, description and date are filled after they pick their accounts in the portal, with a preview of their own transactions. When off, setup only asks them to choose accounts. Mappings already saved are kept if you turn it off, and you can still set them through the API. | | **Providers** | all we allow | Which bank-data providers your users may connect through: today GoCardless, Akahu and SnapTrade. A value can only narrow the set, never widen it. | | **Countries** | all | Which countries your users may connect banks in (ISO-3166-1 alpha-2). | | **Default connection limit** | Synci default | Bank connections per user. A per-user `connection_limit` overrides it. | | **Email your users** | on | Whether Synci emails your users about expiring, failing and disabled connections. | | **Transaction enrichment** | off | Merchant name, logo and category on transactions. Requires a Pro plan; no per-transaction charge. | | **History depth** | provider default | How much transaction history a new connection requests, capped per institution. | | **Data retention** | 365 days | How long Synci keeps your users' transaction data. | The portal filters its bank picker on providers and countries, so a user never sees an institution they cannot connect. ### Before you turn off "email your users" Synci's expiry emails are a large part of what you get here. A bank consent lapses every 90 to 180 days, and someone has to tell the user and give them somewhere to re-authorize. Off means that is yours to do. To take it over, subscribe to `connection.expiring` (a week before and again a day before), `connection.expired`, `connection.failing` and `connection.disabled`, and when one arrives, send the user a portal session with that `connection_id` so the portal goes straight to the renewal. Left on, your users get the mail instead: it names your app, and its link drops them straight into the portal to fix it. ## Billing You pay for a Managed app the same way every other Synci customer pays: **a Basic or Pro subscription on your own account**, priced per bank connection. There is no separate developer invoice and nothing to reconcile. Your subscription quantity is a single pool, shared between your own personal bank connections and every connection your users create across every Managed app you own. Buy 50, and your users can hold 50 between them. **Registering users is free.** You only pay when one of them actually connects a bank. Up to 200 connections are self-serve. Beyond that, or if you would rather be billed monthly on actual usage than prepay a quantity, [get in touch](mailto:support@synci.io): those are commercial agreements rather than something you can set up yourself. When a connection is refused, the code tells you which ceiling you hit: | `code` | Meaning | | - | - | | `no_subscription` | You have no active plan. Not "full", but never started. | | `trial_limit_reached` | You are on a free trial; unreviewed apps share a small allowance. | | `capacity_reached` | You have used the connections you subscribed for. Raise your plan quantity. | ## Review and going live | State | What it permits | | - | - | | **Testing** | Real banks, real data, capped: a small number of registered users and, while you are on a free trial, 2 live connections across all your unreviewed apps. Counted per developer account, because creating another app is free. | | **Pending review** | Under review by Synci. | | **Approved** | Caps lifted; the portal stops showing your users an "unreviewed" notice and starts showing your logo. | | **Rejected** | Edit and resubmit; a reason is given. | To submit for review you need `developer_name`, `homepage_url` and `privacy_policy_url` set, and an active Synci plan. Paying lifts the trial cap immediately, reviewed or not: you bought the slots. Until you are reviewed, the portal tells your users so and falls back to an initial avatar rather than your logo. An unvetted image never reaches a user. ## Your users are yours * **Deleting a user is real**, and it frees their email address for re-registration. * **They can revoke a bank connection themselves** from the portal at any time. * **They are not a Synci account.** They cannot log in, cannot be reached by Synci sales, and see no Synci dashboard. * **If your app is suspended or removed**, Synci emails them once to say your app can no longer access their bank connections. ## Common errors | Status | When | | - | - | | `401` | Bad Basic credentials on the Managed Users API, or an expired or revoked Bearer token on the Data API. | | `402` | No active Synci plan. Subscribe before registering users, or webhook endpoints on an approved app. | | `403` | Connection capacity or trial ceiling reached, webhook endpoint limit reached, app suspended, or a scope your app does not hold. | | `404` | The user or resource does not exist, or is not yours. | | `409` | Email or `external_user_id` already used by your app. | | `422` | Validation failed; the body lists per-field errors. | | `429` | Rate limit exceeded. | ## Support Email [support@synci.io](mailto:support@synci.io). Include your client ID (never your secret) and, where relevant, the `user_id`. Both are safe to quote. # MCP: Connect AI assistants Source: https://docs.synci.io/guides/mcp Give AI assistants like Claude read-only access to your financial data through Synci's Model Context Protocol server. Synci ships a **Model Context Protocol (MCP)** server, so AI assistants like Claude can answer questions about your finances using live data from your connected accounts. Access is strictly **read-only**: the MCP server cannot create, modify, or delete anything in your Synci account. ```text MCP endpoint theme={"dark"} https://api.synci.io/mcp ``` ## Connecting a client Add the endpoint above to any MCP client that supports remote servers with OAuth (for example Claude, or other assistants that support MCP connectors). The client discovers Synci's OAuth configuration automatically and registers itself; no manual API keys are involved. During authorization you will be asked to: **1. Sign in to your Synci account** **2. Approve the permission** The assistant requests **"Access your financial data through AI assistants (read-only)"**. **3. Select which accounts the assistant may see** You can revoke the connection at any time from your Synci settings. Connecting requires an active Synci subscription. ## What the assistant can do | Tool | Returns | | - | - | | `list_connections` | Your bank and provider connections and their health status | | `list_accounts` | Your connected financial accounts (bank, investment, crypto) | | `get_account_balance` | Recent balance snapshots for one account | | `list_transactions` | Transactions within a date range | | `list_holdings` | Investment and crypto positions (symbol, quantity, market value) | > [!NOTE] > > Data reflects the most recent sync from your bank or broker, not real-time values. Sensitive identifiers (IBANs, account numbers, card numbers, owner names) are always omitted from MCP responses. ## Security model The MCP server follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), which requires OAuth 2.1 with **Dynamic Client Registration** (RFC 7591). AI clients register themselves at `https://api.synci.io/oauth/register` when you connect them. That endpoint is intentionally open: it is how Claude, ChatGPT and other assistants obtain a `client_id` without a manual approval step, and it is not a vulnerability on its own. What a self-registered client can do is bounded by the controls below. > [accordion] Scope confinement > > Every self-registered client is pinned to the single `mcp:use` scope, regardless of what it requests at registration or authorization. That scope grants access to the read-only MCP tools listed above and nothing else. It cannot reach the REST API, the Developer Program surfaces, or any write operation. > [accordion] PKCE is mandatory > > Self-registered clients are public clients (no client secret). The authorization endpoint rejects any request from a public client that does not carry a `code_challenge` with the `S256` method, so an intercepted authorization code cannot be exchanged for a token. > [accordion] Redirect URIs are exact-match > > The `redirect_uri` presented at authorization must exactly match one registered for that client. At registration, redirect URIs must use `https`; plain `http` is accepted only for `localhost` loopback callbacks, and browser-reserved schemes such as `javascript:` or `data:` are refused. > [accordion] Consent is never skipped > > Every self-registered client goes through the Synci consent screen. The screen names the client, shows the exact host that will receive the authorization code, and warns when the assistant is not one we recognise. Curated branding is only applied when the client's redirect host matches a known AI tool, so a client that merely names itself after a known assistant is shown as unrecognised. You also choose which accounts the assistant may see, and an active Synci subscription is required. > [accordion] Rate limiting and monitoring > > Registration is rate-limited per IP, and rejected registrations are reported to our error monitoring. > [accordion] Revocation > > You can disconnect any assistant from your Synci settings at any time. Access tokens expire after one hour and refresh tokens after 30 days. If you believe you have found a security issue that bypasses one of these controls, contact us at [support@synci.io](mailto:support@synci.io). We do not run a paid bug bounty program. # Building OAuth apps Source: https://docs.synci.io/guides/oauth Register an app and implement the OAuth 2.0 authorization code flow with PKCE so other Synci users can share their data with you. OAuth apps let **other Synci users** grant your application access to their financial data. Unlike a personal access token (which only reaches your own account), an OAuth app uses a consent flow: the user reviews the permissions you request, picks which accounts to share, and can revoke access at any time. Synci implements **OAuth 2.0 authorization code grant with PKCE**. ## Which kind of app do you need? Two self-serve app types share this API. You pick one when you create the app, and **the choice is permanent**: it decides who is billed for every bank connection the app creates. | | **Standard** | **Managed users** | | - | - | - | | Your users | Have their own Synci account | Have no Synci account and never will | | How they authorize | OAuth consent screen (this guide) | You register them server-to-server; they use a hosted portal | | Who pays for connections | Each user's own Synci plan | **You**, out of your own plan | | What you can request | Any scope | Bank-data scopes only | | Redirect URIs | Required | Not used | **Standard** is the right default: if the people you are building for already have Synci accounts, or would happily create one, this guide is all you need. **Managed users** exists for products that want bank connections embedded in their own experience, where sending people off to create a Synci account would lose them. See [Managed apps](/guides/managed-apps). The flow is quite different from the one below. ```mermaid theme={"dark"} flowchart TD A{Do your users have
their own Synci accounts?} -->|Yes, or they'd create one| B[Standard app] A -->|No, and they never will| C[Managed app] B --> D[OAuth consent screen
They pay for their connections] C --> E[Managed Users API + hosted portal
You pay for their connections] ``` Building at high volume, or want a commercial arrangement other than a standard plan (usage-based billing, for example)? Those are done by agreement rather than self-serve: [get in touch](mailto:support@synci.io). The rest of this guide covers **Standard** apps. ## Register your app In the Synci dashboard, go to **Developers → Apps → Create app**. You will set: * **Name**: shown to users on the consent screen. * **Redirect URIs**: up to 5 exact-match `https://` URLs where Synci sends users back after they approve. No wildcards, no trailing-slash differences. While your app is in **testing**, `http://` is also allowed for loopback hosts (`localhost`, `127.0.0.1`, `::1`) so you can develop locally; loopback URIs must be replaced with `https://` before you submit for review. * **Permissions (scopes)**: the scopes your app may request. On any given authorization you can request a subset, never more. See the scope table in [Authentication](/guides/authentication#scopes). * **Branding**: developer or company name, description, homepage, privacy policy, and logo. Required before you can submit for review. ### Client credentials Every app is a **confidential** (server-side) client: * **Client ID**: public identifier, safe to embed in URLs. * **Client secret**: shown **once** at creation. Store it securely on your server and never expose it in a browser or mobile app. You can rotate it at any time (the previous secret stops working immediately). > [!NOTE] > > Public or PKCE-only clients (browser-only or mobile apps without a backend) are not currently offered. If you need one, [get in touch](mailto:support@synci.io). ## The authorization flow **1. Generate PKCE values and state** Create a random `code_verifier` (43 to 128 characters), then derive the challenge: ```text theme={"dark"} code_challenge = BASE64URL( SHA256( code_verifier ) ) ``` Also generate a random `state` string to protect against CSRF. **2. Redirect the user to the authorize endpoint** ```http theme={"dark"} GET https://api.synci.io/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=accounts:read%20transactions:read &state=RANDOM_STATE &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 ``` Scopes are space-separated (URL-encoded as `%20`) and must be a subset of the scopes your app is registered for. The user signs in, reviews your app's branding and the requested permissions, and, when account-scoped permissions are requested, selects which accounts your app may access. Users approve the request as a whole; they cannot deny individual scopes. **On approval**, Synci redirects back to your redirect URI: ```text theme={"dark"} https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STATE ``` Verify that `state` matches what you sent before continuing. The code is single-use and expires after about 10 minutes. **On denial**, the redirect carries `?error=access_denied` instead of a code. **3. Exchange the code for tokens** From your backend, POST to the token endpoint. The body must be **`application/x-www-form-urlencoded`**, not JSON: ```bash Request theme={"dark"} curl -X POST https://api.synci.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d redirect_uri=https://yourapp.com/callback \ -d code_verifier=YOUR_CODE_VERIFIER \ -d code=AUTH_CODE ``` ```json Response theme={"dark"} { "token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOi...", "refresh_token": "def50200..." } ``` Access tokens are valid for **1 hour**. Refresh tokens are valid for **30 days** and rotate on every use. **4. Call the API** ```bash theme={"dark"} curl "https://api.synci.io/api/v1/finance/accounts" \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Accept: application/json" ``` The token can reach exactly what the user approved: requests outside the granted scopes return `403`, and accounts the user did not select return `404`. **5. Refresh tokens** When the access token expires, exchange the refresh token (again form-urlencoded): ```bash theme={"dark"} curl -X POST https://api.synci.io/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=refresh_token \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d refresh_token=YOUR_REFRESH_TOKEN ``` Each refresh returns a new access token **and** a new refresh token. Always store the newly returned refresh token; the one you just used is invalidated. ## App review lifecycle | State | Who can authorize | Editing | | - | - | - | | **Testing** | You, plus the first 10 other people who authorize it | Changes take effect immediately | | **Pending review** | No new authorizations | Locked while under review | | **Approved** | Any Synci user; the consent screen no longer warns that Synci has not approved the app | Changes to branding, scopes, and redirect URIs are queued for re-review before taking effect | | **Rejected** | No one | Edit and resubmit; a reason is provided | You can return an approved app to development at any time; this unpublishes it without revoking existing user connections. Deleting an app or rotating its secret requires password confirmation in the dashboard. ### The pre-review limit An app that has not been reviewed is open to anyone, but only up to **10 people besides you**. That is enough to run a real beta, and it bounds how far an unvetted app can reach before Synci has looked at it. * You never consume a slot, so 10 means 10 other people. * **Revoking frees a slot**, and an abandoned authorization frees one when its refresh token lapses. * Someone who already authorized can always re-authorize, even at the limit. * Approval removes the limit entirely, and existing authorizations carry over. Once the limit is reached, a new user sees an "app isn't taking new users yet" page rather than your consent screen. Your app payload reports where you stand: ```json theme={"dark"} "early_users": { "used": 3, "limit": 10, "reached": false } ``` `limit` is `null` once approved. ## Good to know > [accordion] The token endpoint is form-urlencoded > > Per the OAuth spec, `/oauth/token` expects `application/x-www-form-urlencoded`. Sending JSON fails with `invalid_client`. > [accordion] Redirect URIs match exactly > > No wildcards, and `https://yourapp.com/callback` is not the same as `https://yourapp.com/callback/`. > [accordion] Users can revoke your app at any time > > From their Synci settings. All of its tokens stop working immediately and the user must authorize again. > [accordion] Suspended apps are cut off immediately > > Existing tokens are rejected and refresh is blocked. > [accordion] Request minimal scopes > > Users are more likely to approve an app that asks only for what it needs, and reviews go faster. ## Common errors | Error | Cause | | - | - | | `invalid_client` | Wrong or missing `client_id` or `client_secret`, or the token request was sent as JSON instead of form-urlencoded. | | `invalid_grant` | The authorization code expired or was already used, the `code_verifier` does not match the original `code_challenge`, or the refresh token was already rotated or revoked. | | `invalid_scope` | A requested scope is not among the scopes your app is registered for. | | `access_denied` (on the redirect) | The user declined the authorization request. | | An "app isn't taking new users yet" page | Your app is unreviewed and has reached its 10-user pre-review limit. Submit for review to lift it. | | `403` on an API call | The token lacks the required scope for that endpoint. | | `404` on an account | The user did not grant your app that account. | # Pagination, filtering, and sorting Source: https://docs.synci.io/guides/pagination How list endpoints page through results, narrow them down, and embed related data. All list endpoints share the same conventions for paging through results, narrowing them down, and controlling what related data is embedded. ## Pagination List endpoints are paginated with `page[number]` and `page[size]` query parameters: ```http theme={"dark"} GET /api/v1/finance/transactions?page[number]=2&page[size]=50 ``` * **Default page size**: 25 * **Maximum page size**: 100 Responses wrap the results in `data` and include `links` and `meta` for navigation: ```json theme={"dark"} { "data": [ ... ], "links": { "first": "https://api.synci.io/api/v1/finance/transactions?page[number]=1", "last": "https://api.synci.io/api/v1/finance/transactions?page[number]=9", "prev": "https://api.synci.io/api/v1/finance/transactions?page[number]=1", "next": "https://api.synci.io/api/v1/finance/transactions?page[number]=3" }, "meta": { "current_page": 2, "from": 51, "to": 100, "per_page": 50, "total": 423, "last_page": 9 } } ``` Iterate by following `links.next` until it is `null`, or use `meta.last_page`. ## Filtering List endpoints accept `filter[...]` query parameters. The available filters for each endpoint are listed on its reference page. For example, transactions support filters such as: ```http theme={"dark"} GET /api/v1/finance/transactions ?filter[financial_account_id]=123 &filter[booking_date_after]=2026-06-01 &filter[booking_date_before]=2026-06-30 &filter[search]=spotify ``` Date-range filters (`booking_date_after`, `booking_date_before`, `value_date_after`, `value_date_before`) are inclusive on both ends. Multiple filters combine with AND. Where a filter accepts multiple values, separate them with commas: ```http theme={"dark"} GET /api/v1/finance/accounts?filter[integrator]=GOCARDLESS,AKAHU ``` ## Sorting Use the `sort` parameter with a field name; prefix with `-` for descending order: ```http theme={"dark"} GET /api/v1/finance/transactions?sort=-booking_date ``` Sortable fields per endpoint are listed in the reference. ## Including related data Some endpoints can embed related resources in the response via the `include` parameter, saving you extra round trips: ```http theme={"dark"} GET /api/v1/finance/transactions?include=financial_account,enriched ``` For example, transactions support includes such as `financial_account` (and its connection and institution via dot notation, like `financial_account.financial_connection.institution`), `transfer_logs`, `enriched` (transaction enrichment data), and `trade` (brokerage trade details). Available includes are documented per endpoint in the reference; fields backed by an include are absent unless you request it. # Rate limits Source: https://docs.synci.io/guides/rate-limits Request limits across the API and how to handle 429 responses. Rate limits protect the API and the underlying bank providers. When you exceed a limit, the API responds with `429 Too Many Requests` and a JSON `message`. Where applicable, the response includes a `Retry-After` header (seconds until you can retry). ## Limits | What | Limit | | - | - | | General API requests | **100 requests per minute** per user | | Manual account sync (`POST /finance/accounts/{id}/sync`) | Per account and data type; a `429` means that data was synced recently, and the message says roughly how long to wait | | Transfer and rule undo/retry endpoints | 6 requests per minute | | Webhook test deliveries (`POST /webhooks/{id}/test`) | 5 requests per minute | | Destination connection refresh | 1 request per minute per connection | > [!TIP] > > Bank data itself is refreshed on a schedule that depends on your plan and each connection's configuration, so polling the API faster than your sync frequency will not surface new data any sooner. For real-time reactions, use [Webhooks](/guides/webhooks) instead of polling. ## Handling `429` responses * Back off and retry after the `Retry-After` interval (or with exponential backoff if the header is absent). * Spread bulk work over time rather than bursting; with pagination at 100 items per page and 100 requests per minute, you can move a lot of data well within the limits. * Cache data you have already fetched. Transactions and balances only change when a sync runs. # Synci as a SimpleFIN provider Source: https://docs.synci.io/guides/simplefin Any SimpleFIN-compatible app can read a user's accounts, balances, and transactions from Synci. The Synci-specific details a consumer needs. Synci is a [SimpleFIN](https://www.simplefin.org/protocol.html) data source. It implements the SimpleFIN protocol, so any SimpleFIN-compatible app can read a user's financial data (accounts, balances, and transactions) from Synci directly. The connection is read-only and scoped to the accounts the user chooses. ## If you build or run a finance app **If your app already speaks SimpleFIN, it already works with Synci.** There is nothing Synci-specific to build: no partnership, no API keys to request, no Synci SDK. A user creates a Synci destination, copies a standard setup token, and pastes it into the same SimpleFIN screen your app already has. From your side it is an ordinary SimpleFIN source. If SimpleFIN support is on your roadmap, implementing the [spec](https://www.simplefin.org/protocol.html) is all it takes: Synci then becomes a data source your users can pick, for free, the day you ship it. What Synci brings to your users: > [card] Bank coverage beyond North America > > European banks (via GoCardless) and New Zealand banks (via Akahu). > [card] More than checking and savings > > Investment, brokerage and crypto-exchange accounts are exposed as SimpleFIN accounts too. > [card] Data users can shape > > Transactions reflect the user's Synci bank-account rules (payee cleanup, renaming), so what your app imports is already tidy. [Actual Budget](https://actualbudget.org) is the officially supported and tested consumer (see the [Actual Budget setup guide](https://help.synci.io/articles/9212576-actual-budget)). Other SimpleFIN-compatible apps (Buckets, Sparky Budget, ZapYeti, EnvelopeBudget, and others) work through the same flow via the generic **SimpleFIN** destination; see [Apps powered by SimpleFIN](https://help.synci.io/articles/9818693-apps-powered-by-simplefin). The rest of this page covers the Synci-specific details a consumer needs. For the protocol itself (the request/response format), see the [SimpleFIN spec](https://www.simplefin.org/protocol.html). ## Getting a setup token **1. Open the destination in Synci** Go to **Destinations** then **SimpleFIN** (or an app-specific page such as **Actual Budget**). The page is presentation only; the token it produces works with any SimpleFIN consumer. **2. Choose which accounts to expose** Share all of them, or a specific selection. **3. Copy the setup token** Copy the one-time **setup token**. The setup token is a Base64-encoded claim URL. It can be claimed once, expires after about 7 days if unused, and can be regenerated from the destination's page (regenerating invalidates the previous token). Treat it as a secret. ## Claiming the token The token exchange follows the standard SimpleFIN flow: **1. Decode the setup token** Base64-decode the setup token to get a claim URL. **2. Claim it once** `POST` the claim URL once. On success you receive an **access URL** as `text/plain`: ```text theme={"dark"} https://{username}:{password}@api.synci.io/simplefin ``` The credentials are embedded in the URL. Store the access URL; the setup token is now spent. **3. Request accounts from then on** Request `{access URL}/accounts` using HTTP Basic auth built from the embedded `username:password`. The host and path come from the token and the access URL, so a consumer never hardcodes them. A claim that has already been used, expired, or been revoked returns a `403` whose body begins with `Forbidden`. Access credentials are revocable at any time from the destination's page in Synci. Once revoked, requests return `403`. ## Fetching accounts `GET {access URL}/accounts` returns a SimpleFIN Account Set. It is called in two shapes: ```http Balances only theme={"dark"} GET /accounts?balances-only=1 ``` ```http With transactions theme={"dark"} GET /accounts?start-date={epoch}&end-date={epoch}&pending=1&account={id}&account={id} ``` **Balances only** (account discovery, no transactions) returns every exposed account with its balances and an empty `transactions` array. **With transactions:** * `start-date` / `end-date` are Unix timestamps in **seconds**. `end-date` is exclusive. * `pending=1` includes not-yet-posted transactions. * Repeat `account={id}` to limit the response to specific accounts. Ids not exposed by this connection are ignored. * If `start-date` is omitted, the response is capped to roughly the last 90 days. There is no pagination; each call returns a single JSON document. ## Response shape ```json theme={"dark"} { "errors": [], "accounts": [ { "org": { "domain": "inst-916407839591829.synci.io", "sfin-url": "https://api.synci.io/simplefin", "name": "DNB" }, "id": "916407839591829", "name": "DNB Brukskonto", "currency": "NOK", "balance": "12345.67", "available-balance": "12000.00", "balance-date": 1767225600, "transactions": [ { "id": "935986745228429", "posted": 1767139200, "transacted_at": 1767052800, "amount": "-129.00", "description": "REMA 1000 OSLO", "payee": "REMA 1000", "pending": false } ] } ] } ``` Synci-specific guarantees, beyond the base spec: * **Amounts** (`balance`, `available-balance`, transaction `amount`) are signed decimal strings with exactly two decimal places. A negative amount is an outflow. * **Timestamps** (`posted`, `transacted_at`, `balance-date`) are Unix seconds at noon UTC of the relevant calendar date. * **Ids** (account `id`, transaction `id`) are stable across requests, so consumers can safely deduplicate on them. * **`payee`** is always present and non-empty. * A **pending** transaction always carries a `transacted_at`. * **`org`** identifies the underlying institution, not Synci: `org.name` is the institution's name (for example "DNB") and `org.domain` is stable and unique per institution. Consumers that group accounts into "banks" by `org.domain` therefore get one bank per institution, and multiple accounts from the same institution are grouped together. * `available-balance` is included only when Synci has one for the account. ## Limits and behavior * **History:** roughly 90 days when no `start-date` is given. * **Rate limit:** approximately 60 requests per hour per access credential. * **Freshness:** responses are served from Synci's most recent provider sync, not live from the bank, so balances in particular can lag behind. There is no real-time fetch on request. * Only bank-style account data is exposed (accounts, balances, transactions). No other part of the user's Synci account is reachable through these credentials. ## Versioning and stability Synci advertises SimpleFIN version `1.0`, which the protocol's `info` endpoint reports as `{"versions":["1.0"]}`. An issued access URL is permanent for the life of the credential. Any breaking change would ship as a new protocol version and new credentials rather than altering an existing access URL, so a working integration keeps working. ## Security * All requests are over HTTPS. * Credentials are read-only and scoped to exactly the accounts the destination exposes. If the user shared all accounts, that scope includes accounts they connect to Synci later. * Setup tokens are single-use with an expiry; access credentials are revocable at any time from Synci. * Treat both the setup token and the access URL as secrets. # Webhooks Source: https://docs.synci.io/guides/webhooks Receive real-time notifications when transactions arrive, balances change, or accounts come and go. Event types, payloads, signature verification, and retries. Webhooks let your application (or a third-party service like Zapier) receive real-time notifications when something happens in a Synci account: new transactions arrive, a balance changes, an account is added or removed, investment holdings update, or a bank connection is authorized, nears expiry or starts failing. ## Setting up an endpoint Create endpoints in the dashboard under **Developers → Webhooks**, or via the API ([Create new webhook endpoint](/api-reference/webhooks/create-new-webhook-endpoint), scope `webhooks:write`). Each endpoint has: * **URL**: the HTTPS address Synci sends `POST` requests to. * **Events**: the event types you want to receive (see below). You only receive events you subscribe to. * **Account filter**: optionally limit the endpoint to specific bank accounts. Leave empty to receive events for all accounts. * **Omit sensitive identifiers**: strips IBANs, BBANs, and similar identifiers from payloads. Required for Zapier endpoints. * **Signing secret**: a `whsec_...` secret generated when the endpoint is created, used to verify that deliveries really come from Synci. It is shown once on creation and on rotation. > [!NOTE] > > Webhook delivery requires an active Synci subscription. > [!TIP] > > Building a **Managed app**? Register one endpoint for the app instead, through the Managed Users API or the dashboard. It receives these events for every user the app manages. See [Managed apps](/guides/managed-apps#webhooks). ## Event types **Account data** | Event | Fires when | | - | - | | `transactions.created` | New transactions are fetched from the bank | | `transactions.updated` | An existing transaction changes (for example, a pending transaction books) | | `balance.updated` | An account balance changes | | `holdings.updated` | Investment or crypto holdings for an account change | | `account.created` | A financial account is added | | `account.updated` | A financial account's details or status change | | `account.deleted` | A financial account is removed | | `account.backfill.completed` | An account's transaction history backfill has completed (see [below](#knowing-when-history-is-in)) | **Connection lifecycle** (details in [Connection lifecycle](#connection-lifecycle)) | Event | Fires when | | - | - | | `connection.created` | A connection is created and is waiting for the user to authorize with the bank | | `connection.activated` | The user authorized and the connection is live | | `connection.reauthorized` | Consent was renewed on an existing connection | | `connection.expiring` | Consent expires in a week, and again a day before | | `connection.expired` | Consent has expired; the user must reauthorize | | `connection.failing` | Syncing has failed three times in a row | | `connection.recovered` | A failing connection synced successfully again | | `connection.disabled` | Syncing has stopped, with the reason | | `connection.deleted` | The connection was removed | | `connection.backfill.completed` | Every syncing account on a connection has completed its history backfill | > [!NOTE] > > **Payload versions.** Endpoints created today use payload version **v2**, which uses the event names above. Older endpoints on **v1** receive the legacy names `bank_account.created`, `bank_account.updated`, and `bank_account.deleted` instead of `account.*`, and a `bank_account` key instead of `financial_account` in payloads. The version is pinned when the endpoint is created, so existing integrations keep working unchanged. ### Knowing when history is in When an account is connected, Synci stores the most recent transactions first and fetches older history behind it, in windows, until it reaches the account's target date: the account's `sync_start_date` if one is set, otherwise the connection's historical window. `transactions.created` fires for every window, so it cannot tell you that the walk is over. `account.backfill.completed` fires once the walk reaches its target. Its `data` block carries the account embed plus a `backfill` summary: ```json theme={"dark"} { "financial_account": { "id": 123456789, "name": "Main account", "currency": "EUR" }, "backfill": { "history_from": "2025-02-19", "oldest_transaction_date": "2025-02-21", "transactions_count": 1432, "completed_at": "2026-10-12T13:04:11+00:00" } } ``` `history_from` is the date the walk worked toward. `oldest_transaction_date` is the oldest transaction that actually arrived. The two differ when the bank served nothing that old, whether because the account had no activity or because the bank limits how far back it serves data after consent. Synci cannot tell those apart, so compare the dates and decide what to show. `connection.backfill.completed` fires once every enabled, automatically syncing account on the connection has completed, with a `financial_connection` embed, a `completed_at` timestamp and an `accounts` list carrying the same summary per account. Accounts the user left out during setup never hold it back. It is connection-scoped, so an endpoint's account filter does not apply to it. A window the bank refuses (a rate limit, a temporary outage) pauses the walk until the next scheduled sync rather than ending it, so a delayed completion still fires. Both events can fire again later if a manual sync with an earlier start date opens a new backfill and walks it to the end. ### Connection lifecycle Every `connection.*` event carries a `financial_connection` object with the same shape as [Get financial connection](/api-reference/connections/get-financial-connection) (without the accounts; `account.*` events cover those), plus fields specific to the event: | Event | Extra fields | | - | - | | `connection.created` | None | | `connection.activated` | `activated_at` | | `connection.reauthorized` | `previous_status`: the connection's status before the renewal, such as `EXPIRED` | | `connection.expiring` | `stage` (`week` or `day`), `consent_expires_at`, `days_remaining` | | `connection.expired` | `consent_expires_at` | | `connection.failing` | `failure_count`, `integrator_message`: the bank or provider's own error text | | `connection.recovered` | `previous_failure_count` | | `connection.disabled` | `reason`: `user_request`, `consent_expired`, `provider_revoked`, `provider_error`, `health_dead` or `manual` | | `connection.deleted` | `activated_at`, null when the connection was never authorized | | `connection.backfill.completed` | `completed_at`, `accounts` (see [above](#knowing-when-history-is-in)) | ```json connection.expiring (trimmed) theme={"dark"} { "financial_connection": { "id": 916407839591829, "status": "CONNECTED", "institution": { "id": 755065161760068, "name": "DNB" }, "consent_expires_at": "2026-11-02T09:00:00+00:00" }, "stage": "week", "consent_expires_at": "2026-11-02T09:00:00+00:00", "days_remaining": 7 } ``` Connection events are connection-scoped, so an endpoint's account filter never applies to them. > [accordion] Which providers emit which events > > `connection.expiring` and `connection.expired` only fire for banks whose consent expires on a schedule (European banks, and the demo bank with expiring consent). Akahu (New Zealand) and SnapTrade (brokerages and crypto exchanges) grant enduring consent, so their connections never expire. > [accordion] Abandoned authorizations > > A connection the user never authorized emits `connection.created` and nothing else: no `activated`, no `failing`. Treat a `created` without an `activated` within your own timeout as abandoned. > [accordion] Failing and recovered come in pairs > > `connection.failing` fires on the third consecutive failure, not the first, because a single failed sync is usually the bank having a moment. Once you have seen `failing`, expect either `connection.recovered` or `connection.disabled` to follow, so you can clear or escalate whatever you showed the user. ## Delivery format Every delivery is a `POST` with a JSON body in a common envelope: ```json theme={"dark"} { "event_type": "transactions.created", "event_id": "evt_0197f2a4c3de7a1b9f04d2e6b8c11a2f", "webhook_endpoint_id": 123, "created_at": "2026-07-11T10:00:00+00:00", "data": { "...": "event-specific payload" } } ``` **event\_type** `string` One of the event types above. **event\_id** `string` Uniquely identifies the event and is **stable across retries**: if a delivery fails and is retried, the retry carries the same `event_id`. Use it to deduplicate. **webhook\_endpoint\_id** `integer` The endpoint the delivery was sent to. **created\_at** `string` When the event occurred, as an ISO 8601 timestamp. **data** `object` The event-specific payload. Requests are sent with the User-Agent `Synci-Webhooks/1.0`. ## Verifying signatures Every delivery includes an `X-Synci-Signature` header: a hex-encoded **HMAC-SHA256** of the raw request body, keyed with your endpoint's signing secret. Always verify it before trusting the payload. Compute the HMAC over the **raw bytes** of the body (before any JSON parsing), and compare using a constant-time comparison. ```php PHP theme={"dark"} $secret = 'whsec_your_secret'; $signature = $_SERVER['HTTP_X_SYNCI_SIGNATURE'] ?? ''; $payload = file_get_contents('php://input'); $computed = hash_hmac('sha256', $payload, $secret); if (! hash_equals($computed, $signature)) { http_response_code(401); exit; } // Signature valid: parse and process $payload ``` ```javascript Node.js (Express) theme={"dark"} const crypto = require('crypto'); // Use a raw body parser so the bytes are untouched app.post('/webhooks/synci', express.raw({ type: 'application/json' }), (req, res) => { const secret = process.env.SYNCI_WEBHOOK_SECRET; const signature = req.get('X-Synci-Signature') ?? ''; const computed = crypto.createHmac('sha256', secret).update(req.body).digest('hex'); const valid = signature.length === computed.length && crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature)); if (!valid) return res.sendStatus(401); res.sendStatus(200); // acknowledge first const event = JSON.parse(req.body); // then process (ideally on a queue) }); ``` ```python Python (Flask) theme={"dark"} import hashlib import hmac import os from flask import Flask, abort, request app = Flask(__name__) @app.post("/webhooks/synci") def synci_webhook(): secret = os.environ["SYNCI_WEBHOOK_SECRET"].encode() signature = request.headers.get("X-Synci-Signature", "") computed = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest() if not hmac.compare_digest(computed, signature): abort(401) event = request.get_json() # process on a queue return "", 200 ``` ```csharp C# (.NET) theme={"dark"} using System.Security.Cryptography; using System.Text; var secret = "whsec_your_secret"; var signature = Request.Headers["X-Synci-Signature"].ToString(); var body = await new StreamReader(Request.Body).ReadToEndAsync(); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body)); var computed = Convert.ToHexString(hash).ToLowerInvariant(); var valid = CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(computed), Encoding.UTF8.GetBytes(signature)); if (!valid) { return Unauthorized(); } // Signature valid: parse and process body ``` ### Rotating the secret Rotate the signing secret from the endpoint's settings in the dashboard, or via the [Rotate webhook secret](/api-reference/webhooks/rotate-webhook-secret) endpoint. The new secret is returned once; update your verification code immediately, as the old secret stops being used. ## Retries and failure handling * Your endpoint should respond with a **2xx** status within a few seconds. * Failed deliveries (non-2xx or timeouts) are retried automatically with increasing delays: about **1 minute**, then **10 minutes**, then **1 hour**. * If your endpoint responds `410 Gone`, Synci stops sending to it and disables the endpoint immediately. * Endpoints that keep failing are marked unhealthy and eventually disabled automatically, and you are notified. You can re-enable an endpoint from the dashboard once it is fixed. ## Testing Send a test event from the endpoint's page in the dashboard, or via the [Test webhook endpoint](/api-reference/webhooks/test-webhook-endpoint) endpoint. You can choose real data from your account or dummy data. Test deliveries: * Are limited to **5 per minute**. * Are sent once, without retries. * Are signed exactly like real deliveries, so you can verify your signature code end to end. ## Best practices > [accordion] Acknowledge fast, process async > > Return `2xx` immediately and hand the payload to a queue or background worker. Slow responses count as failures and trigger retries. > [accordion] Deduplicate on event\_id > > Retries mean you can receive the same event more than once. `event_id` is stable across retries, so skipping an `event_id` you have already processed makes repeat deliveries harmless. > [accordion] Verify every signature > > Anyone can POST to a public URL; the signature is your proof the event came from Synci. > [accordion] Don't rely on ordering > > Events are delivered independently and retries can arrive out of order. Use the `created_at` timestamp and fetch current state from the API when ordering matters. # Introduction Source: https://docs.synci.io/index Fetch, transform, and transfer banking and financial data with the Synci API. Synci lets you fetch, transform, and transfer your banking and financial data anywhere. The API gives you programmatic access to everything the Synci dashboard can do: accounts, balances, transactions, investment holdings, bank connections, transfer links, rules, destinations, and webhooks. ```text theme={"dark"} Base URL: https://api.synci.io API root: https://api.synci.io/api/v1 ``` The API is REST-based and served over HTTPS only. Requests and responses use JSON, except where noted (the OAuth token endpoint uses form encoding). Send an `Accept: application/json` header with every request. ## What you can build > [card] Reporting and dashboards > > Pull transactions, balances, and holdings across all of a user's connected banks, brokerages, and crypto exchanges. > [card] [Automations](/guides/webhooks) > > Trigger workflows from webhooks when new transactions arrive or balances change. > [card] [Integrations](/guides/oauth) > > Build an OAuth app so other Synci users can securely share their financial data with your product. > [card] [Embedded bank connections](/guides/managed-apps) > > Offer bank connections inside your own product to people who have no Synci account. ## Authentication at a glance All requests are authenticated with a Bearer token: ```http theme={"dark"} Authorization: Bearer YOUR_ACCESS_TOKEN ``` There are two ways to get a token: | You are building... | Use | Where | | - | - | - | | Scripts or a server-to-server integration for **your own** Synci account | **Personal access token** | Dashboard → Developers → API tokens | | An app that **other Synci users** authorize | **OAuth 2.0 app** (authorization code + PKCE) | Dashboard → Developers → Apps | See [Authentication](/guides/authentication) for details, [Building OAuth apps](/guides/oauth) for the full OAuth guide, and [Managed apps](/guides/managed-apps) if your users will never hold a Synci account of their own. ## Quick start **1. Generate a token** In the Synci dashboard, go to **Developers → API tokens** and create a personal access token. Copy it right away; it is shown only once. **2. List your accounts** ```bash theme={"dark"} curl "https://api.synci.io/api/v1/finance/accounts" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` **3. Fetch transactions booked since June 1st** ```bash theme={"dark"} curl "https://api.synci.io/api/v1/finance/transactions?filter[booking_date_after]=2026-06-01&sort=-booking_date&page[size]=50" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Accept: application/json" ``` Every list endpoint pages, filters and sorts the same way. See [Pagination, filtering, and sorting](/guides/pagination). ## Core resources | Resource | What it is | | - | - | | **Financial connections** | A link to one institution (a bank, brokerage, or crypto exchange). Has a health status: `HEALTHY`, `FAILING`, or `DEAD`. | | **Financial accounts** | The individual accounts under a connection (checking, savings, credit, investment). | | **Balances** | Balance snapshots per account, with type and reference date. | | **Transactions** | Booked and pending transactions, with rich detail, enrichment, and (for brokerages) trade data. | | **Holdings** | Current investment and crypto positions per account (symbol, quantity, market value). | | **Transfer links** | Connect a financial account to a destination (YNAB, Lunch Money, Google Sheets) so data flows automatically. | | **Transfer logs** | The record of every transfer attempt, with undo support. | | **Rules** | Conditions and actions that categorize or transform transactions, either persisted in Synci or applied during transfer. | | **Destinations** | Your connected destination apps and their accounts. | | **Webhook endpoints** | Your HTTPS endpoints that receive event notifications. | The full endpoint reference, with every path, parameter, and response schema and in-browser request testing, is under [API reference](/api-reference). ## Guides > [card] [Authentication](/guides/authentication) > > Personal access tokens, OAuth tokens, and scopes. > [card] [Building OAuth apps](/guides/oauth) > > Register an app and implement the authorization flow. > [card] [Managed apps](/guides/managed-apps) > > Bank connections for users who have no Synci account. > [card] [Webhooks](/guides/webhooks) > > Event types, payloads, signature verification, and retries. > [card] [Pagination, filtering, and sorting](/guides/pagination) > > How list endpoints work. > [card] [Errors](/guides/errors) > > Response format and status codes. > [card] [Rate limits](/guides/rate-limits) > > Request limits and how to handle `429` responses. > [card] [MCP: Connect AI assistants](/guides/mcp) > > Give AI tools read-only access to your financial data. ## Versioning The current API version is **v1**, and all documented endpoints live under `/api/v1`. We treat the following as non-breaking and may ship them without notice: adding new endpoints, adding new optional request parameters, adding new fields to responses, and adding new webhook event types (you only receive events you subscribe to). Breaking changes will be announced ahead of time. > [!NOTE] > > A handful of endpoints have a reworked shape under `/api/v2`. Version 2 is in alpha and documented separately under [API v2](/api-reference/v2/introduction); everything not listed there is still served and documented under v1. ## Getting help * **Help center**: [help.synci.io](https://help.synci.io) * **Dashboard**: manage tokens, apps, and webhooks under **Developers** in your Synci account. * **Email**: [support@synci.io](mailto:support@synci.io) # Managed Users API Source: https://docs.synci.io/api-reference/managed/introduction The server-to-server API a Managed app uses to register its users, mint tokens and portal links, list the connections it pays for, and manage its webhook endpoints. The Managed Users API is a separate surface from the data API, with its own base URL and its own authentication. A [Managed app](/guides/managed-apps) uses it to manage the people it has registered with Synci and the app's own [webhook endpoints](/guides/managed-apps#webhooks), which receive events for all of those people. The data they connect is read through the normal [v1 API](/api-reference) with a per-user Bearer token. ```text theme={"dark"} Base URL: https://api.synci.io/managed/v1 ``` ## Authentication Every request uses **HTTP Basic** with your app's client ID as the username and its client secret as the password: ```bash theme={"dark"} curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ https://api.synci.io/managed/v1/users \ -H "Accept: application/json" ``` | Response | Meaning | | - | - | | `401` `invalid_client` | Bad or missing credentials, or the app is not a Managed app. Deliberately indistinguishable, so this endpoint cannot be used to discover which client IDs exist. | | `403` `client_suspended` | The app has been suspended by Synci. | Rate limits are **300 requests per minute per client** and 600 per minute per source address. Errors carry `error` and `message` keys. > [!NOTE] > > The step-by-step flow, from registering a user to reading their accounts, is in the [Managed apps guide](/guides/managed-apps). This section documents each endpoint's parameters and schemas. # List your users' financial connections Source: https://docs.synci.io/api-reference/managed/managed-connections/list-your-users-financial-connections /openapi/managed.json get /connections Every connection your users hold, newest first: bank, brokerage and crypto-exchange alike. Filter by user, status, provider, or `expiring_within_days` for the re-consent sweep. # Create a portal session Source: https://docs.synci.io/api-reference/managed/managed-users/create-a-portal-session /openapi/managed.json post /users/{userId}/portal-sessions Returns a single-use URL that drops this user straight into the connection portal with no sign-in code — use it when they are already authenticated in your own app. Valid once, for a short window. This is how managed users create connections: `POST /finance/connections` refuses a managed user's own token. Pass `institution_id` when the user has already picked their bank in your app, and the portal skips its picker and starts that bank's consent flow itself. Pass `connection_id` instead to renew one of the user's existing connections the same way: it keeps its id and a `connection.reauthorized` webhook follows. A user who has not yet accepted the portal terms is asked to accept first; nothing starts on its own for them. # Delete a user Source: https://docs.synci.io/api-reference/managed/managed-users/delete-a-user /openapi/managed.json delete /users/{userId} Revokes their tokens and permanently deletes the user together with their financial connections, accounts and transactions. This also frees the email address, so the person can sign up for Synci themselves afterwards. # Get a user Source: https://docs.synci.io/api-reference/managed/managed-users/get-a-user /openapi/managed.json get /users/{userId} # List your users Source: https://docs.synci.io/api-reference/managed/managed-users/list-your-users /openapi/managed.json get /users Every end user you have registered, newest first. # Mint a token pair Source: https://docs.synci.io/api-reference/managed/managed-users/mint-a-token-pair /openapi/managed.json post /users/{userId}/tokens Issues a fresh access + refresh token for this user. Equivalent to calling `POST /oauth/token` with `grant_type=managed_user`; provided here so you never need to keep a token you have lost. # Register a user Source: https://docs.synci.io/api-reference/managed/managed-users/register-a-user /openapi/managed.json post /users Creates an end user and returns a token pair for them, so you can start reading their data as soon as they connect a bank. The same pair can be re-minted at any time via `POST /oauth/token` with `grant_type=managed_user`. The user has no Synci account and no password: send them to the connection portal, where they sign in with a code emailed to this address. # Update a user Source: https://docs.synci.io/api-reference/managed/managed-users/update-a-user /openapi/managed.json put /users/{userId} Changes your correlation id or their connection allowance. The email address is not editable — it is the credential they sign in with. # Delete a webhook endpoint Source: https://docs.synci.io/api-reference/managed/managed-webhooks/delete-a-webhook-endpoint /openapi/managed.json delete /webhooks/{webhookId} Deliveries to the endpoint stop immediately. # List webhook events Source: https://docs.synci.io/api-reference/managed/managed-webhooks/list-webhook-events /openapi/managed.json get /webhooks/events The delivery log for your endpoints: one entry per delivery attempt, with the outcome. Use `include=payload` to include the delivered payload. # List your webhook endpoints Source: https://docs.synci.io/api-reference/managed/managed-webhooks/list-your-webhook-endpoints /openapi/managed.json get /webhooks # Register a webhook endpoint Source: https://docs.synci.io/api-reference/managed/managed-webhooks/register-a-webhook-endpoint /openapi/managed.json post /webhooks The endpoint's signing `secret` is included in this response only; it cannot be read again later, but can be rotated at any time. An app may hold a small fixed number of endpoints (403 when exceeded), and an approved app needs a Synci plan first (402). # Rotate the signing secret Source: https://docs.synci.io/api-reference/managed/managed-webhooks/rotate-the-signing-secret /openapi/managed.json post /webhooks/{webhookId}/rotate-secret The new secret is returned once in this response and cannot be read again; the previous secret stops being used immediately. # Send a test delivery Source: https://docs.synci.io/api-reference/managed/managed-webhooks/send-a-test-delivery /openapi/managed.json post /webhooks/{webhookId}/test Delivers a synthetic event with sample data to the endpoint, signed like a real one. Sent once, without retries, regardless of the endpoint's subscriptions. # Show a webhook endpoint Source: https://docs.synci.io/api-reference/managed/managed-webhooks/show-a-webhook-endpoint /openapi/managed.json get /webhooks/{webhookId} # Update a webhook endpoint Source: https://docs.synci.io/api-reference/managed/managed-webhooks/update-a-webhook-endpoint /openapi/managed.json put /webhooks/{webhookId} # Create new Notion connection Source: https://docs.synci.io/api-reference/notion/create-new-notion-connection /openapi/v1.json post /destinations/notion/connections Synci generates an authorization URL (auth_url). Follow it to authorize Synci in your Notion workspace: pick the pages Synci may write to, or duplicate the Synci template. Once authorized, register destination databases and link them to financial accounts. # Delete a destination database Source: https://docs.synci.io/api-reference/notion/delete-a-destination-database /openapi/v1.json delete /destinations/notion/connections/{connectionId}/databases/{databaseId} Removes the Synci record and its transfer links. The database itself remains in the user's Notion workspace. # Delete Notion connection Source: https://docs.synci.io/api-reference/notion/delete-notion-connection /openapi/v1.json delete /destinations/notion/connections/{connectionId} Deleting a connection also deletes its registered destination databases and their transfer links in Synci. The databases themselves remain in the user's Notion workspace. # Get a destination database Source: https://docs.synci.io/api-reference/notion/get-a-destination-database /openapi/v1.json get /destinations/notion/connections/{connectionId}/databases/{databaseId} Returns a single destination database, including its transfer links and health status. # Get Notion connection Source: https://docs.synci.io/api-reference/notion/get-notion-connection /openapi/v1.json get /destinations/notion/connections/{connectionId} Returns a single Notion connection, including its registered databases and health status. # List all Notion connections Source: https://docs.synci.io/api-reference/notion/list-all-notion-connections /openapi/v1.json get /destinations/notion/connections Returns the Notion connections that belong to the authenticated user. Connections that never completed Notion authorization are not included. # List destination databases for a connection Source: https://docs.synci.io/api-reference/notion/list-destination-databases-for-a-connection /openapi/v1.json get /destinations/notion/connections/{connectionId}/databases Returns the Notion databases registered as sync destinations on this connection. # Refresh Notion connection Source: https://docs.synci.io/api-reference/notion/refresh-notion-connection /openapi/v1.json post /destinations/notion/connections/{connectionId}/refresh Re-validates the stored Notion credentials and refreshes the workspace name. Rate limited to once per minute per connection. # Register a destination database Source: https://docs.synci.io/api-reference/notion/register-a-destination-database /openapi/v1.json post /destinations/notion/connections/{connectionId}/databases Either creates a new Synci-managed database under a page shared with Synci (mode=create) or attaches an existing database the user selected (mode=attach). Any properties Synci needs that are missing from an attached database are added; existing properties are never changed. # Search databases shared with Synci Source: https://docs.synci.io/api-reference/notion/search-databases-shared-with-synci /openapi/v1.json get /destinations/notion/connections/{connectionId}/data-sources Lists the Notion databases (data sources) the connection has access to, for attaching an existing database as a destination. # Search pages shared with Synci Source: https://docs.synci.io/api-reference/notion/search-pages-shared-with-synci /openapi/v1.json get /destinations/notion/connections/{connectionId}/pages Lists the Notion pages the connection has access to, for choosing where a new database should be created. # Update a destination database Source: https://docs.synci.io/api-reference/notion/update-a-destination-database /openapi/v1.json put /destinations/notion/connections/{connectionId}/databases/{databaseId} Update the database's display name or property configuration. Newly configured fields are added as properties to the Notion database right away; previously synced pages are not backfilled. # Update Notion connection Source: https://docs.synci.io/api-reference/notion/update-notion-connection /openapi/v1.json put /destinations/notion/connections/{connectionId} Update the connection's display name, or enable/disable syncing through it. # Get rule log Source: https://docs.synci.io/api-reference/rule-logs/get-rule-log /openapi/v1.json get /rule-logs/{logId} # List all rule logs Source: https://docs.synci.io/api-reference/rule-logs/list-all-rule-logs /openapi/v1.json get /rule-logs Rule logs are created any time Synci attempts to apply a rule to a bank transaction. Each rule log references both the rule and the bank transaction, so you can use this endpoint to get all rule logs for a specific rule or bank transaction. # Undo a rule action Source: https://docs.synci.io/api-reference/rule-logs/undo-a-rule-action /openapi/v1.json put /rule-logs/{logId}/undo Reverts a specific rule action by restoring the field's previous value. Creates a new log entry to track the reversal. Works with the following action types: - `SET_FIELD` - `SET_YNAB_FIELD` - `SET_LUNCH_MONEY_FIELD` # Undo multiple rule actions Source: https://docs.synci.io/api-reference/rule-logs/undo-multiple-rule-actions /openapi/v1.json put /rule-logs/undo-bulk Reverts multiple rule actions by restoring the fields' previous values. Creates new log entries to track the reversals. Works with the following action types: - `SET_FIELD` - `SET_YNAB_FIELD` - `SET_LUNCH_MONEY_FIELD` # Apply a rule to existing transactions Source: https://docs.synci.io/api-reference/rules/apply-a-rule-to-existing-transactions /openapi/v1.json post /rules/{ruleId}/apply Runs a `BANK`-scope rule over transactions already stored, exactly as a sync would: actions, rule logs, counterparty pairing and destination updates. With `dry_run` (the default) nothing changes and the response counts what would (`would_change`), what already matches (`already_matching`) and a sample of ids. A real run is queued and returns `202` with the application to poll; one run per rule at a time (`409` otherwise). Undo a run with `PUT /rule-logs/undo-bulk` over the logs at `rule_logs_url`. # Attach rule to a financial account Source: https://docs.synci.io/api-reference/rules/attach-rule-to-a-financial-account /openapi/v1.json post /rules/{ruleId}/accounts/{financialAccountId} Once attached, the rule will be applied to new transactions as they are fetched from the bank. Use `order` to place the rule at a specific position, or `order_list` to provide the complete ordered list of rule IDs for the account. # Attach rule to transfer link Source: https://docs.synci.io/api-reference/rules/attach-rule-to-transfer-link /openapi/v1.json post /rules/{ruleId}/transfer-links/{transferLinkId} Once attached, the rule will be applied to transactions as they are transferred to the destination. # Create a new rule Source: https://docs.synci.io/api-reference/rules/create-a-new-rule /openapi/v1.json post /rules Rules automatically categorize or transform transactions. A rule attached to a financial account runs on new transactions as they are fetched from the bank and its changes are permanent; a rule attached to a transfer link runs on transactions as they are transferred and only changes the destination copy. A rule has **conditions** and **actions**. Conditions decide whether the rule applies; a rule with no conditions always applies. Actions run when it does, and every rule needs at least one. Each action that runs is recorded as a rule log. Action values accept [Liquid templates](https://shopify.github.io/liquid/). Reference transaction fields with `{{ transaction.field_name }}` and transform them with filters: | Filter | Example | Description | |--------|---------|-------------| | `remove` | `{{ transaction.creditor.name \| remove: "PREFIX " }}` | Remove a substring | | `upcase` / `downcase` | `{{ transaction.creditor.name \| upcase }}` | Change case | | `regex_capture` | `{{ transaction.creditor.name \| regex_capture: '/^(\w+)/' }}` | Extract a regex capture group | | `date_add` | `{{ transaction.booking_date \| date_add: 7 }}` | Add or subtract days from a date | | `remove_at` | `{{ transaction.memo \| remove_at: 1, 5 }}` | Remove characters at positions | | `keep_at` | `{{ transaction.memo \| keep_at: 6 }}` | Keep only characters from a position | The standard Liquid filters (`replace`, `truncate`, `split`, `append`, `prepend`, and so on) are available too. Values without `{{` or `{%` are literal. Account rules (`BANK` scope) attach to bank accounts only; attaching one to a crypto or brokerage account returns `422`. Use a transfer link rule for those. How many conditions, actions and attached rules a plan allows is listed on the [pricing page](https://synci.io/pricing). # Delete rule Source: https://docs.synci.io/api-reference/rules/delete-rule /openapi/v1.json delete /rules/{ruleId} # Detach rule from financial account Source: https://docs.synci.io/api-reference/rules/detach-rule-from-financial-account /openapi/v1.json delete /rules/{ruleId}/accounts/{financialAccountId} # Detach rule from transfer link Source: https://docs.synci.io/api-reference/rules/detach-rule-from-transfer-link /openapi/v1.json delete /rules/{ruleId}/transfer-links/{transferLinkId} # Get a rule application Source: https://docs.synci.io/api-reference/rules/get-a-rule-application /openapi/v1.json get /rules/{ruleId}/applications/{applicationId} The state of one run started with **Apply a rule to existing transactions**; poll it until `status` is `COMPLETED` or `FAILED`. # Get rule Source: https://docs.synci.io/api-reference/rules/get-rule /openapi/v1.json get /rules/{ruleId} # List all rules Source: https://docs.synci.io/api-reference/rules/list-all-rules /openapi/v1.json get /rules Rules automatically categorize or transform transactions. A rule attached to a financial account runs on new transactions as they are fetched from the bank; a rule attached to a transfer link runs on transactions as they are transferred to the destination. See **Create a new rule** for how conditions, actions and Liquid templates work. # Preview a rule against transactions (dry-run) Source: https://docs.synci.io/api-reference/rules/preview-a-rule-against-transactions-dry-run /openapi/v1.json post /rules/preview Accepts either a `rule_id` to load an existing rule, or an inline `rule` definition (conditions + actions). Provide an array of `transaction_ids` to test against — each transaction must belong to the authenticated user. No data is persisted. Action values support **Liquid templates** for dynamic expressions. Use `{{ transaction.field_name }}` to reference fields and Liquid filters (`remove`, `regex_capture`, `date_add`, `remove_at`, `keep_at`, etc.) to transform values. Plain strings are treated as literal values. # Reorder rules for a financial account Source: https://docs.synci.io/api-reference/rules/reorder-rules-for-a-financial-account /openapi/v1.json put /rules/{ruleId}/accounts/{financialAccountId} Sets the execution order of the rules attached to a financial account. Provide the complete list of attached rule IDs in the desired order — rules are applied in ascending order. # Reorder rules for a transfer link Source: https://docs.synci.io/api-reference/rules/reorder-rules-for-a-transfer-link /openapi/v1.json put /rules/{ruleId}/transfer-links/{transferLinkId} Sets the execution order of the rules attached to a transfer link. Provide the complete list of attached rule IDs in the desired order — rules are applied in ascending order. # Rule field catalog Source: https://docs.synci.io/api-reference/rules/rule-field-catalog /openapi/v1.json get /rules/field-catalog Returns the fields a rule action can set, grouped by reach, one entry per rule scope (`BANK`, `TRANSFER`). Each group carries the `action_type` to submit and the destinations it reaches, so the editor can render a single grouped field picker and derive the action type from the chosen field — the user never picks an action type directly. The catalog is static (not per-rule); render the entry matching the rule being edited. For transfer rules being attached to a bank account as output rules, restrict to groups where `output_compatible` is true. # Suggest an internal-transfer rule from examples Source: https://docs.synci.io/api-reference/rules/suggest-an-internal-transfer-rule-from-examples /openapi/v1.json post /rules/suggest Derives a `BANK`-scope rule that marks transactions like the given examples as moves to the counterparty account, strongest signal first: the counterparty account number (`HIGH` confidence), a shared prefix of the counterparty name or remittance text (`MEDIUM`), or an amount band (`LOW`). The candidate rule is run over the account's last 365 days so the response lists what it would match (ids capped at 500). `unmatched_example_ids` names examples the derived rule would not catch. Nothing is saved: open the returned rule in the editor and create it with **Create a new rule**. # Update rule Source: https://docs.synci.io/api-reference/rules/update-rule /openapi/v1.json put /rules/{ruleId} Any field may be omitted to leave it unchanged. For rules that have been migrated, only `name`, `description`, `enabled`, and the `value` of existing actions can be updated. # List accounts observed syncing Source: https://docs.synci.io/api-reference/simplefin/list-accounts-observed-syncing /openapi/v1.json get /destinations/simplefin/connections/{connectionId}/accounts Returns the financial accounts a SimpleFIN consumer has been observed syncing through this connection, with first/last pull timestamps. Purely informational: linking happens inside the consumer app, and unlinking there sends no signal (a stale last_pulled_at is the only tell). # Regenerate setup token Source: https://docs.synci.io/api-reference/simplefin/regenerate-setup-token /openapi/v1.json post /destinations/simplefin/connections/{connectionId}/regenerate-token Issues a fresh one-time setup token for an unclaimed connection, invalidating the previous token. Claimed connections cannot regenerate; create a new connection instead. # Revoke access credentials Source: https://docs.synci.io/api-reference/simplefin/revoke-access-credentials /openapi/v1.json post /destinations/simplefin/connections/{connectionId}/revoke Permanently invalidates the connection's SimpleFIN credentials (and any unclaimed setup token). Actual's subsequent pulls receive an authorization error until the account is relinked with a new connection. # Create a new transfer link Source: https://docs.synci.io/api-reference/transfer-links/create-a-new-transfer-link /openapi/v1.json post /transfer-links Transfer links are used to transfer transactions from a financial account to a destination. Once a transfer link is created, Synci will automatically transfer bank transactions to the destination according to the link's configuration. Financial accounts and destinations can have multiple transfer links. # Delete transfer link Source: https://docs.synci.io/api-reference/transfer-links/delete-transfer-link /openapi/v1.json delete /transfer-links/{transferLinkId} Once a transfer link is deleted, Synci will no longer automatically transfer bank transactions to the destination. # Get transfer link Source: https://docs.synci.io/api-reference/transfer-links/get-transfer-link /openapi/v1.json get /transfer-links/{transferLinkId} # List all transfer links Source: https://docs.synci.io/api-reference/transfer-links/list-all-transfer-links /openapi/v1.json get /transfer-links Transfer links are used to transfer transactions from a financial account to a destination. Once a transfer link is created, Synci will automatically transfer bank transactions to the destination according to the link's configuration. Financial accounts and destinations can have multiple transfer links. # Retry transfers for a specific transfer link Source: https://docs.synci.io/api-reference/transfer-links/retry-transfers-for-a-specific-transfer-link /openapi/v1.json post /transfer-links/{transferLinkId}/retry-transfers This endpoint allows you to retry failed transfers for a specific transfer link. Transfers are processed asynchronously. Each transaction can be retried at most once per minute, and transactions that have failed 10 or more times on the transfer link are skipped. # Retry transfers for all transfer links Source: https://docs.synci.io/api-reference/transfer-links/retry-transfers-for-all-transfer-links /openapi/v1.json post /transfer-links/retry-transfers This endpoint allows you to retry failed transfers for all transfer links associated with your account. Transfers are processed asynchronously. Each transaction can be retried at most once per minute per transfer link, and transactions that have failed 10 or more times on a transfer link are skipped. # Update transfer link Source: https://docs.synci.io/api-reference/transfer-links/update-transfer-link /openapi/v1.json put /transfer-links/{transferLinkId} # Get transfer log Source: https://docs.synci.io/api-reference/transfer-logs/get-transfer-log /openapi/v1.json get /transfer-logs/{transferLogId} # List all transfer logs Source: https://docs.synci.io/api-reference/transfer-logs/list-all-transfer-logs /openapi/v1.json get /transfer-logs Transfer logs are created any time Synci attempts to transfer a bank transaction. Since destinations and financial accounts can have multiple transfer links, transfer logs are created for each transfer link. You should use transfer logs to track the status of a transaction. For example, by getting the latest transfer log for a transaction and transfer link, you can see if the transfer was successful or not. # Undo a transfer Source: https://docs.synci.io/api-reference/transfer-logs/undo-a-transfer /openapi/v1.json put /transfer-logs/{transferLogId}/undo Deletes the transaction from the destination it was transferred to. Creates a new transfer log entry to track the undo action. # Undo multiple transfers Source: https://docs.synci.io/api-reference/transfer-logs/undo-multiple-transfers /openapi/v1.json put /transfer-logs/undo-bulk Deletes the transactions from the destinations they were transferred to. Creates new transfer log entries to track the undo actions. # Get the authenticated user Source: https://docs.synci.io/api-reference/user/get-the-authenticated-user /openapi/v1.json get /user Basic identity for the user who authorized your application: id, name, email, whether the email address is verified, and the account creation date. Requires the `profile:read` scope. # Create new webhook endpoint Source: https://docs.synci.io/api-reference/webhooks/create-new-webhook-endpoint /openapi/v1.json post /webhooks The endpoint's signing `secret` is included in this response only — it cannot be read again later, but can be rotated at any time. The number of webhook endpoints is subject to your plan's limits (403 when exceeded). # Delete webhook endpoint Source: https://docs.synci.io/api-reference/webhooks/delete-webhook-endpoint /openapi/v1.json delete /webhooks/{webhookEndpointId} Deliveries to the endpoint stop immediately. # Get webhook endpoint Source: https://docs.synci.io/api-reference/webhooks/get-webhook-endpoint /openapi/v1.json get /webhooks/{webhookEndpointId} Returns a single webhook endpoint, including its delivery health. # List all webhook endpoints Source: https://docs.synci.io/api-reference/webhooks/list-all-webhook-endpoints /openapi/v1.json get /webhooks The webhook endpoints registered by the authenticated user. Signing secrets are not included — they are only returned on creation and rotation. OAuth apps see only the endpoints they created themselves. # List all webhook events Source: https://docs.synci.io/api-reference/webhooks/list-all-webhook-events /openapi/v1.json get /webhooks/events The delivery log for your webhook endpoints: one entry per delivery attempt, with the outcome. Use `include=payload` to include the delivered payload in each event. # Rotate webhook secret Source: https://docs.synci.io/api-reference/webhooks/rotate-webhook-secret /openapi/v1.json post /webhooks/{webhookEndpointId}/rotate-secret Generates a new signing secret for the endpoint. The new secret is returned once in this response and cannot be read again; the previous secret stops being used immediately. # Test webhook endpoint Source: https://docs.synci.io/api-reference/webhooks/test-webhook-endpoint /openapi/v1.json post /webhooks/{webhookEndpointId}/test Sends a test delivery of the chosen event type to the endpoint, regardless of its event subscriptions. Provide `financial_account_id` to build the payload from one of your accounts where possible; otherwise sample data is sent. # Update webhook endpoint Source: https://docs.synci.io/api-reference/webhooks/update-webhook-endpoint /openapi/v1.json put /webhooks/{webhookEndpointId} The `payload_version` field is pinned at creation and cannot be changed here. Re-enabling a disabled endpoint is subject to your plan's limit on active endpoints (403 when exceeded).