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

# Find a candidate and get recommended roles

> Look up one of your CRM candidates, then get the roles Paraform recommends you submit them to.

This takes two calls. The first finds the candidate and returns their `candidateId`. The second returns the roles Paraform recommends for that candidate. You need a personal key with `candidates:read` and `roles:read`.

## Find the candidate

[`GET /recruiter/candidates`](/recruiter/api-reference/list-recruiter-candidates) returns the candidates in your own CRM, newest first. It never includes candidates held by your agency teammates, even if you're an agency owner. Filter it down to one person.

```bash theme={null}
# By LinkedIn profile link
curl -G "https://external-api.paraform.com/api/external/v1/recruiter/candidates" \
  -H "Authorization: Bearer $PARAFORM_API_KEY" \
  --data-urlencode "linkedinUrl=https://www.linkedin.com/in/jane-doe"

# By email
curl -G "https://external-api.paraform.com/api/external/v1/recruiter/candidates" \
  -H "Authorization: Bearer $PARAFORM_API_KEY" \
  --data-urlencode "email=jane@example.com"
```

Each row has a `candidateId`. Keep it for the next call.

| Parameter | Use |
| - | - |
| `linkedinUrl` | The candidate's profile link, `linkedin.com/in/<handle>`. Any casing works, with or without `https://` and `www.`. Other LinkedIn links return `400` with `code` `invalid_linkedin_url`. |
| `email` | An exact, case-insensitive match on an email you can see for the candidate |
| `query` | Free-text search over name, headline, location, and employer names |
| `tags`, `relationshipStatus` | Match any of the values you send. Repeat the parameter to send several. |

Filters combine with AND. If nothing matches, you get an empty list, not an error. Rows never include an email address. Hidden and archived candidates are left out. See [Pagination](/pagination) for paging through more than one page.

## Get recommended roles

[`GET /recruiter/candidates/{candidateId}/recommended-roles`](/recruiter/api-reference/list-recommended-roles-for-candidate) returns the roles Paraform recommends you submit the candidate to, strongest first.

```bash theme={null}
curl "https://external-api.paraform.com/api/external/v1/recruiter/candidates/$CANDIDATE_ID/recommended-roles" \
  -H "Authorization: Bearer $PARAFORM_API_KEY"
```

The candidate must be in your own CRM. Any other `candidateId`, including a teammate's, returns `404`.

### Wait while Paraform generates

The first time you ask for a candidate, Paraform may need to build their recommendations. The response then has `status` set to `generating` and an empty `roles` list:

```json theme={null}
{
  "candidateId": "…",
  "status": "generating",
  "roles": [],
  "message": "We're generating recommendations for this candidate. Try again in about 15 to 30 seconds.",
  "retryAfterSeconds": 20
}
```

Wait `retryAfterSeconds`, then repeat the same request. Repeating it is safe. A `status` of `ready` means the roles are final for now.

<Note>
  `ready` with an empty `roles` list means Paraform finished and found no available recommendations. It doesn't mean no role could ever fit. Fall back to [`GET /recruiter/roles`](/recruiter/guides/browse-roles) with filters that match the candidate.
</Note>

Stored recommendations are served as they are, and refreshed in the background once they're about a week old.

### What each role includes

Each role has the same fields as [`GET /recruiter/roles`](/recruiter/api-reference/list-recruiter-roles), including `myRole`, `fee`, and `urls`, plus a `recommendation`:

| Field | What it tells you |
| - | - |
| `recommendation.band` | `ENDORSED` is a strong match. `SUGGESTED` is worth a look. |
| `recommendation.fit` | The reasoning: a verdict (`rating` and `label`), a `summary`, and each requirement with its assessment, reasoning, and evidence from the candidate's profile. `complete` is `false` while parts are still being written. |

`recommendation.fit` is `null` when no saved analysis exists yet. The role is still a valid recommendation, and a later request can fill the analysis in.

The list is short, at most 20 roles. It only includes roles you can see and could submit this candidate to. If your agency hides payment from members, every `fee` amount is `null`. Treat `null` as hidden, never as zero.

## Errors

Errors you can fix carry `details.candidatesUrl`, a link to the Candidates page, and name it in the message. See [Errors](/errors) for the envelope.

| Status | `code` | What to do |
| - | - | - |
| `404` | none | The candidate isn't in your CRM, or not one you're allowed to see. Check the `candidateId`. |
| `422` | `candidate_profile_incomplete` | `details.missing` lists `resume`, `preferences`, or both. Add what's missing, then try again. |
| `422` | `candidate_resume_unreadable` | Paraform couldn't read the resume. Upload it again, then try again. |
| `422` | `candidate_not_recommendable` | Paraform can't recommend roles for this candidate. There's nothing to fix. |
| `503` | `recommendations_unavailable` | Generation failed repeatedly for this candidate in the last day. This isn't "no matches", so don't treat it as an empty result. Try again later. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.