šŸŽ‰ 50% off Pluto Suite Pro for your first 3 months Ā· Get the launch price šŸŽ‰
Why Pluto Suite Features How it works Pricing
Resources
Help
Start free → Log in

Developer reference for the Pluto Suite public API (API keys + webhooks). API keys are managed in the dashboard at Settings → Developers (Admin+).

Base URL: https://api.plutosuite.com. All request/response bodies are JSON. See the Endpoint reference below for per-resource fields.

Conventions

These hold across every endpoint below, read once, then skip to the resource you need.

  • Money is always integer cents. 1050 means $10.50. There are no decimal-dollar fields.
  • Timestamps are Unix epoch seconds (integers), e.g. 1720051200, not milliseconds, not ISO strings. (The one exception: trip startedAt/endedAt also tolerate millisecond values and normalize them.) Webhook delivery envelopes use ISO-8601 (see Webhooks).
  • IDs are opaque strings, don't parse them.
  • Unknown request fields are silently ignored (the server validates against a fixed schema and drops anything else), so a typo'd field name = silent no-op, not an error.
  • List responses wrap the array in a key: most are { "items": [...] }, but customers return { "customers": [...] } and vendors { "vendors": [...] }. Several also include a top-level count. Every list item carries id and createdAt.
  • Errors are { "error": "<code>", "msg"?: "<human message>" } with a matching HTTP status (400 bad input, 402 plan limit, 403 scope/role, 404 not found, 409 duplicate, 422 validation, 429 rate/plan cap).

Authentication

Create an API key in plutosuite.com → Developers. Keys look like plt_<48 hex> and are shown once, only a SHA-256 hash is stored.

curl https://api.plutosuite.com/api/v1/invoices \
  -H "Authorization: Bearer plt_your_key_here"
  • The key is business-scoped, no x-business-id header needed (it's ignored).
  • Scopes: read allows GET/HEAD; write additionally allows POST/PATCH/DELETE. A read key calling POST gets 403 insufficient_scope.
  • Writes act as the user who created the key (audit trail shows them).
  • No 2FA and no Firebase session, the key is the whole credential. Treat it like a password; revoke immediately if leaked (revocation is instant, ≤60s cache).
  • Limit: 10 active keys per business. Rate limit: shared 300 req/min per IP.

Allowed surface

API keys are confined to business-data endpoints (allowlist enforced server-side; anything else returns 403 path_not_allowed):

/api/v1/invoices            /api/v1/recurring_invoices
/api/v1/customers           /api/v1/vendors
/api/v1/bills               /api/v1/receipts
/api/v1/transactions        /api/v1/trips
/api/v1/trip_tags           /api/v1/jobs
/api/v1/tax_rates           /api/v1/categories
/api/v1/reports/*           /api/v1/daily_close
/api/v1/webhooks

Never available to keys: billing, team management, payroll, settings, security, diagnostics. Role mapping inside the surface: write keys act as ADMIN, read keys as ACCOUNTANT, endpoint-level RBAC still applies on top.

/api/v1/webhooks (create/list/update/delete/test subscriptions) is reachable so integrations like Zapier/Make can self-manage their own webhook subscriptions with the key. It's gated by requireRole('ADMIN'), so only a write key (→ ADMIN) can manage hooks; a read key (→ ACCOUNTANT) is role-blocked here.

Polling list endpoints

For pull-based integrations (Zapier polling triggers, cron jobs), every list endpoint above accepts an optional, stable sort so you can reliably page newest-first:

  • ?sort=created, order by immutable createdAt (default when this param is present).
  • ?order=asc | ?order=desc, direction (default desc, i.e. newest first).
  • Existing limit/offset (or endpoint-specific) pagination still applies.

Omit sort and each endpoint keeps its human-facing default order (e.g. invoices by issue date, customers by name). Always dedupe on the item id, deliveries and polls are at-least-once. Every list item carries id and createdAt.

Resources

Each resource supports GET /api/v1/<resource> to list (paginated, poll-friendly, see above) and POST /api/v1/<resource> to create. GET needs a read (or write) key; POST needs a write key.

ResourceCreate, key fields (money in cents, times in epoch seconds)
invoicescustomerName, customerEmail, type (invoice | estimate), status (draft | sent, sent issues it to the ledger), items[] { description, quantity, unitCents }, dueDate, notes. Requires a Pro plan.
billsvendorName or vendorId, status (open | draft), items[], issueDate, dueDate, notes.
customersname (required), email, phone, address, paymentTermsDays, taxExempt, currency.
vendorsname (required), email, phone, address, paymentTermsDays, taxNumber.
transactionsamountCents (required), direction (EXPENSE | INCOME), description, category, occurredAt.
tripsstartedAt (required), endedAt, distanceMeters (12.4 km → 12400), classification (business | personal), purpose.

Lookups for "find or create": customers/vendors accept ?search=<name or email>; invoices accept ?number=<invoice number>.

Example, create an invoice

curl -X POST https://api.plutosuite.com/api/v1/invoices \
  -H "Authorization: Bearer plt_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "customerName": "Acme Co",
    "customerEmail": "ap@acme.example",
    "status": "sent",
    "items": [{ "description": "Consulting", "quantity": 3, "unitCents": 15000 }]
  }'
{ "ok": true, "id": "cl9x…", "invoiceNumber": "INV-0001" }

Create responses return the new record (or, for invoices, { ok, id, invoiceNumber }, re-fetch via GET /api/v1/invoices?number=INV-0001 for the full object).

Idempotency on create

Every POST accepts an optional clientRequestId (any stable unique string, one per logical create). If a create succeeds but the response is lost and you retry with the same clientRequestId, the server returns the original record instead of a duplicate, a 409 whose body carries existingId. Reuse that id rather than treating it as an error.

Webhooks

Create a webhook in Developers with a public https:// endpoint and a set of events. The signing secret (whsec_…) is shown once.

Events (v1), payloads are a stable, additive-only contract: fields may be added, never removed or repurposed. Every payload carries the keys listed below (nullable where noted).

EventFires whenPayload data
invoice.createdAny invoice is created (including drafts)invoiceId, invoiceNumber, customerName, customerEmail, totalCents, currency, type
invoice.sentAn invoice/estimate is emailed or marked sentinvoiceId, invoiceNumber, customerName, customerEmail, totalCents, currency, type, emailed
invoice.paidAn invoice settles, manual mark-paid, partial payments reaching zero balance, Stripe checkout, or an auto-matched e-TransferinvoiceId, invoiceNumber, customerName, totalCents, currency, paidMethod
estimate.acceptedAn estimate is accepted (customer-facing accept link, or marked accepted internally)invoiceId, invoiceNumber, customerName, totalCents, currency, type=estimate
customer.createdA customer is createdcustomerId, name, email
vendor.createdA vendor is createdvendorId, name, email
bill.createdA vendor bill is createdbillId, vendorName, totalCents, currency, status
bill.paidA bill reaches paid-in-fullbillId, vendorName, totalCents, currency, status=paid
receipt.parsedOCR extracts usable fields from a new receipt (any intake path: app, email, SMS)receiptId, merchant, amountCents, occurredAt, category, potentialDuplicateId
transaction.createdA single transaction is created via app/API (NOT bulk bank/CSV/Shopify imports, those are suppressed to avoid webhook storms)transactionId, description, amountCents, direction, category, reviewed
transaction.categorizedA transaction is reviewed/categorized (transitions to reviewed)transactionId, description, amountCents, direction, category, reviewed=true
trip.completedA mileage trip is finalizedtripId, distanceKm, classification, startedAt
daily_close.completedThe nightly 3 AM close finishes for your business (fires every night, even quiet ones)autoCategorized, autoReviewed, pending{...}, closedThrough

Test delivery, POST /api/v1/webhooks/:id/test { event? } sends a synthetic payload (ids prefixed test_, data.test = true) to that subscription so you can validate your endpoint + signature check without waiting for a real event. Defaults to the subscription's first subscribed event. Returns { ok, event, deliveredStatus }.

Delivery format, POST, JSON body:

{
  "id": "6f0c…",              // unique delivery id
  "event": "invoice.paid",
  "createdAt": "2026-07-04T21:00:00.000Z",
  "data": { ... }
}

Verify the signature (always do this):

const crypto = require('node:crypto')
const expected = 'sha256=' + crypto.createHmac('sha256', WHSEC).update(rawBody).digest('hex')
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-pluto-signature']))

Semantics: at-least-once, fire-and-forget, 5-second timeout, no automatic retries in v1 (a failed delivery increments the visible failure counter, poll the API to reconcile if you miss one). Respond 2xx quickly; do slow work async. URLs must be public https (no localhost/private ranges).

MCP server

An MCP (Model Context Protocol) server over this API is available, a local stdio server that authenticates with a plt_ key and lets Claude/Cursor/ChatGPT query and act on the books (list_invoices, get_report, create_invoice, …). Contact us at support@plutosuite.com to get set up.