Inspiration API

Contract v2.6.1 — frozen. Breaking changes require a version bump and a parallel endpoint.

The API surface that creates and manages Inspirations — the static-asset entities you drop onto the grid: decks, docs, demo HTML, images. This is the single source of truth for any client that writes Inspirations: the web UI, the CLI (grid plug), and API automation.

1. Naming and verb

The user-facing verb is drop. The user-facing noun is Inspiration. The endpoints reflect this:

The CLI command is grid plug <path> — plug is the unified verb for putting any node on the grid.

2. Auth

All endpoints require:

If the header resolves to a grid you have no membership in, the response is 403.

3. POST /api/v2/inspirations — create

Request is multipart/form-data:

FieldRequiredTypeConstraintsNotes
fileyesbinarymax 50 MBThe Inspiration payload
nameyesstring3–40 chars, ^[a-z][a-z0-9-]*$URL-safe slug
display_namenostring1–100 charsHuman-readable; defaults to name
scopeyesenumpersonal | space | orgWhere this Inspiration lives
space_idconditionalUUIDRequired if scope: space; you must be a member of that space
visibilityyesenumprivate | space | org | linkWho can see it
expires_in_daysnointeger1–365, or null for pinnedDefault 30
pin_reasonconditionalstring5–280 charsRequired if expires_in_days: null
source_typeyesenumsee the table belowHow the payload was authored
uploaded_viayesenumweb | cli | claude_extension | claude_mcp | apiAttribution
inspired_bynoUUID[]Up to 10 entity idsInspirations this one builds on (see inspired_by)

Slug format: the name you submit is the base slug. The server appends a 4-char hex suffix (-XXXX) to prevent URL enumeration and cross-user collisions. The response name and url fields reflect the full suffixed slug. Use the returned value when constructing links — don't reconstruct from your original input.

Scope and visibility — interaction rules

Supported source_type values

ValueFile types acceptedRendered as
html.html, .htm, .zip (zip must contain index.html at root; nested directories preserved)Static site
react_single_file.jsx, .tsx, .jsWrapped in minimal scaffold, served static
markdown.mdStyled doc render
pdf.pdfIn-browser viewer
pptx.pptxEmbedded viewer
docx.docxEmbedded viewer
xlsx.xlsxEmbedded viewer
image.png, .jpg, .jpeg, .svg, .webpGallery wrapper

The server validates the extension matches the declared source_type. Mismatch returns 415.

Response — 201 Created

{
  "entity_id": "7f8e2a4c-...",
  "name": "q2-pricing-deck-f8a2",
  "display_name": "Q2 Pricing Deck",
  "realm": "inspiration",
  "kind": "inspiration",
  "url": "https://acme.cloudgrid.io/q2-pricing-deck-f8a2",
  "scope": "personal",
  "space_id": null,
  "visibility": "org",
  "expires_at": "2026-05-13T00:00:00Z",
  "pinned": false,
  "source_type": "pptx",
  "status": "provisioning",
  "view_count": 0,
  "reactions": { "sparked": 0, "useful": 0, "build_on": 0, "impressive": 0, "discuss": 0 },
  "owner": { "user_id": "a1b2c3d4-...", "name": "Marcus Chen", "slug": "marcus" },
  "grid_slug": "acme",
  "space": null,
  "inspired_by": [],
  "created_at": "2026-04-13T14:22:31Z",
  "brain_status": "pending",
  "caller_permissions": { "can_edit": true, "can_archive": true, "can_pin": true }
}

The reactions map may also carry historical keys (viewed, resonated, love) as read-only pass-throughs alongside the five pressable kinds — no key ever disappears from the wire shape, so don't reject unknown reaction keys. See Reactions.

caller_permissions is server-computed: what you may do on this Inspiration (can_edit / can_archive / can_pin are true for the owner and grid admins). Use these flags to render action controls rather than re-implementing the check. They are also present on list rows.

When scope: space, the space field is populated with { space_id, slug, name }.

Status semantics

Display the URL immediately on 201 but expect a brief 503 window. Poll GET /api/v2/inspirations/:id at 1-second intervals if you want to confirm live before showing an "Open" control.

brain_status semantics

Enrichment is fire-and-forget. The upload response does not block on it.

4. GET /api/v2/inspirations/:id — read

:id accepts either an entity_id (UUID) or a name (slug), resolved against your active grid. Anonymous callers can only look up by UUID; a name-shaped param from an anonymous caller returns the same 404 NOT_FOUND shape as an unknown id, so the endpoint does not leak whether a slug exists.

Access

You must have read access per the Inspiration's visibility. The four modes:

Visibility requires a live-or-building state. Declared visibility only takes effect for viewers other than the owner or a grid admin while the entity is serving or on its way to serving (running, building, provisioning, paused, or a failed redeploy still serving its last good version). In any other state — draft, stopped, expired, archived, failed-never-live — effective visibility collapses to private: detail reads return 404 NOT_FOUND and list responses omit the row. The owner and grid admins keep seeing their rows. Stored visibility is never mutated.

Response

Same shape as the create response, with current values, plus:

Side effects

Calling this endpoint as an authenticated user counts as a view for that user (deduplicated by user-day). Anonymous calls (link visibility, no JWT) increment the counter using IP-day deduplication.

5. List endpoints

GET /api/v2/inspirations

Returns Inspirations visible to you. Visibility filtering is applied server-side — an Inspiration you cannot see is never returned, regardless of query parameters. Responses only include Inspirations in your active grid.

ParamTypeDefaultNotes
ownerstring (user slug, or me)Only Inspirations owned by this user. me resolves to the caller.
spacestring (space slug)Only Inspirations scoped to this space. You must be a member.
sortcreated_at_desc | view_count_desc | reactions_desccreated_at_desc
cursorstringOpaque cursor from the previous response's next_cursor.
limitinteger501–100.

Response is { items, next_cursor, total_returned }. Each item is a summary row: entity_id, name, display_name, url, visibility, view_count, reactions, owner, sparse enrichment fields, created_at, caller_permissions, and three retention fields:

The summary omits inspired_by — call GET /api/v2/inspirations/:id when you need the full shape.

GET /api/v2/runtimes

Returns Runtimes (Apps and Agents) visible to you. Runtimes are always grid-scoped; every runtime in your active grid is visible to every member.

ParamTypeDefaultNotes
kindapp | agent | allall
ownerstringUser slug.
space_idstring (space UUID)Only runtimes tagged to this space.
sortcreated_at_desc | updated_at_descupdated_at_desc
cursor, limit50Same as above.

Each item carries entity_id, name, display_name, kind, url, status, owner, sparse enrichment fields, created_at, updated_at. A never-deployed draft additionally carries sparse expires_at + expires_in_days (drafts expire after a platform-set TTL, 14 days by default); live, paused, and failed entities carry no retention clock.

6. PATCH /api/v2/inspirations/:id — update

Only the owner or a grid admin can update. Non-owner non-admin gets 403; cross-grid gets 404.

Request body (all fields optional; pass at least one):

FieldTypeConstraints
display_namestring1–100 chars
descriptionstring | nullup to 280 chars; null clears it
visibilityprivate | authenticated | grid | linkfour-mode model; see Visibility. Legacy values are accepted for a transition window and mapped on store.
visibility_spacesstring[]honored only when the effective visibility is grid (space-slug list; ['everyone'] / omitted = whole grid)
link_indexedbooleanhonored only when the effective visibility is link (omitted = false)
scopepersonal | space | orgscope is independent of visibility; unchanged unless explicitly set
space_idstring | nullUUID; required when scope: space; you must be a member; cleared on move to personal/org

Side effects of a scope change:

Response: 200 with the refreshed full Inspiration shape, including caller_permissions.

StatusCodeWhen
400VALIDATION_FAILEDempty body, invalid length, invalid enum
400SCOPE_INVALIDscope and visibility combination not in the allowed set (legacy visibility values only)
400SPACE_REQUIREDscope: space without space_id
403SPACE_NOT_MEMBERnot a member of the target space
403FORBIDDENnot owner, not admin
404NOT_FOUNDentity missing, wrong realm, or cross-grid
409NAME_COLLISIONslug conflicts at the target scope

7. Visibility

Every entity — Inspiration or Runtime — carries one of four visibility modes. Two modes carry a sub-state:

ModeWho can see the entitySub-state
privateonly the creator
authenticatedany logged-in CloudGrid user (network-wide)
gridgrid members in at least one of the listed spacesvisibility_spaces: string[]. ['everyone'] (default) = all grid members. A custom subset = only members of the listed spaces.
linkanyone with the URLlink_indexed: boolean. false (default) = not crawlable (noindex + sitemap exclusion). true = public and indexable.

Read responses carry two sparse fields, each emitted only for the mode it belongs to: visibility_spaces (for grid, default ['everyone']) and link_indexed (for link, default false).

PATCH /api/v2/entities/:id/visibility

The write surface for the visibility model on any entity. :id accepts an entity_id (UUID) or a name/slug scoped to your active grid. You must own the entity or be a grid admin; otherwise 403 NOT_OWNER. Cross-grid access surfaces as 404.

FieldRequiredTypeNotes
visibilityyesprivate | authenticated | grid | linkThe deprecated org value is rejected on this route (400 VALIDATION_FAILED, hint to use grid).
visibility_spacesnostring[]Only valid when visibility: grid. Non-empty array of space slugs. Omitted means ['everyone'] (whole grid). An unknown slug returns 400 SPACE_NOT_FOUND.
link_indexednobooleanOnly valid when visibility: link. Omitted means false.

Supplying visibility_spaces for a non-grid mode, or link_indexed for a non-link mode, returns 400 VALIDATION_FAILED. Each mode clears the sub-state it does not own, so a transition between modes never leaves stale fields behind.

For runtime entities, a visibility change re-applies the entity's Ingress so the serving auth posture (gated vs public) tracks the new visibility. Inspirations are enforced at the serving read path, so there is no re-apply step.

Response — 200: { entity_id, name, visibility, visibility_spaces?, link_indexed?, updated_at }.

PATCH /api/v2/inspirations/:id/visibility

The Inspiration parallel of the entity route, so clients carry one visibility shape across both realms. Same body, same validation, same response shape. One difference: this route still accepts legacy visibility values (org, space, link-anonymous, link-indexed, public) for a transition window and maps them to the four-mode model on store. A legacy value combined with either sub-state field is rejected — use the canonical mode.

This route writes visibility only. It never touches scope or the scope-owned space_id — scope is where the Inspiration lives; visibility is who can see it.

8. Errors

All errors follow one shape:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Some fields were not accepted.",
    "details": [
      { "field": "name", "issue": "already_exists", "hint": "Try 'q2-pricing-deck-v2'." }
    ],
    "request_id": "req_..."
  }
}
StatusWhenRetryable
400Validation failure (bad field, invalid scope+visibility combination, mismatched source_type)No
401Missing or invalid JWTNo
403No access to the grid or spaceNo
404Not found, or visibility hides it from the callerNo
409Name collision within scopeNo
413File too largeNo
415Unsupported file type for the declared source_typeNo
422File contents malformedNo
426CLI below the minimum supported version (CLI_UPGRADE_REQUIRED) — upgrade and retryAfter upgrade
429Rate limited (see rate limits)Yes, after Retry-After
500Server errorYes, with backoff
503Grid or scope resources temporarily unavailableYes, with backoff
CodeWhen
SCOPE_INVALIDscope+visibility combination is invalid (e.g. scope: org, visibility: private)
SPACE_REQUIREDscope: space without space_id
SPACE_NOT_MEMBERscope: space for a space you aren't in
INSPIRED_BY_INVALIDone or more inspired_by entity ids don't exist or aren't visible to you
ORG_HEADER_REQUIREDthe active-grid header is missing on a write for a user with more than one grid membership. Single-grid users fall back to their only membership silently.

9. POST /api/v2/entities/:id/reactions — toggle a reaction

Leave or toggle a reaction on any entity you can read — app, agent, or Inspiration. (POST /api/v2/inspirations/:id/reactions is a deprecated alias with the identical shape; new clients use the entities path.)

{ "kind": "sparked" }

kind must be one of the five pressable kinds: sparked | useful | build_on | impressive | discuss. Three historical kinds (viewed, resonated, love) are retired as pressable and return 400 VALIDATION_FAILED on a press — their historical counts are preserved and still surface in reactions. Page views are counted by the visit beacon, not a reaction press.

Response — 200 OK

{
  "entity_id": "7f8e2a4c-...",
  "reactions": { "viewed": 0, "resonated": 1, "love": 5, "sparked": 3, "useful": 2, "build_on": 0, "impressive": 4, "discuss": 0 },
  "user_reactions": ["sparked", "impressive"]
}

Toggle behavior

POSTing the same kind twice toggles it off. The reaction state is strongly consistent; the count is derived asynchronously, so it never makes the toggle decision wrong.

Auth

You must have read access to the entity. Anonymous reactions (link visibility, no JWT) are not supported — only logged-in users can react. Reactions on private Inspirations are refused (403), and reactions on archived entities are refused.

10. POST /api/v2/views/:entity_id — visit beacon

Records one visit to a shared serving URL. The served public page is cached and byte-identical for every viewer, so a recipient opening a shared link never calls the detail endpoint — this beacon is the cache-safe signal. A tiny inline snippet on the served page fires it once on first paint. It is a thin wrapper over the same per-day-deduplicated view counter the detail read uses — not a separate counter.

Filtering (so the count means something)

11. Rate limits

LimitWindowApplies to
10 Inspirations created per minuteper userAll clients
100 Inspirations created per hourper gridAll clients
50 MB max file sizeper requestPer tier — Starter is 50 MB; future tiers raise this
60 reactions per minuteper userAll clients
200 Inspiration reads per minuteper userAll clients

Exceeding a limit returns 429 with a Retry-After header (seconds) and the specific limit hit in error.details.

12. Active grid context

All write endpoints require the X-CloudGrid-Grid header to specify which grid you are acting in. The value is the grid's slug (e.g. acme). The server resolves the slug and validates membership. If the slug is unknown or you have no membership, the response is 403 ORG_NOT_ACCESSIBLE. For reads, the header is optional and defaults to your primary grid.

13. The inspired_by relationship

When creating an Inspiration, you can pass inspired_by: [entity_id, ...] listing up to 10 other Inspirations that inspired this one. Each referenced id creates a link. You must have read access to each referenced source — you can only declare a source you can see; otherwise the request fails with INSPIRED_BY_INVALID.

Links are realm-agnostic: a Runtime can be inspired by an Inspiration, and an Inspiration can build on another Inspiration.

Link endpoints

14. What this contract does not cover