API Reference

Tip

Beta: This API is in beta. Endpoints, response shapes, and documentation may change without a version bump; watch release notes and update integrations accordingly.

The CleanupOwl API is organized around REST. It uses predictable, resource-oriented URLs under /tp/api/, accepts JSON request bodies on POST requests (Content-Type: application/json), returns JSON responses in a consistent envelope, and uses standard HTTP response codes, authentication, and verbs (GET, POST).

There is no /v1-style path prefix. Breaking changes and behavior updates are communicated out of band (for example, release notes or partner channels); keep an eye on those if you maintain an integration.

Scans run in the background. After you start a scan, poll for results until the run finishes or fails—see Quick start for the full polling flow. Solvers fix one row at a time using prep and apply. There is no “fix every row in one call” option.

Not a developer?

This API is for integrators: scripts, partner tools, internal automation, and LLM agents. To use CleanupOwl without writing to the API, use the main CleanupOwl application and QuickBooks Online (QBO)–connected workflows in the product UI.

Base URL

https://app.cleanupowl.com

Append /tp/api/... to that host for API requests (for example, https://app.cleanupowl.com/tp/api/connections).

Authentication

The CleanupOwl API uses API keys to authenticate requests. You create and manage keys in the CleanupOwl application, where you choose the scopes (permissions) each key needs.

Each key is a secret string you send as a Bearer token. Scopes say what the key may do—for example: run scans, list issues, use solvers, or only open connections and billing. The full list is under Scopes.

API keys grant access to your CleanupOwl data and actions tied to your account. Keep them private. Do not put them in public git repos, browser-side code, shared docs, or support threads. If a key is exposed, revoke it and create a new one.

All requests must use HTTPS. Requests without a valid Authorization: Bearer <your-api-key> header fail with 401 or 403. The API does not accept unauthenticated calls.

Authorization: Bearer <your-api-key>

Authenticated request

curl -sS "https://app.cleanupowl.com/tp/api/connections" \
  -H "Authorization: Bearer YOUR_API_KEY"

Use a key from your CleanupOwl account in place of YOUR_API_KEY.

Scopes

ScopeRequired for
scanGET /tp/api/issues, scan endpoints such as /tp/api/scan/...
solveGET /tp/api/solvers, GET /tp/api/issues/:issueId/solvers, POST /tp/api/solve/prep, POST /tp/api/solve/apply, GET /tp/api/scans/:scanId/solves
(none)GET /tp/api/billing, GET /tp/api/connections, GET /tp/api/connect

Auth error schema

HTTP status is 401 or 403. The body uses the standard error envelope.

error.codeHTTPMeaning
E_MISSING_API_KEY401No Authorization: Bearer header
E_INVALID_API_KEY401Unknown or invalid key
E_API_KEY_REVOKED401Key revoked
E_API_KEY_EXPIRED401Key expired
E_SCOPE_FORBIDDEN403Key missing required scope
{
  "success": false,
  "error": {
    "code": "E_INVALID_API_KEY",
    "message": "Invalid API key format"
  }
}

Integrations should branch on error.code; use message for display and logging.

Quick start

Learn how to scan a connected QuickBooks Online company for bookkeeping issues and apply a fix—using only curl from your terminal.

Introduction

This guide walks you through the full flow: create an API key, connect a company, run a scan, read the results, and fix one row. Each step explains what the call does and what to do with the response before moving on.

Only curl is required. Every command prints JSON—read it and copy the values you need into the next command.

For full reference documentation on any endpoint, see the sections below: Issues, Scans, Solvers, Connections.


1. Create an API key

Every request must include an API key in the Authorization header. The key also controls what you can do—see Scopes.

Go to your CleanupOwl account and create a key. Choose the scopes you need:

  • scan — to run scans and list issues
  • solve — to prep and apply fixes
  • No scope needed for connections and billing
Tip

Keep your key secret. Do not commit it to git or paste it in public docs. If it leaks, revoke it and create a new one immediately.

Set your key and base URL as environment variables so you can copy the commands below without editing them:

export TP_BASE_URL="https://app.cleanupowl.com"
export TP_API_KEY="your-api-key-here"

To verify your key works, call the issues endpoint:

curl -sS "$TP_BASE_URL/tp/api/issues" \
  -H "Authorization: Bearer $TP_API_KEY"

A 200 with a list of issues means your key is valid. A 401 means the key is wrong, expired, or missing the scan scope.


2. Connect a QuickBooks Online company

Scans and fixes run against a specific QBO company. If no company is connected yet, get the browser link:

curl -sS "$TP_BASE_URL/tp/api/connect" \
  -H "Authorization: Bearer $TP_API_KEY"

Open data.connectionUrl in a browser. The user signs in to CleanupOwl and completes QBO authorization. Once done, the company appears in the connections list.


3. Get your companyId

The companyId is required on every scan and solve call. It is the _id field from the connections list—not the QBO realm id.

curl -sS "$TP_BASE_URL/tp/api/connections" \
  -H "Authorization: Bearer $TP_API_KEY"

In the response, find the company you want and copy its _id value.

export COMPANY_ID="paste-_id-from-connections-here"

4. Start a scan

A scan pulls data from QBO, checks it for bookkeeping issues, and stores the results. You need a completed scan before you can fix anything.

curl -sS -X POST "$TP_BASE_URL/tp/api/scan/start" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"companyId\":\"$COMPANY_ID\"}"

From the response, copy data.scanId. Also note data.pollAfterSecondswait at least that many seconds before your first poll. The server sets this value; it is commonly 300 seconds.

export SCAN_ID="paste-scanId-from-response-here"

5. Poll until the scan finishes

Scans run in the background. You need to keep checking until the status changes.

Wait at least pollAfterSeconds after starting, then run this command. Repeat it with exponential backoff (5s → 10s → 20s → 30s, max 30s between attempts):

curl -sS "$TP_BASE_URL/tp/api/scan/$SCAN_ID?companyId=$COMPANY_ID" \
  -H "Authorization: Bearer $TP_API_KEY"

Check data.scan.status in each response:

  • completed — results are ready, continue to the next step
  • failed — check data.scan.error and decide whether to retry
  • anything else — wait and poll again

6. Read the results

When the scan is completed, data.scan.results holds every issue found—organized as a map of issueId → rows.

Each row represents one flagged record. To fix a row you need its recordId, which is one of Id, _id, entityId, or id on the row object. For details, see Identifying a row.

Pick an issue bucket, then pick a row you want to fix, and note its issueId and recordId.


7. Prep the fix

Before applying, call prep to get the available solvers, row-level options, and any risk flags for that specific row.

curl -sS -X POST "$TP_BASE_URL/tp/api/solve/prep" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"scanId\":\"$SCAN_ID\",\"issueId\":\"YOUR_ISSUE_ID\",\"recordId\":\"YOUR_RECORD_ID\",\"companyId\":\"$COMPANY_ID\"}"

In the response:

  • data.solvers — pick a solverId from this list
  • data.recordFixOptions and data.metaFixOptions — use these to build userInput if the solver needs it
  • data.resolvedByOtherFix: true — skip this row, it was already fixed
  • data.needsRevalidation: true — QBO data changed; run a new scan before applying

8. Apply the fix

This is the call that changes data in QuickBooks Online.

curl -sS -X POST "$TP_BASE_URL/tp/api/solve/apply" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"scanId\":\"$SCAN_ID\",\"issueId\":\"YOUR_ISSUE_ID\",\"recordId\":\"YOUR_RECORD_ID\",\"companyId\":\"$COMPANY_ID\",\"solverId\":\"YOUR_SOLVER_ID\"}"

Read the response:

  • success: true with data.action — the fix was applied. Check data.message for a description.
  • success: true with data.needsInput: true — the solver needs more information. Add userInput with the fields listed in data.userInputSchema and POST again with the same ids.
  • success: false — something went wrong. Read error.code and error.message to fix the request.

9. Review what was fixed

To see an audit trail of every solve attempt on the scan:

curl -sS "$TP_BASE_URL/tp/api/scans/$SCAN_ID/solves?companyId=$COMPANY_ID" \
  -H "Authorization: Bearer $TP_API_KEY"

Each entry in data.solves shows the action, success, solverId, and executedAt timestamp for one attempt.


10. Check your plan limits

To see how many fixes you have used and how many remain:

curl -sS "$TP_BASE_URL/tp/api/billing" \
  -H "Authorization: Bearer $TP_API_KEY"

If usage.fixesRemaining is 0, you have reached your plan limit. The apply endpoint returns E_FIX_LIMIT_EXCEEDED (402) when this happens.


Next steps

Response format

Every response—success or failure—uses the same outer shape:

{
  "success": true,
  "data": { }
}

When a request fails, success is false and the body contains an error object instead of data:

{
  "success": false,
  "error": {
    "code": "E_ERROR_CODE",
    "message": "Human-readable description"
  }
}

The data object shape is endpoint-specific. See each endpoint section for its full attribute list.

Solver needs more input — POST /tp/api/solve/apply

POST /tp/api/solve/apply has a special case: the solver may need additional fields before it can run. When this happens the response is still HTTP 200 with success: true—it is not a failure. The signal is data.needsInput: true:

{
  "success": true,
  "data": {
    "needsInput": true,
    "missingFields": ["targetAccountId"],
    "userInputSchema": { }
  }
}

When you receive data.needsInput: true, add a userInput object with the fields listed in missingFields and POST to /tp/api/solve/apply again using the same scanId, issueId, recordId, companyId, and solverId. Do not treat this as an error.

Error

All error responses share the same shape regardless of endpoint:

{
  "success": false,
  "error": {
    "code": "E_MISSING_COMPANY_ID",
    "message": "companyId is required"
  }
}

error.code is a stable string you can branch on in code. error.message is a human-readable explanation intended for logs and dashboards—its wording may change between releases, so do not match on it in production logic.

Errors

The CleanupOwl API uses conventional HTTP status codes together with a JSON body to show whether a request succeeded. In general: codes in the 2xx range mean the HTTP request was accepted and you should read success and either data or error in the body. Codes in the 4xx range mean the request failed because of the client input, auth, or permissions (wrong or missing parameters, invalid key, wrong scope, unknown resource, and similar). Codes in the 5xx range mean something went wrong on the server side; treat these as uncommon and retry with backoff when safe.

Many 4xx responses include an error.code string you can branch on in code. error.message is meant for humans—logs, dashboards, or support—not as a stable contract.

Error object attributes

When success is false, the body includes an error object:

  • code (string)
    Stable machine-readable code, for example E_MISSING_COMPANY_ID. Use this in switch / if logic.
  • message (string)
    Human-readable explanation. Wording may change with update; do not match on the full string in production logic.

Successful responses use success: true and data. For POST /tp/api/solve/apply, a 200 with success: true can still mean the solver needs more fields—see Solver needs more input under Response format (data.needsInput), which is not the same as success: false.

HTTP status code summary

HTTPMeaningTypical case
200OKsuccess: true with data. For apply, may include data.needsInput (still success).
400Bad RequestInvalid or incomplete payload; wrong solver/issue pairing; business validation failed. Check error.code.
401UnauthorizedMissing or invalid API key, expired/revoked key, or QuickBooks Online (QBO) authentication failure for the company.
403ForbiddenKey lacks scope, or companyId is not allowed for this user.
404Not FoundUnknown scan, record, or route resource.
500Internal Server ErrorUnexpected server-side failure; retry later if appropriate.
503Service UnavailableDependency unavailable (for example billing not configured in this environment).

Other status codes may appear for specific routes; each endpoint section lists its error.code values where relevant.

Common error.code values

These appear across many endpoints; additional codes are documented per route below.

error.codeTypical HTTPWhenWhat to do
E_MISSING_API_KEY401No Bearer tokenSend Authorization: Bearer ...
E_INVALID_API_KEY401Bad keyFix or rotate key
E_SCOPE_FORBIDDEN403Wrong scopeUse a key with scan / solve as needed
E_MISSING_COMPANY_ID400Required companyId missingPass body or query companyId
E_COMPANY_ACCESS_DENIED403Company not linked to key userUse GET /tp/api/connections for allowed company ids
E_QBO_AUTH_FAILED401QuickBooks Online (QBO) connection invalidReconnect QBO in CleanupOwl

Common mistakes

MistakeWhat goes wrongFix
Using anything other than connections[]._id for companyIdpollUrl, scan payloads, and solve calls will not line upSet companyId to _id from GET /tp/api/connections every time
Omitting companyId on scan GET, history, or solves400 E_MISSING_COMPANY_IDAlways pass query or body companyId
Wrong recordId404 E_RECORD_NOT_FOUNDUse Id, _id, entityId, or id from the row in results — see scan section
solverId does not match issueId400 E_SOLVER_ISSUE_MISMATCHCall GET /tp/api/issues/:issueId/solvers first
Polling every secondSlow scans, noisy trafficWait pollAfterSeconds, then backoff — 5→10→20→30s max
Treating needsInput as a failureApply never completesHTTP 200 with data.needsInput: add userInput and POST apply again
Ignoring 400 responses on applySolver requirements not metRead error.code and message, then supply userInput or fix the payload and retry

What we find

When you run a scan, CleanupOwl checks the connected QuickBooks Online company against a set of detectors. Each detector looks for one specific type of bookkeeping problem. The table below lists every detector and what it checks for.

Duplicate transactions

DetectorWhat it finds
Duplicate Payment in Undeposited FundsReceive Payment entries sitting in Undeposited Funds with the same customer, amount, and date (±1 day). Often caused by POS sync issues or double-entry mistakes.
Duplicate Bank Feed Transactions (Same Date, Amount, Account)Categorized bank feed transactions that share the same date, amount, and bank account. The later-created transaction in each group is flagged as the auto-delete candidate.
Duplicate Bank Feed Transactions (Date/Amount/Account in Review)Transactions in the For Review stage that share the same date, amount, and account. You choose which one to keep; the rest are excluded.
Duplicate Bank Feed Transactions (Memo Match in Review)Transactions in the For Review stage that share the same memo or description. You choose which one to keep; the rest are excluded.
Duplicate Bank Feed Transactions Based on MemoCategorized bank feed transactions grouped by matching memo within the same account and direction (money in vs. out). Groups of two or more are flagged as potential duplicates.
Merge Duplicate CustomersCustomers with very similar names and matching contact details (email, address, or phone) that are likely the same person entered twice.

Undeposited funds & cash handling

DetectorWhat it finds
Undeposited Funds Balance & Bypassed DepositsTwo issues in one: payments posted directly to a bank account bypassing Undeposited Funds, and items stuck in Undeposited Funds that were never deposited.
Old Items in Undeposited FundsCustomer payments and sales receipts posted to Undeposited Funds that were never moved to a bank account.
Cash Received but Not DepositedCash payments and sales receipts (excluding cheques) in Undeposited Funds with no matching bank deposit.
Payments Deposited Directly to Bank (Bypassing Undeposited Funds)Receive Payments where the Deposit To field was set to a bank account instead of Undeposited Funds while Undeposited Funds still carries a balance. Can cause reconciliation issues.
Cheque Received but Not DepositedCheque payments in Undeposited Funds that have not been deposited to a bank account.

Vendor & bill management

DetectorWhat it finds
Vendor Payments Not Applied to BillsVendor payments that exist but have not been linked to any open bill, leaving the bill balance unpaid and the payment floating.
Vendor Payments to Expenses When No Open Bills ExistPayments recorded as expenses for a vendor that has no open bills, which can distort vendor balances and accounts payable.
Aged Unapplied Vendor Credits and DebitsVendor credits that have never been applied to a bill or payment, meaning your business is leaving money on the table.

Customer & receivables

DetectorWhat it finds
Unapplied Customer CreditsCustomer credits that have never been applied to an invoice or refunded, causing receivable balances to appear higher than they are.
Open Customer ReceivablesCustomers with outstanding invoice balances where payment has not been received.
Sales Receipt Used Instead of Receive PaymentSales Receipts created for customers who already have open invoices. This records duplicate revenue and leaves the original invoice unpaid.
Sales Recorded Without InvoiceRevenue transactions (Sales Receipts, Deposits, Journal Entries) that credit income accounts without a corresponding invoice to the customer.

Inventory

DetectorWhat it finds
Sales Before Purchase — Negative InventoryItems sold before they were purchased, resulting in negative inventory quantities that make financial reports unreliable.
Wrong Item Selection — Negative InventoryTransactions where the wrong inventory item was selected, causing one item to go negative while another is overstated.
Inventory Purchases Booked to ExpenseInventory purchases recorded as expenses instead of assets, understating inventory value and overstating expenses.
Missing Inventory Opening BalanceInventory items with no opening balance set, meaning the starting quantity and value are unknown.

Expense & income classification

DetectorWhat it finds
Incorrect Expense ClassificationExpenses booked to the wrong category—for example, legal fees recorded as travel. The amount is correct but it appears on the wrong line of the Profit & Loss.
Sales Tax Payment Recorded as ExpenseSales tax payments to a tax agency booked as a P&L expense instead of clearing the Sales Tax Payable liability account. Overstates expenses and leaves the liability balance wrong.
Personal Expenses Charged to BusinessPersonal purchases recorded in the business books, making it impossible to see the true cost of running the business.
Personal Income Charged to BusinessPersonal income recorded as business revenue, inflating business income and potentially causing incorrect tax filings.

Assets, liabilities & compliance

DetectorWhat it finds
Depreciation Not ChargedFixed assets (equipment, vehicles, etc.) with no depreciation recorded, causing profit to be overstated and asset values on the balance sheet to be too high.
Missing Opening Balances for AssetsAsset accounts with no opening balance entered, leaving the balance sheet incomplete from the start date.
Missing Opening Balances for LiabilitiesLiability accounts with no opening balance entered, making the financial snapshot incomplete.
Contractor Payment Not Flagged for 1099Payments to contractors that are not marked for 1099 reporting, which can result in missed tax filing obligations.

Bank statement reconciliation

DetectorWhat it finds
Missing Transactions from Bank StatementTransactions that appear in an uploaded bank or credit card statement but are absent from QBO. Requires an uploaded statement file.
Bank Statement TransactionsDisplays all transactions from an uploaded bank statement so you can verify the file was parsed correctly before running reconciliation checks.

Onboarding

DetectorWhat it finds
Onboarding Health Check DiagnosticA comprehensive diagnostic for new clients. Checks uncoded transaction backlogs, reconciliation status using activity patterns, and balance sheet sanity (unusual liability patterns, credit cards, payroll liabilities, retirement accounts).

1. Issues

An issue is a category of bookkeeping problem, identified by a stable string id (for example duplicate-bank-feed-date-amount). The list endpoint returns id, name, and description only—no solver list.

Endpoints:

GET /tp/api/issues

Returns the catalog of issue types your integration can scan for and solve against.

Scope: scan

Parameters

No path or query parameters.

Returns

On success, HTTP 200 with success: true and a data object. See Response attributes.

Response attributes

  • issues (array of objects) — Issue summaries. Each element:
    • id (string) — Use as issueId on solve/apply and as keys under results on a completed scan.
    • name (string) — Short title.
    • description (string or array of string, optional) — Plain text or bullet-style strings.

Example response

{
  "success": true,
  "data": {
    "issues": [
      {
        "id": "cheque-not-deposited",
        "name": "Cheque Received but Not Deposited",
        "description": "Identifies cheque payments that were posted to Undeposited Funds but have not been deposited to a bank account."
      },
      {
        "id": "contractor-payment-not-flagged-for-1099",
        "name": "Contractor Payment Not Flagged for 1099",
        "description": [
          "First paragraph explaining the issue.",
          "Second paragraph with impact or how to fix it."
        ]
      }
    ]
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/issues" \
  -H "Authorization: Bearer $TP_API_KEY"

Error codes

error.codeHTTPMeaning
E_ISSUES_LOAD_FAILED500Issue definitions could not be loaded

2. Scans

A scan pulls QuickBooks Online (QBO) data when sync is not skipped, runs issue checks, and stores results by issueId. Scans run in the background: POST /tp/api/scan/start comes back right away; then poll GET /tp/api/scan/:scanId until data.scan.status is completed, or handle failed / still running. A full company can take minutes—see step 5 in Quick start for the polling flow.

Endpoints:

POST /tp/api/scan/start

Starts a background scan for one company.

Scope: scan

Parameters

JSON body:

FieldTypeRequiredDescription
companyIdstringYesSame as connection _id from GET /tp/api/connections
skipSyncbooleanNoIf true, skip QuickBooks Online sync before scan; default false
periodobjectNoDate range for analysis
period.startDatestringNoISO date, e.g. "2024-01-01"
period.endDatestringNoISO date
reportYearnumberNoFocus year
materialityThresholdnumberNoMinimum amount to flag
skipstring[]NoIssue ids to skip
onlystring[]NoOnly run these issue ids
overridesobjectNoPer-detector option overrides
syncPeriodobjectNoSync window for QBO pull

Returns

On success, HTTP 200 with success: true and data containing the new scan id and polling hints.

Response attributes

FieldTypeDescription
scanIdstringUse in GET /tp/api/scan/:scanId and solve endpoints
statusstringe.g. "started"
pollAfterSecondsnumberMinimum suggested delay before the first poll (often 300)
pollUrlstringRelative URL including companyId query for convenience

Example request

{
  "companyId": "YOUR_COMPANY_DOCUMENT_ID",
  "period": {
    "startDate": "2024-01-01",
    "endDate": "2024-12-31"
  }
}

Example response

{
  "success": true,
  "data": {
    "scanId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "status": "started",
    "pollAfterSeconds": 300,
    "pollUrl": "/tp/api/scan/65f1a2b3c4d5e6f7a8b9c0d1?companyId=YOUR_COMPANY_DOCUMENT_ID"
  }
}

Request sample

curl -sS -X POST "$TP_BASE_URL/tp/api/scan/start" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"companyId":"YOUR_COMPANY_DOCUMENT_ID"}'

Error codes

error.codeHTTPMeaning
E_MISSING_COMPANY_ID400Missing companyId
E_COMPANY_ACCESS_DENIED403No access to company
E_QBO_AUTH_FAILED401QBO session invalid

GET /tp/api/scan/:scanId

Gets one scan. You see results when the scan is done. Put the scanId from POST /tp/api/scan/start in the URL. To list past scans, use history where scanId would go—or call GET /tp/api/scan/history.

Scope: scan

Parameters

Path

ParameterTypeDescription
scanIdstringThe scan id, or history to list scans

Query

ParameterTypeRequiredDescription
companyIdstringYesCompany id
limitnumberNoFor history lists: page size
skipnumberNoFor history lists: offset

Returns

On success, HTTP 200 with success: true and data.scan. When data.scan.status is completed, read data.scan.results (object keyed by issueId). For POST /tp/api/solve/apply you need scanId, companyId, issueId, and recordId from a row in the matching bucket.

Response attributes

data.scan (object) — selected fields:

FieldTypeDescription
_idstringScan id
companyIdstringInternal company id
externalIdstringQBO realm / company id
startedAtstringISO timestamp
completedAtstring or nullWhen the scan finished
statusstringe.g. pending, running, completed, failed
issueIdsstring[]For history: every issue in the job. For single scan: issues that have rows in results
totalResultsnumberTotal flagged rows
severityobjectCounts with high, medium, low
executionTimeMsnumberOptional
erroranyOptional failure payload
isLatestbooleanWhether this is the latest completed full scan for the company
resultsobjectMap issue id → per-issue result (see below)

Issue buckets with count: 0 are omitted from results. Only the fields in this section are treated as a stable integration contract; other properties may appear for the product UI—ignore undocumented fields.

results[issueId] (object):

FieldTypeDescription
categorystringOptional grouping
labelstringHuman-readable title
descriptionstring or string[]Optional
messagestringOptional
countnumberFlagged records
severityobjectOptional per-bucket breakdown
paramsobjectOptional run parameters
metaobjectExtra context (totals, solver options, etc.)
executionTimeMsnumberOptional
recordsarrayList-shaped rows; see Identifying a row
tableobjectTabular shape
table.columnsarray{ key, label, format?, width? }
table.rowsarrayRows with entityId, entityType, _id when present, fixOptions

Identifying a row for POST /tp/api/solve/apply

Set recordId to a string equal to one of Id, _id, entityId, or id on the target row. For table-shaped issues, use entityId or row _id on table.rows. For list-shaped issues (records), use Id or entityId. Leading-underscore fields are primarily for the app UI.

Example response

{
  "success": true,
  "data": {
    "scan": {
      "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "companyId": "YOUR_COMPANY_DOCUMENT_ID",
      "externalId": "9341455852565781",
      "startedAt": "2026-03-31T09:17:27.328Z",
      "completedAt": "2026-03-31T09:18:48.518Z",
      "status": "completed",
      "issueIds": ["open-customer-receivables", "inventory-purchases-booked-to-expense"],
      "totalResults": 23,
      "severity": { "high": 19, "medium": 3, "low": 1 },
      "isLatest": true,
      "results": {
        "inventory-purchases-booked-to-expense": {
          "category": "Inventory",
          "label": "Inventory Purchases Booked to Expense",
          "description": ["…"],
          "count": 4,
          "severity": { "high": 0, "medium": 1, "low": 3 },
          "table": {
            "columns": [
              { "key": "txnType", "label": "Transaction Type", "format": null },
              { "key": "purchaseAmount", "label": "Amount", "format": "currency" }
            ],
            "rows": [
              {
                "txnType": "Bill",
                "purchaseAmount": "$830.09",
                "_id": "3871",
                "_entity": "bill",
                "entityId": "3871",
                "entityType": "bill"
              }
            ]
          },
          "params": { "aiConfidenceThreshold": 0.8 },
          "meta": { "totalFlagged": 4 }
        }
      }
    }
  }
}

Request sample

SCAN_ID="65f1a2b3c4d5e6f7a8b9c0d1"
COMPANY_ID="YOUR_COMPANY_DOCUMENT_ID"

curl -sS "$TP_BASE_URL/tp/api/scan/$SCAN_ID?companyId=$COMPANY_ID" \
  -H "Authorization: Bearer $TP_API_KEY"

Error codes

error.codeHTTPMeaning
E_MISSING_COMPANY_ID400Missing companyId query
E_COMPANY_ACCESS_DENIED403No access
E_SCAN_NOT_FOUND404Unknown scan id, or scan does not belong to the company for this companyId

GET /tp/api/scan/history

Lists recent scans for one company. The URL is GET /tp/api/scan/history—same idea as get scan, but the path uses history instead of a scan id.

Scope: scan

Parameters

Query

ParameterTypeRequiredDefaultDescription
companyIdstringYesCompany id
limitnumberNo20Page size
skipnumberNo0Offset

Returns

On success, HTTP 200 with success: true and data.scans plus data.latestScanId.

Response attributes

FieldTypeDescription
scansarraySummary rows
latestScanIdstringLatest completed scan id in this list window

Each scans[] element:

FieldTypeDescription
_idstringScan id
startedAtstringISO
completedAtstring or nullISO
statusstringScan status
issueIdsstring[]Issues that ran in this job
totalResultsnumberTotal flagged rows
severityobjectOptional
executionTimeMsnumberOptional
erroranyOptional
isLatestbooleanWhether this row is the latest completed scan in the list

Example response

{
  "success": true,
  "data": {
    "scans": [
      {
        "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
        "startedAt": "2026-03-31T09:17:27.328Z",
        "completedAt": "2026-03-31T09:18:48.518Z",
        "status": "completed",
        "issueIds": ["open-customer-receivables", "duplicate-bank-feed-date-amount"],
        "totalResults": 45,
        "isLatest": true,
        "error": null,
        "executionTimeMs": 24132,
        "severity": { "high": 32, "medium": 7, "low": 6 }
      }
    ],
    "latestScanId": "65f1a2b3c4d5e6f7a8b9c0d1"
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/scan/history?companyId=YOUR_COMPANY_DOCUMENT_ID&limit=10&skip=0" \
  -H "Authorization: Bearer $TP_API_KEY"

3. Solvers

Solvers change data in QuickBooks Online (QBO) to fix a single record surfaced by a scan. Always inspect requiresInput and userInputSchema before calling apply.

Tip

A solver only fixes the issues it is built for. The wrong pairing returns E_SOLVER_ISSUE_MISMATCH. Discover valid solvers with GET /tp/api/issues/:issueId/solvers or GET /tp/api/solvers.

Endpoints:

GET /tp/api/solvers

Returns every registered solver and the issue types each one supports.

Scope: solve

Parameters

No path or query parameters.

Returns

On success, HTTP 200 with success: true and data.solvers.

Response attributes

data.solvers (array). Each element:

FieldTypeDescription
idstringPass as solverId on apply when required
namestringDisplay name
descriptionstringOptional
forIssuestring, string[], or "*"Issue id(s) this solver handles, or universal
requiresInputbooleanIf true, expect userInput or a needsInput round-trip
userInputSchemaobject or nullField specs: select, text, number, hidden; use optionsFrom when options come from paths on the scan record

Example response

{
  "success": true,
  "data": {
    "solvers": [
      {
        "id": "reclassify-expense-to-inventory",
        "name": "Reclassify to Inventory Item",
        "description": "Reclassify an expense line to an inventory item or inventory asset account.",
        "requiresInput": true,
        "userInputSchema": {
          "targetItemId": {
            "type": "select",
            "label": "Select Inventory Item",
            "required": false,
            "optionsFrom": "record.fixOptions.inventoryItems"
          }
        },
        "forIssue": "inventory-purchases-booked-to-expense"
      }
    ]
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/solvers" \
  -H "Authorization: Bearer $TP_API_KEY"

GET /tp/api/issues/:issueId/solvers

Returns solvers registered for one issue id.

Scope: solve

Parameters

Path

ParameterTypeDescription
issueIdstringSame as id from GET /tp/api/issues

Returns

On success, HTTP 200 with success: true, data.solvers, and data.hasSolvers.

Response attributes

FieldTypeDescription
solversarraySame element shape as GET /tp/api/solvers
hasSolversbooleanfalse when no solvers exist for this issue

Example response

{
  "success": true,
  "data": {
    "solvers": [
      {
        "id": "reclassify-expense-to-inventory",
        "name": "Reclassify to Inventory Item",
        "requiresInput": true,
        "userInputSchema": { },
        "forIssue": "inventory-purchases-booked-to-expense"
      }
    ],
    "hasSolvers": true
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/issues/inventory-purchases-booked-to-expense/solvers" \
  -H "Authorization: Bearer $TP_API_KEY"

POST /tp/api/solve/prep

Returns solver metadata, row-level options, and risk flags for one scan row—everything you need to choose solverId and build userInput before POST /tp/api/solve/apply. This mirrors the internal get_fixing_prep_data tool shape.

Scope: solve

Parameters

JSON body:

FieldTypeRequiredDescription
scanIdstringYesScan that contains the record
issueIdstringYesIssue bucket id
recordIdstringYesRow id—see Identifying a row
companyIdstringYesCompany id

Returns

On success, HTTP 200 with success: true and the fields below.

Response attributes

FieldTypeDescription
issueIdstringEcho of request issueId
scanIdstringScan id
hasSolversbooleanfalse when no solver is registered for this issue
messagestringPresent when hasSolvers is false
solversarrayWhen hasSolvers is true, same shape as GET /tp/api/issues/:issueId/solvers
recordFixOptionsobject or nullRow-level options (bills, suggested ids, etc.)
metaFixOptionsobject or nullIssue- or scan-level options (accounts, items, bank accounts, …)
hasLinkedTransactionsbooleanQBO links (e.g. payment ↔ invoice)
linkedTransactionsarrayWhen linked: { txnType, txnId } entries
resolvedByOtherFixbooleanDo not apply; already resolved elsewhere
resolvedByobject or nullContext when resolvedByOtherFix
needsRevalidationbooleanQBO data changed; run a new scan before applying
modifiedByobjectPresent when needsRevalidation
entityConflictsobject or nullSame entity flagged under multiple issues

Request sample

curl -sS -X POST "$TP_BASE_URL/tp/api/solve/prep" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scanId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "issueId": "inventory-purchases-booked-to-expense",
    "recordId": "3871",
    "companyId": "YOUR_COMPANY_DOCUMENT_ID"
  }'

Error codes

error.codeHTTPMeaning
E_MISSING_PARAMS400Missing scanId, issueId, or recordId
E_MISSING_COMPANY_ID400Missing companyId
E_SCAN_NOT_FOUND404Unknown scan
E_RECORD_NOT_FOUND404Row not in this issue’s results for the scan
E_DETECTOR_NOT_FOUND404No results bucket for this issueId on the scan
E_COMPANY_ACCESS_DENIED403No access to company

POST /tp/api/solve/apply

Runs a solver on one row from a finished scan. Use it after POST /tp/api/solve/prep, or when you already know the solver and userInput. CleanupOwl applies the fix in QuickBooks Online (QBO). The response says what changed or was skipped. If more fields are needed, you still get HTTP 200, success: true, and data.needsInputPOST again with the same ids plus userInput.

Scope: solve

Parameters

JSON body:

FieldTypeRequiredDescription
scanIdstringYesScan containing the record
issueIdstringYesIssue bucket id
recordIdstringYesRow id—see Identifying a row
companyIdstringYesCompany id
solverIdstringNoSolver to run; a default applies if omitted
userInputobjectNoValues matching userInputSchema
externalIdstringNoOptional; companyId is enough to resolve the company

Returns

On success, HTTP 200 with success: true and solver-specific data (see Response attributes). When more fields are required, data.needsInput is true instead—see Needs more input response.

Response attributes

Typical success data fields:

FieldTypeDescription
solverIdstringSolver that ran
solverNamestringDisplay name
issueIdstringIssue id
actionstringe.g. deleted, updated, skip
messagestringHuman-readable outcome
successbooleanOperation outcome inside data

Example request

{
  "scanId": "65f1a2b3c4d5e6f7a8b9c0d1",
  "issueId": "inventory-purchases-booked-to-expense",
  "recordId": "3871",
  "solverId": "reclassify-expense-to-inventory",
  "companyId": "YOUR_COMPANY_DOCUMENT_ID",
  "userInput": {
    "targetItemId": "235",
    "quantity": 1
  }
}

Example response

{
  "success": true,
  "data": {
    "solverId": "reclassify-expense-to-inventory",
    "solverName": "Reclassify to Inventory Item",
    "issueId": "inventory-purchases-booked-to-expense",
    "action": "updated",
    "message": "Expense reclassified to inventory"
  }
}

Needs more input response

{
  "success": true,
  "data": {
    "needsInput": true,
    "missingFields": ["targetAccountId"],
    "userInputSchema": {
      "targetAccountId": {
        "type": "select",
        "label": "Select New Account",
        "required": true,
        "optionsFrom": "record.fixOptions.targetAccounts"
      }
    }
  }
}

Send userInput with the requested fields and POST again with the same scanId, issueId, recordId, companyId, and solverId.

Request sample

curl -sS -X POST "$TP_BASE_URL/tp/api/solve/apply" \
  -H "Authorization: Bearer $TP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scanId": "65f1a2b3c4d5e6f7a8b9c0d1",
    "issueId": "inventory-purchases-booked-to-expense",
    "recordId": "3871",
    "companyId": "YOUR_COMPANY_DOCUMENT_ID",
    "solverId": "reclassify-expense-to-inventory"
  }'

Error codes

error.codeHTTPMeaning
E_MISSING_PARAMS400Missing scanId, issueId, or recordId
E_MISSING_COMPANY_ID400Missing companyId
E_SOLVER_ISSUE_MISMATCH400Solver not valid for this issue
(solver validation)400Preconditions not met (for example required userInput) — success: false with error
E_SCAN_NOT_FOUND404Scan not found
E_RECORD_NOT_FOUND404No matching row in scan results
E_SCAN_ARCHIVED400Scan archived; run a new scan
E_ENTITY_FROZEN400Record already resolved
E_ENTITY_MODIFIED400QBO data changed; re-scan
E_FIX_LIMIT_EXCEEDED402Plan fix limit reached
E_QBO_AUTH_FAILED401Reconnect QuickBooks Online (QBO)
E_COMPANY_ACCESS_DENIED403No access to company

GET /tp/api/scans/:scanId/solves

Lists recent solve attempts for a scan (newest first, capped server-side).

Scope: solve

Parameters

Path

ParameterTypeDescription
scanIdstringScan id

Query

ParameterTypeRequiredDescription
companyIdstringYesCompany id
issueIdstringNoFilter to one issue

Returns

On success, HTTP 200 with success: true and data.solves.

Response attributes

data.solves (array). Each entry includes at least:

FieldTypeDescription
_idstringLog id
scanIdstringScan
recordIdstringRow
issueIdstringIssue
solverIdstring or nullSolver used
solverNamestring or nullDisplay name
actionstringe.g. skip, updated
successbooleanWhether the fix succeeded
executedAtstringISO timestamp
entityobjectOptional { entityType, entityId }

Other properties may appear; integrate only against the fields above.

Example response

{
  "success": true,
  "data": {
    "solves": [
      {
        "_id": "65f1a2b3c4d5e6f7a8b9c0d2",
        "scanId": "65f1a2b3c4d5e6f7a8b9c0d1",
        "recordId": "3699",
        "issueId": "vendor-expenses-without-open-bills",
        "action": "skip",
        "success": true,
        "executedAt": "2026-03-31T09:33:16.025Z",
        "solverId": null,
        "solverName": null,
        "entity": { "entityType": "purchase", "entityId": "3699" }
      }
    ]
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/scans/$SCAN_ID/solves?companyId=$COMPANY_ID" \
  -H "Authorization: Bearer $TP_API_KEY"

Applying solves

Recommended: call POST /tp/api/solve/prep with the same scanId, companyId, issueId, and recordId you will use for apply. Use data.solvers, recordFixOptions, metaFixOptions, and the risk flags to build userInput and decide whether apply is safe.

Lightweight path: GET /tp/api/issues/:issueId/solvers or GET /tp/api/solvers only exposes schemas—they omit row-level recordFixOptions and metaFixOptions.

Call POST /tp/api/solve/apply with scanId, companyId, issueId, recordId, optional solverId, and optional userInput. If data.needsInput is true, add userInput and POST apply again.


4. Billing

Endpoints:

GET /tp/api/billing

Returns plan name, limits, and usage counters for the authenticated account.

Scope: none (valid API key only)

Parameters

No path or query parameters.

Returns

On success, HTTP 200 with success: true and data as below.

Response attributes

FieldTypeDescription
planobjectCurrent plan
plan.namestringDisplay name
plan.limitsobjecte.g. { "maxCompanies": 2 }
usageobjectUsage counters
usage.fixesUsednumberFixes used in the billing period
usage.fixesLimitnumberMonthly cap, or -1 for unlimited
usage.fixesRemainingnumberRemaining fixes, or -1 if unlimited
usage.canRunScanbooleanWhether scans are allowed on the plan
upgradeobjectUpgrade hint
upgrade.availablebooleanWhether a higher tier exists
upgrade.nextPlanobject or null{ id, name }
upgrade.messagestring or nullPrompt text

Use plan.name and plan.limits in billing UI. plan.id is not always returned.

Example response

{
  "success": true,
  "data": {
    "plan": {
      "name": "All-Access Pass",
      "limits": { "maxCompanies": 2 }
    },
    "usage": {
      "fixesUsed": 0,
      "fixesLimit": -1,
      "fixesRemaining": -1,
      "canRunScan": true
    },
    "upgrade": {
      "available": false,
      "nextPlan": null,
      "message": null
    }
  }
}

Error codes

error.codeHTTPMeaning
E_PAYMENTS_DISABLED503Billing subsystem not configured in this deployment

5. Connections

Endpoints:

GET /tp/api/connections

Lists QuickBooks Online companies linked to the API key’s user. Use each object’s _id as companyId in scan and solve calls.

Scope: none

Parameters

No path or query parameters.

Returns

On success, HTTP 200 with success: true and data.connections.

Response attributes

data.connections (array of QuickBooks Online (QBO) connections). Each element:

FieldTypeDescription
_idstringUse as companyId
externalIdstringQBO company / realm id
companyNamestringDisplay name
accountingSystemstringe.g. qbo
isSandboxbooleanSandbox realm
statusstringe.g. active
syncStatusstringSync pipeline status
syncLockedbooleanSync lock
syncUpdatedAtstringISO
syncErroranyOptional
createdAtstringISO
lastAccessedAtstringISO

Example response

{
  "success": true,
  "data": {
    "connections": [
      {
        "_id": "69b2df85dc1974c5e28656ae",
        "externalId": "9341455852565781",
        "accountingSystem": "qbo",
        "companyName": "Example Co.",
        "isSandbox": true,
        "status": "active",
        "syncStatus": "ready",
        "syncLocked": false,
        "syncUpdatedAt": "2026-03-31T06:31:25.689Z",
        "syncError": null,
        "createdAt": "2026-03-12T15:45:09.699Z",
        "lastAccessedAt": "2026-03-26T08:41:58.959Z"
      }
    ]
  }
}

Request sample

curl -sS "$TP_BASE_URL/tp/api/connections" \
  -H "Authorization: Bearer $TP_API_KEY"

GET /tp/api/connect

Returns a browser URL that starts QuickBooks Online OAuth. The user must already be signed in to CleanupOwl in that browser session.

Scope: none

Parameters

No path or query parameters.

Returns

On success, HTTP 200 with success: true and data.connectionUrl / data.instructions.

Response attributes

FieldTypeDescription
connectionUrlstringAbsolute URL to open
instructionsstringHuman-readable steps

Example response

{
  "success": true,
  "data": {
    "connectionUrl": "https://app.cleanupowl.com/auth/qbo/init?returnTo=/v2/scans",
    "instructions": "Open this URL in your browser to connect your QuickBooks Online company. You must be logged in to CleanupOwl."
  }
}

Postman collection

The runnable Postman collection lives in CleanupOwl/cleanupowl-automation-skill on GitHub. Download cleanupowl-tp-api-postman-collection.json and import it into Postman, or use Import → Link with the raw JSON URL. It covers every endpoint with pre-configured paths and collection variables—fill in your values and run.

Suggested variables to set in your Postman environment:

VariableWhat to put there
baseUrlhttps://app.cleanupowl.com
apiTokenYour API key
companyId_id from GET /tp/api/connections
scanIdscanId from POST /tp/api/scan/start
issueIdAn issue id from the issues list
recordIdA row id from the scan results
solverIdA solver id from prep or the solvers list

The collection does not duplicate schemas, error codes, or polling rules—use this reference for those details and Postman as a quick way to run requests.

Companion files

The following resource complements this HTTP reference with additional context for Cursor and other IDE agents:

  • SKILL.md — Same repo as the Postman collection. Covers when to use the API, prerequisites, connections, key concepts, and tool patterns.

Contact Us

If you need help or think you have found a bug, reach out to us directly. We typically respond within 24 hours.