/api/v1/underwrite/batch
Submit up to 50 applications in one request.
- Scope
underwrite:write- Rate limit
- 10 requests / min
- Idempotency
Idempotency-Keysupported
Authentication
Send your key as Authorization: Bearer …. It must carry the scope underwrite:write; a key without it receives 403 forbidden.
Headers
Idempotency-KeystringReplays the first response for a repeated request. Reusing a key with a different body is idempotency_key_reused (409), never a silent replay.
Request body
application/jsonrequiredapplicationsobject[]requiredEach element is the body of POST /v1/underwrite. Order is preserved.
- product_slug, productSend one of these.
product_slugis the canonical spelling;productis accepted as an alias and is ignored whenproduct_slugis present. - requested_amount, amountSend one of these, in whole dollars.
requested_amountis the canonical spelling;amountis accepted as an alias and is ignored whenrequested_amountis present. - applicant.name, applicant.first_name + applicant.last_nameSend the applicant's full
name, or the splitfirst_nameandlast_name. Whennameis present the split fields are ignored.
product_slugstring | nullrequired(one of: product_slug, product)The product's slug, as listed by GET /v1/products. Required unless product is sent.
productanyrequired(one of: product_slug, product)Alias of product_slug: a string, read only when product_slug is absent. Ignored, whatever its type, when product_slug is sent.
applicantobjectrequirednamestring | nullrequired(one of: applicant.name, applicant.first_name + applicant.last_name)Full name. Required unless first_name and/or last_name is sent.
first_nameanyrequired(one of: applicant.name, applicant.first_name + applicant.last_name)With last_name, the split spelling of name: a string; a non-string part is ignored.
last_nameanyrequired(one of: applicant.name, applicant.first_name + applicant.last_name)With first_name, the split spelling of name: a string; a non-string part is ignored.
emailstringrequiredmin length 1typestring | nullnullableIndividualBusinessindividualbusinessnullIndividual or Business, case-insensitive. Defaults from the product's category when omitted. Any other value is a 400 naming applicant.type.
phonestring | nullnullableeinstring | nullnullableEmployer Identification Number, XX-XXXXXXX or nine digits. Business applicants only. Returned on the case as applicant.ein; sending it here is what makes that field populate, and it is also written into application_data.ein.
ownerobject | nullnullableThe primary owner or personal guarantor. Typed because credit and background checks run against these values — do not put them in application_data. ssn is encrypted at rest and never returned.
first_namestringrequiredmin length 1Owner's legal first name.
last_namestringrequiredmin length 1Owner's legal last name.
ssnstringpattern ^(\d{3}-?\d{2}-?\d{4}|\d{4})$Full SSN (123-45-6789 or 123456789) or just the last four. THE FULL VALUE IS NEVER STORED: it is reduced to the last four at the request boundary and the remainder is discarded, because the bureau integration identifies on last-four plus name, date of birth and address. Never returned by any endpoint.
date_of_birthstringpattern ^\d{4}-\d{2}-\d{2}$YYYY-MM-DD. Required by most bureaus to resolve an identity.
ownership_percentnumbermin 0max 1000-100. The share of the business this owner holds.
titlestringRole at the business, e.g. CEO.
addressstringHome street address.
citystringstatestringmin length 2max length 2Two-letter US state code.
postal_codestringmetadataobject | nullnullableArbitrary key-value pairs for your own reference; stored and returned verbatim.
consentobject | nullnullableThe applicant's authorizations. PRESENCE of the object is what records consent; credit_check is what a bureau pull requires. The timestamp and the caller's IP are recorded server-side and cannot be supplied.
credit_checkbooleanTrue when the applicant authorized a consumer credit report on the owner named in applicant.owner. This is the permissible-purpose assertion the bureau pull relies on.
background_checkbooleanTrue when the applicant authorized identity and background verification.
termsbooleanTrue when the applicant accepted your terms of service.
accuracybooleanTrue when the applicant attested that the information supplied is accurate.
agreement_urlstringLink to the exact agreement text the applicant accepted. Stored for audit.
accepted_text_versionstringYour version identifier for that text, e.g. esign-v3. Stored for audit.
requested_amountnumber | string | nullrequired(one of: requested_amount, amount)WHOLE DOLLARS, never cents: 0 to 2147483647, as a number or a numeric string. Required unless amount is sent.
amountanyrequired(one of: requested_amount, amount)Alias of requested_amount: a number or a numeric string, read only when requested_amount is absent. Ignored, whatever its type, when requested_amount is sent.
application_dataobject | nullnullableThe product's form fields, read by the analysis. Every value is stored as a string; a nested object or array is JSON-encoded.
bank_linkanyThe boolean true also mints a bank-verification link and returns it as bank_link_url. Any other value (including the string "true") requests no link.
bank_verificationstring | nullnullableplaidstatementsnonenullHow this platform proves the applicant's banking. plaid mints a bank-verification link and is the same as bank_link: true. statements means you upload the bank statements yourself to POST /v1/cases/{id}/documents with category financial: no link is minted, no email is sent, and the case waits for those files rather than being analyzed immediately with nothing to analyze — the first financial upload starts the analysis. none underwrites on credit and background evidence alone. Defaults to plaid when bank_link is true, otherwise none. Sending this together with bank_link is accepted when the two agree; a contradiction (for example statements beside bank_link: true) is refused with 400 validation_error naming bank_verification, rather than one silently winning.
Refused parameters4
Sending any of these returns 400 validation_error naming the field. Each entry explains what to do instead.
webhook_url'webhook_url' is not accepted: there is no per-submission webhook destination. Register an endpoint ONCE with POST /v1/webhooks and it receives every event you subscribed to, in this key's mode. This field was previously accepted and ignored, which is why it is now refused rather than silently dropped.
webhookUrl'webhookUrl' is not accepted: there is no per-submission webhook destination. Register an endpoint ONCE with POST /v1/webhooks. The parameter is spelled 'webhook_url', and it is not accepted either.
callback_url'callback_url' is not accepted: there is no per-submission webhook destination. Register an endpoint ONCE with POST /v1/webhooks.
idempotency_key'idempotency_key' is not accepted as a body field: send it as the 'Idempotency-Key' HTTP header.
Response
201application/jsonSerializedBatchbatch_idstringrequiredstatusstringrequiredpending until every item has terminated, then completed. There is no intermediate value.
totalnumberrequiredItems submitted. Immutable: a batch never grows.
succeedednumberrequiredTerminated successfully.
failednumberrequiredTerminated in failure — at creation, or after the analysis gave up.
pendingnumberrequiredStill outstanding. total - succeeded - failed.
created_atstring | nullrequirednullablecompleted_atstring | nullrequirednullableNull until the batch completes; set once, by the fence that emits the event.
applicationsSerializedBatchItem[]requiredPER-ITEM OUTCOMES, not just a count. A batch that reported only totals
could say "12 submitted, 3 failed" and leave a partner to diff their own
request against GET /v1/cases to discover WHICH three — which defeats the
purpose of handing back a batch id instead of 50 case ids.
Errors
The statuses this operation can return, with the error.code values behind each. Every error body is the shared envelope described under Errors in Getting started.
400validation_error401unauthorized403forbidden409idempotency_key_reused429rate_limit_exceeded500internal_error