Deducr Platform API
Deducr is the financial brain behind your business software. Push financial records — income, expenses, payroll runs, tax payments — from any system, and Deducr keeps the books: categorization, bank reconciliation against your connected Plaid accounts, and tax-ready reports. Built for multi-business owners — plug in a billing system, a marketplace, or a vertical SaaS product and let Deducr keep the books.
Overview
The API is REST over HTTPS with JSON bodies. All endpoints live under a versioned base path — breaking changes will only ever ship under a new version prefix.
https://deducr.qityol.com/api/v1Amounts are decimal dollars (e.g. 2800.00), dates are YYYY-MM-DD, and timestamps are ISO 8601 UTC. Every resource belongs to exactly one organization — the business whose books Deducr keeps. Your API key determines which organization you act on; there is no cross-organization access.
Authentication
Create keys at /developers/keys. Keys look like dk_live_…, are shown in full exactly once, and are stored hashed — treat them like passwords, server-side only, never in browser code or mobile apps.
curl https://deducr.qityol.com/api/v1/organization \
-H "Authorization: Bearer dk_live_YOUR_API_KEY"Swap dk_live_YOUR_API_KEY for the key you copied from /developers/keys — every other example on this page uses the same placeholder.
Scopes
| Field | Type | Required | Description |
|---|---|---|---|
| records:read | scope | No | List, fetch, and summarize financial records. |
| records:write | scope | No | Create financial records. |
| webhooks:manage | scope | No | Register, list, update, and delete webhook endpoints. |
New keys carry all three scopes. Verify a key and see its organization with GET /v1/organization:
{
"organization": {
"id": "6c1a…",
"name": "Example Inc.",
"created_at": "2026-07-15T18:00:00Z"
},
"scopes": ["records:read", "records:write", "webhooks:manage"]
}Testing before you go live
There is no separate sandbox mode — every key writes real records to its organization's books, and records can't be deleted via the API. Create a dedicated Test organization and key at /developers/keys to build and verify your integration, then switch to a production key once you're ready to send live data.
The Source Header
Every write request must carry X-Deducr-Source — a stable name for the system sending the data (we recommend its domain). The source is stored on each record, namespaces your external IDs, and lets one organization ingest from several systems without collisions.
X-Deducr-Source: billing.example.comIdempotency
Record creation is idempotent per (organization, source, external_id). Set external_idto the record's primary key in your system (an invoice UUID, a payroll run ID). Resubmitting the same record never duplicates it — the API reports it under duplicates instead. This means you can safely retry failed requests and re-export your entire history at any time.
Errors
Errors use conventional HTTP status codes and a stable envelope:
{
"error": {
"code": "invalid_request",
"message": "One or more records failed validation.",
"details": { "errors": [{ "index": 2, "message": "occurred_on is required (YYYY-MM-DD)" }] }
}
}| Field | Type | Required | Description |
|---|---|---|---|
| unauthorized | http 401 | No | Missing, malformed, or revoked API key. |
| forbidden | http 403 | No | Key is valid but lacks the required scope. |
| not_found | http 404 | No | Resource does not exist in your organization. |
| invalid_request | http 400 | No | Validation failed; details lists field errors. |
| rate_limited | http 429 | No | Too many requests — back off and retry. |
| internal_error | http 500 | No | Something failed on our side; safe to retry. |
Financial Records
A record is one financial event in your system. Four types: income (money earned), expense (money spent), payroll (wages paid), and tax (tax payments).
/v1/recordsCreate one record (send the object directly) or up to 500 (send { "records": [ … ] }). Requires records:write and X-Deducr-Source.
| Field | Type | Required | Description |
|---|---|---|---|
| record_type | enum (income|expense|payroll|tax) | Yes | The kind of financial event. |
| external_id | string | Yes | The record's ID in your system — the idempotency key. |
| amount | decimal | Yes | Non-negative, in dollars. Direction comes from record_type. |
| occurred_on | date (YYYY-MM-DD) | Yes | The date it happened. |
| category | string | No | Your categorization, e.g. "SaaS Subscription". Deducr's AI fills gaps. |
| description | string | No | Human-readable summary. |
| counterparty | string | No | Who paid / was paid, e.g. "Example Corp", "Contoso Supplies". |
| currency | string (ISO 4217) | No | Defaults to USD. |
| metadata | object | No | Any JSON you want echoed back (invoice numbers, customer IDs…). |
curl -X POST https://deducr.qityol.com/api/v1/records \
-H "Authorization: Bearer dk_live_YOUR_API_KEY" \
-H "X-Deducr-Source: billing.example.com" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"record_type": "income",
"external_id": "inv_9f2b1c",
"amount": 2800.00,
"occurred_on": "2026-06-01",
"category": "SaaS Subscription",
"description": "Invoice INV-2026-0051 — June Example Pro plan",
"counterparty": "Example Corp",
"metadata": { "invoice_status": "paid", "customer_id": "cus_18aa" }
},
{
"record_type": "payroll",
"external_id": "payrun_2026_06_06",
"amount": 6450.00,
"occurred_on": "2026-06-06",
"category": "Team Payroll",
"description": "Bi-weekly payroll — 12 employees"
}
]
}'{
"created": [ { "id": "rec_…", "record_type": "income", "external_id": "inv_9f2b1c", … } ],
"duplicates": ["payrun_2026_06_06"],
"summary": { "received": 2, "created": 1, "duplicates": 1 }
}/v1/recordsLists records, newest first. Requires records:read. Filters: record_type, source, from / to (bounds on occurred_on), limit(≤ 200). Pass the last record's id as starting_after to page; has_more tells you when to stop.
/v1/records/{id}Fetches a single record by its Deducr ID.
Summary & Reports
/v1/records/summaryAggregated totals for a date range — by type, by category, and by month — sized for rendering dashboards without pulling every record. Same from, to, source filters.
{
"record_count": 214,
"totals": {
"by_type": { "income": 140700.00, "expense": 31240.55, "payroll": 74520.00, "tax": 8912.00 },
"by_category": { "SaaS Subscription": 85200.00, "Consulting Income": 47100.00, … },
"by_month": { "2026-06": { "income": 23450.00, "expense": 5120.10, "payroll": 12420.00, "tax": 0 }, … },
"net": 26027.45
}
}Webhooks
Deducr pushes events to your endpoints instead of making you poll. Deliveries are signed, retried with exponential backoff (1m, 5m, 30m, 2h, 12h; 5 attempts), and carry the event envelope shown below. Return any 2xx quickly; do heavy work async.
Manage endpoints via the API below, or visually at /developers/keys — where organization admins can also view and copy each endpoint's signing secret at any time.
/v1/webhook-endpointsBody: { "url": "https://…", "event_types": ["record.created"] } (omit event_types to receive everything). The response includes the signing secret (whsec_…) — store it in your consumer's environment. Requires webhooks:manage.
/v1/webhook-endpoints/v1/webhook-endpoints/{id}/v1/webhook-endpoints/{id}Delivery format
POST <your url>
Content-Type: application/json
deducr-event-type: record.created
deducr-signature: t=1752602400,v1=5f8c1e…
{
"id": "evt_…",
"type": "record.created",
"created_at": "2026-07-15T18:00:00Z",
"data": { "record": { "id": "rec_…", "record_type": "income", … } }
}Verifying signatures
Recompute HMAC-SHA256 over `${t}.${rawBody}` with your endpoint secret, compare in constant time, and reject timestamps older than 5 minutes:
import { createHmac, timingSafeEqual } from "crypto";
export function verifyDeducrSignature(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Deliveries are at-least-once — rare retries after a slow 2xx can duplicate an event, so key your processing on the event id.
Event Reference
| Field | Type | Required | Description |
|---|---|---|---|
| record.created | live | No | A financial record was ingested. data.record holds the full record. |
| invoice.created | reserved | No | Coming with Stripe payments: an invoice/payment link was created. |
| invoice.paid | reserved | No | Coming with Stripe payments: a payer completed payment. |
| payment.failed | reserved | No | Coming with Stripe payments: a payment attempt failed. |
| payroll.completed | reserved | No | Coming with embedded payroll: a payroll run settled. |
| bank.transaction.reconciled | reserved | No | Coming with reconciliation: a Plaid transaction matched a record. |
Reserved names are published here so integrations can subscribe by name today and light up automatically as each capability ships.
Integration Guide
A complete integration is four steps:
- Create an organization and API key at /developers/keys, store the key in your server environment.
- Verify connectivity with
GET /v1/organization. - Push your financial history with batched
POST /v1/records— idempotency makes re-runs safe. Then push new records as they happen (or nightly). - Register a webhook endpoint and react to events instead of polling.
Suggested file structure (Next.js)
Production Deducr integrations tend to split into three small, testable pieces that map directly onto the steps above:
| Field | Type | Required | Description |
|---|---|---|---|
| lib/deducr/client.ts | typed API client | No | Wraps fetch calls to /v1/organization, /v1/records, and /v1/records/summary behind typed functions. |
| lib/deducr/actions.ts | server action / job | No | Batches your financial history into POST /v1/records calls — run once for backfill, then on a schedule or from your app's own write paths. |
| app/api/webhooks/deducr/route.ts | route handler | No | Verifies the deducr-signature header and reacts to event types instead of polling. |
This mirrors the natural shape of the problem — a thin typed client, a batching export job, and a signature-verified webhook receiver — so it's a reasonable default to start from rather than a strict requirement.
Roadmap
The platform grows in deliberate phases; everything above is live today (Phase 1).
- Phase 2 — Payments: Stripe-backed invoices and payment links (
POST /v1/invoices), withinvoice.paidwebhooks closing the loop. - Phase 3 — Reconciliation: automatic matching of Plaid bank transactions against ingested records, invoices, and claims.
- Phase 4 — Payroll: embedded payroll orchestration with journal entries and tax withholding handled by a licensed provider.
- Phase 5 — Statements & filing: P&L, balance sheet, quarterly estimates, and accountant-ready tax packages.
Questions or integration help: contact support. API version 2026-07-15.