# Glop Creator API For agents and other clients participating as Street users, see the [Street account API](street/api.md). It uses account credentials distinct from the draft-upload creator tokens below. ## General Street building widgets Uploaded Glops are not limited to the feed: a currently public, non-beta READY uploaded release with `index.html` can be registered as an immutable widget definition and placed at any owner's constructed building or outdoor side street. External-page bundles, drafts, hidden sources and suspended creators are ineligible. Owners use **Remodel → Widgets**; the same editor registers public sources (including another owner's), places objects, and edits/removes instances. Publication still requires the uploader's normal browser approval; registration does not publish or widen access to a private upload. The account API route catalog is: | Method | Route under `/api` | Purpose | | --- | --- | --- | | GET / POST | `/street/widget-types` | Public eligible catalog / register a custom definition | | DELETE | `/street/widget-types/:id` | Creator removes an unused custom definition | | GET / POST | `/street/widgets?plot=N` (GET), `/street/widgets` (POST) | List / owner creates an instance | | GET / PUT / DELETE | `/street/widgets/:id` | Read / owner edits / owner removes with revision CAS | | GET / POST | `/street/widgets/:id/state` | Custom instance public JSON state / nearby signed-in CAS write | | GET / POST | `/street/widgets/:id/chess` | Authoritative game for one chess instance | | GET / POST | `/street/widgets/:id/jukebox` | Queue and stream signaling for one jukebox instance | Use account sessions or Street credentials, not draft-upload creator tokens. Public configuration/state must not contain secrets. Definitions pin an immutable release; live source revocation hides public instances/actions, while the owner can list unavailable instances for cleanup. Property snapshots use `widgets`; instance CRUD replaces the old property media-prop contract and global adapters. The full request/response schemas, placement axes/radians/dimensions, 16-instance limit, 8 KiB config, 32 KiB state, state/config revision fence and presence rules are in [Street API](street/api.md#general-building-widgets). [SDK](SDK.md#street-widget-instances) documents sandbox availability and `glop.widget.getInfo/getState/setState` with a shared-state example. Widget mounts do not inherit feed-only host privileges. Base URL: `https://glopit.lol/api`. JSON requests and responses. The CLI at `/glop-cli.mjs` uses these same endpoints. Run with Node 20+; no package installation is required. ## Browser upload Sign in, choose **+**, then select or drop a ZIP, HTML file, or built app folder. Glop checks the files, uploads a private draft, and shows a playable preview before publication. A single HTML file is renamed to `index.html`; ZIPs may contain one enclosing folder. Upload limits: 256 files, 64 MiB/file, 128 MiB total. ZIP input may be up to 129 MiB compressed; the JSON upload envelope may be up to 192 MiB including base64. Stored bundles are capped at 512 MiB per project and 1 GiB per account; each project may retain up to 20 bundles. Use relative, included scripts and assets. Images and videos must be assets inside a site bundle; standalone image and video uploads are not supported. Use `glop.fetch()` for external APIs. Use `glop.openURL()` and `glop.location.get()` for host-confirmed browser actions; native popup and location APIs remain blocked inside uploads. See [SDK.md](SDK.md). ## John's authorized draft automation John has authorized automated draft uploads to his production account `admin` without per-upload approval. The operator-minted credential lives only at `/home/jt/.local/state/glop-owner/credentials.json` (directory 0700, file 0600). Never print it, commit it, or include it in an upload. Use: ```sh GLOP_CONFIG_DIR=/home/jt/.local/state/glop-owner node cli/glop.mjs upload ./dist --origin https://glop.it --name "My Glop" --caption "Description" ``` The same command with `--project ID` uploads another private version; it does not update a published post. The token may create owned projects, upload/read drafts, delete unattached bundles, and change only `deviceTarget` through project PATCH. Server authorization denies publication, replacement of published posts, other project/visibility changes and account administration, regardless of legacy publish grants. Return the private draft URL. Only John may approve publication through his signed-in browser; agents must never approve on his behalf. Tokens expire after 30 days and are revocable under Connected tools. Renew only with explicit operator authority; do not broaden permissions. Other accounts still use the device authorization flow below. ## Authorize a coding tool 1. `POST /creator/device` with `{"name":"My coding tool"}` (no credentials). Returns `deviceCode`, `userCode`, `verificationUrl`, `expires`, and a polling `interval` in seconds. 2. Show the verification URL and user code to the person. They open it in their signed-in browser, verify the code, and approve draft uploads. Tokens start with draft-only access. Explicit per-Glop grants can authorize API publication later. The tool never needs their account password. 3. Poll `POST /creator/device/poll` with `{"deviceCode":"..."}` no faster than the indicated interval. A `202` response means pending; a `200` response includes `token`, `tokenId`, `username`, `canPublish`, and `expires`. `410` means expired or already exchanged. The code expires after ten minutes and is exchanged once. 4. Store the returned token outside the app/source, in a private credentials file. Send it as `Authorization: Bearer glop_...` on subsequent API calls. Do not combine bearer tokens with browser cookies. Never include it in a Glop bundle or URL. Tokens expire after 30 days. The person can revoke them under **Your account → Connected tools**. The same screen can create a token manually. Draft-only tokens can create Glops, read their owner's Glops, upload/list files, and delete unattached uploads. They cannot publish, access another person's Glops, use runtime rooms, or change account settings. ## Upload a draft `GET /creator/me` returns the connected account; `canPublish` is always false for creator tokens, and `projects` lists `{id, name, canPublish: 0, canManageSpaces}` for shared-space grants. `POST /projects`: ```json {"name":"My Glop","description":"A short caption"} ``` Returns `project.id`. Optional `handle` must be unique. Use an `Idempotency-Key` header and reuse it for an identical retry. The owner is always the authenticated account; a supplied owner ID has no effect. `POST /projects/:projectId/playable-bundles` with an `Idempotency-Key`: ```json {"entry":"index.html","files":[{"path":"index.html","base64":"BASE64_ENCODED_BYTES"}]} ``` Returns `bundle.id`, `bundle.status`, and `draftUrl`. Resolve the draft URL against the site origin and show it to the uploader. Opening it requires the owning account. The person can review the preview, choose **Edit** to change the caption/name or replace files, then **Review changes** and **Approve & publish**. Uploads stay private until browser approval or an explicitly authorized API publication. Draft URLs do not carry account credentials or grant access to other accounts. `GET /projects/:projectId/playable-bundles` lists uploads and their statuses. `GET /projects/:projectId` returns the owned project and publications. `DELETE /projects/:projectId/playable-bundles/:bundleId` deletes an unattached upload. Matching retry keys return the original project or bundle. Different content with the same key is rejected (`409`). Interrupted keyed uploads can retry the same bytes and key; they reuse the pending upload. A deleted resource is not resurrected by an old key (`410`). Keep the key through network failures. Do not start a new upload just because a response was lost. ## Approve publication Open the returned `draftUrl` in the owning account. It mounts the linked version in the private-feed view, with one **Fullscreen** button (desktop mouse capture is requested by the same click). Browser fullscreen denial retains Glop's in-page fullscreen. **Publish** stays in the chrome and opens review only when selected; nothing is approved automatically. Existing `#edit` and `&compose=1` links still open the editor. Signed-in testers with a valid `#test` link get the running draft and Fullscreen, never owner publication controls; revoked links remain denied. The review preview must load before **Approve & publish** becomes available. **Edit** lets the owner change the name/caption, replace site files and their assets. **Review changes** saves the draft and returns to the preview; it does not publish. All creator tokens are forbidden from calling `POST /projects/:projectId/posts`, including tokens with legacy account-wide or per-project publishing flags. There is no publishing grant or automated exception. Return the draft URL to the user; the user opens the approval dialog and selects **Approve & publish** themselves. Never use their browser session to approve on their behalf. This applies to new Glops and updates. Approving a first upload creates a post. For an existing Glop, approval replaces the latest post in place, preserving its ID, link, feed position, stars and comments. Edits remain private until approval. Owner-session requests include `replacePostId` for an update. Retried approvals are idempotent. Approval opens `https://glopit.lol/#release=POST_ID`. Public browsing is available to guests; writing and shared storage require sign-in. ## CLI ```sh node glop-cli.mjs login node glop-cli.mjs upload ./dist --name "My Glop" --caption "Try it" node glop-cli.mjs upload ./my-glop.zip --name "My Glop" node glop-cli.mjs whoami node glop-cli.mjs publish --project PROJECT_ID --bundle BUNDLE_ID --request-id MY_RELEASE ``` `upload` accepts a directory, ZIP, or HTML file and returns a private draft link. Credentials and upload-retry journals live under `~/.config/glop` with private file permissions. Override with `GLOP_CONFIG_DIR` for isolated testing. Use `--origin http://127.0.0.1:PORT` for a local server. HTTPS is required otherwise. Redirects are rejected. `--project ID` uploads a new bundle to an existing owned Glop. `--request-id ID` explicitly starts a new operation; reuse it for retries. Uploading identical input normally resumes the previous operation. ## Project-scoped operator grants A self-hosted operator can grant a connected tool permission for specific Glops after the owner authorizes it. This command requires privileged server access; it is not an unauthenticated endpoint, and a token cannot grant itself access: ```sh docker exec glop node scripts/creator-permissions.mjs \ --database /data/glop.sqlite --username OWNER --token-id TOKEN_ID \ --projects PROJECT_ID,OTHER_PROJECT_ID --spaces ``` `--spaces` permits GET/POST shared-space management only for those projects. `--revoke` removes the selected grants. The complete set is applied atomically, and every project must belong to the named token owner. Expired, revoked and suspended tokens cannot be granted access. Publication cannot be granted. Sharing writes recheck the grant in their database transaction. Revoking the token removes all its grants. API access to other accounts, account settings, runtime rooms and runtime storage remains unavailable to creator tokens. `publish` uses a local retry journal and a server idempotency key. It updates the latest post in place, preserving its link, stars and comments. Reuse the same `--request-id` when retrying the same publication; different input with that ID is rejected. Review the bundle before explicitly running this command. ## Errors `400` invalid input, `401` sign-in/token required, `403` denied scope or ownership, `409` conflict, `410` expired/deleted resource, `413` size/quota limit, `429` rate limit, `503` transient storage error. Browser session calls require CSRF protection; creator tokens authorize only the documented creator routes. API tokens are separate from the sandboxed runtime SDK. ## Caption and preview consistency Captions and project descriptions accept up to **2,000 characters** in browser creation, draft editing, publication, and the API. The CLI preserves the supplied caption instead of truncating it to 220 characters. The feed may visually shorten a caption without changing its stored text. Draft review supports phone, landscape, desktop, and current-window viewports, with optional simulated feed chrome. These controls change only preview layout; publication still requires the owner to approve the reviewed draft. ## Source download privacy Set `hideSource: true` when creating a project, updating its metadata, or approving a post to hide source downloads from other viewers. Omit it to preserve the existing setting; new projects default to shared downloads. Only the owner can change it. The project and feed metadata expose `hideSource`, and the download endpoint enforces it even with a direct URL. Owners retain download access. This setting applies to all posts in the Glop. It does not conceal code or assets delivered to a browser for playback. ## Account recovery Signup accepts an optional `recoveryEmail`. Browser signup/login and `/api/auth/me` return only that account's `user.recoveryEmail` (or null). Signed-in browser sessions can `PUT /api/auth/recovery-email` with `{recoveryEmail}` and the normal CSRF token. Addresses are private to the account and allowlisted admins. Saving an address does not verify it; access to that mailbox authorizes password recovery. `POST /api/auth/forgot-password` accepts `{username, email}` with an exact Origin header. It returns the same message for matching and nonmatching accounts. Resend sends a 30-minute link to the saved address when the details match. Delivery is limited to one request per account per 10 minutes and 100 messages per day across the service. Both recovery endpoints share a 20-attempt hourly client-IP limit. `POST /api/auth/reset-password` accepts `{token, password}` with an exact Origin. Tokens are stored only as hashes, expire, and are consumed atomically. Changing the password or recovery email invalidates outstanding links. A successful reset revokes sessions, connected tools, device grants, and playable access. Suspended accounts cannot use email resets. Links carry the token in a URL fragment, which the reset page removes from browser history immediately. Normal sign-in follows. Provider failures invalidate the link and emit a log without account details; the public response stays generic. Admins can `POST /api/admin/users/:id/reset-password` with `{password, reason}`. Passwords must contain 12–128 characters. Existing admin-account protections, CSRF checks, origin checks, and write-time session guards apply. One transaction updates the salted password hash, records an audit action without the password, and revokes sessions, connected tools, pending device grants and playable access. Suspension and published content remain unchanged. Admins should establish ownership separately and share the new password privately. The additive `recovery_emails` table is created by the existing atomic runtime schema migration; existing user rows, password hashes and sessions are preserved. ## Public signup and policy agreement Signup is public and no longer accepts an invitation password as authorization. `POST /api/auth/signup` requires `username`, `password`, `policyVersion: "2026-09-18"`, `acceptPolicies: true`, and `ageBand: "teen" | "adult"` (13–17 or 18+). `recoveryEmail` remains optional. Read `/legal.html` before agreeing. No birthday is collected. An under-13 response blocks signup in that browser for one day with an essential cookie and does not create an account. Existing accounts receive `policyVersion: null` until they review the policies. `PUT /api/auth/policies` accepts the three policy fields, with session + CSRF. Other application APIs return 428 until agreement; logout, account settings, and admin moderation remain available. Existing agreement/age records cannot be rewritten through this endpoint. Personalization defaults off. Users 13 and older may separately opt in using `PUT /api/auth/personalization` with `{ "enabled": true }`; it uses the viewing, star, and follow signals described in the Privacy Notice. A false choice deletes active recommendation viewing history and leases. Signup and policy review accept an optional `personalization` boolean, default false; enabling it is separate from agreeing to the terms. Recommendation queries exclude non-opted-in accounts from models. `POST /api/reports` requires no account, but requires the same Origin and JSON. Fields: category (`safety`, `copyright`, `intimate-image`, `privacy`, `appeal`, `other`), target, details, contact email, signature (empty for ordinary reports), and boolean attestation. Copyright/intimate-image reports require target, signature, and attestation. Limits: 10 accepted attempts/hour/network address. Returns a report reference. Admins review `/api/admin/reports` and resolve a report with `POST /api/admin/reports/:id/resolve` and a reason, after taking any required moderation action. This saves an audit record; it does not send email. ### Admin analytics `GET /api/admin/analytics?days=7|30|90` requires an admin browser session. It returns UTC daily `series` entries with `day`, `signups`, and `pageViews`, plus `pageViewsStarted`. Pass `interval=hour` for 24 hourly buckets or `interval=minute` for 60 minute buckets; the default is `interval=day`. Bucket keys and boundaries use UTC. Signup counts use existing account creation dates. Page views are aggregate public HTML page loads on the VPS, including reloads and automated visits. Admin pages, previews, API calls, and assets are excluded. Unknown historical page views are `null`, never fabricated zeros. `GET /api/admin/resources` also returns `server.history`: the latest sample in each five-minute interval over the last 24 hours. CPU use is measured in cores against `cpuLimit`; memory, disk, database, and stored-file measurements include their actual limits. Measurements persist across VPS restarts. Sites beta has no VPS resource history or page-view collector. ## Creator profiles Public profiles are at `/@username`. `GET /api/profiles/:username` returns public identity, following/follower/star counts, and up to 24 published Glops. Pass the returned `next` cursor as `?before=` for older posts. Drafts and beta-only Glops are excluded. A signed-in browser can `PUT /api/profile` with `{ "displayName": "Name", "bio": "About me" }` and its CSRF token. The name is limited to 60 characters and the bio to 160. This updates only the current account. Follow controls use the existing `PUT /api/users/:id/follow` route. ## Glop covers An uploaded site can include a root-level `cover.png` (PNG, at most 5 MiB, maximum 4096 × 4096). The upload validates the image before storing the bundle. The browser uploader also has an optional PNG cover picker, which adds this file for you. To change only the cover, open Edit, choose a new image, Review changes, then Approve update. The existing site files and public link stay the same. Bundled covers are included in the source download; separately edited covers do not change the downloadable site files. Both use the existing storage quota. Bundle and profile-post responses include `cover`, a URL or `null`. `GET|HEAD /api/playable-bundles/:bundleId/cover` serves the PNG: published public Glops are readable by guests, drafts remain owner-only, and private beta covers require beta access. Hiding source downloads does not hide the public cover. Glops without a cover use a title card; no screenshots are generated. `POST /api/projects/:id/covers` uploads raw `image/png` bytes (same limits), using the owner's browser session and CSRF token. It returns `{cover:{id,url}}`; retrying the same image reuses its stored bytes. The image remains owner-only until a browser approves it by passing `coverId` to the existing `POST /api/projects/:id/posts` publication route. A cover from another project is rejected. Omit `coverId` to keep the current cover, or use `null` to return to the bundled image/title card. Post responses include `coverId`. `GET|HEAD /api/covers/:id` applies the same public/draft/beta access boundary. ## Supported devices Project creation, project PATCH, and publication POST accept `deviceTarget`: `all` (default), `mobile`, or `desktop`. Omission preserves the existing choice. Editing a published Glop applies this setting when the update is approved. Project and feed responses include `deviceTarget`. Glop displays an open-on-supported-device notice instead of mounting incompatible apps; phones and tablets count as mobile. This is a compatibility setting, not an access-control boundary. Creator previews remain available on all devices. Creator credentials may PATCH `/projects/:id` with **only** `{"deviceTarget":"desktop"}` (or `all`/`mobile`) on their own Glops. This changes compatibility immediately, including existing posts; it does not approve or publish a draft. Mixed patches containing other metadata, visibility or ACL fields are denied in full. Use `node cli/glop.mjs set-device --project PROJECT_ID --device desktop --origin https://glop.it`; the CLI reads the project back to verify the setting. Publication remains browser-owner-only. ## Shared-space management Use the signed-in owner's browser session and its `X-CSRF-Token` on mutations. Creator tokens require an explicit `--spaces` grant for the selected project; use the bearer token without browser cookies or CSRF headers. The creator editor exposes these operations under **Edit → Shared spaces**; changes are immediate, not staged with a release. Private-beta projects cannot create or connect spaces. `GET /api/projects/:projectId/spaces` returns: - `owned`: spaces `{id, name, members}` owned by this project. Members include `project_id`, `projectName`, `permission`, invitation `revision`, and `accepted`. - `connections`: invitations/connections `{id, name, ownerProjectId, ownerProjectName, permission, revision, accepted}`. `accepted` is `0` or `1`. `POST /api/projects/:projectId/spaces` accepts these JSON bodies: | Action | Body fields besides `action` | Authorization | | --- | --- | --- | | `create` | `name` (1–60 characters) | Project owner; returns `201 {space: {id, name}}` | | `grant` | `space`, `targetProjectId`, `permission: "read"` or `"write"` | Space-owning project owner | | `revoke` | `space`, `targetProjectId` | Space-owning project owner | | `connect` | `space`, `revision` from the current invitation | Invited project owner | | `disconnect` | `space` | Invited/connected project owner | | `delete` | `space` | Space-owning project owner; permanently deletes data | Other successes return `200 {ok: true}`. Every grant, including a reissue, creates a fresh invitation revision and resets acceptance. A stale `connect` fails with `409`. Disconnect/decline removes the invitation; rejoining requires a new grant. Missing/withdrawn rows return `409`, unauthorized owners `403`, invalid fields `400`, and quotas `413`. Creation names are unique per owning project (`409` on duplicate). Runtime data continues through `POST /api/storage/:postId` with the existing `action`, `key`, `value`, and `revision` fields plus optional `space` (UUID). Omitting `space` uses the existing isolated project store. Supplying it selects an explicitly authorized shared space; it never exposes that owner's ordinary store. The iframe host supplies the actual mounted post ID. Use `glop.spaces.open(id)` from uploaded code, not direct account API requests. Runtime reads and writes require a signed-in viewer, a published requesting Glop, a published space-owning Glop, and current sharing permission. Read-only members can `get`/`list`; write members can also `set`/`delete` keys. Permissions are enforced inside the database statement, including revision-checked writes. See [SDK shared spaces](SDK.md#cross-glop-shared-spaces) for limits and lifecycle. ## External hosting Publish a page hosted on your own public HTTPS domain without uploading its assets. Use the creator's external URL field, or `node cli/glop.mjs external https://your-domain.com/glop/ --name "My Glop"`. The existing `POST /api/projects/:id/playable-bundles` endpoint accepts `{"externalURL":"https://your-domain.com/glop/"}` instead of `entry` and `files`. Ownership, idempotency, draft privacy, owner approval, and version count limits still apply. The response includes `bundle.externalURL`; publication uses the returned bundle ID as usual. Capabilities return `{entry, external:true, progress}` only to authorized viewers. Load `` before your app scripts (or host a downloaded copy), allow Glop in your `frame-ancestors` policy, and serve modules/fonts/fetched assets/APIs with credential-free CORS (`Access-Control-Allow-Origin: *`). The frame keeps `sandbox="allow-scripts"`; localStorage, cookies and service workers are unavailable. URL fragments are reserved for the host channel. The SDK works through the existing authenticated host messaging channel. Content loads directly from your server; Glop stores only its URL and optional cover. Glop upload byte limits do not apply. Your hosting provider controls transfer limits. Draft privacy does not protect the external website, and hidden source downloads do not hide externally hosted files. Updates at that URL appear without re-uploading; use versioned URLs for stable releases. See `/docs#external-hosting` for setup.