The shape of an agent
- Webhooks are the triggers. Subscribe to the events you care about and treat each delivery as “something changed on deal X”. Never poll for changes.
- Reads give context.
GET /v1/deals/{id}?include=business,people,offers,submissions,documents,activityreturns the full picture in one call. - Lender matching is the recommender. You do not need to encode lender criteria in your agent; the location already maintains them.
- Writes are idempotent. Every action that sends something takes an
Idempotency-Key, so an agent that crashes and restarts cannot email a lender twice. - Merchant follow-up is one call. Portal links and document requests reach the merchant through the channels the location already has.
Building blocks
Triggers
See Webhooks for verification and retries.
Recommender
GET /v1/deals/{id}/lender-matches runs the location’s criteria against the deal and explains itself.
view=recommendedis the shortlist: score at or above the location threshold with the data to support it.view=eligibleadds lenders that clear every hard gate but score lower. Use it when the shortlist is empty.auto_eligibleis the flag to gate automation on. It means recommended and no unknown gates, so nothing about the deal could flip the result once the missing data arrives.checks[]carries one entry per criterion withkind(gateorfactor), what the lender requires, what the deal has, and whether it passed or was skipped for lack of data. It is written to be shown to a user or pasted into a prompt.unknown_gatesandmeta.deal.missing_fieldstell you what to ask the merchant for.
Submitting
POST /v1/deals/{id}/submissions with lender_ids, an optional document_ids list, and an Idempotency-Key. Use the webhook event id that triggered the decision as the key: the same event can then never produce two sends. Pass "send": false to record a proposed submission for a rep to send from the app.
Merchant follow-up
POST /v1/deals/{id}/portal-linkwithsend_viaemail,smsornoneissues a 24-hour magic link to the merchant portal, where the merchant can upload documents and see shared offers.sent,sent_viaandsent_toin the response tell you whether it went out; a failed send still returns the link withsent: falseandmeta.warning.expires_in_daysis accepted but ignored in v1. Requiresportal:send.POST /v1/deals/{id}/document-requestsasks for specific documents. See Documents and uploads.
Inbound calls and texts
When an unknown number calls or texts, resolve it first. Encode the+ as %2B.
Response 200
GET /v1/people/{id} adds their businesses (with role and ownership) and the deals they own:
Response 200
GET /v1/deals/{id}?include=offers,submissions,documents for what to say. An empty data array from the lookup means a genuinely new contact; create the person with POST /v1/people and continue.
Guardrails
- Scopes. Give the agent’s key only the scopes its job needs. A reader that summarises deals needs no write scope at all. A missing scope is a clear
403withcode: "missing_scope", which an agent can surface as “I am not allowed to do that here”. - Roles. Prefer
manager(the default); it cannot change settings or the team. Useuserfor an agent that should only see the deals it created. - Idempotency everywhere. Any
POSTthat sends email or SMS gets anIdempotency-Keyderived from the triggering event. Retries then replay instead of re-sending. - Rate limits. 300 requests per minute per credential. Fan out across deals with a queue, not a loop; a
429carriesRetry-After. - Stage rules.
POST /v1/deals/{id}/stageenforces the board’s rules; a refused move returns400withcode: "stage_rule"and the same message a rep would see. Do not work around it. - Human approval where money moves. Submit automatically only for
auto_eligiblematches; route the rest, and every offer decision, to a rep. - Log request ids. Store
X-Request-Idwith every action so a wrong send can be traced.
Worked example
An underwriting agent that runs from “deal created” to “offer picked”.1
deal.created arrives
Fetch the deal with
?include=business,people,documents. Run GET /v1/deals/{id}/lender-matches?view=recommended. If meta.deal.completeness_percent is low or no bank statements are on file, request them:2
statement.analyzed arrives
Read
GET /v1/deals/{id}/statement-analysis for NSFs, negative days and positions. Re-run lender matches. Filter to auto_eligible: true, sort by score and approval_rate, keep the top three.3
Submit
4
offer.received arrives
GET /v1/deals/{id}/offers, compare payback and daily payment against the positions from statement analysis, and create a task for the rep with the comparison (POST /v1/deals/{id}/tasks). The rep picks in the app, or the agent calls POST /v1/offers/{id}/primary when the rule is explicit.5
Send the merchant the portal link
document.uploaded brings the loop back to the agent.6
Later: advance.renewal_ready
POST /v1/deals/{id}/renewal creates the linked renewal deal on the same business, and the loop starts again from deal.created.