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-reactyou built against, so it only understands the messages that version knew about. A push the host began sending later —THEME_CHANGEis 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/useSharedStoragestores, 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{
message: string;
fatal: boolean;
}BLOCK_INIThost → blockBlockInitPayloadBLOCK_READYblock → hostfire-and-forget{
height: number;
sdkVersion?: string;
sdkName?: 'blocks-react' | 'sdk';
}NAVIGATEblock → hostfire-and-forgetpage-only{
path: string;
scope?: BlockNavigateScope;
target: 'current' | 'new_tab';
}RESIZE_IFRAMEblock → hostfire-and-forget{
height: number;
}RESUMEhost → blockSUSPENDhost → blockTRACK_EVENTblock → hostfire-and-forget{
eventName: string;
properties?: Record<string, unknown>;
}Auth & token
CONSENT_UNAVAILABLEhost → blockConsentUnavailablePayloadREQUEST_CONSENTblock → hostfire-and-forget{
scopes?: string[];
}REQUEST_SIGN_INblock → hostfire-and-forget{
returnUrl?: string;
}REQUEST_TOKENblock → hostrequest → reply{
requestId: string;
blockInstanceId: string;
}TOKEN_REFRESH_RESPONSE{
requestId: string;
token: WrappedToken;
}or a TOKEN_REFRESH push when no requestId was sent
TOKEN_REFRESHhost → block{
token: WrappedToken;
}Generation workflows
CANCEL_WORKFLOWblock → hostrequest → reply{
requestId: string;
workflowId: string;
}WORKFLOW_CANCELED{
requestId: string;
snapshot: BlockWorkflowSnapshot;
}ESTIMATE_WORKFLOWblock → hostrequest → reply{
requestId: string;
body: WorkflowBody;
}ESTIMATE_RESULT{
requestId: string;
snapshot: BlockWorkflowSnapshot;
}ESTIMATE_WORKFLOW_BATCHblock → hostrequest → reply{
requestId: string;
bodies: WorkflowBody[];
}ESTIMATE_BATCH_RESULT{
requestId: string;
snapshots?: BlockWorkflowSnapshot[];
aggregate?: BlockEstimateBatchAggregate;
error?: string;
}POLL_WORKFLOWblock → hostrequest → reply{
requestId: string;
workflowId: string;
waitSeconds?: number;
}WORKFLOW_STATUS{
requestId: string;
snapshot: BlockWorkflowSnapshot;
}SUBMIT_WORKFLOWblock → hostrequest → reply{
requestId: string;
body: WorkflowBody;
idempotencyKey?: string;
}WORKFLOW_SUBMITTED{
requestId: string;
snapshot: BlockWorkflowSnapshot;
}App subqueue
CANCEL_APP_WORKFLOWblock → hostrequest → replypage-only{
requestId: string;
workflowId: string;
}CANCEL_APP_WORKFLOW_RESULT{
requestId: string;
result?: {
workflow: AppWorkflow;
};
error?: string;
}QUERY_APP_WORKFLOWSblock → hostrequest → replypage-only{
requestId: string;
params?: AppWorkflowsParams;
}APP_WORKFLOWS_RESULT{
requestId: string;
result?: {
workflows: AppWorkflow[];
cursor: string | null;
};
error?: string;
}Buzz
GET_BUZZ_ACCOUNTSblock → hostrequest → replypage-only{
requestId: string;
}BUZZ_ACCOUNTS_RESULT{
requestId: string;
result?: {
accounts: BlockBuzzAccount[];
};
error?: string;
}GET_BUZZ_BALANCEblock → hostrequest → reply{
requestId: string;
}BUZZ_BALANCE_RESULT{
requestId: string;
balance?: {
blue: number;
green: number;
yellow: number;
};
error?: string;
}GET_BUZZ_TRANSACTIONSblock → hostrequest → replypage-only{
requestId: string;
params?: BlockBuzzTransactionsParams;
}BUZZ_TRANSACTIONS_RESULT{
requestId: string;
result?: {
cursor?: string;
transactions: BlockBuzzTransaction[];
};
error?: string;
}GET_DAILY_COMPENSATIONblock → hostrequest → replypage-only{
requestId: string;
params?: BlockDailyCompensationParams;
}DAILY_COMPENSATION_RESULT{
requestId: string;
result?: {
resources: BlockDailyCompensationResource[];
hasPublishedResources: boolean;
};
error?: string;
}OPEN_BUZZ_PURCHASEblock → hostrequest → reply{
requestId: string;
suggestedAmount?: number;
}BUZZ_PURCHASE_RESULT{
requestId: string;
purchased: boolean;
newBalance?: number;
}Viewer
GET_VIEWERblock → hostrequest → replypage-only{
requestId: string;
}VIEWER_RESULT{
requestId: string;
viewer?: BlockViewer;
error?: string;
}Pickers & upload
IMAGE_SCAN_RESOLVEDhost → block{
requestId: string;
imageId: number;
result: BlockImageScanResult;
}OPEN_CHECKPOINT_PICKERblock → hostrequest → reply{
requestId: string;
/** Optional ecosystem-family filter. Absent ⇒ unconstrained. Never ''. */
baseModelGroup?: string;
/** Currently-selected versionId so the picker can pre-highlight it. */
currentVersionId?: number;
}CHECKPOINT_PICKER_RESULT{
requestId: string;
selected?: BlockCheckpointInfo;
}OPEN_IMAGE_PICKERblock → hostrequest → replypage-only{
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;
}IMAGE_PICKER_RESULT{
requestId: string;
selected?: BlockPickedImageInfo;
selectedImages?: BlockPickedImageInfo[];
error?: string;
}OPEN_IMAGE_UPLOADblock → hostrequest → replypage-only{
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;
}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;
}OPEN_RESOURCE_PICKERblock → hostrequest → replypage-only{
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;
};
}RESOURCE_PICKER_RESULT{
requestId: string;
selected?: BlockResourceInfo;
selectedResources?: BlockResourceInfo[];
error?: string;
}SET_USER_CHECKPOINTblock → hostrequest → reply{
requestId: string;
versionId: number | null;
}USER_CHECKPOINT_SET{
requestId: string;
ok?: boolean;
error?: string;
}Per-app storage
APP_STORAGE_DELETEblock → hostrequest → reply{
requestId: string;
key: string;
}APP_STORAGE_DELETE_RESULT{
requestId: string;
ok?: boolean;
deleted?: boolean;
error?: string;
}APP_STORAGE_GETblock → hostrequest → reply{
requestId: string;
key: string;
}APP_STORAGE_GET_RESULT{
requestId: string;
value: unknown;
error?: string;
}APP_STORAGE_LISTblock → hostrequest → reply{
requestId: string;
prefix?: string;
limit?: number;
cursor?: string;
}APP_STORAGE_LIST_RESULT{
requestId: string;
keys: Array<{
key: string;
updatedAt: string;
}>;
nextCursor?: string;
error?: string;
}APP_STORAGE_QUOTAblock → hostrequest → reply{
requestId: string;
}APP_STORAGE_QUOTA_RESULT{
requestId: string;
usedBytes: number;
rowCount: number;
limitBytes: number;
limitRows: number;
error?: string;
}APP_STORAGE_SETblock → hostrequest → reply{
requestId: string;
key: string;
value: unknown;
}APP_STORAGE_SET_RESULT{
requestId: string;
ok?: boolean;
error?: string;
sizeBytes?: number;
}Shared storage
SHARED_APPENDblock → hostrequest → reply{
requestId: string;
value: SharedStorageValue;
}SHARED_APPEND_RESULT{
requestId: string;
key: string;
error?: string;
rateLimit?: SharedStorageRateLimit;
}SHARED_GETblock → hostrequest → reply{
requestId: string;
key: string;
}SHARED_GET_RESULT{
requestId: string;
item: SharedStorageItemWire | null;
error?: string;
}SHARED_GET_COUNTblock → hostrequest → reply{
requestId: string;
key: string;
}SHARED_GET_COUNT_RESULT{
requestId: string;
count: number;
error?: string;
}SHARED_GET_COUNTSblock → hostrequest → reply{
requestId: string;
keys: string[];
}SHARED_GET_COUNTS_RESULT{
requestId: string;
counts: Record<string, number>;
error?: string;
}SHARED_LISTblock → hostrequest → reply{
requestId: string;
prefix?: string;
limit?: number;
cursor?: string;
}SHARED_LIST_RESULT{
requestId: string;
items: SharedStorageItemWire[];
nextCursor?: string;
error?: string;
}SHARED_REPORTblock → hostrequest → reply{
requestId: string;
key: string;
reason?: string;
}SHARED_REPORT_RESULT{
requestId: string;
ok?: boolean;
error?: string;
rateLimit?: SharedStorageRateLimit;
}SHARED_UNVOTEblock → hostrequest → reply{
requestId: string;
key: string;
}SHARED_UNVOTE_RESULT{
requestId: string;
count: number;
error?: string;
rateLimit?: SharedStorageRateLimit;
}SHARED_UPDATEblock → hostrequest → reply{
requestId: string;
key: string;
value: SharedStorageValue;
}SHARED_UPDATE_RESULT{
requestId: string;
ok?: boolean;
error?: string;
rateLimit?: SharedStorageRateLimit;
}SHARED_VOTEblock → hostrequest → reply{
requestId: string;
key: string;
}SHARED_VOTE_RESULT{
requestId: string;
count: number;
error?: string;
rateLimit?: SharedStorageRateLimit;
}SHARED_WITHDRAWblock → hostrequest → reply{
requestId: string;
key: string;
}SHARED_WITHDRAW_RESULT{
requestId: string;
ok?: boolean;
deleted?: boolean;
error?: string;
rateLimit?: SharedStorageRateLimit;
}Wildcard packs & resource files
GET_RESOURCE_FILEblock → hostrequest → replypage-only{
requestId: string;
modelVersionId: number;
fileId?: number;
}RESOURCE_FILE_RESULT{
requestId: string;
file?: BlockResourceFile;
error?: BlockResourceFileErrorCode;
}GET_WILDCARD_PACKblock → hostrequest → replypage-only{
requestId: string;
modelVersionId: number;
}WILDCARD_PACK_RESULT{
requestId: string;
pack?: BlockWildcardPack;
error?: BlockWildcardPackErrorCode;
}Other
BLOCK_HELLOblock → hostfire-and-forgetBLOCK_MESSAGE_REJECTEDblock → hostfire-and-forget{
type: string;
}CREATE_POST_FROM_APPblock → hostrequest → replypage-only{
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;
}CREATE_POST_RESULT{
requestId: string;
result?: BlockCreatePostResult;
error?: string;
}GET_ENTITY_REACTIONSblock → hostrequest → reply{
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>;
}ENTITY_REACTIONS_RESULT{
requestId: string;
result?: BlockEntityReactionsResult;
error?: BlockEntityReactionErrorCode | string;
}GET_IMAGES_BY_IDSblock → hostrequest → replypage-only{
requestId: string;
imageIds: number[];
}IMAGES_RESULT{
requestId: string;
result?: {
images: BlockGatedImage[];
};
error?: string;
}OPEN_MEDIA_UPLOADblock → hostrequest → replypage-only{
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;
}MEDIA_UPLOAD_RESULT{
requestId: string;
selected?: BlockMediaUploadInfo;
error?: string;
}PUBLISH_GENERATION_OUTPUTSblock → hostrequest → replypage-only{
requestId: string;
workflowId: string;
imageIndexes?: number[];
title?: string;
}PUBLISH_RESULT{
requestId: string;
result?: {
imageIds: number[];
};
error?: string;
}ROUTE_CHANGEDhost → block{
subPath: string;
}RUN_CONFIRMED_STEPblock → hostrequest → replypage-only{
requestId: string;
body: WorkflowBodyPassThroughStep;
}CONFIRMED_STEP_RESULT{
requestId: string;
snapshot?: BlockWorkflowSnapshot;
error?: string;
}RUN_TRAININGblock → hostrequest → replypage-only{
requestId: string;
body: WorkflowBodyTraining;
}TRAINING_RESULT{
requestId: string;
snapshot?: BlockWorkflowSnapshot;
error?: string;
}SAVE_IMAGEblock → hostrequest → replypage-only{
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;
}SAVE_IMAGE_RESULT{
requestId: string;
ok?: boolean;
error?: string;
}SET_COLLECTION_FOLLOWblock → hostrequest → reply{
requestId: string;
/** Positive integer. A numeric STRING is refused, never coerced. */
collectionId: number;
/** `true` ⇒ follow, `false` ⇒ unfollow. */
follow: boolean;
}COLLECTION_FOLLOW_RESULT{
requestId: string;
result?: BlockCollectionFollowResult;
error?: BlockCollectionFollowErrorCode | string;
}SET_ENTITY_REACTIONblock → hostrequest → reply{
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;
}ENTITY_REACTION_RESULT{
requestId: string;
result?: BlockEntityReactionResult;
error?: BlockEntityReactionErrorCode | string;
}THEME_CHANGEhost → block{
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.