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

# Composition acts

> Pin a ready-made composition act to POST /v1/images with action_id, browse the catalog at GET /v1/actions, and let the tri-state control automatic act detection.

A **composition act** (an *act*, for short) is a ready-made way of arranging your subjects in the frame — a pose or interaction the platform reproduces with the people you supply. Instead of describing the whole arrangement in your prompt, you pin an act by its `action_id` and let it handle the composition while your prompt handles everything else. This guide covers what an act is, how to browse the catalog at [`GET /v1/actions`](/api-reference/openapi), how `action_id` behaves on `POST /v1/images`, and how an act is matched to the number of subjects you send.

## What a composition act is

Each act in the catalog is a small, self-describing record:

```json theme={null}
{
  "id": "3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c",
  "name": "Over-the-shoulder",
  "description": "One subject framed from behind another",
  "supported_character_counts": [1, 2],
  "preview_url": "https://api.aurous-labs.com/…signed…"
}
```

* **`id`** — the identifier you pass as `action_id` on `POST /v1/images`. Treat it as opaque: it comes from `GET /v1/actions` and nowhere else.
* **`name`** / **`description`** — human-readable labels for building a picker in your own UI.
* **`supported_character_counts`** — the subject counts this act is built for (see [Count matching](#count-matching)). Pin the act with a matching number of subjects.
* **`preview_url`** — a signed preview image URL served by the API (`GET /v1/actions/{id}/preview`). The image bytes stream through the API, so the URL always points at `api.aurous-labs.com` — never at a storage host. It is short-lived (about an hour) and re-minted every time you list the catalog, so fetch it fresh rather than storing it. `null` when the act has no preview image.

## The catalog

List every act available to your team:

```bash theme={null}
curl https://api.aurous-labs.com/v1/actions \
  -H "X-Api-Key: $AUROUS_API_KEY"
```

```json theme={null}
[
  {
    "id": "3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c",
    "name": "Over-the-shoulder",
    "description": "One subject framed from behind another",
    "supported_character_counts": [1, 2],
    "preview_url": "https://api.aurous-labs.com/…signed…"
  }
]
```

The response is a flat array — the catalog is small and curated, so there is no pagination. Fetch a single act by id to re-check its liveness and current `supported_character_counts` (which can change as the act's reference material is updated):

```bash theme={null}
curl https://api.aurous-labs.com/v1/actions/3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c \
  -H "X-Api-Key: $AUROUS_API_KEY"
```

An id that is unknown, not currently live, or not visible to your team returns `404 resource_not_found` — the same response a nonexistent id would give, so the catalog never reveals which acts exist outside your team.

## Pinning an act: the `action_id` tri-state

`action_id` on `POST /v1/images` has three distinct states, and the difference between "omitted" and "explicit `null`" is meaningful:

| You send                | Behavior                                                                                                             |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `action_id` **omitted** | **Automatic.** The platform may detect and apply a matching act based on your prompt and subjects.                   |
| `action_id: null`       | **Detection off.** No act is applied — a plain subjects/prompt generation, even if one would otherwise have matched. |
| `action_id: "<id>"`     | **Pinned.** Exactly this act is applied. It must be live and visible to your team, and support your subject count.   |

So omitting the field opts in to automatic detection (the default behavior); sending an explicit `null` opts out for that one request; sending an id pins a specific act. This mirrors how you might already treat an unset field versus a deliberately-null one — here the distinction is load-bearing, so send the state you mean.

## Count matching

An act declares the subject counts it supports in `supported_character_counts`. When you pin an act **and** send subjects, the number of subjects must be one of those counts:

* **Matching count** — the act is applied. For example, an act whose `supported_character_counts` is `[1, 2]` accepts one or two subjects.
* **Unsupported count** — the request returns `400 action_not_available`, and the message names the counts the act does support (e.g. `"This act supports 1 or 2 subjects; you sent 3."`). Fix the request client-side — drop a subject, or pick a different act — and resubmit.

Sending an act with **zero subjects** is always valid and skips the count check entirely: the act renders a new person described by your prompt. This is the one case where `supported_character_counts` does not apply.

The count that is matched against `supported_character_counts` is the number of **subject entries** — one per element of `subjects[]`, plus the folded legacy `character_id` / `reference_image_urls` (each folds into a single entry). Every `character` subject counts as one, and every `reference` subject counts as one **regardless of how many `image_urls` it carries** — a reference subject with three images is still one subject for count matching. So an act whose `supported_character_counts` is `[1, 2]` accepts one or two subject entries, whether those subjects reference one image each or several.

This is a different count from the reference-image **budget**: across all subjects, the total number of reference images (plus the act's own reference material) must stay within the 10-image limit, or the request returns `400 too_many_reference_images` (`param` is `subjects`). Count matching is about how many *subjects* you send; the budget is about how many *images* they add up to.

## Quickstart

Pin an act and compose it with two saved characters:

```bash theme={null}
curl -X POST https://api.aurous-labs.com/v1/images \
  -H "X-Api-Key: $AUROUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "prompt": "Two colleagues at a product launch, warm evening light",
    "action_id": "3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c",
    "subjects": [
      { "type": "character", "character_id": "char_01HXMQ7Z3K8Y2VNABCDEFGHJKM" },
      { "type": "character", "character_id": "char_01HXMQ7Z3K8Y2WNABCDEFGHJKM" }
    ]
  }'
```

```json theme={null}
{
  "object": "inference",
  "id": "img_01HXMQ7Z3K8Y2VNABCDEFGHJKM",
  "status": "pending",
  "prompt": "Two colleagues at a product launch, warm evening light",
  "action": { "id": "3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c", "name": "Over-the-shoulder" },
  "subjects": [
    { "type": "character", "character_id": "char_01HXMQ7Z3K8Y2VNABCDEFGHJKM", "image_count": 1 },
    { "type": "character", "character_id": "char_01HXMQ7Z3K8Y2WNABCDEFGHJKM", "image_count": 1 }
  ],
  "image_count": 1,
  "created_at": "2026-07-18T10:00:00Z"
}
```

Poll `GET /v1/images/{id}` (or register a [webhook endpoint](/webhooks)) until `status` reaches a terminal value, same as any other generation.

## Rules

| Combination                                            | Result                                                                                                                        |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `action_id` **and** `lora_id`                          | 400 `parameter_invalid_combination` — an act picks its own model and can't be paired with a style. `param` is `action_id`.    |
| `action_id` **and** `context_images`                   | 400 `mutually_exclusive_input` — compose an act with subjects (or nothing), not loose context images. `param` is `action_id`. |
| `action_id` **and** `subjects` at a supported count    | **Allowed** — the act is applied.                                                                                             |
| `action_id` **and** `subjects` at an unsupported count | 400 `action_not_available` — the message names the supported counts. `param` is `action_id`.                                  |
| `action_id` **and** zero subjects                      | **Allowed** — renders a new person from your prompt (count check skipped).                                                    |
| Unknown / not-live / cross-team `action_id`            | 404 `resource_not_found` — no existence leak.                                                                                 |
| `action_id` + subjects that exceed the 10-image budget | 400 `too_many_reference_images`. `param` is `subjects`.                                                                       |

`action_id: null` and an omitted `action_id` never trigger the combination rules above — they mean "detection off" and "detection on", not "pin an act."

## The response

Every `GET`/`POST` response for a generation — and every webhook delivery, which carries the same shape — now includes an `action` field:

```json theme={null}
{
  "action": { "id": "3f2b6c1e-8a4d-4e2b-9c7a-1d5e8f0a2b3c", "name": "Over-the-shoulder" }
}
```

* `action` echoes the act that was applied — whether you pinned it with `action_id` or it was detected automatically — as `{ id, name }` and nothing more. It never carries the preview image, the reference material, or any internal selection detail.
* `action.name` may be `null` when a display name wasn't recorded for the act, but `action.id` is always present when an act was applied.
* `action` is `null` on every generation that didn't apply an act — plain prompt-only generations, style (`lora_id`) generations, context-image generations, and all video generations. It's a new, always-present, nullable key: existing integrations that don't read it are unaffected.

## Retired styles still serve as acts

A single style in [`GET /v1/loras`](/api-reference/openapi) and a composition act can share the same underlying model. When that model is **retired as a style**, one thing changes that's worth calling out:

* It **disappears from `GET /v1/loras`** — it's no longer offered as a browsable style, and automatic style matching won't pick it. This is the one behavior change to be aware of: a style that was previously listed can drop off the list once it's retired.
* Its **act stays in `GET /v1/actions`** as long as the act itself is live — the actions catalog is the only place a retired model surfaces publicly.
* An **explicit `lora_id`** you've already persisted for that model keeps working on `POST /v1/images` — retiring a style hides it from discovery but never breaks a request that names it directly.

The list at `GET /v1/loras` was never a stability contract, so this isn't a breaking change — but if you cache the catalog, expect a retired style to vanish from it while everything that references it by id keeps working.

## Estimates

`POST /v1/images/estimate` mirrors the create path: pin an `action_id` and it runs the identical act gates — the existence/visibility check (unknown → 404 `resource_not_found`) and, when subjects are present, the count check (unsupported → 400 `action_not_available`) — without dispatching or charging. Acts don't change the price (their reference material is free), so an act estimate quotes the standard single-image rate. As with any estimate, inline `https://` reference URLs are validated for shape but never fetched — a URL-specific problem only surfaces at create.

## Where to next?

* [Subjects](/guides/subjects) — compose one or more identities into a single image
* [Context images](/guides/context-images) — loose, prompt-directed references with no identity grouping
* [Create an image](/api-reference/images/create-image) — full request/response shape
* [`action_not_available`](/errors#action_not_available) — the count-mismatch error in the error reference
* [Cost transparency](/guides/cost-transparency) — how the credit receipt works
