# Matchmaking

Complete a Project's Criteria, calculate funding recommendations, and create a Deal for a chosen recipient. Start with an existing Project; to create a business and its client-visible funding request first, follow [Find funding matches](https://docs.staging.levr.ai/guides/source-matchmaking.md).

A Project can stay private to your Portal; client matchmaking access is not required for that workflow. If you [add a client funding request](https://docs.staging.levr.ai/guides/source-business.md#enable-the-clients-matchmaking-experience), it shares the Project's Criteria and matching work. Matching produces recommendations; creating and submitting a Deal are later steps.

## Before you start

Use a server-side API key with access to this Project and `projects:read`, `project_criteria:write`, and `project_matching:write`. Creating a Deal additionally needs `deals:write`. The [Deal guide](https://docs.staging.levr.ai/guides/deal-application.md) explains how to complete and submit the application with `deal_applications:write`.

The receiving Portal must have matchmaking enabled and published recipient Criteria available to compare against. If the project has a linked client funding request, the business must still have an active relationship with your Portal.

Every request includes `Authorization: Bearer <API_KEY>`. JSON writes use `Content-Type: application/json`. Give each mutation its own `Idempotency-Key` and retain the key and body for retries. Examples use illustrative IDs and abbreviated responses. Replace them with the IDs and URLs your API returns; [Get started](https://docs.staging.levr.ai/rest-api.md) provides the API origin.

## 1. Read the questions and current answers

Read the Project to get its business ID and application links:

```http
GET /v1/projects/proj_example/
```

**Response · selected fields**

```json
{
  "id": "proj_example",
  "business": { "id": "biz_example" },
  "contract_url": "https://YOUR_API_HOST/v1/projects/proj_example/matchmaking-application/contract/",
  "answers_url": "https://YOUR_API_HOST/v1/projects/proj_example/matchmaking-application/answers/"
}
```

Save `business.id` for Deal creation, then follow the two application URLs:

```http
GET /v1/projects/proj_example/matchmaking-application/contract/
GET /v1/projects/proj_example/matchmaking-application/answers/
```

The contract describes each question's exact `block_key`, `field_key`, value type, and available operations. A question might look like this **within the returned `questions` array**:

```json
{
  "label": "Requested funding",
  "block_key": "<amount-block-key>",
  "fields": [
    {
      "field_key": "<amount-field-key>",
      "value_schema": { "type": "string" }
    }
  ]
}
```

These keys illustrate the structure; they are not universal field names. Use your contract's keys and constraints. Complete the relevant business and funding information, including conditional questions and repeated entries.

Save the answers response's `ETag` header for the next write. Its `answers_revision` identifies the version you reviewed. Answers are paginated: follow `next_cursor` until it is null when reviewing the full application.

## 2. Save or edit Criteria

Send only the values you want to change. This example assumes the contract accepts the amount as a string.

```http
PATCH /v1/projects/proj_example/matchmaking-application/answers/
If-Match: <ETag from the answers GET>
Idempotency-Key: equipment-criteria-1
```

```json
{
  "updates": [
    {
      "block_key": "<amount-block-key>",
      "field_key": "<amount-field-key>",
      "value": "250000"
    }
  ]
}
```

**Response · 200, selected fields**

```json
{
  "answers_revision": 2,
  "updated_answers": [
    {
      "block_key": "<amount-block-key>",
      "field_key": "<amount-field-key>",
      "value": "250000"
    }
  ]
}
```

The response confirms the saved values and includes the new `ETag`. Use it for your next edit; you do not need another read just to confirm the write. To correct the amount later, send the corrected value with the latest ETag and a new key. On `412`, read and reconcile concurrent edits before sending another update.

For rows such as owners or assets, use the contract's `repeatable_items_url` and retain each returned `item_id`. A row's position is not its identity.

## 3. Attach supporting documents

Choose a file question from the contract. Request an upload using `operations.file_uploads_url`:

```http
POST <operations.file_uploads_url>
```

```json
{
  "block_key": "<financial-statements-block-key>"
}
```

For a file inside a repeated row, also send its `item_id`.

**Response · selected fields**

```json
{
  "id": "upl_example",
  "upload_url": "https://YOUR_API_HOST/v1/projects/proj_example/matchmaking-application/answers/files/upl_example/",
  "multipart_field": "file",
  "max_size_bytes": 26214400
}
```

Follow the returned URL and size limit; do not construct the URL from this example. The upload URL expires at the response's `expires_at`. Upload the file bytes as multipart, with the latest answers ETag:

```sh
curl -X POST "$UPLOAD_URL" \
  -H "Authorization: Bearer $LEVR_API_KEY" \
  -H "If-Match: $ANSWERS_ETAG" \
  -H "Idempotency-Key: equipment-statements-1" \
  -F "file=@financial-statements.pdf"
```

Let your HTTP client set the multipart boundary. A successful upload returns the attached `file`, the new `answers_revision`, and its `ETag`. Keep that revision for matching. Use `operations.files_url` when you need to list attachments; file bytes do not go in the answer JSON.

If the upload response is lost, retry the same URL with the same key, filename, bytes, and original `If-Match`. Requesting a new upload URL starts a separate transfer and can attach a duplicate. If an unused URL expires, request a new one.

## 4. Start matching

Review the saved Criteria, then send their current `answers_revision`:

```http
POST /v1/projects/proj_example/matchmaking-runs/
Idempotency-Key: equipment-matching-1
```

```json
{
  "expected_answers_revision": 3
}
```

**Response · 202, selected fields**

```json
{
  "id": "pmrun_example",
  "status": "queued",
  "answers_revision": 3,
  "url": "https://YOUR_API_HOST/v1/project-matchmaking-runs/pmrun_example/",
  "results_url": "https://YOUR_API_HOST/v1/project-matchmaking-runs/pmrun_example/results/"
}
```

Save the run ID and URLs before continuing. Retry a lost response with the same key and body to recover the same run. A Project can have only one active calculation; do not start another while your saved run is in progress.

## 5. Wait for results

Poll the returned `url` while the status is `queued` or `running`. Space requests apart and respect rate-limit responses.

```http
GET /v1/project-matchmaking-runs/pmrun_example/
```

**Response · selected fields**

```json
{
  "id": "pmrun_example",
  "status": "completed",
  "is_stale": false,
  "impactful_missing_fields": [],
  "results_url": "https://YOUR_API_HOST/v1/project-matchmaking-runs/pmrun_example/results/"
}
```

Alternatively, configure a Webhook Destination for `project_matchmaking_run.status_changed` before starting the run. Follow [Webhook setup and signature verification](https://docs.staging.levr.ai/rest-api-reference/webhooks/). This event reports settled statuses: `completed`, `partially_complete`, `failed`, or `cancelled`. It does not report every progress update.

**Webhook · selected fields**

```json
{
  "type": "project_matchmaking_run.status_changed",
  "data": {
    "object": {
      "id": "pmrun_example",
      "project": "proj_example",
      "status": "completed"
    }
  }
}
```

Verify the signature, deduplicate by the event ID, and fetch the run for its current state. Keep polling as a recovery path. For `partially_complete`, review the available results and unresolved failures before using them. A failed or cancelled run is not a completed assessment.

### Stop or resume a calculation

To stop an active calculation, POST to `/v1/project-matchmaking-runs/{run_id}/cancel/` with a new `Idempotency-Key` and an empty JSON body (`{}`). Poll the run to confirm its status. Cancellation preserves any results that have already completed.

To resume unresolved work on a failed or cancelled run, POST to `/v1/project-matchmaking-runs/{run_id}/retry/` with `{}` and a new `Idempotency-Key`. The response is `202`; keep polling the same run. Retry uses that run's original Criteria and recipient snapshot. If you changed the Criteria, start a new calculation after the old one settles instead.

A second calculation while one is active is rejected. A shortlist write is also rejected if the recipient's package version differs from the version you reviewed. Read the current run or results and review the change before trying again.

Use webhooks or REST polling for integration updates. The Portal handles its own websocket connection.

## 6. Review recommendations

Fetch `results_url`. When `next_cursor` is non-null, pass its value as the next request's `cursor` query parameter, preserving the same filters and page size. Continue until it is null.

```http
GET /v1/project-matchmaking-runs/pmrun_example/results/
```

**Response · selected fields**

```json
{
  "items": [
    {
      "id": "pmres_example",
      "recipient": { "id": "northstar-finance", "label": "Northstar Finance" },
      "classification": "match",
      "package_version": 2,
      "availability": "available",
      "is_selected": false
    }
  ],
  "next_cursor": null
}
```

Review each result's evaluation and `availability`, including missing information and uncertainty. Classifications include `match`, `partial_fit`, `insufficient_information`, `not_a_match`, and `not_assessed`. An empty list does not establish a suitable match. If the run is stale, review the changed Criteria and start a new run after the current run settles.

## 7. Save a shortlist

Read the selections for this run and save **that response's ETag**. Selections have their own revision; do not reuse the answers ETag.

```http
GET /v1/projects/proj_example/matchmaking-selections/?run_id=pmrun_example
```

Then add the reviewed result, using its `id` and `package_version`:

```http
PATCH /v1/projects/proj_example/matchmaking-selections/
If-Match: <ETag from the selections GET>
Idempotency-Key: equipment-shortlist-1
```

```json
{
  "run_id": "pmrun_example",
  "additions": [
    {
      "match_result_id": "pmres_example",
      "expected_package_version": 2
    }
  ]
}
```

The response contains the updated selections and revision. Saving a selection records a shortlist choice; it does not create a Deal. This step is optional if your integration goes straight from a reviewed result to Deal creation.

## 8. Create and submit a Deal

For a chosen, available recipient, pass the result's **`recipient.id`** to Deal creation. The match result ID used for selections is a different identifier.

```http
POST /v1/deals/
Idempotency-Key: equipment-northstar-deal-1
```

```json
{
  "business_id": "biz_example",
  "project_id": "proj_example",
  "recipient_id": "northstar-finance"
}
```

Create one Deal per intended recipient and save each response. Then follow [Complete and submit a Deal](https://docs.staging.levr.ai/guides/deal-application.md) to read its application, fill any remaining answers, upload required documents, check readiness, and submit. A match does not establish approval or funding.

In the Portal, open the Project to review Criteria, results, and selections; open the created Deal for its application and submission state. For MCP integrations, use the [Matchmaking tools](https://docs.staging.levr.ai/mcp/resources/matchmaking.md) and discover their live contracts rather than translating REST paths into tool names.

[REST operations](https://docs.staging.levr.ai/rest-api-reference/projects/index.md) · [MCP tools](https://docs.staging.levr.ai/mcp/resources/matchmaking.md)
