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:
- You join as a partner and request API access from your dashboard. Canary issues a test key.
- You call
GET /ping, thenPOST /applicationswith obviously fake data. - You attach documents and confirm your webhook endpoint verifies our signature.
- 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.
| Method | Path | What it does | Status |
|---|---|---|---|
| GET | /api/partner/v1 | API index, no auth | Live |
| GET | /api/partner/v1/ping | Confirm key, environment, partner code | Live |
| GET | /api/partner/v1/products | Products open to you + required fields | Live |
| POST | /api/partner/v1/applications | Create a funding application | Live |
| GET | /api/partner/v1/applications | List applications credited to you | Live |
| GET | /api/partner/v1/applications/{id} | Status + documents | Live |
| POST | /api/partner/v1/applications/{id}/documents | Attach bank statements, IDs | Live |
| POST | /api/partner/v1/quotes | Indicative pricing where a live source exists | Live |
| POST | /api/affiliate/v1/lead | Lead intake for widgets and simple form posts | Live |
| POST | /api/affiliate/v1/click | Record a click, returns clickRef | Live |
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.
- Join as a partner. Create an affiliate account or a sales agent account, or sign in if you already have one.
- 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.
- 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. - 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.
Base URL & versioning
https://www.canaryfinancial.com/api/partner/v1Paths 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/jsonKeys 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.
| Tier | Read (GET) | Quotes | Submissions |
|---|---|---|---|
| No key (per IP) | 10 burst · 1/s · 1,000/day | 3 · 1/s · 50/day | 2 · 1/s · 10/day |
| Sandbox | 20 · 5/s · 10,000/day | 10 · 2/s · 1,000/day | 5 · 1/s · 200/day |
| Standard | 50 · 10/s · 50,000/day | 20 · 5/s · 5,000/day | 10 · 2/s · 1,000/day |
| Premium | 200 · 50/s · 500,000/day | 100 · 25/s · 50,000/day | 50 · 10/s · 10,000/day |
Every response carries the headers below. Back off when RateLimit-Remaining reaches 0 rather than waiting for a 429.
| Header | Meaning |
|---|---|
RateLimit-Limit | Burst size for this endpoint class |
RateLimit-Remaining | Requests you can make right now |
RateLimit-Reset | Seconds until the burst is fully refilled |
X-Quota-Limit / X-Quota-Remaining | Daily cap for this class, and what is left today |
Retry-After | On 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" }| HTTP | code | When |
|---|---|---|
| 400 | idempotency_key_required | POST /applications without an Idempotency-Key |
| 400 | invalid_json / invalid_query | Body is not a JSON object, or list parameters are out of range |
| 400 | no_files, file_too_large, extension_mismatch, ... | Upload rejected; see Upload documents |
| 401 | missing_key / invalid_key | No key, or the key is not recognised or was revoked |
| 403 | api_not_enabled | API access is not on for this key's environment |
| 403 | product_not_enabled | Your live key is not approved for that product |
| 403 | live_not_open | Live submissions are temporarily paused; keep using your test key |
| 403 | partner_suspended | Your partner account is suspended |
| 404 | not_found | Unknown path, or the application is not yours |
| 409 | idempotency_mismatch | Idempotency-Key reused with a different body |
| 409 | quotes_unavailable | No live pricing source for that product right now |
| 413 | payload_too_large | JSON body over 256 KB |
| 415 | unsupported_media_type | Wrong Content-Type (JSON, or multipart for documents) |
| 422 | validation_failed | Required fields missing or invalid; see fields |
| 422 | consent_required | Live application without the required consents |
| 429 | rate_limited / daily_quota_exceeded | See Rate limits |
| 5xx | server_error | Retry 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
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
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
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 } ] }product | For |
|---|---|
business | Working capital, term loans, lines of credit, equipment and other small-business funding |
sba | SBA-backed loans |
dscr | Rental-property loans qualified on the property's cash flow |
real-estate | Other real estate investor financing |
personal | Consumer personal loans |
Create an application
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.
| Field | Required | Notes |
|---|---|---|
product | Yes | business, sba, dscr, real-estate, personal |
external_id | Recommended | Your id, max 80 characters. A repeat returns the first application with "replayed": true |
owner.first_name, owner.last_name | Yes | |
owner.email, owner.phone | Yes | Phone with at least 10 digits |
owner.dob | Yes | YYYY-MM-DD; applicant must be 18+ |
funding.amount, funding.purpose | Yes | Amount in USD, greater than 0 |
business.name, business.entity_type | business, sba, dscr, real-estate | Operating name; LLC, Corporation, Sole Proprietor, Partnership |
business.industry, business.years_in_business, business.monthly_revenue | business, sba | Revenue is average monthly gross, USD |
property.address, property.state, property.type, property.estimated_value | dscr, real-estate | Subject property |
property.monthly_rent | dscr | Gross monthly rent, USD |
owner.annual_income, owner.address.zip | personal | Income in USD; owner.address is { line1, city, state, zip } |
business.ein, owner.ssn | No | 9 digits; encrypted; never returned |
business.legal_name, business.address, owner.credit_score, owner.ownership_percent, funding.urgency | No | Helps the funding desk; business.address is { line1, city, state, zip } |
consents.credit_pull, consents.terms | Live | Must be true and reflect what the applicant actually agreed to |
consents.captured_at, .ip, .text_version | Live | When, from where, and which wording the applicant saw |
consents.sms | No | Only true if the applicant ticked a separate text-message opt-in |
notes | No | Max 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
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
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
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
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.
| Event | When |
|---|---|
application.received | We stored your application |
application.submitted | The file went to a funding partner |
application.documents_needed | More documents are required |
application.offer | An offer is available for the applicant |
application.funded / application.declined | Final 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
| Date | Change |
|---|---|
| 2026-10-15 | v1 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-15 | Docs 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