---
title: Bulk recipient actions
description: Issue, reissue, remind, expire, or delete many recipients in one call
method: POST
path: /api/v1/workspaces/{slug}/certificates/{id}/recipients/bulk
group: Issues
scopes: certificates:write
---

# Bulk recipient actions

# Bulk recipient actions

Applies one action to many recipients of a certificate. Name the recipients explicitly with `issue_ids`, or describe them with a `filter` and let the API resolve it — useful when the cohort is larger than a page.

Recipients the action cannot apply to are reported as `skipped` rather than failing the call, so a mixed selection does the obvious thing.

## Authorization

```http
Authorization: Bearer vc_...
```

Requires `certificates:write` (or full access) and an active Pro seat.

## Path parameters

  
- `slug*` string — Workspace slug Example: `acme`.

  
- `id*` UUID — Certificate id Example: `550e8400-e29b-41d4-a716-446655440000`.

## Body parameters

  
- `action*` string — `issue`, `reissue`, `remind`, `expire`, or `delete` Example: `issue`.

  
- `issue_ids?` UUID[] — Recipients to act on, up to 500. Omit when using `filter`. Example: `["9b2c1d4e-5f67-4890-a123-456789abcdef"]`.

  
- `filter?` object — Acts on every recipient matching `search` and `status`, the same filters [List issues](/docs/api/issues/list) takes

  
- `exclude_ids?` UUID[] — Recipients to leave out of a `filter` selection, up to 500

## What each action does

| Action | Applies to | Effect |
| --- | --- | --- |
| `issue` | Pending recipients | Issues and emails their certificate. Costs one credit each. |
| `reissue` | Issued recipients on an older design | Issues a new version on the latest published design and emails them. Costs one credit each. |
| `remind` | Pending recipients behind an unlock form | Sends a fresh unlock link, retiring the previous one. Free. |
| `expire` | Issued recipients | Sets expiry to now, so the certificate stops verifying. No email, no credit. |
| `delete` | Any recipient | Removes the recipient. Issued certificates stop resolving publicly and credits are not returned. |

`issue` and `remind` are rejected while the certificate is a draft: publishing it (`PATCH /certificates/{id}` with `status: "active"`) delivers every queued recipient in one step.

## Batching

`issue`, `reissue`, and `remind` process at most 200 recipients per call. `remaining` reports how many eligible recipients are left — repeat the same request until it reads `0`. `expire` and `delete` are not batched.

Credits for `issue` are checked once for the whole batch, so a shortfall returns `402` before anything is charged.

## Request

```bash
curl -X POST \
  https://api.verifycate.com/api/v1/workspaces/acme/certificates/550e8400-e29b-41d4-a716-446655440000/recipients/bulk \
  -H "Authorization: Bearer vc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "issue",
    "filter": { "status": "ready_to_send" }
  }'
```

```javascript
const certificateId = "550e8400-e29b-41d4-a716-446655440000";
const url = `https://api.verifycate.com/api/v1/workspaces/acme/certificates/${certificateId}/recipients/bulk`;

let remaining = 0;
do {
  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: "Bearer vc_your_api_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ action: "issue", filter: { status: "ready_to_send" } }),
  });
  const result = await res.json();
  remaining = result.remaining;
} while (remaining > 0);
```

```go
body := strings.NewReader(`{"action":"expire","issue_ids":["9b2c1d4e-5f67-4890-a123-456789abcdef"]}`)
req, _ := http.NewRequest(
	"POST",
	"https://api.verifycate.com/api/v1/workspaces/acme/certificates/550e8400-e29b-41d4-a716-446655440000/recipients/bulk",
	body,
)
req.Header.Set("Authorization", "Bearer vc_your_api_key")
req.Header.Set("Content-Type", "application/json")
```

```python
import json, urllib.request

req = urllib.request.Request(
    "https://api.verifycate.com/api/v1/workspaces/acme/certificates/550e8400-e29b-41d4-a716-446655440000/recipients/bulk",
    method="POST",
    data=json.dumps({"action": "expire", "filter": {"status": "issued"}}).encode(),
    headers={
        "Authorization": "Bearer vc_your_api_key",
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as res:
    print(res.read().decode())
```

## Response schema

- `errors?` array — 
- `failed*` integer — 
- `matched*` integer — 
- `remaining*` integer — 
- `skipped*` integer — 
- `succeeded*` integer — 

## Response

### 200

```json
{
  "matched": 240,
  "succeeded": 200,
  "skipped": 0,
  "failed": 0,
  "remaining": 40,
  "errors": []
}
```

### 400

Returned when the certificate is still a draft, or when no recipients were named.

```json
{
  "title": "Bad Request",
  "status": 400,
  "detail": "publish the certificate before issuing recipients",
  "code": "certificate_draft"
}
```

### 402

```json
{
  "title": "Payment Required",
  "status": 402,
  "detail": "not enough credits to issue certificates",
  "code": "insufficient_credits"
}
```
