Skip to content

Message bridge ​

A block and its host talk over window.postMessage. Every message is a { type, payload } object discriminated by type. The tables below are generated from the published @civitai/app-sdk message unions (for payload shapes) joined with the civitai host parity inventory (for direction, request/reply pairing, and page-only flags).

Most builders never send these by hand — the React hooks wrap them. This page is the contract for advanced use and non-React SDK consumers.

Conventions ​

  • block → host vs host → block — the direction of the message.
  • request → reply — a request-style message that awaits a specific reply type (shown). An unhandled request-style message hangs the block until its SDK timeout, so the host always registers a handler.
  • fire-and-forget — a block → host message with no reply; ignoring it is a silent no-op, never a hang.
  • page-only — handled only by the full-page host (a page app at /apps/run/<slug>), not by the model-slot host today. Slot apps are deferred during the closed beta, so build page apps and you get the full surface.
  • A host → block push added after your block was built is inert until you rebuild. Your bundle compiles in the @civitai/blocks-react you built against, so it only understands the messages that version knew about. A push the host began sending later — THEME_CHANGE is the current example — is silently discarded: the transport finds no validator for the type, matches no pending request and no push listener, and no-ops. No error, no warning, and nothing to see in the console. Nothing rebuilds a deployed block on your behalf, so it stays frozen at its build-time SDK until you resubmit; rebuilding against the current SDK picks up every push added since, automatically.
  • The Per-app storage and Shared storage families are deprecated. They carry the legacy useAppStorage / useSharedStorage stores, which are shut down on 2026-11-10; from then on these messages stop working and their requests fail with an error. The document store that replaces them is not on the bridge: blocks call it over HTTP.

Lifecycle

BLOCK_ERRORblock → hostfire-and-forget
payload
{
    message: string;
    fatal: boolean;
}
BLOCK_INIThost → block
payload
BlockInitPayload
BLOCK_READYblock → hostfire-and-forget
payload
{
    height: number;
    sdkVersion?: string;
    sdkName?: 'blocks-react' | 'sdk';
}
NAVIGATEblock → hostfire-and-forgetpage-only
payload
{
    path: string;
    scope?: BlockNavigateScope;
    target: 'current' | 'new_tab';
}
Model slot: model slot is an embedded panel; host-navigation is out of remit (no NAVIGATE bridge)
RESIZE_IFRAMEblock → hostfire-and-forget
payload
{
    height: number;
}
RESUMEhost → block
payload
(no payload)
SUSPENDhost → block
payload
(no payload)
TRACK_EVENTblock → hostfire-and-forget
payload
{
    eventName: string;
    properties?: Record<string, unknown>;
}

Auth & token

CONSENT_UNAVAILABLEhost → block
payload
ConsentUnavailablePayload
REQUEST_CONSENTblock → hostfire-and-forget
payload
{
    scopes?: string[];
}
REQUEST_SIGN_INblock → hostfire-and-forget
payload
{
    returnUrl?: string;
}
REQUEST_TOKENblock → hostrequest → reply
payload
{
    requestId: string;
    blockInstanceId: string;
}
reply TOKEN_REFRESH_RESPONSE
{
    requestId: string;
    token: WrappedToken;
}

or a TOKEN_REFRESH push when no requestId was sent

TOKEN_REFRESHhost → block
payload
{
    token: WrappedToken;
}

Generation workflows

CANCEL_WORKFLOWblock → hostrequest → reply
payload
{
    requestId: string;
    workflowId: string;
}
reply WORKFLOW_CANCELED
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
ESTIMATE_WORKFLOWblock → hostrequest → reply
payload
{
    requestId: string;
    body: WorkflowBody;
}
reply ESTIMATE_RESULT
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
ESTIMATE_WORKFLOW_BATCHblock → hostrequest → reply
payload
{
    requestId: string;
    bodies: WorkflowBody[];
}
reply ESTIMATE_BATCH_RESULT
{
    requestId: string;
    snapshots?: BlockWorkflowSnapshot[];
    aggregate?: BlockEstimateBatchAggregate;
    error?: string;
}
POLL_WORKFLOWblock → hostrequest → reply
payload
{
    requestId: string;
    workflowId: string;
    waitSeconds?: number;
}
reply WORKFLOW_STATUS
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
SUBMIT_WORKFLOWblock → hostrequest → reply
payload
{
    requestId: string;
    body: WorkflowBody;
    idempotencyKey?: string;
}
reply WORKFLOW_SUBMITTED
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}

App subqueue

CANCEL_APP_WORKFLOWblock → hostrequest → replypage-only
payload
{
    requestId: string;
    workflowId: string;
}
reply CANCEL_APP_WORKFLOW_RESULT
{
    requestId: string;
    result?: {
        workflow: AppWorkflow;
    };
    error?: string;
}
Model slot: app subqueue is a page-only affordance today; slot-apps deferred
QUERY_APP_WORKFLOWSblock → hostrequest → replypage-only
payload
{
    requestId: string;
    params?: AppWorkflowsParams;
}
reply APP_WORKFLOWS_RESULT
{
    requestId: string;
    result?: {
        workflows: AppWorkflow[];
        cursor: string | null;
    };
    error?: string;
}
Model slot: app subqueue is a page-only affordance today; slot-apps deferred

Buzz

GET_BUZZ_ACCOUNTSblock → hostrequest → replypage-only
payload
{
    requestId: string;
}
reply BUZZ_ACCOUNTS_RESULT
{
    requestId: string;
    result?: {
        accounts: BlockBuzzAccount[];
    };
    error?: string;
}
Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred
GET_BUZZ_BALANCEblock → hostrequest → reply
payload
{
    requestId: string;
}
reply BUZZ_BALANCE_RESULT
{
    requestId: string;
    balance?: {
        blue: number;
        green: number;
        yellow: number;
    };
    error?: string;
}
GET_BUZZ_TRANSACTIONSblock → hostrequest → replypage-only
payload
{
    requestId: string;
    params?: BlockBuzzTransactionsParams;
}
reply BUZZ_TRANSACTIONS_RESULT
{
    requestId: string;
    result?: {
        cursor?: string;
        transactions: BlockBuzzTransaction[];
    };
    error?: string;
}
Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred
GET_DAILY_COMPENSATIONblock → hostrequest → replypage-only
payload
{
    requestId: string;
    params?: BlockDailyCompensationParams;
}
reply DAILY_COMPENSATION_RESULT
{
    requestId: string;
    result?: {
        resources: BlockDailyCompensationResource[];
        hasPublishedResources: boolean;
    };
    error?: string;
}
Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred
OPEN_BUZZ_PURCHASEblock → hostrequest → reply
payload
{
    requestId: string;
    suggestedAmount?: number;
}
reply BUZZ_PURCHASE_RESULT
{
    requestId: string;
    purchased: boolean;
    newBalance?: number;
}

Viewer

GET_VIEWERblock → hostrequest → replypage-only
payload
{
    requestId: string;
}
reply VIEWER_RESULT
{
    requestId: string;
    viewer?: BlockViewer;
    error?: string;
}
Model slot: viewer self-read is a page-only affordance; slot-apps deferred

Pickers & upload

IMAGE_SCAN_RESOLVEDhost → block
payload
{
    requestId: string;
    imageId: number;
    result: BlockImageScanResult;
}
OPEN_CHECKPOINT_PICKERblock → hostrequest → reply
payload
{
    requestId: string;
    /** Optional ecosystem-family filter. Absent ⇒ unconstrained. Never ''. */
    baseModelGroup?: string;
    /** Currently-selected versionId so the picker can pre-highlight it. */
    currentVersionId?: number;
}
reply CHECKPOINT_PICKER_RESULT
{
    requestId: string;
    selected?: BlockCheckpointInfo;
}
OPEN_IMAGE_PICKERblock → hostrequest → replypage-only
payload
{
    requestId: string;
    /**
     * Which of the viewer's libraries to offer. OMIT the key for both. The
     * host de-duplicates repeats and refuses an empty array or an unknown
     * member.
     */
    sources?: BlockImagePickerSource[];
    /**
     * Ask for SEVERAL images in one picker session. `max` is a whole
     * number ≥ 1; the host clamps anything above 20 to 20. The reply is
     * then `selectedImages`, not `selected`. OMIT the key for a single
     * pick.
     */
    multiple?: {
        max: number;
    };
    /**
     * Offer only images in a published, non-`Private` post: `true`
     * narrows the `'posted'` library to those. It does not filter by
     * rating or by any other post setting. The `'generated'` library is
     * unaffected. OMIT it (or `false`) for today's behaviour, which also
     * offers images in a `Private` post. Send a boolean or nothing: the
     * host refuses any other value with `{ requestId, error }`.
     *
     * Set it when the pick becomes PUBLIC-FACING, e.g. an App Store
     * sub-listing card image (`imageId` on
     * `POST /api/v1/blocks/sub-listings/upsert`, `app.subListings.upsert`
     * in `@civitai/sdk`), which the store refuses for a non-public image.
     * Leave it off when the pick is a private input (a generation source).
     *
     * 🔴 A HOST THAT PREDATES THIS FIELD IGNORES IT and still offers
     * `Private`-post images, so an app setting it must still handle the
     * store's `image_not_public` refusal.
     */
    publicOnly?: boolean;
}
reply IMAGE_PICKER_RESULT
{
    requestId: string;
    selected?: BlockPickedImageInfo;
    selectedImages?: BlockPickedImageInfo[];
    error?: string;
}
Model slot: the image picker is a page-only affordance today; the shared dispatcher NACKs it with an IMAGE_PICKER_RESULT error
OPEN_IMAGE_UPLOADblock → hostrequest → replypage-only
payload
{
    requestId: string;
    purpose?: BlockUploadPurpose;
    asyncScan?: boolean;
    /** In-tab image bytes; no picker. Page apps only. See the comment above. */
    bytes?: ArrayBuffer;
    /** Optional name for a `bytes` upload (host-sanitised). */
    filename?: string;
}
reply IMAGE_UPLOAD_RESULT
{
    requestId: string;
    selected?: BlockUploadedImageInfo | BlockGenerationSourceImageInfo | BlockPendingImageInfo | BlockTrainingDataUploadResult;
    /** A `bytes` upload's refusal or failure: a host string or a forwarded server message. */
    error?: string;
}
Model slot: host-mediated image upload is a page-only affordance today; the model slot has no such surface
OPEN_RESOURCE_PICKERblock → hostrequest → replypage-only
payload
{
    requestId: string;
    resourceType: BlockResourcePickerType;
    /** Optional base-model family hint (ecosystem key or baseModel name). */
    baseModelGroup?: string;
    /**
     * Ask for SEVERAL resources in one picker session. LoRA only — the host
     * refuses it with a Checkpoint. `max` is a whole number ≥ 1; the host
     * clamps anything above 5 (the `additionalResources` cap) to 5. The
     * reply is then `selectedResources`, not `selected`. OMIT the key for
     * a single pick: its absence is what keeps that path unchanged.
     */
    multiple?: {
        max: number;
    };
}
reply RESOURCE_PICKER_RESULT
{
    requestId: string;
    selected?: BlockResourceInfo;
    selectedResources?: BlockResourceInfo[];
    error?: string;
}
Model slot: model slot uses the narrower OPEN_CHECKPOINT_PICKER; the wider resource picker is a page-only affordance
SET_USER_CHECKPOINTblock → hostrequest → reply
payload
{
    requestId: string;
    versionId: number | null;
}
reply USER_CHECKPOINT_SET
{
    requestId: string;
    ok?: boolean;
    error?: string;
}

Per-app storage

APP_STORAGE_DELETEblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply APP_STORAGE_DELETE_RESULT
{
    requestId: string;
    ok?: boolean;
    deleted?: boolean;
    error?: string;
}
APP_STORAGE_GETblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply APP_STORAGE_GET_RESULT
{
    requestId: string;
    value: unknown;
    error?: string;
}
APP_STORAGE_LISTblock → hostrequest → reply
payload
{
    requestId: string;
    prefix?: string;
    limit?: number;
    cursor?: string;
}
reply APP_STORAGE_LIST_RESULT
{
    requestId: string;
    keys: Array<{
        key: string;
        updatedAt: string;
    }>;
    nextCursor?: string;
    error?: string;
}
APP_STORAGE_QUOTAblock → hostrequest → reply
payload
{
    requestId: string;
}
reply APP_STORAGE_QUOTA_RESULT
{
    requestId: string;
    usedBytes: number;
    rowCount: number;
    limitBytes: number;
    limitRows: number;
    error?: string;
}
APP_STORAGE_SETblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
    value: unknown;
}
reply APP_STORAGE_SET_RESULT
{
    requestId: string;
    ok?: boolean;
    error?: string;
    sizeBytes?: number;
}

Shared storage

SHARED_APPENDblock → hostrequest → reply
payload
{
    requestId: string;
    value: SharedStorageValue;
}
reply SHARED_APPEND_RESULT
{
    requestId: string;
    key: string;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
SHARED_GETblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply SHARED_GET_RESULT
{
    requestId: string;
    item: SharedStorageItemWire | null;
    error?: string;
}
SHARED_GET_COUNTblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply SHARED_GET_COUNT_RESULT
{
    requestId: string;
    count: number;
    error?: string;
}
SHARED_GET_COUNTSblock → hostrequest → reply
payload
{
    requestId: string;
    keys: string[];
}
reply SHARED_GET_COUNTS_RESULT
{
    requestId: string;
    counts: Record<string, number>;
    error?: string;
}
SHARED_LISTblock → hostrequest → reply
payload
{
    requestId: string;
    prefix?: string;
    limit?: number;
    cursor?: string;
}
reply SHARED_LIST_RESULT
{
    requestId: string;
    items: SharedStorageItemWire[];
    nextCursor?: string;
    error?: string;
}
SHARED_REPORTblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
    reason?: string;
}
reply SHARED_REPORT_RESULT
{
    requestId: string;
    ok?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
SHARED_UNVOTEblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply SHARED_UNVOTE_RESULT
{
    requestId: string;
    count: number;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
SHARED_UPDATEblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
    value: SharedStorageValue;
}
reply SHARED_UPDATE_RESULT
{
    requestId: string;
    ok?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
SHARED_VOTEblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply SHARED_VOTE_RESULT
{
    requestId: string;
    count: number;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
SHARED_WITHDRAWblock → hostrequest → reply
payload
{
    requestId: string;
    key: string;
}
reply SHARED_WITHDRAW_RESULT
{
    requestId: string;
    ok?: boolean;
    deleted?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}

Wildcard packs & resource files

GET_RESOURCE_FILEblock → hostrequest → replypage-only
payload
{
    requestId: string;
    modelVersionId: number;
    fileId?: number;
}
reply RESOURCE_FILE_RESULT
{
    requestId: string;
    file?: BlockResourceFile;
    error?: BlockResourceFileErrorCode;
}
Model slot: model slot has no resource-file surface; page-only by decision (civitai-app-starters#664)
GET_WILDCARD_PACKblock → hostrequest → replypage-only
payload
{
    requestId: string;
    modelVersionId: number;
}
reply WILDCARD_PACK_RESULT
{
    requestId: string;
    pack?: BlockWildcardPack;
    error?: BlockWildcardPackErrorCode;
}
Model slot: model slot has no wildcard-pack import surface; the resolve+parse bridge is a page-only affordance

Other

BLOCK_HELLOblock → hostfire-and-forget
payload
(no payload)
BLOCK_MESSAGE_REJECTEDblock → hostfire-and-forget
payload
{
    type: string;
}
CREATE_POST_FROM_APPblock → hostrequest → replypage-only
payload
{
    requestId: string;
    /** Image sources, in POST ORDER. At least one; the host caps the total. */
    sources: BlockPostSource[];
    title?: string;
    detail?: string;
    /** Requested tag NAMES. Resolved against EXISTING tags only. */
    tags?: string[];
    /** Optional model-version gallery attach. Gated hard server-side. */
    modelVersionId?: number;
}
reply CREATE_POST_RESULT
{
    requestId: string;
    result?: BlockCreatePostResult;
    error?: string;
}
Model slot: every eligible image comes from the app subqueue, which is a page-only affordance; a model-slot block has no source images to post
GET_ENTITY_REACTIONSblock → hostrequest → reply
payload
{
    requestId: string;
    entityType: BlockReactionEntityType;
    /** 1 to `REACTION_READ_MAX_IDS` positive integers. Duplicates are collapsed. */
    ids: number[];
    /** A ticket per id, keyed by the id as a string. */
    tickets?: Record<string, string>;
}
reply ENTITY_REACTIONS_RESULT
{
    requestId: string;
    result?: BlockEntityReactionsResult;
    error?: BlockEntityReactionErrorCode | string;
}
GET_IMAGES_BY_IDSblock → hostrequest → replypage-only
payload
{
    requestId: string;
    imageIds: number[];
}
reply IMAGES_RESULT
{
    requestId: string;
    result?: {
        images: BlockGatedImage[];
    };
    error?: string;
}
Model slot: shared-grid gated read is a page-only affordance today; the model slot has no such surface
OPEN_MEDIA_UPLOADblock → hostrequest → replypage-only
payload
{
    requestId: string;
    /** What kind of media to upload. The host refuses any other value. */
    kind: BlockMediaKind;
    /** Required. The only accepted value today. */
    purpose: 'generationSource';
    /** In-tab bytes: present = bytes mode (no picker), absent = picker mode. */
    bytes?: ArrayBuffer;
    /** Bytes mode only: the type the app believes it is sending; must match the content. */
    mimeType?: string;
}
reply MEDIA_UPLOAD_RESULT
{
    requestId: string;
    selected?: BlockMediaUploadInfo;
    error?: string;
}
Model slot: media upload is a page-only affordance today; the shared dispatcher NACKs it with a MEDIA_UPLOAD_RESULT error
PUBLISH_GENERATION_OUTPUTSblock → hostrequest → replypage-only
payload
{
    requestId: string;
    workflowId: string;
    imageIndexes?: number[];
    title?: string;
}
reply PUBLISH_RESULT
{
    requestId: string;
    result?: {
        imageIds: number[];
    };
    error?: string;
}
Model slot: shared-grid publish is a page-only affordance today; the model slot has no such surface
ROUTE_CHANGEDhost → block
payload
{
    subPath: string;
}
RUN_CONFIRMED_STEPblock → hostrequest → replypage-only
payload
{
    requestId: string;
    body: WorkflowBodyPassThroughStep;
}
reply CONFIRMED_STEP_RESULT
{
    requestId: string;
    snapshot?: BlockWorkflowSnapshot;
    error?: string;
}
Model slot: confirmed steps are page-only on the server; the shared dispatcher NACKs it with a CONFIRMED_STEP_RESULT error
RUN_TRAININGblock → hostrequest → replypage-only
payload
{
    requestId: string;
    body: WorkflowBodyTraining;
}
reply TRAINING_RESULT
{
    requestId: string;
    snapshot?: BlockWorkflowSnapshot;
    error?: string;
}
Model slot: training is page-only on the server; the shared dispatcher NACKs it with a TRAINING_RESULT error
SAVE_IMAGEblock → hostrequest → replypage-only
payload
{
    requestId: string;
    /** Own-output URL — origin-allowlisted host-side. Mutually exclusive with `imageId` / `bytes`. */
    url?: string;
    /** Cross-user image id — routed through the gated per-viewer read. Mutually exclusive with `url` / `bytes`. */
    imageId?: number;
    /**
     * In-tab file bytes — classified by CONTENT host-side, size-capped host-side.
     * Page apps only. Mutually exclusive with `url` / `imageId`.
     */
    bytes?: ArrayBuffer;
    /**
     * Optional download filename (host-sanitized). For `bytes`, a `.json`
     * name selects JSON for text that parses as JSON, and the extension is
     * forced from the classified type.
     */
    filename?: string;
}
reply SAVE_IMAGE_RESULT
{
    requestId: string;
    ok?: boolean;
    error?: string;
}
Model slot: download bridge is a page-only affordance today; the paid-output apps are page apps, the model slot has no such surface
SET_COLLECTION_FOLLOWblock → hostrequest → reply
payload
{
    requestId: string;
    /** Positive integer. A numeric STRING is refused, never coerced. */
    collectionId: number;
    /** `true` ⇒ follow, `false` ⇒ unfollow. */
    follow: boolean;
}
reply COLLECTION_FOLLOW_RESULT
{
    requestId: string;
    result?: BlockCollectionFollowResult;
    error?: BlockCollectionFollowErrorCode | string;
}
SET_ENTITY_REACTIONblock → hostrequest → reply
payload
{
    requestId: string;
    entityType: BlockReactionEntityType;
    /** Positive integer. A numeric STRING is refused, never coerced. */
    entityId: number;
    /** One of `APP_REACTIONS`. `Dislike` is refused as `invalid-request`. */
    reaction: BlockAppReaction;
    /** `true` ⇒ the reaction exists afterwards; `false` ⇒ it does not. */
    reacted: boolean;
    /** 1 to `REACT_TICKET_MAX_LENGTH` characters. */
    ticket?: string;
}
reply ENTITY_REACTION_RESULT
{
    requestId: string;
    result?: BlockEntityReactionResult;
    error?: BlockEntityReactionErrorCode | string;
}
THEME_CHANGEhost → block
payload
{
    theme: Theme;
}

Payloads reference SDK types

Some payload fields are typed as named SDK interfaces (for example WorkflowBody, BlockViewer, AppWorkflow). Those come from @civitai/app-sdk/blocks — import the package to get the full type definitions in your editor.

Civitai Developer Documentation