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.

Base URL
https://deducr.qityol.com/api/v1

Amounts 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.

Every request
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

FieldTypeRequiredDescription
records:readscopeNoList, fetch, and summarize financial records.
records:writescopeNoCreate financial records.
webhooks:managescopeNoRegister, list, update, and delete webhook endpoints.

New keys carry all three scopes. Verify a key and see its organization with GET /v1/organization:

Response
{
  "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.com

Idempotency

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 envelope
{
  "error": {
    "code": "invalid_request",
    "message": "One or more records failed validation.",
    "details": { "errors": [{ "index": 2, "message": "occurred_on is required (YYYY-MM-DD)" }] }
  }
}
FieldTypeRequiredDescription
unauthorizedhttp 401NoMissing, malformed, or revoked API key.
forbiddenhttp 403NoKey is valid but lacks the required scope.
not_foundhttp 404NoResource does not exist in your organization.
invalid_requesthttp 400NoValidation failed; details lists field errors.
rate_limitedhttp 429NoToo many requests — back off and retry.
internal_errorhttp 500NoSomething 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).

POST/v1/records

Create one record (send the object directly) or up to 500 (send { "records": [ … ] }). Requires records:write and X-Deducr-Source.

FieldTypeRequiredDescription
record_typeenum (income|expense|payroll|tax)YesThe kind of financial event.
external_idstringYesThe record's ID in your system — the idempotency key.
amountdecimalYesNon-negative, in dollars. Direction comes from record_type.
occurred_ondate (YYYY-MM-DD)YesThe date it happened.
categorystringNoYour categorization, e.g. "SaaS Subscription". Deducr's AI fills gaps.
descriptionstringNoHuman-readable summary.
counterpartystringNoWho paid / was paid, e.g. "Example Corp", "Contoso Supplies".
currencystring (ISO 4217)NoDefaults to USD.
metadataobjectNoAny JSON you want echoed back (invoice numbers, customer IDs…).
Request — batch export from a billing system
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"
      }
    ]
  }'
Response — 201 Created
{
  "created": [ { "id": "rec_…", "record_type": "income", "external_id": "inv_9f2b1c", … } ],
  "duplicates": ["payrun_2026_06_06"],
  "summary": { "received": 2, "created": 1, "duplicates": 1 }
}
GET/v1/records

Lists 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.

GET/v1/records/{id}

Fetches a single record by its Deducr ID.

Summary & Reports

GET/v1/records/summary

Aggregated 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.

GET /v1/records/summary?from=2026-01-01&to=2026-06-30
{
  "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.

POST/v1/webhook-endpoints

Body: { "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.

GET/v1/webhook-endpoints
PATCH/v1/webhook-endpoints/{id}
DELETE/v1/webhook-endpoints/{id}

Delivery format

Headers + body of each delivery
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:

Node.js / Next.js route handler
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

FieldTypeRequiredDescription
record.createdliveNoA financial record was ingested. data.record holds the full record.
invoice.createdreservedNoComing with Stripe payments: an invoice/payment link was created.
invoice.paidreservedNoComing with Stripe payments: a payer completed payment.
payment.failedreservedNoComing with Stripe payments: a payment attempt failed.
payroll.completedreservedNoComing with embedded payroll: a payroll run settled.
bank.transaction.reconciledreservedNoComing 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:

  1. Create an organization and API key at /developers/keys, store the key in your server environment.
  2. Verify connectivity with GET /v1/organization.
  3. Push your financial history with batched POST /v1/records — idempotency makes re-runs safe. Then push new records as they happen (or nightly).
  4. 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:

FieldTypeRequiredDescription
lib/deducr/client.tstyped API clientNoWraps fetch calls to /v1/organization, /v1/records, and /v1/records/summary behind typed functions.
lib/deducr/actions.tsserver action / jobNoBatches 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.tsroute handlerNoVerifies 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), with invoice.paid webhooks 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.