Skip to content

Your store listing ​

Your store listing is the card shoppers see in the /apps store, which is still gated while the store is pre-GA. It is separate from your app's deployment: an app can be approved and serving at <slug>.civit.ai while its listing is still a draft nobody can find (see Deployed is not the same as listed in the store).

The listing is minted as a draft when you run civitai app submit, so you can fill it in while your app is in review and it goes live the day the app is approved.

Withdrawing a first-version submission deletes its listing

Withdrawing a first-version submission deletes the listing and all its media (a moderator's rejection keeps it, so a re-submit lands on the same listing) — see Changing the bundle while a request is still pending.

The publish floor ​

A listing cannot go live without an icon AND a cover — the publish floor, and the most common reason an approved app is not in the store. Screenshots are optional (up to eight).

civitai app listing status prints what is attached and what the floor still needs:

console
$ civitai app listing status
App:             my-app
Listing status:  draft
Icon:            MISSING (required)
Cover:           MISSING (required)
Screenshots:     0

⚠ Not publishable yet — missing icon and cover.
  Add one:  civitai app listing set-icon <file>
  Add one:  civitai app listing set-cover <file>

civitai app listing status is not a pure read

On a live (approved) listing — off-site apps included — the read behind it opens the revision draft server-side (idempotently, the same one each time), so a script polling it keeps a revision open on your listing. Read it once per change, with or without --json. For CI, use civitai app doctor, which is a pure read.

Which app a listing command acts on ​

By default every app listing subcommand reads blockId from the block.manifest.json in the working directory. Run it from elsewhere and it stops before building a request:

console
$ cd /tmp && civitai app listing status
Error: could not resolve the app — run this from your app directory (with block.manifest.json) or pass --slug: no block.manifest.json found in . — is this an App project? run `civitai app create` to create one

Two flags, on every subcommand, name the app instead: --slug <blockId> skips the manifest, and --dir <path> points at the app directory.

Attaching media ​

bash
civitai app listing set-icon ./assets/icon.png                         # required
civitai app listing set-cover ./assets/cover.png                       # required
civitai app listing add-screenshot ./assets/shot-1.png --caption "Grid view"   # optional, up to 8

assets/ is scaffolded by every template, with a README of the requirements and no placeholder images — the files above are ones you supply.

Screenshots are managed by id — the alsc_… values listing status prints:

bash
civitai app listing rm-screenshot alsc_01H8XYZ            # remove one
civitai app listing reorder alsc_02 alsc_01 alsc_03       # pass ALL current ids, in the new order

reorder takes the whole set, in order — there is no "move one" form, and a partial or unknown set is rejected.

What is checked, and by whom ​

The CLI checks, locally, before anything is uploaded: the file's format and byte size — nothing else.

kindhow manyformatbyte cap (the file you pass)
icon1, requiredpng / jpeg / webp≤ 2 MiB
cover1, requiredpng / jpeg / webp≤ 4 MiB
screenshotup to 8, optionalpng / jpeg / webp≤ 2 MiB

It prints the decoded dimensions, which every server-side bound depends on, on the upload line:

Uploading icon (37.3 KiB, 1024×1024)…

The platform checks, when the image is attached: dimensions and aspect ratio. The CLI does not reproduce these, so the table below is guidance, not a local gate — the server's rejection names the bound and the measured value (icon must be square-ish (aspect 2.00 outside 0.9–1.1)).

kindaspect (width ÷ height)minimum size
icon0.9 – 1.1 — square or near-square (1:1 is fine)128 px on the shorter side
cover1.3 – 2.4 — landscape, ~4:3 to ~21:9640 px wide
screenshot0.4 – 2.6 — either orientation320 px on the shorter side

Easy starting points: a 512 × 512 icon and a 1600 × 900 cover. These are platform constants that can move; a rejection always carries the current bound.

Four behaviours the numbers do not tell you ​

Icons are re-encoded server-side, and the re-encode is what gets capped. The upload is downscaled to at most 1024 px on its longer side and re-encoded to PNG — aspect preserved, never enlarged (so the 128 px floor still bites). The platform then caps that re-encoded PNG at 1 MiB, a different measurement from the CLI's 2 MiB on your file: a photographic 1024 × 1024 JPEG of a few tens of KiB can still be refused, because its PNG re-encode exceeds 1 MiB. The lever is pixel dimensions, not heavier compression. The CLI cannot predict the re-encoded size (it depends on how compressible your artwork is), so its error repeats this mechanism.

An icon's upper bound is a PIXEL count, not a file size. A source above roughly 16 megapixels (about 4096 × 4096) is refused however small the file — a flat 5000 × 5000 PNG of a few hundred KB is rejected. 1024 × 1024 is plenty, since that is what the server re-encodes to.

Covers and screenshots are not rescaled. Ship them at the size you want shown.

A wrong image is rejected fast. The CLI attaches before waiting on the content scan, so a shape rejection arrives in seconds rather than after a scan that can take two minutes. The scan verdict is still waited on: these commands never report success on a pending or blocked scan, and a failure says what state the listing was left in.

Editing a listing that is already LIVE ​

Once a listing is approved and live, only a material edit goes through review. Every media command — set-icon, set-cover, add-screenshot, rm-screenshot, reorder — is material: it writes into a revision, a shadow copy that goes to a moderator, and your public listing is untouched until that revision is approved. Describe it with --changelog "<what changed>". The text fields are not material — see The text fields apply IN PLACE.

The attach commands open a revision and submit it ​

set-icon, set-cover, add-screenshot and reorder each make one complete change, so on a live listing they open the revision, write into it, and submit it:

✓ Icon staged on a revision — pending moderator review (alpr_…).
Your live listing is unchanged until a moderator approves the revision.

-y / --yes skips the confirmation on the three attach commands. reorder takes --changelog but has no confirmation to skip, so it has no -y, and it never uploads anything. On a draft listing there is no revision, so all four apply to the listing itself (✓ Icon set, ✓ Reordered N screenshots).

…unless the listing is still below the publish floor ​

A revision cannot go to a moderator until the listing has both an icon and a cover. So when one command cannot clear the floor alone, it stages without submitting: it writes its change into the revision, leaves it open, prints what is still missing, and exits 0. The next command reuses the same revision and submits both together.

✓ Icon staged on an open revision — not submitted for review yet.
Your live listing is unchanged; nothing reaches a moderator until the publish floor is met.

So civitai app listing set-icon … && civitai app listing set-cover … works as a one-liner.

The CLI treats the refusal as progress only when, read back from the listing, all three hold: the refusal was a 400 (a 500/503 still fails with its usual exit code); the change is really there (by image id for an icon or cover, by alsc_… id for a screenshot, and for reorder the exact ids in the order you passed); and the floor is really unmet. If it cannot read the listing back, you get the error and exit 2.

rm-screenshot stages and does not submit ​

On a live listing the ids listing status prints belong to the open revision, so a removal lands there — and it is not submitted. Your public gallery goes on showing the screenshot until the revision is approved:

✓ Screenshot removal staged on an open revision — not submitted for review yet.

That lets you stage several removals in one review cycle. On a draft listing the removal applies directly: ✓ Screenshot removed.

submit-revision is what publishes staged work ​

bash
civitai app listing submit-revision --changelog "Dropped the outdated grid shot"

This sends the open revision, as it stands, to moderator review — the only way to publish a removal. It is idempotent: a revision already awaiting review returns that same request.

It refuses, without submitting, when the listing is not live (a draft or pending listing has no revision; rejected and removed get their own message) and when there is no open revision.

Below the publish floor, submit-revision FAILS where the attach commands exit 0

Exit 2: the submit is what you asked for, so it cannot report success. The floor gap is printed alongside the server's refusal.

The text fields apply IN PLACE ​

civitai app listing set-text writes the listing's tagline, description and category:

bash
civitai app listing set-text --tagline "Batch upscaling, in your browser"
civitai app listing set-text --category utility --slug my-app
civitai app listing set-text --clear tagline,category

This is the exception to everything above

These are not "material" changes, so set-text never opens a revision. It applies in place, immediately and publicly, on every listing status — including an approved, live one. There is no revision to review, and nothing to abandon if you change your mind.

The flags are sent as one patch, so a run either applies or does not. At least one is required.

  • Clearing and emptying differ. --tagline "" sets an empty string; --clear tagline sets it to null. --clear takes a comma-separated list and cannot be combined with the matching value flag.
  • Blanking a field needs --yes, so an unset shell variable cannot silently empty a public field. Whitespace-only counts as blank, and the check is on the value you passed — a blank set is refused even on an already-empty field.
  • category must be one of generation, games, utility, discovery, moderation, analytics, other.
  • On-site apps are refused (exit 1). Their copy is manifest-governed: the platform overwrites name / tagline / description / category from block.manifest.json on every subsequent-version approval. Edit the manifest and submit a new version instead.
    • description is a top-level string key in block.manifest.json, next to name and tagline: "description": "What the app does, in a few sentences." The platform reads it and trims it; a missing or blank one leaves the listing's description empty. The published JSON Schema and the manifest reference generated from it do not list this key yet, so an editor will not suggest it. It is accepted all the same: the schema does not forbid extra top-level keys, and civitai app validate passes with it set.
  • The server rate-limits these edits, at roughly 30 an hour.

An open-source app's store detail page can carry a Source row linking to the code (never on a grid card; omitted when unset). Where you set it depends on the app's kind:

kindwhere the link liveshow it gets there
on-sitethe repository key in block.manifest.jsonflows to the listing when a moderator approves a version, and is re-synced from the manifest on every approved version after that — remove the key and the link is cleared
off-sitethe listing itselfcivitai app listing set-source-repo <url>

set-source-repo refuses an on-site app (exit 1) and names the manifest key instead.

bash
civitai app listing set-source-repo https://github.com/me/my-app
civitai app listing set-source-repo --clear          # remove the link
civitai app listing set-source-repo https://github.com/me/my-app --json

What the server accepts ​

The URL must be a repository root on github.com, gitlab.com or codeberg.org — https://<host>/<owner>/<repo>, with no deeper path. A trailing / or .git, a query string and a fragment are accepted and normalised away. A deep link such as /owner/repo/tree/main is rejected.

The CLI does not pre-validate the URL

It only checks that you passed something; an unacceptable URL comes back as the server's own message. The server's rule is stricter than the manifest's pattern (each path segment's characters, a stripped trailing .git), so for the on-site key, passing civitai app validate is necessary, not sufficient.

This is a material change, unlike set-text

On an approved listing the server stages the change on a revision and the listing re-enters moderator review. The live listing is unchanged until that revision is approved — run civitai app listing submit-revision to send it. The command says which branch the server took; --json carries it as requiresReview and shadowId. On a draft or pending listing it applies directly.

What counts as a "change". The server compares canonical forms, so re-setting the link you already have, setting a / or .git spelling of it, or --clear on a listing with no link applies in place, stages nothing, and reports requiresReview: false.

Some states are refused outright rather than staged ​

Each has its own exit code:

stateexit
you unpublished the listing yourself — a material change is blocked while it is down2
a moderator removed the listing3
any other failure the server does not classify1

The first is 2 because the server answers MATERIAL_CHANGE_BLOCKED as an HTTP 400. The CLI echoes the server's sentence verbatim; on a moderator takedown it adds that your account's access is not the problem and that only a moderator relisting it helps.

If a revision was already open ​

A material edit joins the open revision (from an rm-screenshot or an attach command) rather than getting its own, and approving it publishes everything staged there and copies its text back over the live listing. The command says so and points you at civitai app listing status.

A non-material edit applies in place without joining it — but the open revision can still carry a different source link that replaces yours when approved, so the command warns on that path too. --json reports it as openRevision either way.

Reading a listing back in a script ​

civitai app listing status --json also names the two listing ids the human output omits: a live listing's parent and, once a revision is open, its shadow. Which one a change is addressed to decides whether the server accepts it:

console
$ civitai app listing status --slug my-app --json
{
  "slug": "my-app",
  "parentId": "apl_01KXPENN7GJV51HBG649P3Y99M",
  "shadowId": "apl_01M064YJGDR973P73WHC1FGDP9",
  "status": "approved",
  "hasPendingRevision": false,
  "assets": {
    "icon":  { "present": true,  "imageId": 4711 },
    "cover": { "present": false, "imageId": null },
    "screenshots": [
      { "id": "alsc_01M0…", "order": 0, "caption": "Grid view", "imageId": 8123 }
    ]
  },
  "floor": { "met": false, "missing": ["cover"] }
}
  • shadowId is null when no revision draft exists — every draft or pending listing, and a live one nobody has edited — never absent, so .shadowId answers on every listing.
  • status is the parent's lifecycle status.
  • hasPendingRevision means submitted for review, not "a shadow exists".
  • floor.missing is [] when the floor is met.
  • The server's editTargetId is not included. Address a change to shadowId when it is non-null and parentId otherwise.

This call is a write on a live listing — see the warning at the top of this page.

Off-site apps ​

An off-site app is a registered URL, so it has no block submission. Every app listing subcommand still works on one: the CLI tries the submission first and, when there is none, selects the listing by slug.

If both lookups answer not found (an app with no listing row at all), the CLI exits 4 and points at civitai app view <slug>, which shows the icon and cover the listing serves, and at the App-store listing UI on civitai.com, where the registering account manages that media — which may not be you.

A listing your account does not own resolves by slug and is then refused 403 (exit 3). Any other failure from either lookup — a 403 from the invite-gated submissions route, a 5xx, a dropped connection — keeps its own message and exit code rather than being reported as "listing not reachable".

Gating a release with app doctor ​

civitai app doctor answers one question — is this listing ready to publish, and if not, what do I do about it? — for every app you own or hold an accepted collaborator seat on, or for just the one you name:

bash
civitai app doctor                 # every app you can work on
civitai app doctor my-app          # just one
civitai app doctor --json | jq -e .ok

The findings are the platform's, classified into two severities:

SeverityCodesMeaning
BLOCKINGmissing-icon, missing-cover, blocked-mediaThe listing cannot publish until it is fixed. Sets the exit code — unless the listing is delisted.
ADVISORYno-screenshots, empty-description, empty-tagline, empty-category, scanning-mediaRecommended, but nothing is held up.

The exit code is 1 when a blocking problem was found on a listing that can still publish, 0 otherwise — including advisories only, blocking problems only on delisted listings, and no listings at all. So it gates a release script without parsing anything:

bash
civitai app doctor my-app || exit 1

--json emits the same verdict as one object and uses the same exit codes, so a script must branch on the code before trusting the payload. Every other code keeps its usual meaning — 3 not authorized, 4 no such app of yours, 5 transport.

Delisted listings are reported but do not gate ​

An app whose status is removed still gets a full row, in its own section. Its blocking problems are counted in summary.blocking and excluded from summary.gating, so one old removed app cannot fail every run. Republish it and it gates again on the next run. Only an explicit removed is excluded; an unrecognised status still gates.

The --json shape ​

FieldMeaning
okThe verdict, and the structured form of the exit code. Follows summary.gating, not summary.blocking.
apps[]Every listing checked, gating apps first, delisted last. Each carries slug, name, appListingId, appBlockId (null for off-site), status, role, kind, delisted, and a blocking and an advisory array — never null, [] when empty.
apps[].delistedtrue when this listing's status is removed, i.e. its blocking problems were reported and not counted.
apps[].kindonsite or offsite — it decides the fix for the three text codes.
summary.truncatedtrue when the server's page cap may have hidden listings. Computed from the server's page, so it is reported on a single-app run too.
summary.blockingEvery blocking problem found, so it matches the arrays you can see.
summary.gatingThe subset on listings that can still publish. This is the exit code.
summary.advisory / summary.apps / summary.delistedAdvisory findings, apps checked, and how many of them are delisted.

Ask ok for "may this ship", summary.blocking for "is anything wrong anywhere".

Each finding names a fix that actually works ​

missing-icon / missing-cover / no-screenshots print the exact set-icon / set-cover / add-screenshot command with --slug already filled in. scanning-media prints no command at all, because the scan finishes on its own.

blocked-media depends on the slot

An icon or a cover is replaced — the new asset overwrites the slot. A screenshot must be REMOVED: add-screenshot appends, so the blocked row stays and go-live is still refused. That arm prints three commands — listing status to find the alsc_ id, rm-screenshot, and, on an approved listing, submit-revision (the removal is not auto-submitted).

The three text problems — description, tagline, category — are kind-aware:

kindfix printedwhy
off-sitethe listing's web editor URLthe copy is author-supplied
on-siteblock.manifest.json, then civitai app submit a new versionthe copy is manifest-governed

An on-site edit made in the browser would be reverted at the next approval (see above). For an off-site listing, civitai app listing set-text is the CLI equivalent of the web editor — see The text fields apply IN PLACE.

Two properties worth knowing ​

  • It is a PURE READ. Unlike civitai app listing status, it opens no revision draft, so it is safe in a loop or in CI.
  • It sees apps civitai app status cannot — listings you acquired by ownership transfer or hold a collaborator seat on. app status reads only what you submitted.

An app with nothing wrong is reported as complete explicitly, never as a blank section.

Next ​

Civitai Developer Documentation