Documentation
Referral partners send funding deals to Loanable over HTTPS with an API key — the same idea as posting a file to a lender API. Version 1.0. Access is issued by Loanable on request, not self-serve.
Docs updated: August 26, 2026 · API version 1.0.0
This is a server-to-server API. Never put a live or test key in a browser, mobile app, Postman public workspace, or git. Loanable stores only a hash of the secret. If a key leaks, revoke it in the Partner Portal and ask us to issue a new one.
The Partner API lets an approved referral partner create a funding application in Loanable, attach supporting documents, and read status for files credited to their partner code. It is the inbound equivalent of Loanable posting a deal to a lender API.
Typical integration:
GET /ping, then POST /deals with sandbox data.POST /deals/{id}/documents.deal.created, deal.updated, and deal.document_added.Machine-readable spec: GET /api/v1/partner/openapi.json (no auth). Human docs stay on this page.
API access is not on by default. A signed-in partner cannot mint keys. Email apply@loanableusa.com or message Loanable from the Partner Portal and ask for API access.
Keys look like lnb_test_… or lnb_live_…. After access is enabled, signed-in partners can revoke a leaked key from Resources → Partner API. They cannot create a replacement — Loanable issues the next secret.
Maximum 5 active keys per environment (test and live). Revoke one before asking for another.
https://www.loanableusa.com/api/v1/partner
All paths in this guide are relative to that base. The current version is v1. Breaking changes ship under a new prefix (/api/v2/partner), not by silently changing v1. Additive fields on responses are not breaking.
TLS 1.2+ is required. HTTP is not accepted. There is no sandbox hostname — environment is selected by which key you send.
Unauthenticated index:
GET https://www.loanableusa.com/api/v1/partner
{
"name": "Loanable Partner API",
"version": "1.0.0",
"docs": "https://www.loanableusa.com/partner-api",
"openapi": "https://www.loanableusa.com/api/v1/partner/openapi.json"
}
Every authenticated request needs the secret in one of these headers:
Authorization: Bearer lnb_live_YOUR_SECRET Content-Type: application/json
or
X-Api-Key: lnb_live_YOUR_SECRET Content-Type: application/json
Do not send both. Query-string keys are not supported and never will be — they leak in logs and Referer headers.
Missing, unknown, or revoked keys return 401. A well-formed key that cannot be used (API not enabled, IP not on the allowlist, partner inactive) returns 403 so you can tell “wrong secret” from “right secret, blocked.”
lnb_test_)Create sandbox deals (source: partner_api_sandbox). They never go to lenders and are not counted as referrals. Consents are optional so you can wire the integration first. List/get only return sandbox deals.
lnb_live_)Create real files, email the applicant, credit your partner code, and can fire webhooks. consents.credit_auth and consents.terms are required. List/get never return sandbox deals.
Live keys can list every non-sandbox application credited to your referral code — including deals submitted through the Partner Portal or website with your code — not only source=partner_api. Filter with ?source=partner_api if you only want API posts.
Retries happen. Use both of these:
Idempotency-Key header (max 128 characters). Loanable stores the SHA-256 of the raw JSON for 24 hours. The same key + same body replays the original status and JSON. The same key + a different body returns 409 idempotency_mismatch.external_id in the body (max 80 characters). Unique per partner and environment. A later create with the same id returns 200 with replayed: true instead of inserting a second file.Generate a new Idempotency-Key per logical submit (UUID is fine). Do not reuse keys across different applicants.
| Bucket | Limit | Keyed by |
|---|---|---|
| Well-formed keys | 120 requests / 15 minutes | Connecting server IP |
| Missing or garbage keys | 40 requests / 15 minutes | Connecting server IP |
| POST /deals | 40 creates / hour | API key |
| JSON body | 256 KB | per request |
Successful-looking keys are not starved by scanners hitting 401. GET /openapi.json and the unauthenticated index are not counted.
When you hit a limit you receive 429 rate_limited plus standard RateLimit-* headers (limit, remaining, reset). Include request_id if you write in to support.
All errors are JSON:
{
"error": "Validation failed",
"code": "validation_error",
"request_id": "a1b2c3d4-…",
"fields": [
{ "field": "owner.email", "message": "A valid owner email is required" }
]
}
fields is only present on validation failures. Always log request_id.
| HTTP | code | When |
|---|---|---|
| 401 | missing_key | No Authorization / X-Api-Key |
| 401 | invalid_key | Secret does not match a stored hash |
| 401 | revoked_key | Key was revoked |
| 403 | api_disabled | Partner does not have API access enabled |
| 403 | ip_not_allowed | Connecting IP is not on the key allowlist |
| 403 | partner_inactive | Partner account is not active |
| 403 | partner_pending | Partner application is still under review |
| 403 | insufficient_scope | Key is missing the required scope |
| 409 | idempotency_mismatch | Idempotency-Key reused with a different body |
| 413 | payload_too_large | JSON over 256 KB |
| 415 | unsupported_media_type | POST without application/json |
| 422 | validation_error | Required fields missing or invalid |
| 429 | rate_limited | Too many requests |
| 404 | not_found | Unknown path, or deal is not yours |
| 400 | no_files / file_* | Upload rejected (see documents) |
| 500 | create_failed etc. | Unexpected server error — retry with backoff + Idempotency-Key |
A deal that is not credited to your partner code looks like not_found. We do not confirm that the id exists for someone else.
GET /deals returns the newest deals first.
| Query | Default | Max |
|---|---|---|
limit | 50 | 100 |
offset | 0 | 10000 |
status | — | exact match, 50 chars |
source | all (live) | partner_api or portal |
The response includes count, limit, offset, and has_more. When has_more is true, request the next page with offset += limit.
/pingConfirms the key, environment, and partner code. No scope required. Use this as a health check from your CRM.
curl https://www.loanableusa.com/api/v1/partner/ping \ -H "Authorization: Bearer lnb_test_YOUR_SECRET"
{
"ok": true,
"environment": "test",
"partner_code": "CP1234",
"company": "Northstar Capital",
"request_id": "…"
}
/deals scope deals:writeCreates an application credited to your partner code. Returns 201 on insert, 200 when external_id already exists.
curl -X POST https://www.loanableusa.com/api/v1/partner/deals \
-H "Authorization: Bearer lnb_live_YOUR_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9f3a1c02-88d1-4b0e-9c1a-2e7b4d6a1100" \
-d '{
"external_id": "crm-88421",
"product_type": "working_capital",
"business": {
"name": "Acme Services",
"legal_name": "Acme Services LLC",
"entity_type": "LLC",
"industry": "Retail",
"years_in_business": "4",
"ein": "12-3456789",
"address": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701",
"monthly_revenue": 45000
},
"owner": {
"first_name": "Jane",
"last_name": "Owner",
"email": "jane@acme.com",
"phone": "5125550100",
"ssn": "123-45-6789",
"dob": "1982-04-12",
"ownership_percent": 100,
"credit_score": "680"
},
"funding": {
"amount": 75000,
"purpose": "working capital",
"urgency": "immediately"
},
"consents": { "credit_auth": true, "terms": true },
"notes": "Existing customer for 3 years"
}'
A successful create returns the public deal object (no SSN/EIN), plus application_id and tracking_code. Save id — you need it to attach documents and to poll status.
Live posts send applicant personal information, including SSN and EIN when you provide them. Those values are encrypted at rest and are never returned on GET, list, or webhooks. Use of the API is covered by your Referral Partner Agreement, the Data Processing Addendum, and our Privacy Policy and Terms of Service.
Three shapes are accepted. Unknown keys are dropped (they are not stored). SSN and EIN never go into unstructured additional_data.
| Field | Required | Notes |
|---|---|---|
external_id | Recommended | Your CRM id, max 80 chars, unique per partner + environment |
product_type | No | See product types below. Default general |
notes | No | Max 5,000 characters |
business.name | Yes | DBA / operating name |
business.legal_name | No | |
business.entity_type | No | e.g. LLC, Corp, Sole Prop |
business.industry | No | |
business.years_in_business | No | String is fine ("4") |
business.ein | No | 9 digits; encrypted; never returned |
business.address | No | String or { line1, city, state, zip } |
business.city / state / zip | No | State max 50 chars |
business.monthly_revenue | No | Number or "45,000" |
owner.first_name / last_name | Yes | |
owner.email | Yes | Must look like an email. Live deals email the applicant a signup link |
owner.phone | Yes | At least 10 digits |
owner.ssn | No | 9 digits when provided; encrypted; never returned |
owner.dob | No | YYYY-MM-DD. Invalid dates are dropped (the deal still creates) |
owner.ownership_percent | No | Defaults to 100 |
owner.credit_score | No | String, max 20 |
funding.amount | Yes | Positive number. Personal-loan products are capped at $100,000 |
funding.purpose | No | Comma-separated values are split into a list |
funding.urgency | No | e.g. immediately |
consents.credit_auth | Live yes | Applicant authorized a credit pull. This is not SMS consent. |
consents.terms | Live yes | Applicant agreed to Loanable terms |
consents.sms | No | Set true only if the applicant checked a dedicated SMS opt-in (frequency, HELP, STOP disclosed; not required to apply). Loanable will not text them otherwise. consents.transactional is an alias. |
Aliases collapse to these keys: general, working_capital, mca, term_loan, line_of_credit, equipment, sba, personal, personal_loan, commercial_re, real_estate, ironfund_re, bridge, fix_and_flip.
Examples: "Working Capital" → working_capital, "SBA 7(a)" → sba, "personal loan" → personal_loan.
If you already post businessName, firstName, lastName, email / clientEmail, phone / clientPhone, fundingAmount, ssn, ein, we map those the same way as nested JSON.
If the body has personal[], business.business_name / business_legal_name, or funding.funding_amount_requested, we treat it as a Lendflow-style workflow payload and map:
| Inbound | Loanable |
|---|---|
business.business_dba / business_name | business.name |
business.business_legal_name | business.legal_name |
business.business_ein | business.ein |
business.business_addresses[0] | address / city / state / zip |
personal[0].personal_first_name etc. | owner.* |
personal[0].personal_ssn_itin.ssn | owner.ssn |
funding.funding_amount_requested | funding.amount |
external_id or client_tracking_token | external_id |
You still add consents for live posts. A Lendflow body without consents succeeds on a test key and returns 422 on a live key.
/deals scope deals:readcurl "https://www.loanableusa.com/api/v1/partner/deals?limit=50&offset=0&source=partner_api" \ -H "Authorization: Bearer lnb_live_YOUR_SECRET"
{
"deals": [ { "id": "APP-…", "status": "pending", "external_id": "crm-88421", "sandbox": false } ],
"count": 1,
"limit": 50,
"offset": 0,
"has_more": false
}
List rows include contact fields (email/phone) for deals credited to you. They do not include SSN, EIN, offers, or file bytes.
/deals/{id} scope deals:readReturns the public deal plus:
Common status values include pending, in_review, submitted, funded, and declined. Treat status as an opaque string and display it; new values can appear without a version bump.
/deals/{id}/documents scope documents:writeMultipart only. Repeat the files field (or documents) once per file. Max 10 files, 10 MB each.
curl -X POST https://www.loanableusa.com/api/v1/partner/deals/APP-…/documents \ -H "Authorization: Bearer lnb_live_YOUR_SECRET" \ -F "files=@bank-statement.pdf" \ -F "files=@id.jpg"
We inspect magic bytes, not just the extension. Allowed: PDF, JPEG, PNG, TIFF, DOC, DOCX, XLS, XLSX, CSV, TXT.
Rejected with 400:
| code | Meaning |
|---|---|
no_files | Neither files nor documents was attached |
empty_file | Zero-byte upload |
file_too_large | Over 10 MB |
file_type_rejected | Extension not on the allowlist |
extension_mismatch | Name says PDF, contents are not |
executable_or_markup | PE/ELF, HTML, SVG, script |
archive_or_unknown_zip | Zip that is not a valid DOCX/XLSX |
unrecognized_type | Contents not an allowed document |
You cannot download stored file bytes through the Partner API.
Optional. Set an HTTPS URL in the Partner Portal (or ask Loanable to set it). We POST signed JSON when a deal you own is created, its status changes, or documents are added.
| Event | When |
|---|---|
deal.created | After a successful API create |
deal.updated | When the application status (or related fields) change in Loanable |
deal.document_added | After documents are stored on a deal you own |
Content-Type: application/json User-Agent: Loanable-PartnerWebhook/1.0 X-Loanable-Event: deal.updated X-Loanable-Timestamp: 1710000000 X-Loanable-Signature: t=1710000000,v1=hex_hmac_sha256
{
"id": "evt_1710000000_dealupdated",
"type": "deal.updated",
"created": 1710000000,
"data": {
"deal": {
"id": "APP-…",
"tracking_code": "TC-…",
"status": "funded",
"business_name": "Acme Services LLC",
"owner_name": "Jane Owner",
"email": "jane@acme.com",
"phone": "5125550100",
"funding_amount": 75000,
"external_id": "crm-88421",
"sandbox": false
}
}
}
Compute HMAC-SHA256(webhook_secret, timestamp + "." + raw_body) using the raw bytes of the JSON (do not re-serialize). Compare the hex digest to v1 with a constant-time equals. Reject the request if:
Node.js:
const crypto = require('crypto');
function verifyLoanableWebhook(secret, signatureHeader, rawBody, now = Math.floor(Date.now() / 1000)) {
const t = /(?:^|,)\s*t=(\d+)/.exec(signatureHeader);
const v1 = /(?:^|,)\s*v1=([a-f0-9]+)/i.exec(signatureHeader);
if (!t || !v1) return false;
if (Math.abs(now - Number(t[1])) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(t[1] + '.' + rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(v1[1].toLowerCase(), 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python:
import hmac, hashlib, time, re
def verify_loanable_webhook(secret, header, raw_body, now=None):
now = int(time.time() if now is None else now)
t = re.search(r'(?:^|,)\s*t=(\d+)', header)
v1 = re.search(r'(?:^|,)\s*v1=([a-fA-F0-9]+)', header)
if not t or not v1:
return False
if abs(now - int(t.group(1))) > 300:
return False
expected = hmac.new(secret.encode(), f"{t.group(1)}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1.group(1).lower())
data.deal.id + type..internal, and cloud metadata hosts are blocked (SSRF).Each key can be locked to a list of IPv4 addresses, IPv4 CIDRs (for example 203.0.113.0/24), and exact IPv6 addresses. CIDR matching is IPv4-only today.
The address we check is the TCP peer of the connection to Loanable — not X-Forwarded-For. If you call us through a NAT or reverse proxy, allowlist the egress IP that we actually see. An empty allowlist means any IP may use the key (still over TLS, still hashed).
Mismatch returns 403 ip_not_allowed.
Keys issued today include:
| Scope | Endpoints |
|---|---|
deals:write | POST /deals |
deals:read | GET /deals, GET /deals/{id} |
documents:write | POST /deals/{id}/documents |
Missing scope → 403 insufficient_scope. GET /ping does not require a scope. A future key may be issued with a subset; do not assume every key can write.
PARTNER_API_PEPPER). A database dump is not enough to impersonate a partner.Smith & Jones LLC are not entity-encoded into the file.More on platform security: loanableusa.com/security.
const res = await fetch('https://www.loanableusa.com/api/v1/partner/deals', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.LOANABLE_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.code + ' ' + err.request_id + ' ' + err.error);
}
const body = await res.json();
console.log(body.deal.id);
import os, uuid, requests
r = requests.post(
'https://www.loanableusa.com/api/v1/partner/deals',
headers={
'Authorization': 'Bearer ' + os.environ['LOANABLE_API_KEY'],
'Idempotency-Key': str(uuid.uuid4()),
},
json=payload,
timeout=30,
)
r.raise_for_status()
print(r.json()['deal']['id'])
$ch = curl_init('https://www.loanableusa.com/api/v1/partner/deals');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('LOANABLE_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
There is no official SDK. Import the OpenAPI document into Insomnia, Postman, or your generator of choice. Keep the collection private.
| Date | Change |
|---|---|
| 2026-08-26 | Expanded public docs and OpenAPI. GET /deals returns offset, has_more, and optional source. Unauthenticated GET /api/v1/partner index. Webhook delivery retries once on timeout/429/5xx. JSON 256 KB cap enforced at parse time. |
| 2026-08 | v1 launch: hashed keys issued by Loanable, sandbox vs live, file scanning, signed webhooks, IP allowlists. |
request_id, HTTP status, and code.Loanable is a dba of Nextgen Capital Solutions LLC (Nevada). Loanable is not a lender. The Partner API is for approved referral partners only.