# convertCandidate

Source: /openapi/thomas-federated.openapi.json

## Operation

| Method | URL |
|---|---|
| POST | https://api.thomas.co/v1/candidates/{id}/convert |

| Field | Value |
|---|---|
| operationId | convertCandidate |
| method | POST |
| server | https://api.thomas.co |
| path | /v1/candidates/{id}/convert |
| tags | `Recruitment` |
| summary | Convert a candidate to a colleague |
| badges | `{"name":"Admin"}`, `{"name":"Owner"}` |
| scopes | `users:w` |

## Description

Turn a hired candidate into a colleague, atomically and one-way. `keepHidden: true` keeps the new colleague out of the directory until revealed. A Partner API client requires a tenant context, `users:w` and the tenant’s `users` source-of-record claim, and may send an optional `Idempotency-Key`.

## Request Parameters

| Name | In | Required | Schema | Description |
|---|---|---|---|---|
| id | path | true | string |  |
| Idempotency-Key | header | false | string | An opaque client-generated key, 1-255 visible ASCII characters excluding comma. Optional, and recommended for every write. Repeating a request with the same key and the same canonical request returns the ORIGINAL response instead of performing the work twice. Reusing the key for a DIFFERENT canonical request is refused with 409 IDEMPOTENCY_KEY_REUSED, and an equivalent request still in flight is refused with 409 IDEMPOTENCY_REQUEST_IN_PROGRESS and a Retry-After header. Omit it and the operation behaves exactly as it does without this header: the work is performed, and no replay protection applies. Sending the header with an EMPTY value counts as omitting it; any other malformed value — a repeated header, a comma-separated pair, or a value outside the charset or length above — is refused with 400 IDEMPOTENCY_KEY_INVALID. |
| Thomas-On-Behalf-Of | header | false | string | Optional for a Partner API client on this operation. Send it to act on behalf of one named person: thomas:<companyIndividualId> or partner:<partnerReference>. The call is then authorised as that person, within that person's access circles and the scopes your Grant holds. Omit it to call as your API client. A signed-in person never sends it. |
| Thomas-Tenant-Id | header | true | string:uuid | Required for API clients: the Tenant this call acts in. Send exactly one. Signed-in users don't send it — their Tenant comes from their session. The header only selects the Tenant; your Grant for it decides access, and is checked on every call. |

## Request Body

| Content type | Required | Schema refs |
|---|---|---|
| application/json | true | #/components/schemas/ConvertCandidateDto |

## Responses

| Status | Description | Schema refs |
|---|---|---|
| 200 | The candidate was converted | #/components/schemas/ConvertCandidateResponseDto |
| 400 | Invalid UUID format for id parameter. A Partner may also send a malformed tenant or person selector. The Idempotency-Key header may also be sent in a shape that cannot be bound. Also returned when the Thomas-On-Behalf-Of header is repeated or malformed. | #/components/schemas/ApiErrorResponseDto |
| 401 | Invalid or missing authentication token (AUTH_TOKEN_INVALID) | #/components/schemas/ApiErrorResponseDto |
| 403 | Refused when the caller has insufficient role (owner or admin required), envelope `AUTH_INSUFFICIENT_ROLE`, or when the tenant’s effective plan tier does not include the Candidates capability, envelope `INSUFFICIENT_TIER` — a Partner API client is held to the tenant’s plan too. A Partner API client that is not the tenant’s active `users` source of record is refused with `AUTH_SOURCE_OF_RECORD_REQUIRED`: converting creates an employee. An API client is refused before any tenant data is read or changed when its tenant context, Grant or scope does not authorise the operation (AUTH_TENANT_NOT_GRANTED), when its service membership is absent (AUTH_SERVICE_MEMBERSHIP_MISSING), or when the operation is not published to its principal kind (PRINCIPAL_TYPE_NOT_ALLOWED). An API client holds no organisation role, so AUTH_INSUFFICIENT_ROLE never applies to it. | #/components/schemas/ApiErrorResponseDto |
| 404 | The id resolves to no candidate this caller may act on in this tenant. A missing id and another tenant’s id answer identically. On behalf of a person, a candidate outside that person’s access circles answers identically too, and an unavailable named person is refused. Also returned when the person named in Thomas-On-Behalf-Of is unavailable. | #/components/schemas/ApiErrorResponseDto |
| 409 | The candidate is already a colleague (CANDIDATE_ALREADY_CONVERTED), or a concurrent change aborted the conversion (CANDIDATE_CONVERT_CONFLICT). The Idempotency-Key may also conflict with an attempt already recorded against it. | #/components/schemas/ApiErrorResponseDto |
| 429 | Rate limit exceeded. | #/components/schemas/ApiErrorResponseDto |

### Response 409 headers

| Name | Description | Schema |
|---|---|---|
| Retry-After | Present as `1` only when an equivalent idempotent request is in progress | {"type":"string","example":"1"} |

### Response 429 headers

| Name | Description | Schema |
|---|---|---|
| Retry-After | Seconds until the rate-limit block expires before retrying. | {"type":"integer","minimum":0} |
| RateLimit-Limit | Configured request limit for the active rate-limit window. | {"type":"integer"} |
| RateLimit-Remaining | Requests remaining in the active rate-limit window; zero when blocked. | {"type":"integer"} |
| RateLimit-Reset | Seconds until the rate-limit block expires. | {"type":"integer","minimum":0} |

## Artifact Examples

### Response 400 example — HTTP_BAD_REQUEST

```json
{
  "statusCode": 400,
  "message": "Validation failed (uuid is expected)",
  "error": "HTTP_BAD_REQUEST"
}
```

### Response 400 example — AUTH_TENANT_CONTEXT_INVALID (Malformed person selector)

```json
{
  "statusCode": 400,
  "message": "Exactly one valid Thomas-On-Behalf-Of header is required",
  "error": "AUTH_TENANT_CONTEXT_INVALID"
}
```

### Response 400 example — AUTH_TENANT_CONTEXT_INVALID (Missing or malformed tenant selector)

```json
{
  "statusCode": 400,
  "message": "Exactly one valid Thomas-Tenant-Id header is required",
  "error": "AUTH_TENANT_CONTEXT_INVALID"
}
```

### Response 400 example — IDEMPOTENCY_KEY_INVALID

```json
{
  "statusCode": 400,
  "message": "Exactly one valid Idempotency-Key header is required",
  "error": "IDEMPOTENCY_KEY_INVALID"
}
```

### Response 401 example

```json
{
  "statusCode": 401,
  "message": "Invalid or missing authentication token",
  "error": "AUTH_TOKEN_INVALID"
}
```

### Response 403 example — AUTH_INSUFFICIENT_ROLE

```json
{
  "statusCode": 403,
  "message": "Insufficient role privileges",
  "error": "AUTH_INSUFFICIENT_ROLE"
}
```

### Response 403 example — AUTH_TENANT_NOT_GRANTED (Tenant not granted)

```json
{
  "statusCode": 403,
  "message": "API client is not granted access to this tenant",
  "error": "AUTH_TENANT_NOT_GRANTED"
}
```

### Response 403 example — PRINCIPAL_TYPE_NOT_ALLOWED (Principal kind not allowed)

```json
{
  "statusCode": 403,
  "message": "On-behalf access is not permitted for this operation",
  "error": "PRINCIPAL_TYPE_NOT_ALLOWED"
}
```

### Response 403 example — AUTH_TENANT_NOT_GRANTED (No Grant or scope for this tenant)

```json
{
  "statusCode": 403,
  "message": "API client is not granted access to this tenant",
  "error": "AUTH_TENANT_NOT_GRANTED"
}
```

### Response 403 example — AUTH_SERVICE_MEMBERSHIP_MISSING

```json
{
  "statusCode": 403,
  "message": "API client has no active service membership in this tenant",
  "error": "AUTH_SERVICE_MEMBERSHIP_MISSING"
}
```

### Response 403 example — PRINCIPAL_TYPE_NOT_ALLOWED (This operation is not published to the calling principal kind)

```json
{
  "statusCode": 403,
  "message": "API client is not permitted to call this operation",
  "error": "PRINCIPAL_TYPE_NOT_ALLOWED"
}
```

### Response 404 example — CANDIDATE_CONVERT_NOT_FOUND

```json
{
  "statusCode": 404,
  "message": "Candidate not found",
  "error": "CANDIDATE_CONVERT_NOT_FOUND"
}
```

### Response 404 example — AUTH_ON_BEHALF_PERSON_NOT_AVAILABLE

```json
{
  "statusCode": 404,
  "message": "The named person is not available for this operation",
  "error": "AUTH_ON_BEHALF_PERSON_NOT_AVAILABLE"
}
```

### Response 409 example — CANDIDATE_ALREADY_CONVERTED

```json
{
  "statusCode": 409,
  "message": "Candidate is no longer convertible",
  "error": "CANDIDATE_ALREADY_CONVERTED"
}
```

### Response 409 example — CANDIDATE_CONVERT_CONFLICT

```json
{
  "statusCode": 409,
  "message": "Candidate 3f1c8f0e-5a4b-4c1d-9e2f-6a7b8c9d0e1f could not be converted: a concurrent change aborted the transaction and nothing was written. Retry the request.",
  "error": "CANDIDATE_CONVERT_CONFLICT"
}
```

### Response 409 example — IDEMPOTENCY_KEY_REUSED

```json
{
  "statusCode": 409,
  "message": "This Idempotency-Key was already used for a different request",
  "error": "IDEMPOTENCY_KEY_REUSED"
}
```

### Response 409 example — IDEMPOTENCY_REQUEST_IN_PROGRESS

```json
{
  "statusCode": 409,
  "message": "An equivalent request is already in progress",
  "error": "IDEMPOTENCY_REQUEST_IN_PROGRESS"
}
```

### Response 429 example

```json
{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests",
  "error": "HTTP_TOO_MANY_REQUESTS"
}
```
