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

# Grant credits and passes

> Run a credit or entry-pass campaign from your own backend.

This guide grants credits or entry passes to clients from your own systems — a
reactivation campaign, a loyalty reward, or a correction after a manual refund.

Everything here uses the [secret API key](/api/authentication#secret-api-key)
and the Ultimate plan.

<Warning>
  These calls run on your server only. The secret key reads your full client
  list and changes what clients hold, and CORS blocks it in browsers anyway.
</Warning>

## Credits or passes?

Both work the same way; they differ in what the client ends up holding.

**Credits** are a currency-like balance spent across whatever the studio prices
in credits. One endpoint, one balance per client.

**Entry passes** are counted entries that belong to a
[pass group](/api/objects/pass-group). A client holds a separate balance per
group, and an entry granted into one group cannot be spent in another.

## 1. Find your clients

```bash theme={null}
curl -H "Authorization: Bearer $ZENAMU_SECRET_KEY" \
  "https://api.zenamu.com/v1/clients"
```

Select your audience from the response — for example everyone whose
`bookings.lastBookingCompletedDate` is more than six months old and whose
`marketingConsent` is `1`.

Keep each client's numeric **`id`**. That is what the grant endpoints expect,
not the opaque `_id`.

## 2. Pick a pass group

Skip this step for credits, and skip it for passes if the studio has only one
group.

```bash theme={null}
curl -H "Authorization: Bearer $ZENAMU_SECRET_KEY" \
  "https://api.zenamu.com/v1/pass-groups"
```

```json theme={null}
{
  "statusCode": 200,
  "message": "Success",
  "data": [
    { "id": 42, "name": "10-entry pass", "order": 0, "state": "active" },
    { "id": 57, "name": "Unlimited", "order": 1, "state": "archived" }
  ]
}
```

Omit `passGroupId` entirely and entries land in the studio's default group,
which is what integrations written before multi-group support do.

## 3. Grant to one client

```bash theme={null}
curl -X POST "https://api.zenamu.com/v1/clients/123/credits" \
  -H "Authorization: Bearer $ZENAMU_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customEntryCount": { "count": 10, "validityTimeValue": 3, "validityTimeUnit": "month" },
    "currency": "CZK",
    "price": 0,
    "sendEmail": true
  }'
```

```json theme={null}
{
  "statusCode": 200,
  "message": "Success",
  "data": { "clientId": 123, "numberOfCredits": 510, "validTo": "2026-12-01" }
}
```

`numberOfCredits` is the client's balance **after** the grant, not the amount
you added.

You send a duration, not an expiry date — Zenamu computes `validTo` from the
moment of the grant. See [Grant validity](/api/dates#grant-validity) for the
per-unit ceilings.

Set `price` to `0` for a gift. The value is recorded on the grant, so use the
real amount when the client actually paid you.

Passes work identically at `/v1/clients/123/passes`, returning `numberOfEntries`
and accepting an optional `passGroupId`.

## 4. Grant in bulk

Up to 100 clients per request. A client may appear only once in a batch. One
call instead of a hundred keeps you well inside the
[rate limit](/api/rate-limits):

```bash theme={null}
curl -X POST "https://api.zenamu.com/v1/clients/credits/bulk" \
  -H "Authorization: Bearer $ZENAMU_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sendEmail": false,
    "items": [
      { "clientId": 123, "customEntryCount": { "count": 10, "validityTimeValue": 3, "validityTimeUnit": "month" }, "currency": "CZK", "price": 0 },
      { "clientId": 456, "customEntryCount": { "count": 10, "validityTimeValue": 3, "validityTimeUnit": "month" }, "currency": "CZK", "price": 0 }
    ]
  }'
```

`sendEmail` applies to the whole batch. Items are processed sequentially, in the
order you send them.

## Partial success

A bulk response is `200` even when items failed. The batch does not roll back.

```json theme={null}
{
  "statusCode": 200,
  "message": "Success",
  "data": {
    "results": [
      { "clientId": 123, "success": true, "numberOfCredits": 510, "validTo": "2026-12-01" },
      { "clientId": 456, "success": false, "error": { "message": "Client not found" } }
    ],
    "summary": { "total": 2, "succeeded": 1, "failed": 1, "failedClientIds": [456] }
  }
}
```

<Warning>
  Never treat a bulk `200` as "everything worked". Check `summary.failed`.
</Warning>

`summary.failedClientIds` identifies the failed items. It does not make a retry
appropriate by itself. Inspect each result and correct permanent errors first:

```js theme={null}
const res = await grantBulk(items);
const failedResults = res.data.results.filter((result) => !result.success);
const failedItems = items.filter((_, index) => !res.data.results[index].success);

for (const result of failedResults) {
  console.error(result.clientId, result.error.message);
}

// Retry selected failedItems only after you have corrected the cause or
// confirmed that the failure was transient.
```

Never retry the whole batch — successful items would grant a second time. If
you do retry, send only selected failed items after checking the cause.

## Handling errors

| Status | What it means                                                                                                                                     | What to do                                                                           |
| -----: | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|  `400` | Validation failed. On a single pass grant, `passGroupId` may not belong to this studio.                                                           | Fix the payload.                                                                     |
|  `403` | Your plan does not include the private API. On a single pass grant, an explicitly targeted non-default group may also be unavailable on the plan. | Upgrade, or target the default group; see [Plans and access](/api#plans-and-access). |
|  `404` | The key is unrecognized, or the client is not in this studio.                                                                                     | Read the `message`.                                                                  |
|  `429` | Over 100 calls per minute on this endpoint.                                                                                                       | Back off and retry.                                                                  |
|  `500` | Unexpected server error.                                                                                                                          | See below.                                                                           |

In a bulk pass grant, a bad or plan-blocked `passGroupId` is a failed item inside
the `200` response. It does not change the top-level status.

<Warning>
  A `500` on a grant is not safely repeatable. The grant runs in a transaction,
  but the response never confirmed the outcome — re-read the client's balance
  with `GET /v1/clients` before granting again, or you may double-credit them.
</Warning>

## Suppressing emails

Every grant emails the client by default. Set `sendEmail` to `false` when you
send your own campaign email, or when correcting a mistake the client should not
hear about twice.
