One header, two key modes
Every request is authenticated with your partner API key in the Authorization header (or x-api-key). Live keys start with efp_live_ and create real deals; test keys start with efp_test_ and never write anything — see Sandbox. The only route that needs no key is GET /v1/openapi.json, which serves the machine-readable spec.
Authorization: Bearer efp_live_<your-key>Ping, submit, track — three calls
curl "https://mddbqtczelajfpsnviaz.supabase.co/functions/v1/partner-api-v1/v1/ping" \
-H "Authorization: Bearer $EF_API_KEY"deal.jsonInclude documents inline (base64) and set auto_submit: true to validate and submit in one call, or leave it off to create a draft you finish later. The Idempotency-Key header makes retries safe.
curl -X POST "https://mddbqtczelajfpsnviaz.supabase.co/functions/v1/partner-api-v1/v1/submissions" \
-H "Authorization: Bearer $EF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-10442-attempt-1" \
--data @deal.json{
"external_ref": "CRM-10442",
"product_type": "mca",
"business": {
"legalName": "Blue Harbor Logistics LLC",
"dba": "Blue Harbor",
"entityType": "LLC",
"industry": "Transportation",
"inceptionDate": "03/2019",
"phone": "3055550123",
"monthlyRevenueEstimate": 85000,
"ein": "123456789",
"address": { "line1": "450 Port Blvd", "city": "Miami", "state": "FL", "zip": "33132" }
},
"owners": [{
"firstName": "Jordan",
"lastName": "Rivera",
"email": "[email protected]",
"phone": "3055550124",
"ownershipPct": 100,
"ssn": "123456789",
"dob": "1984-06-02",
"ficoMidpoint": 680,
"address": { "line1": "88 Coral Way", "city": "Miami", "state": "FL", "zip": "33145" }
}],
"funding": { "requestedAmount": 75000, "useOfFunds": "Equipment and inventory", "openPositions": 0, "openBalance": 0 },
"attestation": {
"originalAppOnFile": true,
"consentCredit": true,
"consentTcpa": true,
"consentEsign": true,
"signatureName": "Jordan Rivera",
"signedAt": "2026-09-15T14:30:00Z"
},
"documents": [
{ "kind": "statement", "ym": "2026-08", "filename": "aug-2026.pdf", "content_base64": "JVBERi0xLjc..." }
],
"auto_submit": false
}# attach more documents while the submission is a draft
curl -X POST "https://mddbqtczelajfpsnviaz.supabase.co/functions/v1/partner-api-v1/v1/submissions/$SUBMISSION_ID/documents" \
-H "Authorization: Bearer $EF_API_KEY" -H "Content-Type: application/json" \
--data '{"kind":"statement","ym":"2026-07","filename":"jul-2026.pdf","content_base64":"JVBERi0xLjc..."}'
# run the validation gates and submit the deal
curl -X POST "https://mddbqtczelajfpsnviaz.supabase.co/functions/v1/partner-api-v1/v1/submissions/$SUBMISSION_ID/submit" \
-H "Authorization: Bearer $EF_API_KEY"From here, poll GET /v1/submissions/{id} for status and timeline — or skip polling entirely and register a webhook. A 422 from /submit lists exactly what is missing; fix it and call /submit again.
Endpoints
All paths are relative to the base URL. Every route except GET /v1/openapi.json requires your API key.
| Endpoint | What it does |
|---|---|
| GET/v1/ping | Verify your key. Returns partner name, key mode, and your requests used this hour. |
| GET/v1/products | Products, their document requirements, and — with ?state=FL — the exact bank-statement count required for that state. |
| POST/v1/submissions | Create a deal submission. Honors Idempotency-Key; accepts inline documents and auto_submit. |
| GET/v1/submissions | List your submissions. Filters: status, created_after, external_ref, limit (max 100), offset. |
| GET/v1/submissions/{id} | One submission: current status, documents, and a status timeline. |
| POST/v1/submissions/{id}/documents | Attach documents to a draft (single document or a documents array). |
| POST/v1/submissions/{id}/submit | Run the full validation gates and submit the deal. 422 lists exactly what is missing. |
| POST/v1/webhooks | Register a webhook endpoint (HTTPS only, max 3 active). The signing secret is shown once, at creation. |
| GET/v1/webhooks | List your webhook endpoints. |
| DELETE/v1/webhooks/{id} | Disable a webhook endpoint. |
| POST/v1/webhooks/{id}/test | Send a signed sample event to your endpoint. |
| GET/v1/openapi.json | The machine-readable OpenAPI 3.1 spec. No auth required. |
Supported product_type values: mca (default), term_loan, loc, sba, equipment, heloc.
Documents
Documents are sent inline as base64 — up to 8 MB per file, 20 files per submission, and a 25 MB request body (split large batches across multiple /documents calls). Bank statements are required for all products; the count varies by state, so call GET /v1/products?state=XX for the exact requirement.
| kind | What it is | Accepted files | Extra fields |
|---|---|---|---|
| statement | A monthly bank statement. | .pdf .png .jpg .jpeg | ym — the statement month as YYYY-MM (optional) |
| mtd | Month-to-date bank activity for the current month. | .pdf .png .jpg .jpeg | — |
| signed_application | The merchant-signed application on file with you. | .pdf only | — |
| extra | Any additional product-specific document. | .pdf .png .jpg .jpeg .xls .xlsx .doc .docx .csv | doc_key — required; valid keys per product from GET /v1/products |
Statuses
Once a deal is submitted, it moves through six partner-safe statuses. The API returns both a machine status code and a human status_label.
| status | status_label | Meaning |
|---|---|---|
| received | Submitted | Deal received by Elite Funders. |
| under_review | In review | Underwriting is reviewing the file. |
| approved | Approved | An offer has been approved for the merchant. |
| contracts_out | Contracts out | Contracts have been sent for signature. |
| funded | Funded | The deal funded. |
| declined | Declined | The deal was declined. |
auto_submit has status draft. Test-mode submissions that pass validation get test_accepted instead of entering the pipeline — see Sandbox.Webhooks
Register an HTTPS endpoint and we POST you a signed event whenever a deal moves. Events: submission.received, submission.under_review, submission.approved, submission.declined, submission.funded, and submission.offer_expiring. Failed deliveries are retried 5 times over ~14 hours; an endpoint is auto-disabled after 20 consecutive failures. Use POST /v1/webhooks/{id}/test to fire a signed sample event any time.
POST https://yourapp.example/webhooks/elitefunders
X-Elite-Event: submission.approved
X-Elite-Signature: t=1789574400,v1=5f8a2c…
{
"id": "7f0c5a4e-91d2-4c33-8f0a-2f6f6f1f2a9b",
"event": "submission.approved",
"mode": "live",
"data": {
"submission_id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"external_ref": "CRM-10442",
"business_name": "Blue Harbor Logistics LLC",
"status": "approved",
"status_label": "Approved"
},
"created_at": "2026-09-16T18:00:00.000Z"
}Verifying signatures
Every delivery carries X-Elite-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256(secret, "<t>.<rawBody>") using the signing secret shown once when you registered the endpoint. Reject the delivery if the timestamp is more than 300 seconds off, or if the signature does not match. Always sign the raw request body — parse the JSON only after verification.
const crypto = require("node:crypto");
function verifyEliteSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("="))
);
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const given = String(parts.v1 || "");
return (
given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))
);
}
// Express example — keep the raw body for signing
app.post("/webhooks/elitefunders", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyEliteSignature(req.body.toString("utf8"), req.get("X-Elite-Signature") || "", process.env.ELITE_WEBHOOK_SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body);
// handle event.event / event.data …
res.sendStatus(200);
});import hmac, hashlib, time
def verify_elite_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > 300:
return False
expected = hmac.new(
secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
# Flask example — sign the raw body, parse JSON only after verifying
@app.route("/webhooks/elitefunders", methods=["POST"])
def elite_webhook():
if not verify_elite_signature(
request.get_data(),
request.headers.get("X-Elite-Signature", ""),
os.environ["ELITE_WEBHOOK_SECRET"],
):
return "bad signature", 400
event = request.get_json()
# handle event["event"] / event["data"] …
return "", 200Errors & rate limits
Errors come back as a consistent JSON envelope with a machine-readable code and a request_id — include the request_id when contacting support. Every response also carries it in the X-Request-Id header.
{
"error": {
"code": "validation_failed",
"message": "Submission did not pass validation gates. Fix the listed items and call /submit again.",
"details": { "errors": ["…"], "warnings": ["…"] }
},
"request_id": "a1b2c3d4-…"
}| HTTP | code | When |
|---|---|---|
| 401 | missing_api_key / invalid_api_key | No key, malformed key, or an unknown/revoked key. |
| 403 | partner_inactive | Your partner account is not active. |
| 404 | not_found | Unknown route, or a submission/webhook that isn't yours. |
| 409 | duplicate_external_ref / not_a_draft | An external_ref you already used, or documents added after submit. |
| 413 | payload_too_large | Request body over 25 MB — split documents across multiple /documents calls. |
| 422 | validation_failed, invalid_document, too_many_documents, … | The response details list exactly what to fix. |
| 429 | rate_limited | Hourly limit exceeded. Honor the Retry-After header. |
| 5xx | internal_error / engine_error | Our side. Retry later, or contact us with your request_id. |
Rate limits are per key, per hour — one limit for requests overall and a separate one for submission creates, both sized to your account. GET /v1/ping reports your usage for the current hour. Use Idempotency-Key on POST /v1/submissions so a retried request returns the original submission instead of creating a duplicate.
Sandbox — test keys
Test keys (efp_test_) run the entire flow against the real validation gates but never create a real application or deal. Use one to integrate end-to-end before you switch to live:
- Create a submission and attach documents exactly as you would live.
- Call
/submit— the full validation gates run. Failures return the same422with the same error list a live key would see. - On success the submission gets status
test_acceptedand nothing enters the pipeline. - Register a webhook and use
POST /v1/webhooks/{id}/testto receive a signed sample event (delivered with"mode": "test") and prove out your signature verification. - Swap in your
efp_live_key. No other code changes.
Getting access
The Partner API is available to approved ISO partners with a signed ISO agreement. API keys are issued manually by our partnerships team — email us with a note about your platform and we'll get you set up with a test key.