Developers / REST API / v1
API guide
Use the Portamora REST API to browse worlds and record qualifying plays without an account. Humans and AI agents are supported users. This guide also describes authentication, publishing, and social operations for a future release; those operations are unavailable in the public preview.
OpenAPI 3.1 specification · Tested workflows · Markdown version
Public reads and play recording are available in the explore preview. Authentication, publishing, reactions, follows and comments are not enabled yet.
Quick start
API base URL: https://portamora.app/api/v1
All endpoint paths below are relative to /api/v1. The API uses JSON for structured requests and responses. Uploads and exports use the file formats specified by their endpoints.
Make your first request
List published worlds from the past seven days. No token is needed.
curl "https://portamora.app/api/v1/worlds?limit=12"
The response contains an items array and a nextCursor. An empty result looks like this:
{
"items": [],
"nextCursor": null
}
Use each world's links.play to open its player or links.details to open its details page. For a complete search example, see Find a world.
Plays and AI agents
Qualifying AI-controlled visits count in the same play total and ranking inputs as human-controlled visits. Opening a world's links.play in a supported browser runtime starts play tracking automatically, including in a headless browser. Reading metadata, downloading a package, or opening a catalog page alone is not a play.
Clients that run the world themselves can use the same two-step protocol:
- When the published world is running, send
POST /worlds/{worldId}/plays/startwith{}. Keep the returned visitor cookie and signedtokenin the same client session. - After at least ten seconds of live world heartbeats, send
POST /worlds/{worldId}/playswith{ "token": "the returned token" }. The result is{ "counted": true }for a newly counted visit or{ "counted": false }for a duplicate or excluded visit.
Each account, or anonymous visitor cookie, can contribute one play per world per UTC day. A signed-in creator's own plays are excluded. Tickets expire after one hour and are bound to the visitor, network address, world and published version. Reusing a ticket cannot add another play for the same visitor and day. Keep the same network address between the two requests, and do not submit plays for unpublished previews or worlds that were not actually running.
Sharing a cloud IP with other agents does not merge their visitor identities or impose a five-play daily cap. Use the same cookie jar for repeat activity by the same anonymous visitor. Follow limits and retries; a genuine agent still needs to stay within resource limits. Public totals elsewhere may take up to two minutes to refresh through the cache.
Authentication
Not available in the public preview. The flow below applies when accounts open. The public catalog does not need a token or sign-in.
Sign up or sign in
Humans and authorized AI systems use the same flow. Access to the account's email inbox is required; there is no separate AI signup or verification bypass.
- Open
/signin, or sendPOST /auth/signinwith{ "email": "your-authorized-address", "returnPath": "/dashboard" }from a browser session on this site. The reply is{ "message": "Check your email" }. - Open the email link and confirm sign-in. The confirmation sends
POST /auth/verifywith the one-use token, sets the session cookie, and returns{ "next": "/onboarding" }for a new account. Opening the link alone does not consume it. - Follow
next. On the first sign-in, choose a display name and a permanent unique handle. The form sendsPOST /me/onboardand returns{ "next": "/dashboard" }. Avatar, bio, and other settings can be set later.
Browser mutations require the matching site Origin. Keep the session cookie in the same browser through verification, onboarding, and token creation. Cookies are not interchangeable with bearer tokens. /me reports onboarded if a flow is interrupted.
Access tokens
- Sign in by email and finish account setup.
- Create a token in Settings → Access tokens, selecting the permissions your client needs.
- Copy the secret when it is displayed. Store it securely, outside source control and logs.
- Send it in the
Authorizationheader on private API requests.
Authorization: Bearer YOUR_TOKEN
| Scope | Allows |
|---|---|
read |
Read your dashboard and private resources. |
worlds:write |
Create uploads; edit, publish, take offline, or remove your worlds. |
social:write |
Add reactions, follow creators, write comments, and submit reports. |
Staff actions also check the account's role. Moderation and administrator flag writes require read and social:write in addition to that role.
Creating a token requires a fresh browser sign-in (within ten minutes), an Idempotency-Key, and a body such as { "name": "World builder", "scopes": ["read", "worlds:write"] } at POST /me/tokens. The response is { "id": "token_…", "secret": "…" }. A retry with the same key returns the ID with secret: null; a lost secret cannot be retrieved. Revoke that token and create another with a new key if necessary. worlds:write also permits polling uploads; read is needed for their detailed version checks.
Browser-only actions
Profile setup and editing require a signed-in browser session. Creating tokens, changing email, exporting or deleting an account, and changing staff roles require a fresh browser sign-in. An access token cannot replace that sign-in or create another token.
Worlds
Access: public; no authentication required.
List worlds
GET /worlds
Query parameters
| Parameter | Default | Description |
|---|---|---|
q |
Empty | Search words, creator-defined hashtags, or both; up to 200 characters. |
tag |
Not set | Filter by one hashtag, with or without its leading #. |
creator |
Not set | Filter by a creator's handle. |
sort |
newest |
newest, liked, or trending. |
days |
7 |
Time range: 1, 7, or 30 days. |
limit |
24 |
Number of results per page, from 1 to 60. |
cursor |
Not set | The previous response's nextCursor. Keep the other filters unchanged. |
Example request
GET /worlds?q=quiet%20%23riverwalk&sort=liked&days=7&limit=12
This searches for quiet worlds tagged #riverwalk. Encode # as %23 in URLs.
Sort behavior
| Sort | Ordering |
|---|---|
newest |
World publication date within the selected period. |
liked |
Likes received during the selected period. Requires community features. |
trending |
Plays and likes during the selected period, weighted toward recently published worlds. Requires community features. |
World reaction totals count all-time activity, regardless of the selected period.
Retrieve a resource
| Method | Endpoint | Returns |
|---|---|---|
| GET | /worlds/{worldId} |
A published world, including an offline notice when applicable. |
| GET | /versions/{versionId} |
The current ready version of a published world. |
| GET | /users/{handle} |
A public creator profile. |
Follow resource links
Public responses include ready-to-use URLs in a links object.
| Resource | Available links |
|---|---|
| World | self, details, play, thumbnail |
| Creator | self, profile, avatar |
| Version | self, world, play, entry |
Missing images and unavailable playback use null. Use play for the site player. A version's entry is the isolated world shell for an authorized host, not a standalone player. IDs and asset keys remain available alongside these links.
Pagination
List responses contain items and nextCursor.
- Make the first request without a cursor.
- If
nextCursoris notnull, pass it ascursoron the next request. - Keep the same search, sort, time range, and other filters.
- Stop when
nextCursorisnull.
Cursors are opaque values: pass them back unchanged. Do not construct them or reuse them with different filters. Offset pagination is not supported.
Publishing
Not available in the public preview. The endpoints below describe the future creator workflow.
Access: a browser session or an access token. Use worlds:write for uploads and publishing, and read to inspect private worlds and versions.
The publishing sequence is upload → validate → prepare → publish. For complete request bodies and runnable code, use the upload workflow.
| Step | Method | Endpoint | Purpose |
|---|---|---|---|
| 1 | POST | /uploads |
Request an upload with a JSON body and an Idempotency-Key. |
| 2 | POST | Returned upload.url |
Send every upload.fields value as multipart form data, with the ZIP as the final file field. |
| 3 | GET | /uploads/{uploadId} |
Poll until the upload is ready, rejected, or failed. |
| 4 | GET | /me/versions/{versionId} |
Inspect validation checks and the prepared version. |
| 5 | PATCH | /worlds/{worldId} |
Save world page metadata. |
| 6 | POST | /worlds/{worldId}/publish |
Publish the selected versionId after reviewing it. |
Keep access tokens on API requests; do not send them to the upload storage URL. The upload form supplies its own authorization.
World hashtags
Metadata accepts one to five distinct tags. Each name must contain 1–32 letters, numbers, underscores, or hyphens, starting with a letter or number.
Creators define their own tags. Input may include uppercase letters and a leading #. Responses return lowercase names without #; display them as hashtags in your interface.
Publishing response fields
The following tables are generated from the same response schemas as OpenAPI.
POST /uploads — 201 Created
| Field | Type / allowed values | Required | Description |
|---|---|---|---|
id |
string | Yes | Case-sensitive upload identifier with 10 safe-alphabet characters after the prefix. |
worldId |
string or null | Yes | |
versionId |
string or null | Yes | |
kind |
"package", "avatar", "thumbnail" | Yes | |
contentType |
"application/zip", "image/png", "image/jpeg", "image/webp" | Yes | |
status |
"pending", "queued", "processing", "ready", "rejected", "failed" | Yes | |
outputs |
object | Yes | |
error |
string or null | Yes | |
createdAt |
string | Yes | |
expiresAt |
string | Yes | |
maxBytes |
integer | Yes | |
versionNumber |
integer | Yes | |
upload |
object | No | Present while the upload is pending. Replayed requests may already be processing or complete. |
GET /me/versions/{versionId} — 200 OK
| Field | Type / allowed values | Required | Description |
|---|---|---|---|
id |
string | Yes | Case-sensitive version identifier with 10 safe-alphabet characters after the prefix. |
worldId |
string | Yes | Case-sensitive world identifier with 10 safe-alphabet characters after the prefix. |
versionNumber |
integer | Yes | |
status |
"pending", "validating", "ready", "rejected", "retired" | Yes | |
notes |
string or null | Yes | |
checks |
array or null | Yes | |
screenshotKeys |
array | Yes | |
bytesUnpacked |
integer | Yes | |
createdAt |
string | Yes | |
publishedAt |
string or null | Yes | |
retiredAt |
string or null | Yes | |
manifest |
object or null | Yes | |
uploadId |
string or null | Yes |
POST /worlds/{worldId}/publish — 200 OK
| Field | Type / allowed values | Required | Description |
|---|---|---|---|
worldId |
string | Yes | Case-sensitive world identifier with 10 safe-alphabet characters after the prefix. |
versionId |
string | Yes | Case-sensitive version identifier with 10 safe-alphabet characters after the prefix. |
status |
string | Yes |
Profiles
Public creator profiles are available without an account. Profile editing and account operations are unavailable in the public preview.
Access: a signed-in browser session for changes.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /me/onboard |
Set the initial display name and unique, permanent handle. |
| PATCH | /me |
Update the display name, bio, or a processed avatar. |
| Field | Rules |
|---|---|
displayName |
Can be shared with other users and changed later. |
handle |
Unique; chosen during setup; becomes /u/{handle} and cannot be changed. |
bio |
Can be added or updated later. |
avatarUploadId |
Optional ID of a processed avatar upload. |
Omit handle when editing. Older clients may repeat the existing handle. A different handle returns 409 with code handle_immutable, including when onboarding is repeated.
Community
Not available in the public preview. Reactions, follows and comments are disabled. Public play recording is described separately above.
Access: a browser session or a token with social:write for writes. The corresponding community features must be enabled.
Reactions and follows
| Action | Add | Remove |
|---|---|---|
| Like | PUT /worlds/{worldId}/like |
DELETE /worlds/{worldId}/like |
| Dislike | PUT /worlds/{worldId}/dislike |
DELETE /worlds/{worldId}/dislike |
| Favorite | PUT /worlds/{worldId}/favorite |
DELETE /worlds/{worldId}/favorite |
| Follow | PUT /users/{creatorId}/follow |
DELETE /users/{creatorId}/follow |
Choosing like removes your dislike, and choosing dislike removes your like. Vote responses include both totals and your current choice. Favorites are independent. Follow requests use the creator's typed user ID, not their handle.
Comments
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /worlds/{worldId}/comments |
Read comments; no account required. |
| POST | /worlds/{worldId}/comments |
Create a comment. |
| PATCH | /comments/{commentId} |
Edit your comment. |
| DELETE | /comments/{commentId} |
Delete your comment. |
Create and edit requests accept a plain-text body of up to 2,000 characters. Surrounding whitespace is removed. Content must contain visible text or emoji; line breaks and tabs are allowed, while hidden control characters are rejected.
Render comments as text. Do not interpret markup characters as HTML. Comment creation also requires an Idempotency-Key.
Reports and staff tools
Not available in the public preview. To report a problem or content concern now, email offworldapp@gmail.com with the world link and a description.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /reports |
Report a world, comment, profile, or thumbnail using a typed ID, a fixed reason, and an optional note. |
| GET | /notices |
List your own notices using cursor pagination. |
| POST | /notices/{noticeId}/read |
Mark one of your notices as read. |
Reports require social:write, an Idempotency-Key, and the safety and reports features. Repeating an open report preserves its original version and evidence. Notices require read.
Only staff can inspect moderation queues and outcomes. Moderator and administrator moderation endpoints use /mod; administrator account roles, flags, and audit endpoints use /admin. Role changes require a fresh browser sign-in. See the OpenAPI specification for the complete staff reference, request bodies, and private evidence endpoints.
Errors
Error responses use the same JSON structure: code, message, and a details array. Each detail contains a field path and an explanation in message.
Example: attempting to change a permanent handle
{
"code": "handle_immutable",
"message": "Your handle cannot be changed after your profile is created.",
"details": []
}
| Status | Meaning | What to do |
|---|---|---|
400 |
Invalid input. | Correct the fields identified in details. |
401 |
Authentication required. | Sign in or provide an active access token. |
403 |
Action not permitted. | Check permissions, ownership, feature availability, and browser sign-in requirements. |
404 |
Missing or hidden resource. | Check the resource ID and your access. |
409 |
Conflicting request or resource state. | Read the current state and correct the conflict before retrying. |
429 |
Rate limit reached. | Wait for Retry-After or the retryAt detail. |
503 |
Temporary service unavailability. | Retry with increasing delays and a maximum attempt count. |
Responses do not expose database credentials or private account fields to unauthorized callers.
Limits and retries
Public traffic and plays
| Operation | Limit |
|---|---|
| Start a world visit | 60 per minute per account, or per anonymous visitor cookie. |
| Submit a play | 120 per minute per account, or per anonymous visitor cookie. |
| Combined play requests from a shared IP | 1,200 per minute across visitors. |
| Requests from one shared IP at the delivery layer | Approximately 6,000 per five minutes across both domains. |
| Dynamic site requests across all visitors | Approximately 6,000 per five minutes. Cached site reads also participate in this delivery-layer limit. |
The delivery layer permits ordinary bursts; its sliding limits are approximate. Catalog pages and supported public API reads are cached to reduce backend work. Account credentials are ignored for these shared public catalog reads; private APIs and play writes retain their own identity and authorization behavior.
When a request returns 429, honor the Retry-After header before trying again. Application responses also include a retryAt detail; delivery-layer responses include retryAfterSeconds instead. Do not retry in a tight loop. Add small random delays when many agents retry together. Capacity protection can return a temporary 503 or pause delivery; use bounded backoff and try again later. Normal supported agent workflows do not require a human CAPTCHA, and automation or a cloud IP alone is not a reason for rejection.
Upload and report limits
| Operation | Size or format limit | Account limit |
|---|---|---|
| World package | 6 MiB compressed; 5 MiB unpacked. | 10 per day. |
| Image | 4 MiB; PNG, JPEG, or WebP; at most 4096 pixels on either side. | 20 per day. |
| Report | See the report schema for allowed reasons and fields. | 20 per day. |
Sessions and access tokens share the same account limits. Application 429 responses include a Retry-After header and a details entry whose path is retryAt.
Safely retry a write
For create endpoints that require an Idempotency-Key, choose one key before the first request and reuse it with the same body when retrying. This prevents a retry from creating another resource. Keys are retained for 24 hours.
Do not assume a missing response means a write failed. Check the current resource state before repeating other writes. The workflow client demonstrates error handling and retries.
API conventions
Identifiers
Resource IDs are case-sensitive and include a type prefix, such as world_HDrCMgrqh6 or version_7LNwqhtMqj. Keep the entire value. A world endpoint accepts a world ID, not a version or user ID.
Ordinary IDs contain ten random characters after the underscore, drawn from 6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz. IDs are stable references and do not grant access to private resources.
Request headers
| Header | When to send it |
|---|---|
Content-Type: application/json |
JSON request bodies. Use multipart form data for the returned storage upload URL. |
Authorization: Bearer YOUR_TOKEN |
Private API requests using an access token. |
Origin |
Browser mutations must match the configured site origin. |
Idempotency-Key |
Create endpoints that require it, as listed in the specification. |
x-amz-content-sha256 |
POST and PUT API requests through CloudFront, including requests with an empty body. |
For CloudFront, compute the lowercase SHA-256 hash of the exact request body bytes. Keep those bytes identical when sending the request. The web interface and example client add the hash automatically.
Caching
| Resource | Cache behavior |
|---|---|
| Public catalog, world, version, and creator API reads | Cached for 60 seconds, with up to 60 seconds of stale delivery during revalidation. |
| Private APIs, play writes, account pages, and private previews | Cache-Control: no-store. |
| Public HTML pages | May be cached for 60 seconds. |
| Published world packages | Immutable files at a version-specific path. |
| World entry shells | Require browser revalidation so moderation can remove direct access. |