The best MCA underwriting API is not the one with the most endpoints. It is the one that lets every ISO submit through a single, well-authenticated front door, accepts the same deal twice without creating two files, processes documents asynchronously, reports progress through signed webhooks, and returns evidence an underwriter can check, while leaving the funding decision to a person.
This guide is the technical design view for funders and platform teams building or buying that layer. It covers the resource model, authentication options when dozens of ISOs submit, idempotency and cross-ISO duplicate detection, document upload, the status lifecycle, webhook signing, versioning, rate limits, error contracts, sandboxes, audit logging and PII handling. For where the API sits in the wider stack, see our guide to merchant cash advance software, which includes a shorter buyer's checklist. For the operational side of intake, read automating MCA submission intake. Code samples below are illustrative and are not the API of any provider, including Cevrynt, which does not publish a public API.
What is the best API for MCA underwriting?
There is no single best API for MCA underwriting, because the term covers three different interfaces: the submission API that ISOs call, the analysis API a funder calls to process documents, and the status API that reports progress back. Judge each one by identity, idempotency, asynchronous design, evidence in the output and a clear line where the human decision sits.
A broker-facing MCA submission platform is mostly about intake: who is submitting, what they sent and whether it is complete. An underwriting analysis interface is about turning a package of statements, applications and IDs into structured findings with sources. A status interface is about telling the right ISO, and only that ISO, what happened to their deal. Some funders run all three behind one gateway. Others buy analysis from a provider and build the ISO-facing parts themselves. Either way, the design questions are the same.
- Submission API (ISO to funder). Accepts deals, merchants, owners and documents from ISOs and partner platforms. Owns authentication, tenant isolation, validation and duplicate detection.
- Analysis API (funder to processing). Takes a package and returns extracted fields, bank-statement metrics, verification results and flags, each linked to the page it came from.
- Status API and webhooks (funder to ISO). Reports lifecycle changes, stip requests and recorded decisions to the submitting partner, with fields filtered to what that partner may see.
Most funders converge on the shape below: many partners, one gateway that knows which ISO each request belongs to, a queue, and status events back to the submitting ISO.
What resource model should an MCA submission API use?
Model the deal, the merchant, the owners, the documents, the submission event and the submitting partner as separate resources. Keeping them separate is what lets you detect duplicates, re-submit stips, track which ISO owns a deal and keep a clean audit trail.
A single `application` object with nested files works until the same merchant arrives twice, a stip arrives a week later, or a platform starts sending for several ISOs. A slightly richer model avoids those rewrites:
- Partner. The ISO, broker or platform that holds credentials. Carries status (active, suspended), allowed scopes, webhook endpoints and, for platforms, the list of ISOs it may submit for.
- Merchant. The business: legal name, DBA, EIN, entity type, address, industry, start date. A merchant can have many deals over time, including renewals.
- Owner. Each person with an ownership stake or signing authority, with ownership percentage. Sensitive identifiers live here and get the strictest handling.
- Deal. One funding request for one merchant: amount requested, use of funds, existing positions declared by the ISO, and the lifecycle state.
- Submission. The act of a partner sending a deal, with timestamp, partner, idempotency key and the payload version. One deal can collect several submissions, for example an original and a resubmission with stips.
- Document. A file with type, period covered (for statements), hash, size, scan status and the submission it arrived with.
- Stip. A request for more information: what was asked, of whom, when, and which document satisfied it.
- Event. An immutable record of each state change, which doubles as the source for webhooks and the audit log.
Why the submission and the deal are separate objects
Separating the act of sending from the thing being sent is what makes idempotency and duplicate handling tractable. A retried request maps to the same submission. A new submission for an existing deal (the ISO is answering a stip) attaches to that deal instead of opening a new one. A submission from a second ISO for a merchant you already have can be held for review without touching the original deal.
Store every party's IDs
Accept the partner's own reference (`external_ref`) on deals and submissions, return it on every response and every webhook, and index it per partner. ISOs reconcile against their CRM IDs, not yours. Never assume external references are unique across partners: two ISOs can both send `deal-1001`. Your own IDs should be opaque and non-sequential so they cannot be guessed, which matters for the authorization problems covered below.
POST /v1/submissions HTTP/1.1
Authorization: Bearer <access token issued to partner prt_7Q2>
Idempotency-Key: "8d3c1f0a-5b7e-4c1d-9a64-2f0e7b1c9d21"
Content-Type: application/json
{
"iso_id": "iso_C",
"external_ref": "crm-deal-4821",
"merchant": {
"legal_name": "Cedar & Stone LLC",
"ein_last4": "4417",
"entity_type": "llc",
"state": "NJ"
},
"owners": [{ "owner_ref": "own-1", "ownership_pct": 100 }],
"deal": { "amount_requested": 60000, "currency": "USD" },
"declared_positions": 1,
"document_ids": ["doc_9KX", "doc_9KY", "doc_9KZ"]
}
HTTP/1.1 202 Accepted
Location: /v1/submissions/sub_01JB7
Content-Type: application/json
{
"submission_id": "sub_01JB7",
"deal_id": "dl_01JB6",
"external_ref": "crm-deal-4821",
"status": "received",
"status_url": "/v1/submissions/sub_01JB7"
}Two details in that example matter. The request carries an `iso_id`, which only makes sense because this caller is a partner platform allowed to submit for several ISOs; a direct ISO client would not send it. And the response is `202 Accepted`, not `200` with results, because nothing has been analyzed yet. Figures and identifiers are illustrative; Cedar & Stone LLC is our standing illustrative merchant.
How do MCA lenders let multiple ISOs submit through one API without per-ISO keys?
Issue one credential to the partner platform, require an ISO identifier on every request, and check that identifier against the list of ISOs the partner is allowed to represent before doing anything else. Each submission is then stored, scoped and reported per ISO, even though only one secret exists.
That pattern exists because the alternative, a static API key for every ISO, gets unmanageable once ISOs come and go weekly and many of them never touch an API themselves; they work through a CRM or submission platform that does. There are four realistic options, and most funders end up running two of them side by side.
Option 1: one API key per ISO
Simple to explain and to revoke. The costs are operational. Keys are long-lived bearer secrets, they leak into email threads and spreadsheets, rotation needs coordination with every ISO, and you need a secure way to hand each key over. Store only a hash of each key, show it once at creation, give it a recognizable prefix so leaked keys can be found by scanners, and support two active keys per ISO so rotation does not cause downtime.
Option 2: OAuth 2.0 client credentials per ISO
The OAuth 2.0 client credentials grant (RFC 6749, section 4.4) lets a client exchange its own credentials for a short-lived access token. The RFC says this grant must only be used by confidential clients, meaning a server that can keep a secret, and that a refresh token should not be included in the response. That fits ISO back ends and CRMs well. You get short-lived tokens, scopes such as `submissions:write` and `status:read`, and a central place to disable a client. The cost is running an authorization server, or using one, and onboarding ISOs who have never handled OAuth.
Option 3: a partner-scoped credential with an ISO identifier
This is the answer to the per-ISO key question. A platform that serves many ISOs gets one credential (an OAuth client or a key). Every request names the ISO, either in the body, in a header, or as a claim in a token you issue for that ISO. The gateway checks the pair: is this partner allowed to submit for this ISO, right now? If yes, the submission is recorded with both identities. If an ISO leaves the platform or is suspended by the funder, you remove it from the partner's allow-list without rotating anything. If the platform itself is compromised, one revocation cuts off everything it sends.
The risk is trust. The funder is relying on the platform to assert the right ISO, so the allow-list check must happen server-side on every request, and the platform's contract should cover how it authenticates its own ISO users. Log both identities on every event so a dispute about who submitted a deal, and who is owed a commission, can be settled from the record.
Option 4: signed requests or mutual TLS
For high-volume or high-assurance partners, add proof of possession: mutual TLS with a client certificate, or requests signed with a key the partner holds. A stolen bearer token alone is then not enough to submit. It layers on top of options 2 or 3 and adds key lifecycle work on both sides, so it is usually reserved for the largest integrations.
Tenant isolation is the real security boundary
Authentication tells you who is calling. Authorization decides what they can see, and this is where submission APIs most often fail. The OWASP API Security Top 10 (2023) lists broken object level authorization as API1: endpoints that accept an object ID without checking that the caller may access that object. In an MCA context, that is ISO B reading ISO A's deal by changing an ID in the URL. Every query should be filtered by the caller's tenant at the data layer, not only in a controller, and tests should try cross-tenant reads deliberately. OWASP's API3, broken object property level authorization, is the related failure where the object is correct but the fields are not: an ISO should see status, stips and decline reason codes, never internal credit notes, other ISOs' activity or the funder's scoring detail.
How should submissions handle idempotency and duplicate deals across ISOs?
Use an idempotency key so a retried request never creates a second submission, and run a separate duplicate-deal check on merchant identity so the same business sent by two ISOs is caught. The first is a transport problem with a precise answer; the second is a business rule your ISO agreements should define.
Idempotency keys for retries
Networks drop responses. An ISO's system that times out waiting for your `202` will retry, and without protection you get two submissions and, often, two underwriters working the same file. The IETF HTTP APIs working group's Idempotency-Key header draft (still an Internet-Draft, not a finished standard) describes a pattern worth adopting. The client sends a unique key, typically a UUID, and must not reuse it for a different payload. The draft suggests a `400` when a required key is missing, a `422` when a key is reused with a different payload, and a `409` when a retry arrives while the original is still being processed. When the original has finished, the server responds consistently with that completed request. Servers may expire keys, and the draft says that expiry policy should be published.
- Scope keys per partner, so two partners using the same UUID never collide.
- Store a fingerprint of the request body with the key, so a reused key with a changed payload is detected rather than silently replayed.
- Write the key and the submission in the same transaction, or a crash between them recreates the duplicate you were trying to prevent.
- Publish the retention window, for example 24 hours or 7 days, and make it longer than any partner's retry schedule.
Duplicate-deal detection across ISOs
Idempotency does nothing when two different ISOs submit the same merchant, because the requests are genuinely different. That is a matching problem. Match on normalized identity: EIN where provided, normalized legal name and DBA, address, owner identity, and the bank account the statements come from. Exact EIN plus bank account is a strong match; a fuzzy name match on its own is a weak one that should go to a person. Matching also surfaces something underwriters care about for its own reasons, which is the same merchant shopping several funders at once; our guide to detecting stacking in MCA underwriting covers that side.
Worked example, illustrative: ISO A submits Cedar & Stone LLC on Monday at 10:02. On Wednesday a partner platform submits the same EIN and the same operating account for ISO C. The gateway accepts the second submission (it is a valid request), the matcher links it to the existing merchant, and the deal is held with a `possible_duplicate` flag for the funder's ISO desk. What happens next is a policy choice, usually set in ISO agreements: first complete submission wins, the merchant's signed authorization decides, or the funder reviews case by case. The API's job is to make that rule enforceable and visible in the audit log, not to decide it.
Be careful what the second ISO is told. Confirming that another broker already submitted the merchant leaks one partner's pipeline to another. Many funders return a neutral status to the second ISO and handle the conversation directly. Whatever you choose, document it and apply it the same way every time.
How should document upload work: multipart or pre-signed URLs?
For small files, a multipart upload to your API is simplest. For large statement packages, pre-signed URLs that let the partner upload directly to object storage scale better. In both cases, files should land in quarantine and be scanned, type-checked and hashed before any processing reads them.
An MCA package is mostly PDFs: an application, several months of bank statements, an ID, a voided check or bank letter, sometimes an existing MCA agreement. A single package can run to well over a hundred pages, and scanned statements make individual files large.
- Multipart to your API. One request, easy for ISOs, and your gateway sees every byte. It also means your API servers carry the bandwidth and memory, and long uploads tie up connections. Reasonable when files are small and volumes modest.
- Pre-signed URLs. The partner asks for an upload slot (`POST /v1/documents` with type, size and hash), you return a short-lived URL scoped to one object, the partner uploads directly to storage, then confirms. Your API stays light. You must enforce the declared size and type at the storage layer and expire unused slots.
Whichever transport you choose, the server-side pipeline should be the same. Set and publish limits for file size, pages per file and files per submission; OWASP lists unrestricted resource consumption as API4, and an upload endpoint without limits is the easiest way to hit it. Check the real file type from its bytes, not the extension or the declared content type. Compute a SHA-256 hash so identical files are recognized and the evidence can be shown to be unchanged later. Scan for malware before any parser touches the file, and keep files in a quarantine state until the scan passes. Reject encrypted PDFs with a clear error code rather than failing silently in analysis. None of this replaces document forensics; whether a statement has been edited is a separate question, covered in our guide to document fraud detection in underwriting.
What does an MCA submission status lifecycle look like?
A useful lifecycle has a small number of states that every party understands: received, validated, processing, in review, stips requested and decision recorded, plus withdrawn or expired as terminal exits. Each transition is an event, and the decision state is only ever set by a person at the funder.
A few rules keep a lifecycle useful over time:
- Transitions are explicit. Define which moves are allowed (in review to stips requested, yes; decision recorded back to processing, no) and reject the rest in code, not by convention.
- Validation is not underwriting. Validated means the package is complete enough to process: required fields present, statement months covered, files readable. It says nothing about the merchant's quality.
- Processing has sub-steps, not more states. Extraction, bank-statement analysis, verification and policy checks can each report progress as events without multiplying top-level states that every ISO integration must handle.
- Stips loop back. New documents return the deal to processing, then review. Track the stip so turnaround time can be split into waiting-on-ISO and waiting-on-funder.
- Decision recorded carries reason codes. Approved or declined, with codes the ISO can act on. Offers, contracts and funding belong to later systems with their own events.
Turnaround is usually lost in the gaps between these states rather than inside them. Our piece on underwriting turnaround time and fragmented review looks at where that time goes.
How should status webhooks be signed, retried and made idempotent?
Sign every webhook with a per-partner secret over a timestamp and the raw body, retry failed deliveries with backoff, and give every event a unique ID so receivers can ignore repeats. Receivers should verify, de-duplicate, acknowledge quickly and process from a queue.
Webhooks are the better default for ISOs, with a status endpoint kept for recovery. The sending side needs a handful of properties:
- Per-partner secrets. Each partner gets its own signing secret, so one leaked secret does not let an attacker forge events to everyone. Support two active secrets during rotation.
- Signed timestamp. Include the send time inside the signed content so receivers can reject old messages that are being replayed.
- At-least-once delivery. Retry on timeouts and non-2xx responses with exponential backoff over a published window, then mark the endpoint as failing and alert the partner.
- Event IDs and sequence. Delivery order is not guaranteed under retries. Include an event ID for de-duplication and a per-deal sequence number or `occurred_at` so receivers can discard stale updates.
- Thin payloads. Send the event type, IDs, the new state and reason codes. Do not push owner identifiers or bank details in webhook bodies; let authorized receivers fetch details through the API.
On the receiving side, the order of operations is what most implementations get wrong. Parsing JSON first and verifying later breaks the signature, because re-serialized JSON rarely matches the original bytes. The sequence below is the safe order.
// Illustrative only. Header format assumed: "t=<unix seconds>,v1=<hex HMAC>"
import crypto from "node:crypto";
import express from "express";
const TOLERANCE_SECONDS = 300; // pick and publish your own window
export function verifySignature(rawBody, header, secrets, nowMs = Date.now()) {
const parts = Object.fromEntries(
header.split(",").map((pair) => pair.trim().split("=", 2)),
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(nowMs / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const received = Buffer.from(parts.v1 || "", "hex");
const signed = Buffer.concat([Buffer.from(timestamp + "."), rawBody]);
// Two secrets can be active while a partner rotates.
return secrets.some((secret) => {
const expected = crypto.createHmac("sha256", secret).update(signed).digest();
return received.length === expected.length &&
crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post("/webhooks/funder-status",
express.raw({ type: "application/json" }), // keep the raw bytes
async (req, res) => {
const secrets = [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET_PREVIOUS]
.filter(Boolean);
if (!verifySignature(req.body, req.get("X-Signature") || "", secrets)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (await alreadyProcessed(event.id)) return res.sendStatus(200);
await enqueue(event); // do the real work outside the request
return res.sendStatus(200);
});`alreadyProcessed` and `enqueue` stand in for your own storage and queue. The same principles apply to payment webhooks, and our engineering article on webhook idempotency and retries goes deeper on receivers in general.
How should versioning, rate limits and error contracts work?
Version the schema so partners can upgrade on their own schedule, publish per-partner rate limits with `429` and `Retry-After`, and return errors in one machine-readable format with stable codes. These three contracts decide how expensive your API is to integrate and to support.
Versioned schemas
ISOs integrate once and rarely revisit their code, so breaking changes are costly. Pick one versioning scheme (a major version in the path, or a dated version pinned per partner) and treat additive changes as safe: new optional fields, new event types that receivers can ignore, new enum values only if you told partners to expect unknown values. Renaming, removing or changing the meaning of a field is a new version. Run old versions in parallel until a published deprecation date. OWASP's API9, improper inventory management, is the risk of forgotten old versions and debug endpoints that nobody patches. Version the outputs too: metric definitions and policy rule sets change, and a finding should record which definition and which policy version produced it.
Rate limits
Limit per partner, not only per IP, and separately for cheap reads, submissions and uploads, because a burst of large uploads costs far more than status checks. RFC 6585 defines `429 Too Many Requests` for rate limiting and says the response may include a `Retry-After` header saying how long to wait. Return it, document the limits, and give partner platforms higher quotas that reflect the number of ISOs behind them.
Error contracts
Use one error format everywhere. RFC 9457, Problem Details for HTTP APIs, defines a standard JSON object for machine-readable error details (with members such as `type`, `title`, `status`, `detail` and `instance`) so each API does not invent its own. Add a stable error code and, for validation failures, a list of fields with a reason for each, so an ISO's system can show the broker exactly what to fix. Separate the categories clearly: malformed requests (`400`), authentication (`401`), authorization (`403`), not found (`404`, also used when an object exists but belongs to another tenant, so existence is not leaked), conflicts (`409`), semantic validation (`422`) and rate limits (`429`). Business outcomes such as a decline are not errors; they are states with reason codes.
What should a sandbox, audit log and PII policy cover?
A sandbox should behave like production with synthetic data and triggerable failures; the audit log should record who did what to which deal, through which credential; and PII handling should minimize what moves, mask what is logged and delete on a published schedule.
Sandbox
Provide separate credentials and hosts for the sandbox, synthetic merchants and statement PDFs, and deterministic triggers so a partner can force each path: a missing statement month, an unreadable file, a stip request, a duplicate, a recorded approval and a recorded decline. Webhooks should fire from the sandbox exactly as they will in production, including retries and signature checks. Never copy production data into a sandbox to make it realistic.
Audit logging
Every API action should leave an append-only record: the partner, the ISO, the credential or client used, the action, the object, the time, the request ID and the result. Combine that with the underwriting record, meaning which documents were read, which findings were produced and who recorded the decision, and you can answer the questions that matter later: who submitted this deal first, what did the underwriter see, and when. Our article on underwriting audit trails and evidence-linked decisions covers the decision side of that record.
PII handling
An MCA submission carries owner names, dates of birth, Social Security numbers, home addresses and bank account details. Practical rules: encrypt in transit and at rest; never put personal data in URLs or query strings, where it ends up in logs; mask sensitive fields in application logs and error messages; return full identifiers only to callers that need them, and only last-four elsewhere; keep webhooks free of personal data; and publish retention and deletion terms. Privacy and data-security obligations depend on the funder's state footprint, its contracts and its own commitments, and they change, so confirm the specific requirements with counsel rather than relying on an API checklist. When your API also calls third-party data providers, treat their responses as untrusted input too; OWASP lists unsafe consumption of APIs as API10.
What can real-time lender underwriting integration actually mean?
In MCA underwriting, real time can honestly mean immediate acknowledgment, immediate validation feedback and status updates pushed the moment anything changes. It cannot mean an instant funding decision when a person reviews each file, because the decision takes as long as that review takes.
Searches for the best APIs for instant credit decisioning in lending workflows usually blur three different speeds. It helps to name them separately when you design, buy or market an integration:
- Request time (synchronous). Authentication, schema validation, the idempotency check and a `202` with an ID. This should be fast every time, and an ISO should know immediately if a field is missing.
- Processing time (asynchronous). Document extraction, bank-statement analysis, verification lookups and policy checks. It depends on page counts, file quality and third-party response times, so promise status events rather than a fixed duration.
- Decision time (human). An underwriter reviews the findings, asks for stips or records a decision. Software can make this faster by organizing evidence and highlighting exceptions, but it is not an API latency, and it should not be presented as one.
Some funders run automated pre-screens that return an outside-guidelines response quickly, for example when the requested amount or time in business falls outside their published box. That is the lender's own policy applied as a filter, and the lender owns both the rules and the outcome. If an API is described as making instant decisions, ask what is actually being decided, by whose rules, and where a person reviews exceptions. The difference between rules that flag and a person who decides is covered in our piece on human-in-the-loop AI underwriting, and the difference between rule engines and underwriting systems in decision engine vs underwriting engine.
How should you evaluate MCA underwriting API providers?
Evaluate MCA underwriting API providers by running your own representative files through their sandbox and testing the failure paths, not by reading endpoint lists.
- Can two of our ISOs, or a platform acting for several, submit without sharing a credential, and can we revoke one without affecting the others?
- What happens if we send the same request twice with the same idempotency key, and with the same key but a changed body?
- How are duplicate merchants detected across partners, and can we set the ownership rule ourselves?
- What are the file size, page and file-count limits, and what happens to an encrypted or unreadable PDF?
- How are webhooks signed, how long are retries attempted, and can we replay events after an outage?
- Does every metric and flag point back to a document and page, and do outputs carry definition and policy versions?
- Where in the output is the human decision recorded, and can the provider's system ever set it?
- What is logged, what is masked, where is data stored, and how is deletion requested and confirmed?
- How are breaking changes announced, and how long do old versions run?
Weigh evidence on every finding heavily. Our guide to source-linked extraction explains what that looks like in practice, and the broker-side view of what a complete package contains is in how brokers and ISOs submit cleaner deals.
From here, this article is about Cevrynt
How Cevrynt approaches submission and underwriting integrations
Cevrynt does not publish a public API. It is an MCA platform for funders and alternative lenders: AI-assisted underwriting that turns borrower documents and business signals into evidence-linked analysis for human underwriters, across intake, documents, financials, verification, fraud signals, policy and the memo, plus a built-in MCA CRM. That CRM already holds ISO and broker records and submissions per ISO, alongside the deal pipeline, offers and contracts, and funding and servicing, so funders can take ISO submissions straight into it. Where a funder keeps existing systems, as our integrations page explains, connections are scoped around a defined job before or during a pilot: receive the deal, read the approved evidence and return agreed underwriting outputs through the appropriate API, webhook, CRM, LOS, inbox or file-source path, and nothing broader than the workflow requires.
In practice, that means proving the underwriting on representative files first and then building the smallest connection that removes a real hand-off. Findings link back to the source page through Document Intelligence, policy results come back as flags and exceptions for a person through the Policy Engine, and the decision stays with the lender's underwriters. Cevrynt is not a lender: offers come from the lender's own pricing and terms and go out only when the lender decides. The platform overview, security approach and brokers and ISOs page cover the rest.
If you are designing an ISO submission flow and want to see how evidence-linked underwriting would sit behind it, book a walkthrough or talk to our team at sales@cevrynt.com.
