Canary Financial
Rate limits
4 tiers
RateLimit-* headers on every call
Version
v1
JSON over HTTPS
Test keys
Sandbox first
Live keys after certification
Access
By request
Approved partners only
Developers · Partner API

Put Canary lending
inside your platform

Approved partners send business, SBA, DSCR, real estate and personal loan applications to Canary Financial from their own systems, then track each file to a decision.

v1 is live for approved partners. Endpoints marked Live work today: with a test key for building, and with a live key once Canary approves your live access. Webhooks are marked Coming soon and are still in development. Get notified when webhooks open.

Overview

The Partner API is how an approved partner — an ISO, broker, accounting firm, vertical SaaS platform or marketplace — sends a borrower to Canary Financial without re-keying the file. You post the application from your server, attach documents, and receive status updates as the file moves to a decision. Canary chooses which of its funding partners sees the file; that routing never changes your integration.

Submit

One JSON shape for every product, selected with product.

Track

Poll status or receive signed webhooks as the file progresses.

Get credited

Every file is tied to your partner code. Commission is paid only after the deal closes and the funds reach Canary — a lead or application alone earns nothing.

Typical integration:

  1. You join as a partner and request API access from your dashboard. Canary issues a test key.
  2. You call GET /ping, then POST /applications with obviously fake data.
  3. You attach documents and confirm your webhook endpoint verifies our signature.
  4. Canary reviews your test traffic and issues a live key for each approved product.

All APIs

Every partner-facing endpoint and where it stands today. The same list is served as JSON at GET /api/partner/v1.

MethodPathWhat it doesStatus
GET/api/partner/v1API index, no authLive
GET/api/partner/v1/pingConfirm key, environment, partner codeLive
GET/api/partner/v1/productsProducts open to you + required fieldsLive
POST/api/partner/v1/applicationsCreate a funding applicationLive
GET/api/partner/v1/applicationsList applications credited to youLive
GET/api/partner/v1/applications/{id}Status + documentsLive
POST/api/partner/v1/applications/{id}/documentsAttach bank statements, IDsLive
POST/api/partner/v1/quotesIndicative pricing where a live source existsLive
POST/api/affiliate/v1/leadLead intake for widgets and simple form postsLive
POST/api/affiliate/v1/clickRecord a click, returns clickRefLive

No-code options for publishers — the iframe form, the JS widget and tracked links — are covered on the Affiliate Program page.

Get access

API access is never on by default, and signing up does not create an API key. Access is a separate approval on top of your partner account.

  1. Join as a partner. Create an affiliate account or a sales agent account, or sign in if you already have one.
  2. Ask for API access. On your affiliate dashboard or sales agent dashboard, open Partner API and tap Request API access. Tell us what you will send and roughly how many applications a month.
  3. Build against a test key. When we approve the request, a cnk_test_ key appears on your dashboard. Tap View key (once) and copy it into your secret manager — we keep only a fingerprint and cannot show it again. Test traffic never reaches a funding partner.
  4. Go live. After we review your test traffic we issue a cnk_live_ key, enabled product by product. You can revoke either key from the dashboard at any time; we issue the replacement.
This is a server-to-server API. Never put a key in a browser, mobile app, public Postman workspace or source control. If a key is exposed, tell us immediately and we will revoke it and issue a new one.

Base URL & versioning

https://www.canaryfinancial.com/api/partner/v1

Paths below are relative to that base. HTTPS only. There is one hostname; the key you send selects test or live. Breaking changes ship under a new prefix (/api/partner/v2). New response fields are not breaking — ignore fields you do not recognise.

Authentication

Send your secret in the X-Api-Key header (or as Authorization: Bearer) on every authenticated request. Test keys start with cnk_test_, live keys with cnk_live_. Affiliate widget keys (ak_) do not work here.

X-Api-Key: YOUR_API_SECRET Content-Type: application/json

Keys are never accepted in the query string. A missing or unknown key returns 401; a valid key that is not allowed to do what you asked (API access off, product not enabled, partner suspended) returns 403, so you can tell a wrong secret from a blocked one.

Test vs live

Test key

Creates sandbox applications only. Nothing is sent to a funding partner, nothing counts toward commission, and the applicant is never contacted. Consent fields are optional so you can wire up the integration first.

Live key

Creates real applications credited to your partner code. Applicant consents are required. Live and test data never mix: a live key cannot read sandbox files and a test key cannot read live ones.

Idempotency

Networks fail and clients retry. Every POST that creates something requires an Idempotency-Key header (a UUID per submission). Sending the same key with the same body within 24 hours returns the original response instead of creating a second application. The same key with a different body returns 409 idempotency_mismatch. Also send your own external_id so you can match our records to yours.

Rate limits Live

Limits are per key and split by endpoint class, so polling for status can never use up your submission allowance. Each class has a short burst, a steady refill rate and a daily cap (UTC day). Requests with no key or an unrecognised key are limited per IP at the lowest tier.

TierRead (GET)QuotesSubmissions
No key (per IP)10 burst · 1/s · 1,000/day3 · 1/s · 50/day2 · 1/s · 10/day
Sandbox20 · 5/s · 10,000/day10 · 2/s · 1,000/day5 · 1/s · 200/day
Standard50 · 10/s · 50,000/day20 · 5/s · 5,000/day10 · 2/s · 1,000/day
Premium200 · 50/s · 500,000/day100 · 25/s · 50,000/day50 · 10/s · 10,000/day

Every response carries the headers below. Back off when RateLimit-Remaining reaches 0 rather than waiting for a 429.

HeaderMeaning
RateLimit-LimitBurst size for this endpoint class
RateLimit-RemainingRequests you can make right now
RateLimit-ResetSeconds until the burst is fully refilled
X-Quota-Limit / X-Quota-RemainingDaily cap for this class, and what is left today
Retry-AfterOn 429 only: seconds to wait
HTTP/1.1 429 RateLimit-Limit: 10 RateLimit-Remaining: 0 Retry-After: 1 {"ok":false,"error":{"code":"rate_limited","message":"Too many requests. Retry after 1s.","retryAfterSeconds":1}}

When the daily cap is spent the code is daily_quota_exceeded and Retry-After counts down to 00:00 UTC. Need more headroom? Ask us about a higher tier.

Errors

Errors are JSON with a stable machine-readable code. Every response also carries a request_id (and an X-Request-Id header) — quote it when you contact us.

{ "ok": false, "error": { "code": "validation_failed", "message": "Some fields are missing or invalid.", "fields": { "owner.email": "A valid email is required" }, "request_id": "6f1c2a9e-4b7d-4e0a-9c55-1d2e3f4a5b6c" }, "request_id": "6f1c2a9e-4b7d-4e0a-9c55-1d2e3f4a5b6c" }
HTTPcodeWhen
400idempotency_key_requiredPOST /applications without an Idempotency-Key
400invalid_json / invalid_queryBody is not a JSON object, or list parameters are out of range
400no_files, file_too_large, extension_mismatch, ...Upload rejected; see Upload documents
401missing_key / invalid_keyNo key, or the key is not recognised or was revoked
403api_not_enabledAPI access is not on for this key's environment
403product_not_enabledYour live key is not approved for that product
403live_not_openLive submissions are temporarily paused; keep using your test key
403partner_suspendedYour partner account is suspended
404not_foundUnknown path, or the application is not yours
409idempotency_mismatchIdempotency-Key reused with a different body
409quotes_unavailableNo live pricing source for that product right now
413payload_too_largeJSON body over 256 KB
415unsupported_media_typeWrong Content-Type (JSON, or multipart for documents)
422validation_failedRequired fields missing or invalid; see fields
422consent_requiredLive application without the required consents
429rate_limited / daily_quota_exceededSee Rate limits
5xxserver_errorRetry with backoff and the same Idempotency-Key

An application that is not credited to you returns 404, never 403; we do not confirm that another partner's file exists.

Pagination

List endpoints return newest first and accept limit (default 50, max 100) and offset. The response includes has_more; when it is true, request the next page with offset + limit.

API index

GET/api/partner/v1Live

No key needed. Returns the version, this page's URL and every endpoint with its status.

curl https://www.canaryfinancial.com/api/partner/v1
{ "name": "Canary Financial Partner API", "version": "1.0.0", "status": "live", "docs": "https://www.canaryfinancial.com/partner-api", "endpoints": [ { "method": "GET", "path": "/api/partner/v1", "status": "live", ... }, ... ] }

Ping

GET/pingLive

Confirms your key works and tells you which environment and partner code it belongs to. Use it as a health check.

curl https://www.canaryfinancial.com/api/partner/v1/ping \ -H "X-Api-Key: YOUR_TEST_SECRET"
{ "ok": true, "environment": "test", "partner_code": "aff_k7m2p9q3", "company": "Example Partners LLC", "products": ["business", "sba", "dscr", "real-estate", "personal"], "tier": "sandbox", "request_id": "..." }

Products

GET/productsLive

Lists the products your key can submit, the exact fields each one requires, and whether quotes are available. A test key sees every product; a live key sees only the products Canary approved for you.

{ "ok": true, "environment": "live", "products": [ { "product": "business", "required_fields": ["owner.first_name", "owner.last_name", "...", "consents.text_version"], "quotes": false } ] }
productFor
businessWorking capital, term loans, lines of credit, equipment and other small-business funding
sbaSBA-backed loans
dscrRental-property loans qualified on the property's cash flow
real-estateOther real estate investor financing
personalConsumer personal loans

Create an application

POST/applicationsLive

Creates an application credited to your partner code. Returns 201 on create, or 200 with "replayed": true when the external_id or Idempotency-Key already exists.

curl -X POST https://www.canaryfinancial.com/api/partner/v1/applications \ -H "X-Api-Key: YOUR_TEST_SECRET" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3b0c7f2e-5d41-4c8a-9e1f-0a6d2b7c9e44" \ -d '{ "external_id": "crm-10442", "product": "business", "business": { "name": "Harbor Test Bakery", "legal_name": "Harbor Test Bakery LLC", "entity_type": "LLC", "industry": "Food Service", "years_in_business": 3, "ein": "00-0000000", "address": { "line1": "100 Example Ave", "city": "Seattle", "state": "WA", "zip": "98101" }, "monthly_revenue": 38000 }, "owner": { "first_name": "Test", "last_name": "Owner", "email": "owner@example.com", "phone": "2065550100", "dob": "1985-01-01", "ownership_percent": 100 }, "funding": { "amount": 50000, "purpose": "Equipment" }, "consents": { "credit_pull": true, "terms": true, "captured_at": "2026-10-15T14:02:11Z", "ip": "203.0.113.24", "text_version": "partner-consent-2026-10" } }'
{ "ok": true, "application": { "id": "PA-7KQ2M9XW4R", "status": "received", "product": "business", "external_id": "crm-10442", "environment": "test", "business_name": "Harbor Test Bakery", "owner_name": "Test Owner", "amount": 50000, "created_at": "2026-10-15T14:02:12Z", "updated_at": "2026-10-15T14:02:12Z" }, "request_id": "..." }

Save id; you need it for documents and status. On a live key the application goes straight to Canary's funding desk. Sensitive values you send (SSN, EIN) are encrypted and never returned by any endpoint.

Request body

One shape for every product; the Required column says which products need a field. Unknown fields are ignored and not stored.

FieldRequiredNotes
productYesbusiness, sba, dscr, real-estate, personal
external_idRecommendedYour id, max 80 characters. A repeat returns the first application with "replayed": true
owner.first_name, owner.last_nameYes
owner.email, owner.phoneYesPhone with at least 10 digits
owner.dobYesYYYY-MM-DD; applicant must be 18+
funding.amount, funding.purposeYesAmount in USD, greater than 0
business.name, business.entity_typebusiness, sba, dscr, real-estateOperating name; LLC, Corporation, Sole Proprietor, Partnership
business.industry, business.years_in_business, business.monthly_revenuebusiness, sbaRevenue is average monthly gross, USD
property.address, property.state, property.type, property.estimated_valuedscr, real-estateSubject property
property.monthly_rentdscrGross monthly rent, USD
owner.annual_income, owner.address.zippersonalIncome in USD; owner.address is { line1, city, state, zip }
business.ein, owner.ssnNo9 digits; encrypted; never returned
business.legal_name, business.address, owner.credit_score, owner.ownership_percent, funding.urgencyNoHelps the funding desk; business.address is { line1, city, state, zip }
consents.credit_pull, consents.termsLiveMust be true and reflect what the applicant actually agreed to
consents.captured_at, .ip, .text_versionLiveWhen, from where, and which wording the applicant saw
consents.smsNoOnly true if the applicant ticked a separate text-message opt-in
notesNoMax 5,000 characters

The authoritative required-field list for each product is whatever GET /products returns; incomplete posts return 422 naming every missing field.

List & get applications

GET/applicationsLive
GET/applications/{id}Live

The list returns applications you posted through the API with this key's environment, newest first, filterable with status. Getting one application also returns its documents (name, type, size — never the bytes).

curl "https://www.canaryfinancial.com/api/partner/v1/applications?limit=50&offset=0" \ -H "X-Api-Key: YOUR_LIVE_SECRET"
{ "ok": true, "applications": [ { "id": "PA-7KQ2M9XW4R", "status": "in_review", ... } ], "count": 1, "limit": 50, "offset": 0, "has_more": false, "request_id": "..." }

Treat status as an opaque string. Live applications follow the funding desk's decision: today the values are received, in_review, approved, declined, funded and withdrawn. New values may appear without a version change.

Upload documents

POST/applications/{id}/documentsLive

Multipart upload; repeat the files field once per file. PDF, JPEG, PNG and TIFF, up to 10 MB each and 10 files per application. Files are checked by their contents and the name must match; if any file fails, none are stored. Files cannot be downloaded back through the API.

curl -X POST https://www.canaryfinancial.com/api/partner/v1/applications/PA-7KQ2M9XW4R/documents \ -H "X-Api-Key: YOUR_LIVE_SECRET" \ -F "files=@bank-statement-aug.pdf" \ -F "files=@bank-statement-sep.pdf"

Rejections return 400 with no_files, too_many_files, empty_file, file_too_large, unrecognized_type or extension_mismatch.

Quotes

POST/quotesLive

Indicative pricing before a full application, for personal (send funding.amount, funding.purpose, owner.credit_score, owner.zip) and dscr (send property.estimated_value, property.monthly_rent, funding.amount). No SSN, no credit pull. We only return pricing from a live source; when none is connected for a product the answer is 409 quotes_unavailable, never made-up numbers. GET /products tells you in advance ("quotes": true). Test keys always get a fixed sample marked "sandbox": true.

A quote is not an offer of credit; final terms come only from the lender after a full application.

Affiliate lead intake

POST/api/affiliate/v1/leadLive

Available today for partners who only need to pass a contact and let Canary take the application from there. Note the different base path. Identify yourself with your affiliate code, or with the X-Affiliate-Key header for server-side posts.

curl -X POST https://www.canaryfinancial.com/api/affiliate/v1/lead \ -H "Content-Type: application/json" \ -H "X-Affiliate-Key: YOUR_AFFILIATE_KEY" \ -d '{ "product": "business", "name": "Test Owner", "email": "owner@example.com", "phone": "2065550100", "company": "Harbor Test Bakery", "amount": "50000" }'
{ "ok": true, "leadId": 1234, "message": "Thanks -- your request has been received. Our team will be in touch shortly." }

Webhooks Coming soon

Give us an HTTPS URL and we will POST signed JSON when a live application credited to you changes. Test-key applications never call your production URL.

EventWhen
application.receivedWe stored your application
application.submittedThe file went to a funding partner
application.documents_neededMore documents are required
application.offerAn offer is available for the applicant
application.funded / application.declinedFinal outcome

Each delivery carries X-Canary-Signature: t=TIMESTAMP,v1=HEX, where v1 is HMAC-SHA256 of TIMESTAMP + "." + raw body using your webhook secret (separate from your API key). Verify against the raw bytes, reject anything more than 5 minutes old, and compare in constant time.

const crypto = require('crypto'); function canarySignatureOk(secret, header, rawBody) { const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('='))); const ts = Number(parts.t); if (!ts || !parts.v1 || Math.abs(Date.now() / 1000 - ts) > 300) return false; const want = crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest(); const got = Buffer.from(parts.v1, 'hex'); return got.length === want.length && crypto.timingSafeEqual(got, want); }
import hashlib, hmac, time def canary_signature_ok(secret: str, header: str, raw_body: bytes) -> bool: parts = dict(p.strip().split("=", 1) for p in header.split(",")) ts = int(parts.get("t", 0)) if not ts or "v1" not in parts or abs(time.time() - ts) > 300: return False want = hmac.new(secret.encode(), str(ts).encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(want, parts["v1"].lower())
static boolean canarySignatureOk(String secret, String header, byte[] rawBody) throws Exception { Map<String, String> p = new HashMap<>(); for (String kv : header.split(",")) { String[] a = kv.trim().split("=", 2); p.put(a[0], a[1]); } long ts = Long.parseLong(p.getOrDefault("t", "0")); if (ts == 0 || !p.containsKey("v1") || Math.abs(Instant.now().getEpochSecond() - ts) > 300) return false; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256")); mac.update((ts + ".").getBytes(UTF_8)); byte[] want = mac.doFinal(rawBody); return MessageDigest.isEqual(want, HexFormat.of().parseHex(p.get("v1").toLowerCase())); }

Reply 2xx within 10 seconds. Failed deliveries are retried with backoff, so make your handler safe to run twice for the same event id.

Security

  • HTTPS only; keys are accepted in a header, never in a URL.
  • Every call is rate-limited per key and per endpoint class Live
  • Sensitive applicant data (SSN, EIN, bank details) is encrypted at rest and never returned by the API or webhooks.
  • You can only see applications credited to your own partner code; test and live data are kept apart.
  • Revoking a key, or turning a partner's API access off, takes effect within a minute.

Code examples

Create an application with a fresh Idempotency-Key per submission.

const crypto = require('crypto'); const res = await fetch('https://www.canaryfinancial.com/api/partner/v1/applications', { method: 'POST', headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify(application) }); if (res.status === 429) { const wait = Number(res.headers.get('Retry-After') || 1); // schedule a retry after "wait" seconds with the SAME Idempotency-Key } const body = await res.json(); if (!body.ok) throw new Error(body.error.code + ': ' + body.error.message); console.log(body.application.id);
import os, time, uuid, requests idem = str(uuid.uuid4()) for attempt in range(5): r = requests.post( "https://www.canaryfinancial.com/api/partner/v1/applications", headers={"X-Api-Key": api_key, "Idempotency-Key": idem}, json=application, timeout=30) if r.status_code != 429 and r.status_code < 500: break time.sleep(int(r.headers.get("Retry-After", 2 ** attempt))) r.raise_for_status() print(r.json()["application"]["id"])
HttpRequest req = HttpRequest.newBuilder(URI.create("https://www.canaryfinancial.com/api/partner/v1/applications")) .header("X-Api-Key", apiKey) .header("Content-Type", "application/json") .header("Idempotency-Key", UUID.randomUUID().toString()) .POST(HttpRequest.BodyPublishers.ofString(applicationJson)) .build(); HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(res.statusCode() + " " + res.body());
$ch = curl_init('https://www.canaryfinancial.com/api/partner/v1/applications'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . $apiKey, 'Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode($application), CURLOPT_RETURNTRANSFER => true, ]); $body = json_decode(curl_exec($ch), true);

Changelog

DateChange
2026-10-15v1 endpoints live for approved partners: ping, products, applications (create, list, get), documents and quotes. API access is requested from the partner dashboard; keys are issued by Canary and shown once. Webhooks remain in development.
2026-10-15Docs published as a preview with the API index and per-key rate limiting.

Support

Email support@canaryfinancial.com with the endpoint, the HTTP status, the error code and the time of the request. Never include a full API key or applicant SSN in an email.

Ready to integrate?

Join as a partner, then ask for API access. We start every partner on a test key.

Become a partner