---
title: "Has the designer compose a new design from a written brief, in the store's brand, optionally built on images from the st"
method: POST
path: "/api/v1/tools/compose_design"
tags: ["auto_write"]
---

# Has the designer compose a new design from a written brief, in the store's brand, optionally built on images from the st

`POST /api/v1/tools/compose_design`

## Request

- Base URL: `https://app.magistry.io`
- URL: `https://app.magistry.io/api/v1/tools/compose_design`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://app.magistry.io/api/oauth/authorize`, token URL `https://app.magistry.io/api/oauth/token`, scopes: `creative:write`)

Has the designer compose a new design from a written brief, in the store's brand, optionally built on images from the store's studio library or on products from its catalogue. The designer reads the business's own records (its profile, its products or plans at today's prices, its running and planned promotions) and is told to state only the prices, offers and claims found there or in the brief. Works for any kind of business: a product launch, a sale, an event, a menu, an opening, a hiring post, a testimonial. A duration above 0 makes it a motion design with animated layers. Queued: returns a request id whose state shows when the design exists. Uses credits.

## Request body

- object
  - `brief` string, required: What the design is for and what it should say.
  - `placements` string[]: Layouts: square 1:1, portrait 4:5, story 9:16, landscape 1.91:1, wide 16:9. Default square, portrait and story.
  - `duration_seconds` number: 0 for a still image (default); up to 60 for a video.
  - `music` boolean: Videos only: lay an instrumental track made for its length under it (default true). False exports a silent video.
  - `music_mood` string: Videos only: the mood of that track. Left out, the brand kit's music mood.
  - `media_ids` string[]: Images from the store's studio library to build on.
  - `product_ids` string[]: Products from the store's catalogue to feature: the designer gets their names and prices, and their pictures where the studio can show them.
  - `job` 'spotlight' | 'offer' | 'new' | 'proof' | 'how_it_works' | 'before_after' | 'why_us' | 'tip' | 'event' | 'availability' | 'hours' | 'milestone' | 'team' | 'hiring' | 'seasonal': What the design is for; the designer follows that job's structure. One of: spotlight (spotlight a product or plan), offer (announce an offer), new (something new), proof (customer proof), how_it_works (how it works), before_after (before and after), why_us (why choose us), tip (a tip or an answer), event (an event or a date), availability (in stock or available), hours (opening hours or a closure), milestone (a milestone), team (meet the team), hiring (we're hiring), seasonal (a season or a holiday). Leave it out to let the brief decide.
  - `builds_on` string: A design of this store to make the next design after. The designer is shown its words and how it did once posted, keeps what it leads with and says it in new words; the new design joins its test group. A design that repeats its words is refused.

## Response `200`

Tool result

- object
  - `ok` boolean, required
  - `result` unknown, required

## Other responses

- `400`: `invalid_request` — The request is malformed or an argument is unusable. The message names which. `invalid_body` — The body is not a JSON object. `invalid_expiry` — An approval request must end: `expires_at` lies at least 5 minutes and at most 7 days after it is filed. A request with no decision by then closes as expired, which means not approved. `invalid_rule` — The rule could not bind as written: a fact or action outside the vocabulary, an operator that cannot compare that fact, or a limit with no bound. Nothing was saved.
- `401`: `unauthorized` — No credential was presented, or the one presented is not valid. This deliberately does not say whether it was unknown or forged: a 401 that tells those apart is an oracle for guessing tokens. A credential that VERIFIED and is refused answers with its own code (expired, grant_revoked, app_disabled, address_not_allowed). `expired` — The credential verified and its expiry has passed. `grant_revoked` — The OAuth token verified, and the merchant revoked this connection after it was issued. `app_disabled` — The OAuth token verified, and Magistry has disabled this application for every merchant. No token of the application is admitted and none is issued. `address_not_allowed` — The credential verified and carries an IP allowlist that this request did not come from. `credential_rejected` — Diagnose could not resolve the credential at all. `no_credential` — Diagnose was called with no Authorization header, so there is nothing to report on. `invalid_client` — The client authentication on this request did not verify.
- `403`: `forbidden` — The credential is valid and does not hold a scope this tool requires. `insufficient_scope` — As `forbidden`, and the body names `required_scopes`. A webhook subscription needs both `automation:write` to configure the store and the read scope for every type it filters — a webhook is a read. `entity_not_granted` — The credential holds a finance scope, and the entity's owner has not given this app those books (or has taken them back). A finance scope says what an app may do; the owner's grant says which entities. A natural person's books need the person's own explicit delegate grant.
- `404`: `not_found` — No tool of that name is exposed to this credential. Note that a tool hidden by a missing scope answers `forbidden`, not this — the two are told apart deliberately.
- `409`: `idempotency_conflict` — This idempotency key was already used with DIFFERENT arguments. Replaying it would be a second, different write under a key that promises one. `idempotency_in_flight` — A request with this key is still running. This is not an error — it is "ask again", and the response carries `Retry-After`. `no_policy` — The app has no grant for this field on this store. Owning a field is a separate, larger grant than a scope: the reconciler pushes what the owner declares, and nobody else may touch the field meanwhile. `held_by_other` — Another owner holds this field, named in `held_by`. A field has at most one owner, enforced by a unique index, and an app never takes a field over from a live holder. `raced` — Another owner took this field in the same instant as this declaration. The database let exactly one of the two through. `merchant_kept` — The merchant kept an outside change to this field. Once kept, the database refuses every new declaration on it unless a store rule allows declaring it again. `too_many_pending` — This app already has the maximum number of approval requests pending on this store (the body carries `pending` and `limit`). The bound keeps one app from burying the merchant's screen. `already_closed` — The approval request was decided, expired or withdrawn before this call; `status` says which. A closed request never reopens. `store_unavailable` — The store is not accepting approval requests because Magistry is not installed on it. `too_many_rules` — The store already holds the maximum number of rules. `in_flight` — The declaration is being written to the platform at this moment, so it cannot be reverted underneath that write. `overwritten_externally` — Someone other than this app changed the field on the platform after the app's value was applied, and the merchant has not yet decided whether that change stays. A revert now would overwrite it, so nothing was reverted. `finance_proposal_refused` — The books refused the proposal in their current state: the entity's owner has not switched app proposals on (Books → Assistant), or the period it would land in is closed. Nothing was recorded and nothing was posted. `reason` names which. `export_not_ready` — The export job has not produced its file (yet): `status` in the body says whether it is still `queued`/`running` or `failed`, with the failure's own code.
- `410`: `finance_cursor_gone` — The change-feed cursor is older than the feed keeps (guarantees.finance_change_feed_retention_days in /api/v1/changelog.json) and events after it have been removed. The body carries `oldest_cursor`. `export_expired` — The export's file was downloadable until `expires_at` and is no longer kept.
- `413`: `payload_too_large` — The body exceeds the endpoint’s size bound.
- `429`: `rate_limited` — This credential exceeded its per-minute limit. The body carries the limit and the count, and the response carries `Retry-After` and the `RateLimit-*` headers.
- `500`: `simulation_unverified` — The simulation could not prove that its transaction was aborted, so no verdict is given: an answer from a run that may have kept something would read as a clean one. Reported on our side. `server_error` — Something on our side failed. The request may or may not have taken effect.
- `502`: `sandbox_failed` — Creating or seeding the sandbox failed. Provisioning is one transaction, so nothing was created and the key shown to nobody was never stored.
- `503`: `idempotency_unavailable` — The idempotency claim could not be taken, so the write was NOT attempted. This fails closed on purpose: running an unguarded write because the guard was unreadable is how a customer gets refunded twice. `approvals_unavailable` — Approval requests are not enabled on this server yet. Nothing was written. `receipts_unavailable` — Receipts are not enabled on this server yet. Nothing was exported. `policies_unavailable` — Store rules are not enabled on this server yet. Nothing was read or written. `sandbox_unavailable` — Developer sandboxes are not open: the database fence that keeps a sandbox key on demo data is not in place, or the provisioning worker is not configured. Nothing was created. `unavailable` — A read could not be served just now. Nothing was written or exported. `overwrites_unreadable` — We could not confirm that nobody else has changed this field since it was applied, and a revert that cannot check could overwrite a merchant's change. Nothing was reverted. `rls_scoped_execution_unavailable` — This deployment is not currently running the row-level-scoped execution mode the headless surface requires, so it refuses rather than executing with wider reach than the grant. `finance_grants_unavailable` — The entities this app may read could not be established for this request, so nothing was read — an unreadable grant is never treated as a wider one, nor as "no entities". `finance_unavailable` — The books could not be read or a figure could not be computed from them just now, so nothing was returned — never a partial list or a zero that would read as the ledger. `export_unavailable` — The export job could not be started, read or served just now — including a stored file whose bytes do not match the SHA-256 the job recorded, which is never sent. Nothing was started or sent. `corpus_unavailable` — The corpus this endpoint reads could not be loaded.

---

[API](https://skmtc.dev/magistry/apis/magistry-api.md) · [All operations](https://skmtc.dev/magistry/apis/magistry-api/llms.txt) · [OpenAPI document](https://skmtc.dev/magistry/apis/magistry-api/revisions/13dae20d49cb?raw)
