API Reference
Bank statement analysis over HTTPS
Submit a PDF, receive structured revenue, fraud, and debt analysis via signed webhook. Generate credentials at app.clearstaq.com.
New to the API? Start with the conceptual guide: API & Webhooks overview.
Overview
Submit a bank-statement PDF to https://app.clearstaq.com/api/v1/analyses. The endpoint returns 202 Accepted with an analysis id; the full analysis result is delivered asynchronously to your callback_url as a signed webhook.
API access is available on StaqCore and above. Free-tier orgs cannot generate API keys.
Authentication
Every request must include a bearer token issued from your Integrations settings page. Keep both the cg_live_* key and the whsec_* webhook secret out of version control.
Authorization: Bearer cg_live_YOUR_KEY_HERE
API key scopes
Keys carry scoped permissions, so an integration can be granted only the access it needs. Each endpoint states the scope it requires; a request made with a key missing that scope receives 403 scope_required with required_scope and granted_scopes in the error body.
| Scope | Grants |
|---|---|
| analyses:write | Submit documents (POST /api/v1/analyses, POST /api/v1/clients), create businesses (POST /api/v1/businesses), and manage webhook subscriptions, except creating submission.completed subscriptions, which requires analyses:read. |
| analyses:read | Read analysis results, client profiles, and businesses (GET /api/v1/analyses/{id}, GET /api/v1/clients/{id}, GET /api/v1/clients/{id}/open-banking, GET /api/v1/businesses, GET /api/v1/businesses/{id}/submissions, GET /api/v1/people/{id}), list webhook subscriptions, and create submission.completed subscriptions (Available when the client portal launches). |
| applications:write · applications:read · documents:write | The applications API for lead sources: create applications, ask their owners to sign, read them, add their documents (see Applications API below), and subscribe to the application webhooks. |
| partner_responses:write | Only on a key an organization gives a partner: answer that organization's sends to the partner (POST /api/v1/partner-responses). |
OAuth 2.0 access tokens (used by the Zapier integration) are accepted on the same endpoints and follow the same scope checks.
Submit a statement
Multipart upload. Maximum size 25 MB. Only application/pdf is accepted.
document_kind is required. Allowed values are bank_statement, tax_return, and application_form. A missing or invalid value returns HTTP 400 with error invalid_document_kind.
ClearStaqAI Fraud Detection (Gemini visual second-pass) is opt-in. Pass clearstaq_ai_enabled=true on a submission to enable it for that request. Defaults to off; the deterministic metadata fraud score always runs.
curl -X POST https://app.clearstaq.com/api/v1/analyses \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \
-F "file=@./statement.pdf" \
-F "document_kind=bank_statement" \
-F "callback_url=https://your-app.example.com/hooks/clearstaq" \
-F "client_ref=customer-123" \
-F 'metadata={"loan_id":"LN-999"}' \
# Opt in to the ClearStaqAI visual fraud second-pass (default off):
# -F "clearstaq_ai_enabled=true"Receive the result
callback_url accepts HTTP or HTTPS without URL credentials. All resolved IP addresses must be public. Invalid or non-public targets receive 422 invalid_callback_url before the analysis is accepted. Webhook subscriptions continue to require HTTPS.
When the analysis completes, ClearStaq POSTs a signed JSON envelope to your callback_url. Respond with any 2xx status within 10s to acknowledge. Transient failures (5xx, network timeouts, 408, and 429) are retried after 1m, 1m, 5m, and 15m; the fifth attempt is marked dead after about 22m total. Other 4xx responses are permanent and are not retried.
POST /hooks/clearstaq HTTP/1.1
Content-Type: application/json
X-ClearStaq-Event: analysis.completed
X-ClearStaq-Delivery: 2f8b0...
X-ClearStaq-Signature: sha256=<hex hmac>
{
"id": "cb_...",
"event": "analysis.completed",
"client_ref": "customer-123",
"status": "completed",
"result": { "bank_name": "Chase", "true_revenue": 84250.12, ... }
}Poll an analysis
Without a callback_url, poll https://app.clearstaq.com/api/v1/analyses/{id}. A completed bank statement returns the analysis.completed envelope with status and result. A completed tax return or application form with persisted extraction rows returns its extraction envelope with event and data. A completed tax return with no extracted rows uses the legacy analysis.completed envelope with status and result; it does not fabricate data: []. If an application-form extraction row is unavailable, the terminal fallback is its extraction envelope with an empty data array.
curl https://app.clearstaq.com/api/v1/analyses/<analysis_id> \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" # For a completed bank statement, the response has status/result. # For a completed extraction with persisted rows, it has event/data. # An empty tax return uses the analysis.completed envelope.
Verify the signature
Compute HMAC-SHA256 over the raw request body using your webhook signing secret and compare (timing-safe) to the X-ClearStaq-Signature header.
import crypto from "node:crypto"
// In your webhook handler:
export default function handler(req, res) {
const signature = req.headers["x-clearstaq-signature"]
const raw = req.rawBody // the exact JSON bytes ClearStaq sent
const expected =
"sha256=" + crypto.createHmac("sha256", process.env.CLEARSTAQ_WEBHOOK_SECRET).update(raw).digest("hex")
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature ?? ""))
if (!ok) return res.status(400).send("bad signature")
// req.body is the WebhookEnvelope: act on it, then respond 2xx.
res.status(200).send("ok")
}Client profiles (multiple statements)
To analyze a client across several statements at once, POST 1–25 PDFs to https://app.clearstaq.com/api/v1/clients. This creates a ClearStaq client in your org (visible in your webapp), parses every statement, and computes an aggregated profile over the valid statements. Incomplete or non-bank statements are excluded from the profile and listed with a failure reason. Billing is per document (same credits as a single analysis), scaled by file count.
Repeat the file field for each PDF (or use files).document_kind is required and must be bank_statement for this client-profile endpoint. Use /api/v1/analyses for tax_return or application_form; missing or invalid values return HTTP 400 with error invalid_document_kind. Max 25 MB per file. client_name is optional; when omitted, the client is named from the dominant account holder once parsed. clearstaq_ai_enabled is opt-in, same as a single submission.
curl -X POST https://app.clearstaq.com/api/v1/clients \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \ -F "file=@./statement-jan.pdf" \ -F "file=@./statement-feb.pdf" \ -F "document_kind=bank_statement" \ -F "client_name=Acme LLC" \ # Optional: opt in to the ClearStaqAI visual fraud second-pass (default off): # -F "clearstaq_ai_enabled=true"
The endpoint returns 202 Accepted with a client_id, a batch_id, and per-document queue state.
{
"client_id": "8f1c...",
"batch_id": "2a4d...",
"documents": [
{ "id": "d1...", "filename": "statement-jan.pdf", "status": "queued" },
{ "id": "d2...", "filename": "statement-feb.pdf", "status": "queued" }
]
}Poll a client profile
Fetch https://app.clearstaq.com/api/v1/clients/{id} to read the aggregated profile and per-document statuses. Poll until counts.completed + counts.failed === counts.total and profile is non-null. The profile is computed asynchronously shortly after the documents finish parsing.
curl https://app.clearstaq.com/api/v1/clients/<client_id> \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" # Poll until counts.completed + counts.failed === counts.total # AND profile is non-null (the profile is computed asynchronously # shortly after the documents finish parsing).
{
"id": "8f1c...",
"name": "Acme LLC",
"status": "completed",
"profile": { "true_revenue": 168500.24, "fraud_score": 12, ... },
"documents": [
{
"id": "d1...",
"filename": "statement-jan.pdf",
"status": "completed",
"bank_name": "Chase",
"period_start": "2025-01-01",
"period_end": "2025-01-31",
"fraud_score": 8,
"failure_reason": null,
"pages_present": 6,
"pages_expected": 6,
"detected_bank": "Chase"
},
{
"id": "d2...",
"filename": "statement-feb.pdf",
"status": "failed",
"failure_reason": "missing_pages",
"pages_present": 3,
"pages_expected": 6
}
],
"counts": { "total": 2, "completed": 1, "failed": 1 }
}failure_reason values include missing_pages, poor_scan_quality (the scan is too poor to read; upload a clearer copy), not_a_bank_statement:<type>, or another reason string; null when the document parsed successfully.
Prefer webhooks? Subscribe to the batch.completed event (via POST /api/v1/webhooks) and ClearStaq POSTs the computed profile to your endpoint once per submission, with the same HMAC X-ClearStaq-Signature scheme and 5-step retry backoff as analysis.completed. The payload is { event, batch_id, client_id, org_id, counts, profile, created_at }.
Client open-banking envelope
Fetch https://app.clearstaq.com/api/v1/clients/{id}/open-banking to get a client's entire open-banking picture (institutions, accounts, transactions, and the client-level revenue, MCA, and fraud rollup) in one response, instead of fetching and merging one analysis per document. Requires the analyses:read scope.
| Query parameter | Meaning |
|---|---|
| include | Comma-separated subset of institutions, accounts, transactions, analysis. Default: everything except transactions, which must be requested explicitly. Unknown values are ignored. Sections not requested are omitted from the response. |
| transactions_limit | Page size. Default 500, maximum 2000 (larger values are capped). |
| transactions_offset | Transactions to skip. Default 0, maximum 100000; a larger value returns 400 invalid_transactions_offset. |
curl "https://app.clearstaq.com/api/v1/clients/<client_id>/open-banking" \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" # Transactions are opt-in and paginated (newest first): curl "https://app.clearstaq.com/api/v1/clients/<client_id>/open-banking?include=transactions&transactions_limit=500&transactions_offset=0" \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE"
{
"schema_version": "1.0",
"generated_at": "2025-03-04T12:00:00.000Z",
"client": { "id": "8f1c...", "name": "Acme LLC" },
"institutions": [{ "id": "chase", "name": "Chase" }],
"accounts": [
{
"id": "acct_...",
"institution_id": "chase",
"mask": "4412",
"label": null,
"type": null,
"is_composite": false,
"source_account_count": null,
"period": { "start": "2025-01-01", "end": "2025-02-28" },
"balances": { "opening": 10250.4, "closing": 12980.11, "average": 11402.37 },
"documents": ["d1...", "d2..."]
}
],
"transactions": [
{
"id": "t1...",
"account_id": "acct_...",
"document_id": "d2...",
"date": "2025-02-27",
"description": "ACH DEPOSIT",
"counterparty": null,
"amount": 1520.0,
"direction": "credit",
"category": "Revenue"
}
],
"analysis": {
"revenue": { "true_revenue": 168500.24, "estimated_monthly": 84250.12 },
"monthly": [{ "account_id": "acct_...", "month": "2025-01", ... }],
"mca_positions": [],
"fraud": { "score": 12, "risk_level": "low", "findings": [] },
"checks": [],
"excluded_documents": { "failed": 0, "processing": 0, "no_org": 0 },
"ambiguous_accounts": 0,
"unassigned_accounts": 0,
"monthly_replacements": 0,
"excluded_transactions": 0,
"recompute_pending": false,
"fraud_pending": false
}
}Read the data carefully:
- Account ids beginning with
acct_tmp_are provisional and not stable: the same account can come back under a different id later, so never store them as a durable key. All other account ids are stable. institution_idhas two distinct empty-looking states."unknown"is a value: the statement was read but its bank could not be identified.nullmeans no institution is linked to the account at all. Treat them differently: neither one means the field is missing.- Transaction
amountis always positive;direction(credit= money in,debit= money out) gives the sign. analysis.recompute_pendingcovers only the revenue and MCA rollups (true= may be stale,null= no signal). Fraud freshness is reported separately inanalysis.fraud_pending(truewhile fraud analysis is still running on any document).analysis.excluded_documentscounts documents left out of the envelope:failed(parsing failed),processing(not finished yet), andno_org(completed but not attributable to an organization, permanently excluded).- Transactions are ordered newest first. To page through them, advance
transactions_offsetbytransactions_limitand stop on an empty page. Transactions with no amount or type are never returned (they are left out before paging, so they do not shorten a page), and their number is reported inanalysis.excluded_transactions.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_id | Malformed client id. |
| 400 | id_required | Client id missing from the path. |
| 400 | invalid_transactions_offset | transactions_offset is above 100000. |
| 403 | scope_required | Key is missing the analyses:read scope. |
| 404 | not_found | Client not found. Also returned for a client in another organization, so the response never reveals whether an id exists. |
| 500 | internal_error | The envelope could not be built. Safe to retry. |
Businesses & submissions
A business is a company your organization works with: legal name, DBA, EIN, address, and industry. Every document for a business lands in one of its submissions, numbered #1, #2, and so on. A business has at most one open submission at a time, and new documents go into it. When your team marks that submission funded or lost in the webapp, it stops receiving documents and the next document opens the next number.
A submission is a client. Its id is the client_id the endpoints below return, and every client endpoint accepts it: GET /api/v1/clients/{id} for the aggregated profile and GET /api/v1/clients/{id}/open-banking for the open-banking envelope. POST /api/v1/clients works as before and creates a client that is not linked to any business yet; it appears in the webapp under Applications › Needs review, where your team can link it to a business.
| Endpoint | Scope | What it does |
|---|---|---|
| GET /api/v1/businesses | analyses:read | Lists your businesses, newest first, each with its open submission and its number of submissions. Query: page (from 1), per_page (default 25, maximum 100), search (case-insensitive match on the legal name or DBA). |
| POST /api/v1/businesses | analyses:write | Creates a business together with its open submission #1 and returns it with 201. JSON body: legal_name (required), dba, ein, address, industry, start_date (YYYY-MM-DD); text fields up to 500 characters. |
| GET /api/v1/businesses/{id}/submissions | analyses:read | Lists every submission of a business, newest first, with its number, client_id, status, source, and open and close dates. |
| GET /api/v1/people/{id} | analyses:read | Returns a person (an owner or officer) with the businesses they are linked to, their title, and their ownership percentage. |
| POST /api/v1/analyses | analyses:write | With the optional business_id field, files the document into that business's open submission. |
# List businesses, newest first (search matches the legal name or DBA)
curl "https://app.clearstaq.com/api/v1/businesses?search=acme&page=1&per_page=25" \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE"
# Create a business together with its submission #1
curl -X POST https://app.clearstaq.com/api/v1/businesses \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"legal_name":"Acme LLC","ein":"12-3456780","address":"100 Main St, Springfield, IL 62701"}'{
"businesses": [
{
"id": "5b0e...",
"legal_name": "Acme LLC",
"dba": null,
"ein_last4": "6780",
"address": "100 Main St, Springfield, IL 62701",
"industry": null,
"created_at": "2025-01-10T09:30:00.000Z",
"open_submission": { "client_id": "9c3a...", "number": 2, "status": "open" },
"submission_count": 2
}
],
"page": 1,
"per_page": 25,
"total": 1
}Read the data carefully:
- The full EIN is never returned;
ein_last4has its last four digits. The EIN you send is 9 digits (dashes allowed); placeholder numbers are rejected with400 invalid_ein. open_submissionisnullwhen every submission of the business is funded or lost. The next document sent with itsbusiness_idopens a new one.- An EIN already used by one of your businesses returns
409 business_existswith thatbusiness_id; reuse it instead of creating a copy. - Without an EIN, a business with the same name and address returns
409 possible_duplicatewith the matchingcandidates. Repeat the request with?force=trueto create it anyway;forcenever bypasses the EIN check.
List a business's submissions to find the one you need, then read it with the client endpoints.
curl https://app.clearstaq.com/api/v1/businesses/<business_id>/submissions \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE"
{
"business_id": "5b0e...",
"legal_name": "Acme LLC",
"submissions": [
{
"number": 2,
"client_id": "9c3a...",
"status": "open",
"source": "api",
"opened_at": "2025-03-04T12:00:00.000Z",
"closed_at": null
},
{
"number": 1,
"client_id": "8f1c...",
"status": "funded",
"source": "webapp",
"opened_at": "2025-01-10T09:30:00.000Z",
"closed_at": "2025-02-28T17:05:00.000Z"
}
]
}statusis one ofopen,submitted,awaiting_signatures,complete,funded, orlost. Onlyfundedandloststop a submission from receiving documents, and they are set only by your team in the webapp;closed_atis filled then.- For portal applications,
submittedmeans the merchant signed and submitted the application;awaiting_signaturesmeans other required owners still need to sign;completemeans all required signatures were collected and the application PDF was generated. These are submission states, separate from analysis processing statuses. Anopen_submissioncan have any of these states and still receive documents. - Merchant edits revoke signatures and return the submission to
open. Signing again generates a new PDF version. Portal files and PDFs are not exposed through the public API. sourceis the channel that opened the submission:webapp,email,api, orportal;nullwhen it was not recorded.
To add a document to a business, send its business_id with POST /api/v1/analyses. The document goes into the open submission, or into a new one with the next number when none is open, and the 202 response adds a submission object (created is true when this request opened it). The business is checked before anything is stored or billed. Without business_id, the endpoint behaves exactly as described in “Submit a statement”.
curl -X POST https://app.clearstaq.com/api/v1/analyses \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \ -F "file=@./statement.pdf" \ -F "document_kind=bank_statement" \ -F "business_id=<business_id>"
{
"id": "d3...",
"status": "queued",
...
"submission": { "client_id": "9c3a...", "number": 2, "created": false }
}People are the owners and officers linked to your businesses. Only the businesses of your organization are listed.
curl https://app.clearstaq.com/api/v1/people/<person_id> \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE"
{
"id": "4e7d...",
"email": "[email protected]",
"full_name": "Jane Doe",
"phone": null,
"businesses": [
{ "business_id": "5b0e...", "legal_name": "Acme LLC", "title": "Owner", "ownership_pct": 60 }
]
}A key reads and writes only its own organization. A business or person of another organization answers 404, exactly like one that does not exist.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_json | The POST /api/v1/businesses body is not valid JSON (invalid_body when it is not a JSON object). |
| 400 | legal_name_required | legal_name is missing, empty, not a string, or longer than 500 characters. |
| 400 | invalid_field | An optional field is not a string or is longer than 500 characters; field names it. |
| 400 | invalid_ein | ein is not 9 digits or is a placeholder number. |
| 400 | invalid_start_date | start_date is not a YYYY-MM-DD date. |
| 400 | invalid_id | Malformed business or person id. |
| 400 | invalid_business_id | business_id sent to POST /api/v1/analyses is not a valid UUID. |
| 403 | scope_required | Key is missing the scope in the table above. Sending business_id always needs analyses:write. |
| 404 | not_found | Business or person not found in your organization. |
| 404 | business_not_found | business_id is not a business of your organization; nothing is stored or billed. |
| 409 | business_exists | The EIN already belongs to one of your businesses; business_id identifies it. |
| 409 | possible_duplicate | A business with the same name and address exists; candidates lists it. Retry with ?force=true to create anyway. |
| 500 | internal_error | The businesses, submissions, or person could not be read or created. On POST /api/v1/analyses the codes are business_lookup_failed and submission_create_failed. |
| 503 | business_create_unavailable | The business or its submission could not be created right now (submission_unavailable on POST /api/v1/analyses). Retry. |
Applications API (lead sources)
Connect a lead source (your website form, a referral partner) and create complete applications: the business, its owners, the application's answers, then its documents. Create one key per lead source in Settings › Integrations › API & webhooks: its name is the Source of every application it creates. Give it the scopes it needs:
| applications:write | Create applications and ask their owners to sign. |
| documents:write | Add documents to applications. |
| applications:read | Read applications. |
Create an application
POST /api/v1/applications. The business is your organization's business with that EIN, or a new one; its next application gets one or two owners (the ones who sign, owner 1 then owner 2) and the answers. The SSN is accepted, kept encrypted and never returned. answers takes your own application form's questions by their id. Everything is created in one transaction.
Send an Idempotency-Key: a retry with the same key and body returns the first application (200, Idempotent-Replayed: true) instead of creating a second. Keys are kept 24 hours.
curl -X POST https://app.clearstaq.com/api/v1/applications \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \
-H "Idempotency-Key: lead-000481" \
-H "Content-Type: application/json" \
-d '{
"external_ref": "LEAD-000481",
"business": {
"legal_name": "Bayside Tire & Wheel LLC",
"dba": "Bayside Tire",
"ein": "12-3458214",
"entity_type": "llc",
"industry": "Auto Repair",
"start_date": "2016-12",
"address": { "street": "88 Bay St", "city": "Tampa", "state": "FL", "zip": "33602" },
"phone": "8135550188"
},
"owners": [
{ "full_name": "Rosa Alvarez", "email": "[email protected]", "title": "Owner",
"ownership_pct": 100, "ssn": "123-45-6789", "date_of_birth": "1981-04-02" }
],
"application": {
"amount_requested": 80000,
"use_of_funds": "Second location fit-out",
"monthly_revenue": 61000,
"answers": { "your_question_id": 6 }
}
}'201 Created:
{
"id": "9f2c…",
"business_id": "41a7…",
"number": 1,
"status": "open",
"source": "api",
"source_name": "Website form",
"external_ref": "LEAD-000481",
"missing": ["bank_statements", "signatures"],
"links": {
"self": "/api/v1/applications/9f2c…",
"documents": "/api/v1/applications/9f2c…/documents",
"signature_invites": "/api/v1/applications/9f2c…/signature-invites"
}
}| 422 | validation_failed | With errors: [{field, message}], e.g. owners[0].ownership_pct missing. Values are never echoed. |
| 409 | possible_duplicate | No EIN, and a business with the same name and address exists (candidates). Send its EIN, or retry with ?force=true. |
| 409 | application_open | The business already has an open application (application_id): add documents to it. |
| 422 | idempotency_key_reused | The same key was used with another body. |
| 409 | idempotency_in_progress | The first request with this key is still running. Retry shortly. |
Add documents
Bank statements and tax returns as PDF; the other document types of your client portal (driver's license, voided check, …) as PDF, PNG or JPEG, and your own types (custom:<id>) in the formats you chose for them. The format is read from the file's bytes. The files go through the antivirus, and bank statements through the automatic analysis, like your client portal's files. Available once your client portal is live (409 documents_unavailable before).
Up to 10 MB per request, as multipart:
curl -X POST https://app.clearstaq.com/api/v1/applications/<id>/documents \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \ -F "doc_type=bank_statements" \ -F "file=@./march.pdf" \ -F "file=@./april.pdf"
Bigger files (statements up to 50 MB) go straight to storage with upload URLs, valid 2 hours:
# 1. Ask for upload URLs
curl -X POST https://app.clearstaq.com/api/v1/applications/<id>/documents/uploads \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"doc_type":"bank_statements","files":[{"name":"2025.pdf","size_bytes":31457280,"mime_type":"application/pdf"}]}'
# 2. PUT each file to its upload_url
curl -X PUT "<upload_url>" -H "Content-Type: application/pdf" --data-binary @./2025.pdf
# 3. Finish it
curl -X POST https://app.clearstaq.com/api/v1/applications/<id>/documents/<document_id>/complete \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE"Ask the owners to sign
POST /api/v1/applications/{id}/signature-invites emails each owner who signs and hasn't signed yet to review and sign the application in your client portal. Signing is always the owners'. An owner already asked gets the same request again; its email goes out at most once every 24 hours. Optional body: {"emails": ["[email protected]"]}.
curl -X POST https://app.clearstaq.com/api/v1/applications/<id>/signature-invites \ -H "Authorization: Bearer cg_live_YOUR_KEY_HERE"
Read an application
GET /api/v1/applications/{id}: status (open, awaiting_signatures, signed, submitted_to_partners, funded, lost), what's missing, the owners (never an SSN or a date of birth) and whether each signed, the documents by state, the analysis summary once bank statements are analyzed, and the outcome. Or listen to the webhooks.
Application webhooks
Events application.created, application.signed, application.funded, application.lost, partner_send.status_changed and partner_offer.received. Subscribe with POST /api/v1/webhooks and an applications scope: a key's subscriptions hear only the applications that key created, never the partner events. Endpoints added in Settings › Integrations › API & webhooks hear every application and the partner events. The payload holds ids, numbers, statuses and amounts (never a person's data) and is signed like every event (X-ClearStaq-Signature). In the application events data.status is the application's status, as in the GET; in partner_send.status_changed, the send's.
A key sends each event to an address once, and to at most 5 addresses; with only an applications scope it lists and deletes its own subscriptions (deleting needs applications:write). Deliveries are retried up to 8 times over about 18 hours and can arrive out of order: rely on created_at and the GET for the current state. Webhooks are turned on per organization. Ask ClearStaq support (until then POST /api/v1/webhooks answers 404).
{
"id": "5b1e…",
"event": "application.signed",
"created_at": "2026-10-11T12:00:00.000Z",
"data": {
"org_id": "7a2b…",
"application_id": "9f2c…",
"business_id": "41a7…",
"number": 1,
"source": "api",
"source_name": "Website form",
"external_ref": "LEAD-000481",
"status": "signed",
"at": "2026-10-11T12:00:00.000Z"
}
}For partners: answer a send
An organization that sends you applications can give you a response key (scope partner_responses:write). It answers only the sends made to you. Name the send with reference (ClearStaq's, a UUID sent as clearstaq_reference in the API request and as "Our reference" in the email) or with partner_reference, your application id. decision: approve with offers (a new answer replaces your previous offers), decline with reason, documents with documents and/or note, reviewing or funded.
curl -X POST https://app.clearstaq.com/api/v1/partner-responses \
-H "Authorization: Bearer cg_live_THE_KEY_THE_BROKER_GAVE_YOU" \
-H "Content-Type: application/json" \
-d '{
"reference": "<clearstaq_reference>",
"decision": "approve",
"offers": [{ "id": "OF-1", "funded_amount": 50000, "factor_rate": 1.35,
"term_months": 8, "payment_frequency": "daily", "product": "mca" }],
"contact": { "name": "Uma Writer" }
}'Client portal webhook
Available when the client portal launches
Subscribe to submission.completed through the existing POST /api/v1/webhooks endpoint with the analyses:read scope. It reports when all required owner signatures have been collected and the application PDF has been generated. The submission continues accepting documents until your team marks it funded or lost. Other webhook event subscriptions still require analyses:write.
Before launch, subscribing returns 400 invalid_event_type. Demo organizations receive 403 demo_read_only; the existing organization integration gate also applies. included_phases is ignored for this event and is required only for analysis.completed.
curl -X POST https://app.clearstaq.com/api/v1/webhooks \
-H "Authorization: Bearer cg_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"event_type":"submission.completed","target_url":"https://your-app.example.com/hooks/clearstaq"}'The 201 response returns the subscription's secret once. Use it to verify the same signature as other events: X-ClearStaq-Signature: sha256=<hex>, the HMAC-SHA256 of the exact raw request body. Acknowledge a valid delivery with a 2xx response.
{
"id": "evt_...",
"event": "submission.completed",
"created_at": "2026-10-03T12:00:00.000Z",
"data": {
"org_id": "7a2b...",
"business_id": "5b0e...",
"client_id": "9c3a...",
"submission_number": 2,
"status": "complete",
"pdf_id": "6d4e...",
"pdf_version": 1
}
}The envelope contains only identifiers, numbers, and status. Its id identifies the event. It includes no client_ref, metadata, application responses, personal data, files, or download URLs. pdf_id is a correlation identifier; portal files and PDFs are not exposed through the public API. This event is delivered by webhook and is not returned by the analysis polling endpoint.
Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or unrecognised API key. |
| 402 | plan_excluded | The org has no paid plan. |
| 402 | payment_due | The org's last invoice is unpaid. |
| 402 | insufficient_credits | No credits remaining and no paid plan to bill extra credits. |
| 413 | file_too_large | PDF exceeds 25 MB. |
| 415 | unsupported_media_type | Non-PDF upload. |
| 422 | invalid_callback_url | callback_url must use HTTP or HTTPS, contain no URL credentials, and resolve only to public IP addresses. The analysis is not accepted. |
| 429 | rate_limited | Rate limit exceeded. Honor Retry-After. |
| 502 | parser_unavailable | Analysis service could not accept the job. |
Rate limits
Requests are limited to 120 per minute per bearer token. Every /api/v1 response carries the standard RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy headers; exceeding the window returns 429 rate_limited with a Retry-After header. Self-throttle on RateLimit-Remaining rather than retrying into a 429. StaqScale plans can lift the limit.
Versioning & deprecation policy
The API is versioned in the URL path (/api/v1). Backwards-compatible additions (new optional fields, new endpoints, new enum values on fields documented as extensible) ship without a version bump. Breaking changes ship only as a new path version (/api/v2); the prior version then keeps working for at least 12 months, during which responses on deprecated surfaces carry Deprecation and Sunset headers and the change is announced by email to every organization with an active key.
OpenAPI spec & agent access
Download the machine-readable spec to generate SDKs or import into Postman/Insomnia: openapi.json · openapi.yaml
AI agents can call ClearStaq natively via the MCP server at https://clearstaq.com/mcp (Streamable HTTP; manifest at /.well-known/mcp.json), or start from the LLM-friendly index at /llms.txt.