# Hire an AI Employee

Source: https://opvs.ai/docs/guides/hire-an-ai-employee

> Hire a published AI Employee for a brand.

Hire a published AI Employee for a brand. The hire is created immediately and the agent is provisioned in the background — there is no payment step in this flow.

```http
POST /api/v1/employees/{profile_id}/hire
```

`profile_id` is the UUID of a **published** catalog profile (an "AI Employee").

## Who can call it

The same endpoint serves two callers. The endpoint detects which one you are from your credentials.

**Active-brand self-serve.** A signed-in brand user hires for their own brand. The brand must have an active subscription. The brand is always taken from your session — you cannot hire on another brand's behalf.

**Admin grant.** A platform super-admin grants a hire for any brand, optionally recording a reason (for a comped / free hire). Authenticate with an admin API key (`X-Admin-Key`) or an admin dashboard session, and name the target brand in the body.

## Request

### Self-serve

The body is empty — the brand comes from your session.

```bash
curl -X POST https://api.opvs.ai/api/v1/employees/$PROFILE_ID/hire \
  -H "Cookie: $SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### Admin grant

```bash
curl -X POST https://api.opvs.ai/api/v1/employees/$PROFILE_ID/hire \
  -H "X-Admin-Key: $OPVS_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"brand_id": 11, "comp_reason": "Beta partner — comped"}'
```

### Body fields

Field

Type

Notes

`brand_id`

integer

**Admin only, required.** The brand to hire for. Ignored on the self-serve path.

`comp_reason`

string

**Admin only, optional.** Rationale for a comped hire. Ignored on the self-serve path.

## Response

```json
{
  "hire_id": "f1e2d3c4-0000-4444-8888-aaaabbbbcccc",
  "status": "provisioning",
  "onboarding_url": "/dashboard/agents/onboarding?hire=f1e2d3c4-0000-4444-8888-aaaabbbbcccc"
}
```

The hire starts in `provisioning`. The agent (marketplace skills installed, persona deployed) is built out-of-band; poll the hire or open `onboarding_url` to watch it become `active`.

## Status codes

Code

Meaning

`200`

Hire created; provisioning started.

`401`

Not authenticated.

`402`

Self-serve path: the brand's subscription is not active. Ask an admin to grant the hire, or activate a subscription.

`404`

No published employee with that `profile_id`.

`409`

This brand already holds an active hire of this employee. The body's `detail.existing_hire_id` points at it.

`422`

Admin path with no `brand_id`, or the employee has no deployable template yet.

`429`

Rate limit exceeded (5 hires/brand/hour self-serve, 50 grants/admin/hour).

## Cancel a hire

Two endpoints end a hire. They differ in *when* it ends, and in how much of the agent actually goes away.

```http
POST /api/v1/employees/hires/{hire_id}/cancel
POST /api/v1/employees/hires/{hire_id}/cancel-immediate
```

An admin may cancel any hire; a brand user may cancel only a hire their own brand holds.

### Cancel at period end

The agent keeps running until `current_period_end`. Nothing is disabled at call time — the reconcile loop performs the marketplace disable once the date passes.

```json
{
  "hire_id": "f1e2d3c4-0000-4444-8888-aaaabbbbcccc",
  "status": "canceled",
  "cancel_at_period_end": true,
  "effective_at": "2026-10-07T00:00:00Z",
  "marketplace_disable_at": "2026-10-07T00:01:00Z"
}
```

### Cancel immediately

The hire flips to `canceled` now and its marketplace installs are **ref-counted** disabled: an install turns off only if no other active hire of the same brand still references it. Tools disappear within the gateway's 60-second grant-cache TTL, which is why `marketplace_disable_at` is a minute after `effective_at`.

```json
{
  "hire_id": "f1e2d3c4-0000-4444-8888-aaaabbbbcccc",
  "status": "canceled",
  "effective_at": "2026-09-07T12:00:00Z",
  "marketplace_disable_at": "2026-09-07T12:01:00Z",
  "installs_disabled": 2,
  "installs_retained": 1,
  "management_record_deleted": true,
  "gateway_entry_removed": false,
  "agent_torn_down": false
}
```

### Read the teardown fields separately

Cancelling revokes an employee's **tools**. It does not unregister the agent.

Field

Meaning

`management_record_deleted`

The agent's `config.yaml` entry and workspace directory were removed. Best-effort.

`gateway_entry_removed`

Whether the gateway's own registration was removed. **Always **`false`** from a cancel** — removing it needs a separate call plus a gateway restart, which a cancel deliberately does not perform.

`agent_torn_down`

**Deprecated. Always **`false`**.** Read the two fields above instead.

While `gateway_entry_removed` is `false`, the persona **stays reachable** and can still be messaged, with its marketplace tools disabled. If you are cancelling in order to stop an agent answering, cancelling is not sufficient on its own.

`agent_torn_down` previously returned `true` on the Management API delete alone, reporting a teardown that had not happened. It now means what it says: `true` only when the agent is fully gone, which a cancel cannot achieve.

### Status codes

Code

Meaning

`200`

Cancelled.

`401`

Not authenticated.

`403`

The hire belongs to another brand.

`404`

No such hire.

`409`

The hire is not in a state that can be cancelled.
