← Tax Prep Desk / API
Tokens

Drive Tax Prep Desk from your own code

Prep material for an accountant, not tax advice. Nothing is ever filed. US federal only; tax years 2025 and 2026 only. The app never sends the whole ledger: only the totals, the flagged rows and the payees at or near the 1099 threshold travel in pack. Never put an SSN, a tax id, or a bank or card number in question - it is sent as you type it.

Everything the web page does is available over HTTP. Send a small business's ledger facts, as the browser computes them, and get the same packet back: a books check that says whether the ledger is fit to compute a tax number from (ready, proceed_with_assumptions, close_first) with the open items and the gate statement; a 1099 prep list with one candidate per payee at or near the threshold, the suggested form, W-9 requests and duplicate-payee reviews; or the quarterly estimated tax summary with the next payment, its due date and a catch-up line for every missed quarter. The natural use is a month-end job: export the ledger, run the books check, then file the 1099 list or the estimate summary in the folder your accountant reads.

The model never does the arithmetic. Payee totals against the year's threshold, W-9 gaps, likely duplicates, the uncategorized swing, the annualised net, self-employment tax, the federal figure at the assumed rate, missed quarters and the next installment are all worked out by taxkit.js, the same file the web page loads, and sent as pack, a JSON string. See building the body.

Three lanes: the task field

Every request names its lane in task, and one system prompt routes on it.

taskwhat it doeswhat it needs in pack
readyThe books check (tax-prep): verdict, headline, summary, one open_items entry per high or medium flag (ref, issue, effect, fix), gate_statement, basis_statement, mode and mode_reason, plus the shared keys.readiness (the browser's verdict_hint and counts) and rows (the problem rows only, at most 40).
necThe 1099 prep list (tax-season-organizer, path 2): one candidates entry per payee at or near the threshold (form, total, w9, note), excluded, w9_requests, duplicate_reviews, corporate_reviews, processor_note, deadline_note, plus the shared keys.nec (threshold, near floor, due date, id lists), payees (at or near the threshold, then the largest exempt ones, at most 60) and duplicates.
quarterlyThe estimated tax summary (tax-season-organizer, path 1): quarter, payment_due, due_date, one catch_up line per missed quarter, se_text, federal_text, safe_harbor_text, basis_statement, plus the shared keys.estimate (every step of the calculation, or blocked with the reason).

Shared keys in every reply: lane, headline (names the tax year), summary (answers question when there is one), assumptions, accountant_checklist and one prescan_responses entry per prescan flag.

The lanes chain: the ready check's mode says which packet comes next (quarterly, nec or both), and its verdict, gate statement and open items can travel into nec or quarterly as the optional readiness field. See handing the books check on. A missing or unknown task is answered as the closest lane and the reply's lane says which. Always send task.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses the same envelope, so one helper covers the whole API:

{"ok": true, "data": {"job_id": "job_...", "status": "queued"}}
{"ok": false, "error": {"code": "payment_required", "message": "...", "status": 402, "details": {}}}

The token is minted for this app (the guest endpoint takes {"slug":"tax-prep-desk"} in its body), so no slug header is needed afterwards. Send it as Authorization: Bearer ….

The input object IS the request body. There is no {"input": …} wrapper. A wrapped body returns a 200 with an unknown field 'input' warning, and the model never sees your ledger facts.

Error codes

codestatuswhat to do
unauthorized401The token is missing, malformed or expired. Get a new one from the token page.
payment_required402The balance is below min_credits. Call /estimate first and top up.
forbidden403The token is valid but not for this app, or a guest token tried a metered run.
not_found404Unknown job id, unknown collection, or the app slug does not exist.
conflict409The same Idempotency-Key was replayed with a different body. Change the key or send the original input.
validation_error422A field is the wrong type. Every field is a string: tax_year is "2026", not 2026, and pack and readiness must be JSON-encoded strings, not objects.
rate_limited429Too many requests. Back off and retry; do not tight-loop.
internal5xxA server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice.

1. Get a token

The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. A guest token can call /me and /estimate; all three lanes are metered, so a run needs a personal token.

# The token page is the shortest path. It shows the token this browser holds and
# hands you a ready-made shell export:
#
#   https://tax-prep-desk.skillsafe.ai/tokens.html
#   export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead. A guest token is enough
# for /me and /estimate; all three lanes need a personal token
# from signing in.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" -d '{"slug":"tax-prep-desk"}'
# {"ok":true,"data":{"token":"…","subject_type":"guest"}}

2. A tiny client

One helper that sends the token, unwraps data and raises on ok: false.

# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="tax-prep-desk"
TOKEN="$SKILLSAFE_TOKEN"   # from https://tax-prep-desk.skillsafe.ai/tokens.html

call() {                  # call <path> [json-body]
  if [ -n "$2" ]; then
    curl -sS -X POST "$BASE/$1" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d "$2"
  else
    curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
  fi
}

3. Check the session and the balance

call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}

4. Price the run (free)

/estimate returns the model binding and the credits a run would reserve. It creates no job and charges nothing. It also does not validate the body, so check the shape yourself: an object whose every value is a string, task one of the three lanes, and pack a JSON string. Re-estimate per lane - the lanes send different amounts of data and reserve different amounts.

# body.json is the input object itself - no {"input": ...} wrapper. Build it with
# make-body.js below. estimate does not validate it, so check the shape first:
python3 -c 'import json;b=json.load(open("body.json"));assert isinstance(b,dict) and b.get("task") in ("ready","nec","quarterly") and all(isinstance(v,str) for v in b.values()) and all(b.get(k,"").strip() for k in ("business","tax_year","pack")) and isinstance(json.loads(b["pack"]),dict)'
INPUT=$(cat body.json)
LANE=$(printf '%s' "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task"])')

call estimate "$INPUT"
# {"ok":true,"data":{"model":"...","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":...,"min_credits":...,"sponsor_enabled":false,
#   "input_checked":true,"warnings":[]}}
#
# estimate creates no job and charges nothing. hold_credits is RESERVED, not the
# price; charged_credits after the run is usually far lower.

Building the body

Do not hand-assemble pack. Load taxkit.js (it runs unchanged in Node) and let it read your ledger export exactly as the page does. It removes SSN-shaped values, classifies every row, totals the payees, works the estimate and raises the prescan flags, then buildInput picks what each lane may see:

// make-body.js - build the run body with the SAME engine the web page uses.
// Save taxkit.js from https://tax-prep-desk.skillsafe.ai/taxkit.js next to this file.
// Usage: node make-body.js ready|nec|quarterly [readiness.json]
const fs = require("fs");
const K = require("./taxkit.js");

const A = K.analyze({
  business: "Rivera Design Studio - graphic design, sole proprietor, Portland",
  year: "2026",                 // 2025 or 2026 only
  entity: "sole_prop",          // sole_prop, smllc, partnership, scorp, ccorp
  rate: "22",                   // assumed federal rate: 10, 12, 22, 24, 32, 35 or 37
  through: "2026-08-31",        // books closed through (blank: end of the latest month with activity)
  prepared: "2026-09-20",       // today, for the owner
  paid1: "4,000", paid2: "4,000", paid3: "", paid4: "",   // estimated payments made per quarter
  prior_tax: "",                // last year's total tax, for the safe-harbour floor (optional)
  agi_over: false,              // last year's AGI over USD 150,000 (the 110% rule)
  full_year: "",                // the owner's own full-year net, instead of annualising (optional)
  ledger: fs.readFileSync("ledger.csv", "utf8")           // date, payee, account, amount (+ method, W-9)
});
const lane = process.argv[2] || "ready";
const readiness = lane !== "ready" && process.argv[3] ? fs.readFileSync(process.argv[3], "utf8") : null;
const body = K.buildInput(lane, A, {
  question: "I forgot to pay in September. What do I owe now, and who needs a 1099 this year?",
  readiness                     // nec and quarterly only; ignored for ready
});
if (!body) throw new Error("no ledger rows read, or an unknown lane");
fs.writeFileSync("body.json", JSON.stringify(K.mustBeObject(body)));
console.log(lane, A.rows.length, "rows,", K.laneFlags(A, lane).length, "flags for this lane");

Run on the Rivera example ledger from the page, this reproduces the worked inputs below byte for byte. buildInput caps what travels: at most 40 problem rows (ready) and 60 payees (nec), with rows_not_sent / payees_not_sent counting the rest, and question cut at 1,500 characters.

The input fields, every one a string:

fieldrequiredwhat it holds
taskyesready, nec or quarterly. Send it first; it picks the lane.
businessyesThe business in the owner's words (up to 160 characters); "the business" when blank.
tax_yearyes"2025" or "2026" - a string, not a number.
packyesA JSON string (the output of JSON.stringify), never an object. Its keys are listed in the next table.
questionnoThe owner's own question, answered inside summary. Omitted when empty. No SSNs or account numbers.
readinessno (nec and quarterly only)A JSON string of the ready check's handoff: {"verdict","gate_statement","open_items","mode"}. When its verdict is not ready, the gate statement becomes a stated assumption. See below.
retry_notenoOnly on a reformat retry, after a reply that could not be parsed: say what was wrong. Never on a first run.

The keys inside pack:

keylaneswhat it holds
tax_year, prepared_on, books_through, books_through_inferredallThe year (a number here), today, the close date, and whether the close was inferred.
entity, entity_label, assumed_rate_pct, mode_hintallBusiness structure, the assumed federal rate (22 by default), and which packet the date suggests (quarterly, nec or both).
figuresallThe year's se_wage_base, nec_threshold, near_threshold_floor, nec_due, return_due, quarter_due and the source of those figures.
booksallrows_read, rows_in_year, income, expenses, net_profit, uncategorized_rows, uncategorized_total, uncategorized_swing, marginal_rate_pct, owner_draws_excluded, transfers_excluded.
readiness, rows, rows_not_sentreadyverdict_hint, empty_months, duplicate_postings, rows_outside_year, rows_after_close, expense_rows_without_payee, unreadable_rows, contractors_paid, payees_over_threshold, quarterly_ready; the problem rows R# (uncategorized, duplicated, no payee, after the close) with id, date, payee, account, amount, kind, uncategorized.
nec, payees, payees_not_sent, duplicatesnecthreshold, near_floor, due, past_deadline, year_incomplete, counts, and the id lists w9_missing, corporate_looking, card_or_processor; payees V# with status (file, near, exempt), form, total, basis, the split into services, rent, goods, wages, unclassified, card_or_processor, processor_only, plus w9, ledger_1099_tag, looks_corporate, llc, attorney, accounts, methods, payments, first, last; likely-duplicate pairs D# with combined and crosses_threshold.
estimatequarterlymonths_of_books, ytd_net, annual_net, annual_basis, se_base, se_tax, se_note, se_half, adjusted_net, federal_tax, total_liability, required_installment, paid_by_quarter, paid_total, missed (q, due, due_long, amount), quarters_ahead, remaining, next_quarter, payment_due, due_date, due_with_return, single_filer_bracket_pct, safe_harbor, wage_base_hit. Or only blocked, a reason, when no estimate can be computed.
prescan_flagsallThe flags for this lane only: id (F#), severity (high, medium, info), category, ref, message.

Worked inputs

The Rivera Design Studio example from the page: a sole proprietor, tax year 2026, books closed through August 31, a Q3 payment missed. Each body below is valid JSON exactly as sent, with pack as a JSON string; to keep them readable, some rows, payees and flags are left out of each pack (noted under each one), and the decoded pack follows.

ready (3 of 5 problem rows shown):

{
 "task": "ready",
 "business": "Rivera Design Studio - graphic design, sole proprietor, Portland",
 "tax_year": "2026",
 "pack": "{\"tax_year\":2026,\"prepared_on\":\"2026-09-20\",\"books_through\":\"2026-08-31\",\"books_through_inferred\":false,\"entity\":\"sole_prop\",\"entity_label\":\"Sole proprietor\",\"assumed_rate_pct\":22,\"mode_hint\":\"quarterly\",\"figures\":{\"source\":\"2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it.\",\"se_wage_base\":184500,\"nec_threshold\":2000,\"near_threshold_floor\":1333,\"nec_due\":\"2027-02-01\",\"return_due\":\"2027-04-15\",\"quarter_due\":[\"Q1 Jan 1 - Mar 31 due 2026-04-15\",\"Q2 Apr 1 - May 31 due 2026-06-15\",\"Q3 Jun 1 - Aug 31 due 2026-09-15\",\"Q4 Sep 1 - Dec 31 due 2027-01-15\"]},\"books\":{\"rows_read\":52,\"rows_in_year\":52,\"income\":96000,\"expenses\":33999.92,\"net_profit\":62000.08,\"uncategorized_rows\":3,\"uncategorized_total\":2270,\"uncategorized_swing\":785,\"marginal_rate_pct\":34.58,\"owner_draws_excluded\":12000,\"transfers_excluded\":0},\"readiness\":{\"verdict_hint\":\"proceed_with_assumptions\",\"empty_months\":[],\"duplicate_postings\":[\"R43 and R44\"],\"rows_outside_year\":0,\"rows_after_close\":0,\"expense_rows_without_payee\":0,\"unreadable_rows\":0,\"contractors_paid\":12,\"payees_over_threshold\":5,\"quarterly_ready\":true},\"rows\":[{\"id\":\"R26\",\"date\":\"2026-05-09\",\"payee\":\"Home Depot\",\"account\":\"Uncategorized Expense\",\"amount\":640,\"kind\":\"expense\",\"uncategorized\":true},{\"id\":\"R43\",\"date\":\"2026-07-22\",\"payee\":\"FedEx\",\"account\":\"Shipping and postage\",\"amount\":95,\"kind\":\"expense\",\"uncategorized\":false},{\"id\":\"R44\",\"date\":\"2026-07-22\",\"payee\":\"FedEx\",\"account\":\"Shipping and postage\",\"amount\":95,\"kind\":\"expense\",\"uncategorized\":false}],\"prescan_flags\":[{\"id\":\"F1\",\"severity\":\"medium\",\"category\":\"uncategorized\",\"ref\":\"R26,R38,R51\",\"message\":\"3 transactions are uncategorized, totalling USD 2,270. Left out of net profit; at the 34.58% combined marginal rate they could move the annual estimate by about USD 785 either way.\"},{\"id\":\"F2\",\"severity\":\"medium\",\"category\":\"duplicate_posting\",\"ref\":\"R43,R44\",\"message\":\"1 pair of rows share the same date, payee and amount - possible double postings, counted twice in every total.\"},{\"id\":\"F3\",\"severity\":\"info\",\"category\":\"figures\",\"ref\":\"\",\"message\":\"2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it.\"}]}",
 "question": "I forgot to pay in September. What do I owe now, and who needs a 1099 this year?"
}

Its pack, decoded:

{
 "tax_year": 2026,
 "prepared_on": "2026-09-20",
 "books_through": "2026-08-31",
 "books_through_inferred": false,
 "entity": "sole_prop",
 "entity_label": "Sole proprietor",
 "assumed_rate_pct": 22,
 "mode_hint": "quarterly",
 "figures": {
  "source": "2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it.",
  "se_wage_base": 184500,
  "nec_threshold": 2000,
  "near_threshold_floor": 1333,
  "nec_due": "2027-02-01",
  "return_due": "2027-04-15",
  "quarter_due": [
   "Q1 Jan 1 - Mar 31 due 2026-04-15",
   "Q2 Apr 1 - May 31 due 2026-06-15",
   "Q3 Jun 1 - Aug 31 due 2026-09-15",
   "Q4 Sep 1 - Dec 31 due 2027-01-15"
  ]
 },
 "books": {
  "rows_read": 52,
  "rows_in_year": 52,
  "income": 96000,
  "expenses": 33999.92,
  "net_profit": 62000.08,
  "uncategorized_rows": 3,
  "uncategorized_total": 2270,
  "uncategorized_swing": 785,
  "marginal_rate_pct": 34.58,
  "owner_draws_excluded": 12000,
  "transfers_excluded": 0
 },
 "readiness": {
  "verdict_hint": "proceed_with_assumptions",
  "empty_months": [],
  "duplicate_postings": [
   "R43 and R44"
  ],
  "rows_outside_year": 0,
  "rows_after_close": 0,
  "expense_rows_without_payee": 0,
  "unreadable_rows": 0,
  "contractors_paid": 12,
  "payees_over_threshold": 5,
  "quarterly_ready": true
 },
 "rows": [
  {
   "id": "R26",
   "date": "2026-05-09",
   "payee": "Home Depot",
   "account": "Uncategorized Expense",
   "amount": 640,
   "kind": "expense",
   "uncategorized": true
  },
  {
   "id": "R43",
   "date": "2026-07-22",
   "payee": "FedEx",
   "account": "Shipping and postage",
   "amount": 95,
   "kind": "expense",
   "uncategorized": false
  },
  {
   "id": "R44",
   "date": "2026-07-22",
   "payee": "FedEx",
   "account": "Shipping and postage",
   "amount": 95,
   "kind": "expense",
   "uncategorized": false
  }
 ],
 "prescan_flags": [
  {
   "id": "F1",
   "severity": "medium",
   "category": "uncategorized",
   "ref": "R26,R38,R51",
   "message": "3 transactions are uncategorized, totalling USD 2,270. Left out of net profit; at the 34.58% combined marginal rate they could move the annual estimate by about USD 785 either way."
  },
  {
   "id": "F2",
   "severity": "medium",
   "category": "duplicate_posting",
   "ref": "R43,R44",
   "message": "1 pair of rows share the same date, payee and amount - possible double postings, counted twice in every total."
  },
  {
   "id": "F3",
   "severity": "info",
   "category": "figures",
   "ref": "",
   "message": "2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it."
  }
 ]
}

nec (3 of 7 payees, 1 of 2 duplicate pairs and 5 of 10 flags shown; the shared keys are the same as above):

{
 "task": "nec",
 "business": "Rivera Design Studio - graphic design, sole proprietor, Portland",
 "tax_year": "2026",
 "pack": "{\"tax_year\":2026,\"prepared_on\":\"2026-09-20\",\"books_through\":\"2026-08-31\",\"books_through_inferred\":false,\"entity\":\"sole_prop\",\"entity_label\":\"Sole proprietor\",\"assumed_rate_pct\":22,\"mode_hint\":\"quarterly\",\"figures\":{\"source\":\"2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it.\",\"se_wage_base\":184500,\"nec_threshold\":2000,\"near_threshold_floor\":1333,\"nec_due\":\"2027-02-01\",\"return_due\":\"2027-04-15\",\"quarter_due\":[\"Q1 Jan 1 - Mar 31 due 2026-04-15\",\"Q2 Apr 1 - May 31 due 2026-06-15\",\"Q3 Jun 1 - Aug 31 due 2026-09-15\",\"Q4 Sep 1 - Dec 31 due 2027-01-15\"]},\"books\":{\"rows_read\":52,\"rows_in_year\":52,\"income\":96000,\"expenses\":33999.92,\"net_profit\":62000.08,\"uncategorized_rows\":3,\"uncategorized_total\":2270,\"uncategorized_swing\":785,\"marginal_rate_pct\":34.58,\"owner_draws_excluded\":12000,\"transfers_excluded\":0},\"nec\":{\"threshold\":2000,\"near_floor\":1333,\"due\":\"2027-02-01\",\"past_deadline\":false,\"year_incomplete\":true,\"payees_total\":15,\"contractors_paid\":12,\"file_count\":5,\"near_count\":2,\"w9_missing\":[\"V5\"],\"corporate_looking\":[\"V3\"],\"card_or_processor\":[\"V5\",\"V6\",\"V7\"]},\"payees\":[{\"id\":\"V1\",\"name\":\"Quorvale Studios LLC\",\"status\":\"file\",\"form\":\"1099-MISC\",\"total\":8800,\"basis\":8800,\"services\":0,\"rent\":8800,\"goods\":0,\"wages\":0,\"unclassified\":0,\"card_or_processor\":0,\"processor_only\":false,\"w9\":\"on_file\",\"ledger_1099_tag\":null,\"looks_corporate\":false,\"llc\":true,\"attorney\":false,\"accounts\":[\"Rent\"],\"methods\":[\"Check\"],\"payments\":8,\"first\":\"2026-01-01\",\"last\":\"2026-08-01\"},{\"id\":\"V5\",\"name\":\"Pellucid Owl Photo\",\"status\":\"file\",\"form\":\"1099-NEC\",\"total\":2400,\"basis\":2400,\"services\":2400,\"rent\":0,\"goods\":0,\"wages\":0,\"unclassified\":0,\"card_or_processor\":2400,\"processor_only\":true,\"w9\":\"missing\",\"ledger_1099_tag\":null,\"looks_corporate\":false,\"llc\":false,\"attorney\":false,\"accounts\":[\"Contract labor\"],\"methods\":[\"PayPal\"],\"payments\":2,\"first\":\"2026-05-14\",\"last\":\"2026-08-13\"},{\"id\":\"V6\",\"name\":\"Dana Reyes\",\"status\":\"near\",\"form\":\"1099-NEC\",\"total\":1500,\"basis\":1500,\"services\":1500,\"rent\":0,\"goods\":0,\"wages\":0,\"unclassified\":0,\"card_or_processor\":1500,\"processor_only\":true,\"w9\":\"missing\",\"ledger_1099_tag\":null,\"looks_corporate\":false,\"llc\":false,\"attorney\":false,\"accounts\":[\"Contract labor\"],\"methods\":[\"Venmo\"],\"payments\":1,\"first\":\"2026-06-19\",\"last\":\"2026-06-19\"}],\"duplicates\":[{\"id\":\"D2\",\"a\":\"V8\",\"b\":\"V11\",\"a_name\":\"Bob Nguyen\",\"b_name\":\"Robert Nguyen\",\"combined\":2150,\"crosses_threshold\":true,\"why\":\"\\\"bob\\\" and \\\"robert\\\" are the same first name\"}],\"prescan_flags\":[{\"id\":\"F2\",\"severity\":\"medium\",\"category\":\"duplicate_posting\",\"ref\":\"R43,R44\",\"message\":\"1 pair of rows share the same date, payee and amount - possible double postings, counted twice in every total.\"},{\"id\":\"F4\",\"severity\":\"high\",\"category\":\"w9\",\"ref\":\"V5\",\"message\":\"1 payee at or over the USD 2,000 threshold has no W-9 on file - collect before filing (due February 1, 2027).\"},{\"id\":\"F6\",\"severity\":\"high\",\"category\":\"duplicate_payee\",\"ref\":\"V8,V11\",\"message\":\"\\\"Bob Nguyen\\\" and \\\"Robert Nguyen\\\" may be the same payee (\\\"bob\\\" and \\\"robert\\\" are the same first name). Combined USD 2,150 - over the threshold together, under it apart. Confirm before filing; not merged.\"},{\"id\":\"F9\",\"severity\":\"medium\",\"category\":\"processor\",\"ref\":\"V5,V6,V7\",\"message\":\"3 payees were paid at least partly by card or a payment processor. The processor may issue its own 1099-K; the accountant decides whether a 1099-NEC is also needed.\"},{\"id\":\"F10\",\"severity\":\"medium\",\"category\":\"year_incomplete\",\"ref\":\"\",\"message\":\"The books run only through August 31, 2026, so these are year-to-date totals; a payee under the threshold now may cross it by December 31.\"}]}",
 "question": "I forgot to pay in September. What do I owe now, and who needs a 1099 this year?"
}

The lane-specific part of its pack, decoded:

{
 "nec": {
  "threshold": 2000,
  "near_floor": 1333,
  "due": "2027-02-01",
  "past_deadline": false,
  "year_incomplete": true,
  "payees_total": 15,
  "contractors_paid": 12,
  "file_count": 5,
  "near_count": 2,
  "w9_missing": [
   "V5"
  ],
  "corporate_looking": [
   "V3"
  ],
  "card_or_processor": [
   "V5",
   "V6",
   "V7"
  ]
 },
 "payees": [
  {
   "id": "V1",
   "name": "Quorvale Studios LLC",
   "status": "file",
   "form": "1099-MISC",
   "total": 8800,
   "basis": 8800,
   "services": 0,
   "rent": 8800,
   "goods": 0,
   "wages": 0,
   "unclassified": 0,
   "card_or_processor": 0,
   "processor_only": false,
   "w9": "on_file",
   "ledger_1099_tag": null,
   "looks_corporate": false,
   "llc": true,
   "attorney": false,
   "accounts": [
    "Rent"
   ],
   "methods": [
    "Check"
   ],
   "payments": 8,
   "first": "2026-01-01",
   "last": "2026-08-01"
  },
  {
   "id": "V5",
   "name": "Pellucid Owl Photo",
   "status": "file",
   "form": "1099-NEC",
   "total": 2400,
   "basis": 2400,
   "services": 2400,
   "rent": 0,
   "goods": 0,
   "wages": 0,
   "unclassified": 0,
   "card_or_processor": 2400,
   "processor_only": true,
   "w9": "missing",
   "ledger_1099_tag": null,
   "looks_corporate": false,
   "llc": false,
   "attorney": false,
   "accounts": [
    "Contract labor"
   ],
   "methods": [
    "PayPal"
   ],
   "payments": 2,
   "first": "2026-05-14",
   "last": "2026-08-13"
  },
  {
   "id": "V6",
   "name": "Dana Reyes",
   "status": "near",
   "form": "1099-NEC",
   "total": 1500,
   "basis": 1500,
   "services": 1500,
   "rent": 0,
   "goods": 0,
   "wages": 0,
   "unclassified": 0,
   "card_or_processor": 1500,
   "processor_only": true,
   "w9": "missing",
   "ledger_1099_tag": null,
   "looks_corporate": false,
   "llc": false,
   "attorney": false,
   "accounts": [
    "Contract labor"
   ],
   "methods": [
    "Venmo"
   ],
   "payments": 1,
   "first": "2026-06-19",
   "last": "2026-06-19"
  }
 ],
 "duplicates": [
  {
   "id": "D2",
   "a": "V8",
   "b": "V11",
   "a_name": "Bob Nguyen",
   "b_name": "Robert Nguyen",
   "combined": 2150,
   "crosses_threshold": true,
   "why": "\"bob\" and \"robert\" are the same first name"
  }
 ],
 "prescan_flags": [
  {
   "id": "F2",
   "severity": "medium",
   "category": "duplicate_posting",
   "ref": "R43,R44",
   "message": "1 pair of rows share the same date, payee and amount - possible double postings, counted twice in every total."
  },
  {
   "id": "F4",
   "severity": "high",
   "category": "w9",
   "ref": "V5",
   "message": "1 payee at or over the USD 2,000 threshold has no W-9 on file - collect before filing (due February 1, 2027)."
  },
  {
   "id": "F6",
   "severity": "high",
   "category": "duplicate_payee",
   "ref": "V8,V11",
   "message": "\"Bob Nguyen\" and \"Robert Nguyen\" may be the same payee (\"bob\" and \"robert\" are the same first name). Combined USD 2,150 - over the threshold together, under it apart. Confirm before filing; not merged."
  },
  {
   "id": "F9",
   "severity": "medium",
   "category": "processor",
   "ref": "V5,V6,V7",
   "message": "3 payees were paid at least partly by card or a payment processor. The processor may issue its own 1099-K; the accountant decides whether a 1099-NEC is also needed."
  },
  {
   "id": "F10",
   "severity": "medium",
   "category": "year_incomplete",
   "ref": "",
   "message": "The books run only through August 31, 2026, so these are year-to-date totals; a payee under the threshold now may cross it by December 31."
  }
 ]
}

quarterly (3 of 5 flags shown):

{
 "task": "quarterly",
 "business": "Rivera Design Studio - graphic design, sole proprietor, Portland",
 "tax_year": "2026",
 "pack": "{\"tax_year\":2026,\"prepared_on\":\"2026-09-20\",\"books_through\":\"2026-08-31\",\"books_through_inferred\":false,\"entity\":\"sole_prop\",\"entity_label\":\"Sole proprietor\",\"assumed_rate_pct\":22,\"mode_hint\":\"quarterly\",\"figures\":{\"source\":\"2026 figures as published by the IRS and SSA for 2026; the USD 2,000 1099-NEC / 1099-MISC threshold applies to payments made after 2025 under P.L. 119-21. Confirm each figure at irs.gov before relying on it.\",\"se_wage_base\":184500,\"nec_threshold\":2000,\"near_threshold_floor\":1333,\"nec_due\":\"2027-02-01\",\"return_due\":\"2027-04-15\",\"quarter_due\":[\"Q1 Jan 1 - Mar 31 due 2026-04-15\",\"Q2 Apr 1 - May 31 due 2026-06-15\",\"Q3 Jun 1 - Aug 31 due 2026-09-15\",\"Q4 Sep 1 - Dec 31 due 2027-01-15\"]},\"books\":{\"rows_read\":52,\"rows_in_year\":52,\"income\":96000,\"expenses\":33999.92,\"net_profit\":62000.08,\"uncategorized_rows\":3,\"uncategorized_total\":2270,\"uncategorized_swing\":785,\"marginal_rate_pct\":34.58,\"owner_draws_excluded\":12000,\"transfers_excluded\":0},\"estimate\":{\"months_of_books\":8,\"ytd_net\":62000,\"annual_net\":93000,\"annual_basis\":\"projected: YTD USD 62,000 / 8 months x 12\",\"se_base\":85886,\"se_tax\":13141,\"se_note\":\"under the 2026 wage base of USD 184,500 - full 15.3% applies\",\"se_half\":6571,\"adjusted_net\":86429,\"federal_tax\":19014,\"total_liability\":32155,\"required_installment\":8039,\"paid_by_quarter\":[4000,4000,0,0],\"paid_total\":8000,\"missed\":[{\"q\":\"Q3\",\"due\":\"2026-09-15\",\"due_long\":\"September 15, 2026\",\"amount\":8039}],\"quarters_ahead\":1,\"remaining\":16116,\"next_quarter\":\"Q4\",\"payment_due\":16116,\"due_date\":\"2027-01-15\",\"due_with_return\":false,\"single_filer_bracket_pct\":22,\"safe_harbor\":null,\"wage_base_hit\":false},\"prescan_flags\":[{\"id\":\"F1\",\"severity\":\"medium\",\"category\":\"uncategorized\",\"ref\":\"R26,R38,R51\",\"message\":\"3 transactions are uncategorized, totalling USD 2,270. Left out of net profit; at the 34.58% combined marginal rate they could move the annual estimate by about USD 785 either way.\"},{\"id\":\"F12\",\"severity\":\"high\",\"category\":\"missed_quarter\",\"ref\":\"\",\"message\":\"Q3 was due September 15, 2026 and no payment is recorded against it. Shown as its own catch-up line; penalty and interest are for the accountant.\"},{\"id\":\"F13\",\"severity\":\"info\",\"category\":\"safe_harbor\",\"ref\":\"\",\"message\":\"No prior-year tax given, so the safe-harbour floor (100% of last year's tax, 110% above USD 150,000 AGI) cannot be computed.\"}]}",
 "question": "I forgot to pay in September. What do I owe now, and who needs a 1099 this year?"
}

The lane-specific part of its pack, decoded:

{
 "estimate": {
  "months_of_books": 8,
  "ytd_net": 62000,
  "annual_net": 93000,
  "annual_basis": "projected: YTD USD 62,000 / 8 months x 12",
  "se_base": 85886,
  "se_tax": 13141,
  "se_note": "under the 2026 wage base of USD 184,500 - full 15.3% applies",
  "se_half": 6571,
  "adjusted_net": 86429,
  "federal_tax": 19014,
  "total_liability": 32155,
  "required_installment": 8039,
  "paid_by_quarter": [
   4000,
   4000,
   0,
   0
  ],
  "paid_total": 8000,
  "missed": [
   {
    "q": "Q3",
    "due": "2026-09-15",
    "due_long": "September 15, 2026",
    "amount": 8039
   }
  ],
  "quarters_ahead": 1,
  "remaining": 16116,
  "next_quarter": "Q4",
  "payment_due": 16116,
  "due_date": "2027-01-15",
  "due_with_return": false,
  "single_filer_bracket_pct": 22,
  "safe_harbor": null,
  "wage_base_hit": false
 },
 "prescan_flags": [
  {
   "id": "F1",
   "severity": "medium",
   "category": "uncategorized",
   "ref": "R26,R38,R51",
   "message": "3 transactions are uncategorized, totalling USD 2,270. Left out of net profit; at the 34.58% combined marginal rate they could move the annual estimate by about USD 785 either way."
  },
  {
   "id": "F12",
   "severity": "high",
   "category": "missed_quarter",
   "ref": "",
   "message": "Q3 was due September 15, 2026 and no payment is recorded against it. Shown as its own catch-up line; penalty and interest are for the accountant."
  },
  {
   "id": "F13",
   "severity": "info",
   "category": "safe_harbor",
   "ref": "",
   "message": "No prior-year tax given, so the safe-harbour floor (100% of last year's tax, 110% above USD 150,000 AGI) cannot be computed."
  }
 ]
}

5. Run it, then poll

POST /run returns a job_id; poll GET /jobs/{id} until it is terminal. Send an Idempotency-Key built from the lane and the input, such as tax-prep-desk:quarterly:3f9a0c2e71b4d58a:a1, so a retried request returns the same job instead of billing a second run.

# Always send an Idempotency-Key derived from the lane and the input. A retried
# request with the same key returns the SAME job instead of billing a second run.
KEY="tax-prep-desk:$LANE:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"

JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

while :; do
  OUT=$(call "jobs/$JOB")
  STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
  sleep 2
done

# {"ok":true,"data":{"job_id":"job_...","status":"succeeded",
#   "output":{"output":"{\"lane\":\"quarterly\",\"headline\":\"Q4 2026 estimate: USD 16,116, due January 15, 2027 ...\", ...}"},
#   "charged_credits":...,"truncated":false}}
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' > reply.json

6. Or stream it

# Server-sent events. `delta` events carry chunks of the reply; `done` carries the
# status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -H "Accept: text/event-stream" \
  -d "$INPUT"

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"lane\":\"quarterly\",\"headline\":\"Q4 2026 estimate: USD 16,116"}
# event: done   {"status":"succeeded","charged_credits":...,"truncated":false}

7. Parse the reply

The reply is one JSON object in data.output.output. Strip anything outside the outermost braces, then branch on lane.

# reply.json holds data.output.output from step 5. Strip any fence, keep the object:
python3 - <<'EOF'
import json
t = open("reply.json").read()
r = json.loads(t[t.index("{"):t.rindex("}") + 1])
print(r.get("lane"), "-", r["headline"])
if r.get("lane") == "ready":
    print("verdict:", r["verdict"], "mode:", r["mode"])
    for o in r["open_items"]:
        print(o["ref"], "|", o["issue"], "->", o["fix"])
    print(r["gate_statement"])
elif r.get("lane") == "nec":
    for c in r["candidates"]:
        print(c["ref"], c["form"], c["total"], c["w9"], "|", c["note"])
    for w in r["w9_requests"]:
        print("W-9", w["ref"], "|", w["text"])
else:
    print(r["quarter"], r["payment_due"], "due", r["due_date"])
    for c in r["catch_up"]:
        print("catch-up", c["quarter"], c["amount"], "was due", c["due_date"])
EOF

Invariants worth asserting

The web page holds every reply to the browser's arithmetic before it shows it. Do the same:

# assert-reply.py - the core checks, for body.json and reply.json from the steps above.
import json
body = json.load(open("body.json")); pack = json.loads(body["pack"])
t = open("reply.json").read(); r = json.loads(t[t.index("{"):t.rindex("}") + 1])
assert r["lane"] == body["task"] and body["tax_year"] in r["headline"]
ids = [p["id"] for p in r["prescan_responses"]]
assert sorted(ids) == sorted(f["id"] for f in pack["prescan_flags"]), "every flag answered once"
if r["lane"] == "ready":
    order = ["ready", "proceed_with_assumptions", "close_first"]
    assert order.index(r["verdict"]) >= order.index(pack["readiness"]["verdict_hint"])
elif r["lane"] == "nec":
    want = {p["id"]: p for p in pack["payees"] if p["status"] in ("file", "near")}
    assert sorted(c["ref"] for c in r["candidates"]) == sorted(want)
    for c in r["candidates"]:
        assert abs(c["total"] - want[c["ref"]]["basis"]) <= 1 and c["w9"] == want[c["ref"]]["w9"]
else:
    e = pack["estimate"]
    if "blocked" not in e:
        assert abs(r["payment_due"] - e["payment_due"]) <= 1
        assert r["due_date"] == e["due_date"] and r["quarter"] == e["next_quarter"]
        assert sorted((c["quarter"], round(c["amount"]), c["due_date"]) for c in r["catch_up"]) == \
               sorted((m["q"], round(m["amount"]), m["due"]) for m in e["missed"])

Handing the books check on

To build the 1099 list or the estimate on the books check, send the ready reply's handoff as readiness in the nec or quarterly body - exactly what the page carries forward after a ready run: JSON.stringify({verdict, gate_statement, open_items: open_items.map(o => o.ref).join("; "), mode}). When the verdict is proceed_with_assumptions or close_first, the reply puts the gate statement into assumptions (the owner chose to proceed on books that are not settled) and says so once in summary. The ready lane ignores it. For the Rivera books check below, the field is:

{
 "readiness": "{\"verdict\":\"proceed_with_assumptions\",\"gate_statement\":\"Three transactions are still uncategorized, totalling USD 2,270. Until those are coded, your estimate could be off by roughly USD 785 either way. Categorize them, or proceed and it becomes a stated assumption.\",\"open_items\":\"F1,R26,R38,R51; F2,R43,R44\",\"mode\":\"quarterly\"}"
}

which, decoded, is:

{
 "verdict": "proceed_with_assumptions",
 "gate_statement": "Three transactions are still uncategorized, totalling USD 2,270. Until those are coded, your estimate could be off by roughly USD 785 either way. Categorize them, or proceed and it becomes a stated assumption.",
 "open_items": "F1,R26,R38,R51; F2,R43,R44",
 "mode": "quarterly"
}

With the make-body.js above: save the decoded object as readiness.json and run node make-body.js quarterly readiness.json.

The output contract

Every key shown is always present. Arrays may be empty; strings are "" only where noted. An enum is written "a|b|c": the reply carries exactly one of the values. Text fields are plain prose - no Markdown, no bullets, money as USD 12,345.

ready

{"lane":"ready","verdict":"ready|proceed_with_assumptions|close_first","headline":"...","summary":"...",
 "open_items":[{"ref":"F2,R43,R44","issue":"...","effect":"...","fix":"..."}],
 "gate_statement":"...","basis_statement":"...","mode":"quarterly|nec|both","mode_reason":"...",
 "assumptions":["..."],"accountant_checklist":["..."],
 "prescan_responses":[{"id":"F1","status":"confirmed|dismissed","reason":"..."}]}
keytypenotes
lanestring"ready".
verdictenumready, proceed_with_assumptions, close_first; the browser's verdict_hint or one step stricter.
headline, summarystringOne sentence with the year and verdict; 2-3 sentences, answering question.
open_itemsarray of objectsref (flag id, plus row ids), issue, effect, fix - all strings. One per high or medium flag.
gate_statementstringThe sentence the owner reads before any number; says nothing is open when ready.
basis_statementstringWhat the packet is built on; for close_first, which months to close.
mode, mode_reasonenum, stringquarterly, nec or both, and why (which deadline comes first).
assumptions, accountant_checklistarray of strings3-8 each.
prescan_responsesarray of objectsid, status (confirmed or dismissed), reason. One per flag.

nec

{"lane":"nec","headline":"...","summary":"...",
 "candidates":[{"ref":"V1","form":"1099-NEC|1099-MISC|none|accountant_decides","total":8800,"w9":"on_file|missing|unknown","note":"..."}],
 "excluded":[{"ref":"V9","reason":"..."}],"w9_requests":[{"ref":"V5","text":"..."}],
 "duplicate_reviews":[{"ref":"D1","text":"..."}],"corporate_reviews":[{"ref":"V3","text":"..."}],
 "processor_note":"...","deadline_note":"...","assumptions":["..."],"accountant_checklist":["..."],
 "prescan_responses":[{"id":"F4","status":"confirmed|dismissed","reason":"..."}]}
keytypenotes
lanestring"nec".
headline, summarystringThe year, how many 1099s look required, how many W-9s block filing; 2-3 sentences.
candidatesarray of objectsref (string), form (enum 1099-NEC, 1099-MISC, none, accountant_decides), total (number: the payee's basis), w9 (enum on_file, missing, unknown, copied), note (string). In pack order.
excludedarray of objectsref, reason; one per sent exempt payee, [] if none.
w9_requests, duplicate_reviews, corporate_reviewsarrays of objectsref, text; one per id in w9_missing, per D#, per id in corporate_looking.
processor_notestringThe 1099-K overlap note, naming the payees; "" when no payee was paid by card or processor.
deadline_notestringThe filing deadline; whether it passed; whether the totals are year to date.
assumptions, accountant_checklist, prescan_responsesas in ready

quarterly

{"lane":"quarterly","headline":"...","summary":"...","quarter":"Q4","payment_due":16116,"due_date":"2027-01-15",
 "catch_up":[{"quarter":"Q3","amount":8039,"due_date":"2026-09-15","text":"..."}],
 "se_text":"...","federal_text":"...","safe_harbor_text":"...","basis_statement":"...",
 "assumptions":["..."],"accountant_checklist":["..."],
 "prescan_responses":[{"id":"F12","status":"confirmed|dismissed","reason":"..."}]}
keytypenotes
lanestring"quarterly".
headline, summarystring"<quarter> <year> estimate: USD X, due <date>."; 2-3 sentences with the basis and the biggest caveat.
quarterstringestimate.next_quarter (Q1-Q4); "" when blocked.
payment_duenumber or nullExactly estimate.payment_due; null when blocked.
due_datestringExactly estimate.due_date (YYYY-MM-DD); "" when blocked.
catch_uparray of objectsquarter (string), amount (number), due_date (string), text (late, accrues penalty and interest, the accountant computes them). One per missed quarter.
se_text, federal_text, safe_harbor_text, basis_statementstringThe SE step, the federal step (flags a bracket that differs from the assumed rate), the safe-harbour rule with the pack's floor, and the books the estimate is built on.
assumptions, accountant_checklist, prescan_responsesas in ready

Worked replies

Real model replies for the Rivera inputs above (lists shortened to match the abridged inputs; every value is as the model wrote it).

ready:

{
 "lane": "ready",
 "verdict": "proceed_with_assumptions",
 "headline": "Tax year 2026: Rivera Design Studio's books are close enough to build an estimate on, with two small bookkeeping items carried as stated assumptions.",
 "summary": "Through August 31, 2026 the books show USD 96,000 of income, USD 34,000 of expenses and USD 62,000 of net profit, with 3 uncategorized purchases totalling USD 2,270 and one possible double-posted FedEx charge still open. The September payment you missed is the Q3 installment that was due September 15, 2026; it is now late, the quarterly packet built from these books gives the amount, and any penalty and interest are for your accountant to compute. On 1099s, 5 of the 12 contractors you paid are already at or over the USD 2,000 threshold, and the final list is settled at year end for the February 1, 2027 deadline.",
 "open_items": [
  {
   "ref": "F1,R26,R38,R51",
   "issue": "Three purchases sit in Uncategorized Expense: Home Depot USD 640 on 2026-05-09, B&H Photo USD 1,150 on 2026-06-30 and Quillstead Print Shop (paid through Square) USD 480 on 2026-08-21, USD 2,270 in all.",
   "effect": "They are left out of net profit, so at the 34.58% combined marginal rate they could move the annual estimate by about USD 785 either way. The Quillstead Print Shop payment could also be a contractor payment that bears on the 1099 list.",
   "fix": "Code each row to a real account from the receipt: B&H Photo is likely equipment or supplies (if equipment, flag it so your accountant can decide how to depreciate or expense it), Home Depot to supplies or repairs, and Quillstead Print Shop to printing or outside services, noting that it was paid by card through Square."
  },
  {
   "ref": "F2,R43,R44",
   "issue": "Two FedEx charges of USD 95 each are posted on 2026-07-22 to Shipping and postage, same date, payee and amount.",
   "effect": "If one is a double posting, expenses are overstated by USD 95 and net profit is understated by USD 95, so the estimate would come out slightly low.",
   "fix": "Match both rows against the July bank or card statement; if only one FedEx charge of USD 95 cleared that day, delete the duplicate row, otherwise keep both and attach the second receipt."
  }
 ],
 "gate_statement": "Three transactions are still uncategorized, totalling USD 2,270. Until those are coded, your estimate could be off by roughly USD 785 either way. Categorize them, or proceed and it becomes a stated assumption.",
 "basis_statement": "Built on books closed through August 31, 2026, not the raw register.",
 "mode": "quarterly",
 "mode_reason": "The missed Q3 installment due September 15, 2026 is already late and Q4 is due January 15, 2027, so the quarterly packet comes first; the 1099 list is a year-end job for the February 1, 2027 deadline, and today's totals are only year to date.",
 "assumptions": [
  "The business is treated as a sole proprietorship, as entered; your accountant can change this if the structure is different.",
  "Federal tax is figured at the 22% default rate; your accountant sets the right bracket.",
  "The 3 uncategorized rows totalling USD 2,270 are left out of net profit until coded, a swing of about USD 785 on the annual estimate."
 ],
 "accountant_checklist": [
  "Confirm whether any 2026 estimated payments were made for Q1 (due 2026-04-15) or Q2 (due 2026-06-15), and compute penalty and interest on the missed Q3 installment due 2026-09-15.",
  "Decide the bracket to use in place of the 22% assumed rate.",
  "Decide how the B&H Photo purchase of USD 1,150 (R38) is treated once it is coded, if it turns out to be equipment."
 ],
 "prescan_responses": [
  {
   "id": "F1",
   "status": "confirmed",
   "reason": "USD 2,270 of uncategorized spending moves the estimate by about USD 785 and is carried as an open item and an assumption."
  },
  {
   "id": "F2",
   "status": "confirmed",
   "reason": "The R43 and R44 FedEx pair may double-count USD 95 of expense and needs a statement check."
  },
  {
   "id": "F3",
   "status": "confirmed",
   "reason": "The packet relies on the 2026 figures, so the need to confirm them at irs.gov is stated once in the assumptions."
  }
 ]
}

nec:

{
 "lane": "nec",
 "headline": "Tax year 2026: five payees are at or over the USD 2,000 threshold so far and look like 1099 candidates, and one missing W-9 (Pellucid Owl Photo) blocks filing.",
 "summary": "Through August 31, 2026 you paid 12 contractors: 5 are at or over the USD 2,000 threshold (one rent form, four for services, two of which your accountant must rule on) and 2 more sit just under it, and Pellucid Owl Photo still owes you a W-9 before the February 1, 2027 filing deadline. The books stop at August 31, so these are year-to-date totals and more payees may cross the line by December 31. On your question: the Q3 estimated payment was due September 15, 2026, so it is now late; this packet is the 1099 list only, so the amount owed comes from the app's quarterly packet, and any penalty and interest are for your accountant to compute.",
 "candidates": [
  {
   "ref": "V1",
   "form": "1099-MISC",
   "total": 8800,
   "w9": "on_file",
   "note": "USD 8,800 of studio rent paid by check to Quorvale Studios LLC over 8 payments is reported as rent on a 1099-MISC; as an LLC it is reportable unless its W-9 shows it is taxed as a corporation, which the accountant confirms from the W-9 on file."
  },
  {
   "ref": "V5",
   "form": "accountant_decides",
   "total": 2400,
   "w9": "missing",
   "note": "USD 2,400 of contract labor would be a 1099-NEC, but both payments went through PayPal, which may issue its own 1099-K, so the accountant decides whether you file one too; the W-9 is missing either way (F4)."
  },
  {
   "ref": "V6",
   "form": "none",
   "total": 1500,
   "w9": "missing",
   "note": "USD 1,500 of contract labor in one Venmo payment is under the USD 2,000 threshold, so no form for now; check that no payment to Dana Reyes was missed or paid another way, since more work before December 31 could cross the line."
  }
 ],
 "excluded": [],
 "w9_requests": [
  {
   "ref": "V5",
   "text": "Ask Pellucid Owl Photo for a completed W-9 before filing, showing its full legal name and tax classification: you paid USD 2,400 for photography work through PayPal between May 14 and August 13, 2026, and a 1099-NEC may be due by February 1, 2027."
  }
 ],
 "duplicate_reviews": [
  {
   "ref": "D2",
   "text": "Bob Nguyen (V8) and Robert Nguyen (V11) may be the same person, since Bob is a common short form of Robert. Combined USD 2,150, which is over the USD 2,000 threshold together and under it apart. If they are one payee, a 1099-NEC and a W-9 are likely needed; if they are two people, neither needs a form on current totals. Confirm before filing; they are not merged."
  }
 ],
 "corporate_reviews": [
  {
   "ref": "V3",
   "text": "Zentravo Web Works Inc. received USD 6,200 for professional services in 2 ACH payments. The name looks like a corporation, which is generally exempt from 1099-NEC reporting; the accountant checks the tax classification on the W-9 on file and decides whether to file."
  }
 ],
 "processor_note": "Pellucid Owl Photo (V5, PayPal), Dana Reyes (V6, Venmo) and Google Ads (V7, credit card) were paid entirely by card or a payment processor. The processor or card network may issue its own 1099-K for these payments; the accountant decides whether a 1099-NEC is also needed, which matters now only for Pellucid Owl Photo at USD 2,400.",
 "deadline_note": "1099-NEC and 1099-MISC forms for 2026 are due February 1, 2027, and that date has not passed. The books run only through August 31, 2026, so every figure here is a year-to-date total; the list needs rechecking once December is closed.",
 "assumptions": [
  "The USD 2,000 1099-NEC and 1099-MISC threshold, the USD 1,333 near-threshold floor and the February 1, 2027 due date are 2026 figures that must be confirmed at irs.gov before relying on them.",
  "Totals are year-to-date through August 31, 2026; payees under the threshold now may cross it by December 31.",
  "Payees are kept separate exactly as named in the ledger; the two possible duplicates (D1, D2) are not merged."
 ],
 "accountant_checklist": [
  "Collect a W-9 from Pellucid Owl Photo (V5) before February 1, 2027.",
  "Decide whether Zentravo Web Works Inc. (V3) is exempt as a corporation, using the classification on its W-9.",
  "Decide whether a 1099-NEC is needed for Pellucid Owl Photo (V5) alongside any 1099-K from PayPal."
 ],
 "prescan_responses": [
  {
   "id": "F2",
   "status": "confirmed",
   "reason": "Rows R43 and R44 may be one payment posted twice; if either is a contractor or rent payment, that payee's total is overstated until it is fixed."
  },
  {
   "id": "F4",
   "status": "confirmed",
   "reason": "Pellucid Owl Photo (V5) is at USD 2,400 with no W-9, covered in w9_requests."
  },
  {
   "id": "F6",
   "status": "confirmed",
   "reason": "Bob and Robert Nguyen (V8, V11) are covered in D2; together they cross the threshold, apart they do not."
  },
  {
   "id": "F9",
   "status": "confirmed",
   "reason": "V5, V6 and V7 were paid by card or processor; covered in processor_note, and it decides the form for V5."
  },
  {
   "id": "F10",
   "status": "confirmed",
   "reason": "The books end August 31, 2026, so these are year-to-date totals and the near payees V6 and V7 could still cross."
  }
 ]
}

quarterly:

{
 "lane": "quarterly",
 "headline": "Q4 2026 estimate: USD 16,116, due January 15, 2027, plus the missed Q3 installment of USD 8,039 that was due September 15, 2026.",
 "summary": "The missed September payment is its own catch-up line of USD 8,039, and the Q4 installment is USD 16,116, so the two together come to USD 24,155 before any penalty or interest, which your accountant computes. Both figures are built on a year-to-date net of USD 62,000 through August 31, 2026, projected over 12 months to USD 93,000, for a total 2026 liability of USD 32,155 against USD 8,000 paid so far. Who needs a 1099 is a separate year-end check in the 1099 lane, due February 1, 2027 (2027-02-01); the biggest caveat here is a possible double posting (R43, R44) and USD 2,270 of uncategorized items that could move the annual estimate by about USD 785 either way.",
 "quarter": "Q4",
 "payment_due": 16116,
 "due_date": "2027-01-15",
 "catch_up": [
  {
   "quarter": "Q3",
   "amount": 8039,
   "due_date": "2026-09-15",
   "text": "The Q3 installment of USD 8,039 was due September 15, 2026 and no payment is recorded against it, so it is late. It accrues penalty and interest from its own due date until it is paid; your accountant computes those amounts, and paying it now, separately from the Q4 installment, stops further accrual on this line."
  }
 ],
 "se_text": "Self-employment tax is figured on an SE base of USD 85,886 from the projected USD 93,000 net, which is under the 2026 wage base of USD 184,500, so the full 15.3% applies and gives USD 13,141. Half of that, USD 6,571, is deducted before income tax.",
 "federal_text": "After the deductible SE half, adjusted net is USD 86,429; at the assumed 22% federal rate that is USD 19,014 of income tax, for a total 2026 liability of USD 32,155 and a required installment of USD 8,039 per quarter. The 22% assumption matches the single-filer bracket the browser found for this income, but your accountant confirms the right bracket once filing status and deductions are known.",
 "safe_harbor_text": "The safe-harbour rule protects you from the underpayment penalty if your 2026 payments reach 100% of last year's total tax (110% if last year's AGI was over USD 150,000) or 90% of this year's tax. No prior-year tax was given, so the floor cannot be computed; your accountant needs your 2025 return to check whether a lower payment would be safe.",
 "basis_statement": "Built on books closed through August 31, 2026, not the raw register; 3 uncategorized transactions totalling USD 2,270 and USD 12,000 of owner draws are left out of net profit.",
 "assumptions": [
  "Federal income tax is figured at the assumed 22% rate; your accountant can change it once filing status, other household income and deductions are known.",
  "The business is treated as a sole proprietorship reporting on Schedule C, as assumed from the owner's description, not confirmed.",
  "Annual net is projected from 8 months of books: USD 62,000 year to date divided by 8 and multiplied by 12, giving USD 93,000; a slower or busier last four months changes every figure.",
  "State and local income taxes are not included; this is the federal estimate only.",
  "The deductible half of self-employment tax, USD 6,571, is applied before income tax.",
  "No other deductions are applied: QBI, retirement contributions, home office, vehicle, depreciation and self-employed health insurance would all lower the estimate if your accountant confirms them.",
  "2026 figures as published by the IRS and SSA for 2026 are used, including the USD 184,500 wage base; confirm each figure at irs.gov before relying on it.",
  "The 3 uncategorized transactions (USD 2,270) are left out, and the rows R43 and R44 are counted as posted, both of which your accountant may adjust."
 ],
 "accountant_checklist": [
  "Compute the penalty and interest on the missed Q3 installment of USD 8,039 due September 15, 2026, and on the Q1 and Q2 payments of USD 4,000 each against a required USD 8,039.",
  "Get the 2025 return to test the safe-harbour floor before the Q4 payment of USD 16,116 due January 15, 2027.",
  "Confirm the 22% bracket and whether QBI, a SEP-IRA or other retirement contribution, home office, vehicle or self-employed health insurance deductions apply."
 ],
 "prescan_responses": [
  {
   "id": "F1",
   "status": "confirmed",
   "reason": "Rows R26, R38 and R51 (USD 2,270) are left out of net profit and could move the annual estimate by about USD 785 either way; they are listed as an assumption and a checklist item."
  },
  {
   "id": "F12",
   "status": "confirmed",
   "reason": "Q3 had no payment by September 15, 2026 and is shown as its own catch-up line of USD 8,039; penalty and interest are left to the accountant."
  },
  {
   "id": "F13",
   "status": "confirmed",
   "reason": "Without last year's tax the safe-harbour floor cannot be computed, so the accountant needs the 2025 return to check it."
  }
 ]
}

Truncation and partial results

When the balance sits between min_credits and hold_credits, the run is not refused: it executes with a reduced output cap and returns truncated: true. What you hold is a prefix of the reply. The web page closes the cut-off JSON (Recon.closeJson), shows the sections that arrived and says how many it recovered - out of ten for a books check, twelve for a 1099 list and thirteen for an estimate summary. A truncated 1099 list can be missing candidates and a truncated estimate its catch-up lines, so check the flag before treating a reply as complete, then resubmit with the attempt suffix on the Idempotency-Key incremented (tax-prep-desk:nec:<hash>:a2).