> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nextlevelmca.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Building an AI-first workflow

> How to run an agent on top of the API: webhooks as triggers, lender matches as the recommender, idempotent writes and the right guardrails.

The API was designed so an agent can run a deal desk: notice what changed, decide what to do, act, and hand off to a human where judgement is needed. This page shows how the pieces fit and walks through one complete loop. For a chat-driven version of the same thing, see the [MCP server](/mcp/overview).

## The shape of an agent

```mermaid theme={null}
flowchart LR
  W[Webhook event] --> R[Read context via API]
  R --> D{Decide}
  D -->|confident| A[Act via API with Idempotency-Key]
  D -->|needs a human| H[Notify a rep]
  A --> W
  H --> W
```

1. **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.
2. **Reads give context.** `GET /v1/deals/{id}?include=business,people,offers,submissions,documents,activity` returns the full picture in one call.
3. **Lender matching is the recommender.** You do not need to encode lender criteria in your agent; the location already maintains them.
4. **Writes are idempotent.** Every action that sends something takes an `Idempotency-Key`, so an agent that crashes and restarts cannot email a lender twice.
5. **Merchant follow-up is one call.** Portal links and document requests reach the merchant through the channels the location already has.

## Building blocks

### Triggers

| Event                                     | What an agent usually does                                                                                 |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `deal.created`                            | Check what is missing (`meta.deal.missing_fields` from lender matches, statement coverage) and request it. |
| `document.uploaded`, `statement.analyzed` | Re-run matching now that revenue is bank-verified.                                                         |
| `submission.responded`, `offer.received`  | Summarise the response, update the deal, notify the rep.                                                   |
| `deal.stage_changed`                      | Start or stop follow-up cadences.                                                                          |
| `advance.renewal_ready`                   | Open a renewal conversation with the merchant.                                                             |

See [Webhooks](/guides/webhooks) for verification and retries.

### Recommender

`GET /v1/deals/{id}/lender-matches` runs the location's criteria against the deal and explains itself.

* `view=recommended` is the shortlist: score at or above the location threshold with the data to support it.
* `view=eligible` adds lenders that clear every hard gate but score lower. Use it when the shortlist is empty.
* `auto_eligible` is 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 with `kind` (`gate` or `factor`), 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_gates` and `meta.deal.missing_fields` tell 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-link` with `send_via` `email`, `sms` or `none` issues a 24-hour magic link to the merchant portal, where the merchant can upload documents and see shared offers. `sent`, `sent_via` and `sent_to` in the response tell you whether it went out; a failed send still returns the link with `sent: false` and `meta.warning`. `expires_in_days` is accepted but ignored in v1. Requires `portal:send`.
* `POST /v1/deals/{id}/document-requests` asks for specific documents. See [Documents and uploads](/guides/documents-and-uploads).

### Inbound calls and texts

When an unknown number calls or texts, resolve it first. Encode the `+` as `%2B`.

```bash theme={null}
curl "$NLMCA_API/people?phone=%2B12125550123" \
  -H "Authorization: Bearer $NLMCA_KEY"
```

```json Response 200 theme={null}
{
  "data": [
    {
      "id": "5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b",
      "first_name": "Dana",
      "last_name": "Rivera",
      "full_name": "Dana Rivera",
      "email": "owner@acmeplumbing.example",
      "phone": "(212) 555-0123",
      "ssn_masked": "***-**-6789",
      "status": "active",
      "created_at": "2026-09-04T14:02:11.000Z",
      "updated_at": "2026-09-04T14:02:11.000Z"
    }
  ],
  "meta": { "next_cursor": null, "has_more": false }
}
```

The list returns the person only. `GET /v1/people/{id}` adds their `businesses` (with role and ownership) and the `deals` they own:

```json Response 200 theme={null}
{
  "data": {
    "id": "5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b",
    "first_name": "Dana",
    "last_name": "Rivera",
    "full_name": "Dana Rivera",
    "email": "owner@acmeplumbing.example",
    "phone": "(212) 555-0123",
    "ssn_masked": "***-**-6789",
    "status": "active",
    "businesses": [
      {
        "business_id": "0f6b0d2e-1c3a-4b8e-9d2f-7a1e5c4b3d21",
        "legal_name": "ACME Plumbing LLC",
        "dba": "ACME Plumbing",
        "status": "active",
        "role": "owner",
        "ownership_percent": 100,
        "active": true,
        "is_primary_contact": true
      }
    ],
    "deals": [
      {
        "id": "3c9d1f70-8e5a-4d26-b1f4-2a7e6c5d4b39",
        "business_name": "ACME Plumbing LLC",
        "business_id": "0f6b0d2e-1c3a-4b8e-9d2f-7a1e5c4b3d21",
        "requested_amount": 75000,
        "status": "submitted",
        "stage": { "id": "8d7c6b5a-4e3f-4a2b-9c1d-0e9f8a7b6c5d", "label": "Submitted", "phase": "preoffer" },
        "is_primary_owner": true,
        "ownership_percent": 100,
        "created_at": "2026-09-04T14:03:40.000Z",
        "updated_at": "2026-09-04T14:06:15.000Z"
      }
    ],
    "created_at": "2026-09-04T14:02:11.000Z",
    "updated_at": "2026-09-04T14:02:11.000Z"
  },
  "meta": {}
}
```

Then `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 `403` with `code: "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. Use `user` for an agent that should only see the deals it created.
* **Idempotency everywhere.** Any `POST` that sends email or SMS gets an `Idempotency-Key` derived 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 `429` carries `Retry-After`.
* **Stage rules.** `POST /v1/deals/{id}/stage` enforces the board's rules; a refused move returns `400` with `code: "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_eligible` matches; route the rest, and every offer decision, to a rep.
* **Log request ids.** Store `X-Request-Id` with every action so a wrong send can be traced.

## Worked example

An underwriting agent that runs from "deal created" to "offer picked".

<Steps>
  <Step title="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:

    ```bash theme={null}
    curl -X POST "$NLMCA_API/deals/$DEAL/document-requests" \
      -H "Authorization: Bearer $NLMCA_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: evt_6f2c1a9e-3b4d-4c5e-8f7a-1b2c3d4e5f60" \
      -d '{ "items": [{ "type": "bank_statement" }], "channel": "sms" }'
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="Submit">
    ```bash theme={null}
    curl -X POST "$NLMCA_API/deals/$DEAL/submissions" \
      -H "Authorization: Bearer $NLMCA_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: evt_9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" \
      -d '{ "lender_ids": ["6a2d9e11-4f0b-4c7d-8e3a-9b1c2d3e4f50", "9d4b7c21-0a5e-4f13-9c8d-1e2f3a4b5c67", "2f1e0d9c-8b7a-4695-8574-63524130f0e1"] }'
    ```

    Add a note explaining the choice so the rep sees the reasoning on the deal:

    ```bash theme={null}
    curl -X POST "$NLMCA_API/deals/$DEAL/notes" \
      -H "Authorization: Bearer $NLMCA_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "content": "Auto-submitted to Northwind (88), Harbor (81), Coastal (79): all gates pass on bank-verified revenue of $41.2k/mo, 2 NSFs, 1 open position." }'
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="Send the merchant the portal link">
    ```bash theme={null}
    curl -X POST "$NLMCA_API/deals/$DEAL/portal-link" \
      -H "Authorization: Bearer $NLMCA_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: evt_0c1d2e3f-4a5b-4c6d-9e7f-8a9b0c1d2e3f" \
      -d '{ "send_via": "sms" }'
    ```

    The merchant uploads what the lender still needs; `document.uploaded` brings the loop back to the agent.
  </Step>

  <Step title="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`.
  </Step>
</Steps>


## Related topics

- [Quickstart](/getting-started/quickstart.md)
- [Get the pipeline](/api-reference/deals/get-the-pipeline.md)
- [Update an offer](/api-reference/offers/update-an-offer.md)
