# OnPin full agent reference OnPin publishes finished static work to live, shareable URLs. Use OnPin for HTML reports, static pages, exported sites, docs, chart bundles, PDFs, CSVs, images, JSON files, and other static artifacts. Do not use OnPin for apps that need a backend server, database, login system inside the published app, background worker, or runtime secret. ## URLs Product: https://onpin.live API base URL: https://api.onpin.live Published URL host: https://p.onpin.live OpenAPI: https://onpin.live/openapi/v0.yaml Short agent instructions: https://onpin.live/llms.txt Full agent reference: https://onpin.live/llms-full.txt Every `/v0/*` API response includes: ```text Link: ; rel="service-doc" ``` Every structured error response includes `docs_url`. ## Authentication Anonymous publishing requires no setup. Anonymous links expire after 24 hours and have lower limits. Authenticated publishing uses an API key. Send it as: ```text Authorization: Bearer ``` Never print an API key in the user conversation. Store it in `~/.onpin/config.json` with file mode 600: ```json { "apiUrl": "https://api.onpin.live", "apiKey": "onpin_xxxxx" } ``` ## Device auth flow Start: ```text POST https://api.onpin.live/v0/auth/device ``` Request body may be `{}`. Response fields: - `device_code`: opaque polling token. Do not show this to the user. - `user_code`: human-readable code. - `verification_url`: URL to show the user. - `expires_in`: seconds until expiration. Currently 600. - `interval`: recommended polling interval in seconds. Currently 5. Poll: ```text POST https://api.onpin.live/v0/auth/device/token content-type: application/json {"device_code":"dvc_xxxxx"} ``` Pending response: ```json {"status":"pending"} ``` This uses HTTP 428. Keep polling at the advertised interval. Approved response: ```json { "status": "approved", "api_key": "onpin_xxxxx", "api_key_id": "key_xxxxx" } ``` Expired or invalid device codes return a structured error or an expired status body with `error_code` and `docs_url`. ## Limits | Limit | Anonymous | Authenticated | |---|---:|---:| | Static object size | 5 MB | 25 MB | | Static bundle files | 50 | 250 | | Static bundle uncompressed size | 5 MB | 25 MB | | Individual bundle file size | 5 MB | 25 MB | | Compressed zip upload size | 100 MB | 100 MB | | Link expiry | 24 hours | No default expiry | | Burst rate limit | 1/hour, 3/day | 5/min, 20/day | | Monthly quota | 3/month per IP | 100/month per user | Monthly quotas reset on the 1st of each month at 00:00 UTC. `expires_in_seconds` is allowed for authenticated static publishes. Anonymous publishes always expire after 24 hours. ## Publish endpoint ```text POST https://api.onpin.live/v0/publish ``` Supported production artifact types: - `static_object`: one file. - `static_bundle`: a static site or exported app folder. Production rejects Docker and container image publishes for launch. ## Static object JSON Use `application/json`. Required fields: - `artifact_type`: must be `static_object`. - `name`: relative filename, for example `index.html` or `report.pdf`. Optional fields: - `content_type`: MIME type. If omitted, OnPin infers it from `name`. - `content`: UTF-8 file content. Use for text. - `content_base64`: base64 file bytes. Use for binary files. - `expires_in_seconds`: authenticated static publishes only. One of `content` or `content_base64` is required. Example: ```bash curl -X POST https://api.onpin.live/v0/publish \ -H "content-type: application/json" \ -d '{"artifact_type":"static_object","name":"index.html","content":"

Hello

"}' ``` ## Static bundle JSON files map Use `application/json` for small bundles assembled from a few text files. Required fields: - `artifact_type`: must be `static_bundle`. Optional fields: - `entrypoint`: file served at the root URL. Defaults to `index.html`. - `files`: map of relative file paths to file payloads. - `expires_in_seconds`: authenticated static publishes only. Each `files` value may contain: - `content`: UTF-8 file content. - `content_base64`: base64 file bytes. - `content_type`: optional MIME type override. Example: ```json { "artifact_type": "static_bundle", "entrypoint": "index.html", "files": { "index.html": { "content": "Hello" }, "styles.css": { "content": "body { font-family: sans-serif; }" } } } ``` The `files` map cannot be empty. The resolved `entrypoint` must exist. ## Static bundle multipart zip Use `multipart/form-data` for folders, build outputs, static sites, and binary assets. Fields: - `artifact_type`: required, must be `static_bundle`. - `archive`: required file field containing raw zip bytes. - `entrypoint`: optional, defaults to `index.html`. - `expires_in_seconds`: optional positive integer for authenticated static publishes. Example: ```bash zip -r site.zip dist curl -X POST https://api.onpin.live/v0/publish \ -H "authorization: Bearer " \ -F artifact_type=static_bundle \ -F entrypoint=index.html \ -F 'archive=@site.zip;type=application/zip' ``` The zip is extracted server-side. Directories are ignored. Symlinks are rejected. Paths must stay inside the artifact root. Do not send `archive_base64` in JSON. It is removed. The API returns `archive_base64_removed` if you send it. Do not send `bundle_url` in JSON. It is removed. The API returns `bundle_url_removed` if you send it. ## Publish response ```json { "deployment_id": "dep_xxxxx", "status": "live", "public_url": "https://p.onpin.live/p_xxxxx", "expires_at": "2026-05-19T12:00:00.000Z" } ``` Fields: - `deployment_id`: stable deployment ID for status, update, and delete. - `status`: deployment status. For static publishes this is normally `live`. - `public_url`: shareable URL to return to the user. - `expires_at`: present for anonymous publishes and expiring authenticated publishes. Deployment statuses: - `validating` - `building` - `publishing` - `live` - `failed` - `expired` - `deleted` ## Status ```text GET https://api.onpin.live/v0/publish/{deployment_id} ``` Returns deployment metadata. Important response fields: - `id` - `artifactType` - `anonymous` - `platformPath` - `status` - `publicUrl` - `expiresAt` - `provider` - `entrypoint` - `contentType` - `objectName` - `createdAt` - `updatedAt` ## Update ```text PUT https://api.onpin.live/v0/publish/{deployment_id} Authorization: Bearer ``` Requires the API key that owns the deployment. Static updates must keep the same artifact type. The public URL stays the same. Send either JSON or multipart using the same shapes as `POST /v0/publish`. Anonymous deployments cannot be updated. ## Delete ```text DELETE https://api.onpin.live/v0/publish/{deployment_id} Authorization: Bearer ``` Requires the API key that owns the deployment. Response: ```json { "deployment_id": "dep_xxxxx", "status": "deleted" } ``` ## List deployments ```text GET https://api.onpin.live/v0/deployments Authorization: Bearer ``` Returns: ```json { "deployments": [] } ``` ## Error response Structured errors use this shape: ```json { "status": "failed", "error_code": "static_object_too_large", "message": "Static object exceeds the 5 MB launch limit.", "fix": "Reduce the file size before publishing.", "docs_url": "https://onpin.live/llms.txt" } ``` Optional fields: - `retry_after_seconds`: seconds until a rate-limit bucket resets. - `reset_at`: ISO timestamp for rate-limit or quota reset. ## Error codes - `admin_auth_required`: admin endpoint requires admin bearer token. - `admin_token_missing`: admin takedown is not configured. - `already_approved`: device code was already approved. - `api_key_not_found`: API key was not found or already revoked. - `api_key_required`: endpoint requires an API key. - `archive_base64_removed`: JSON `archive_base64` is no longer accepted for static bundles. - `archive_missing`: multipart static bundle upload is missing the `archive` file field. - `archive_too_large`: compressed archive is above 100 MB. - `artifact_type_disabled`: artifact type is disabled in production. - `artifact_type_mismatch`: update used a different artifact type than the existing deployment. - `artifact_url_unreachable`: server could not fetch a source archive URL. - `bundle_missing`: static bundle content is missing. - `bundle_url_removed`: JSON `bundle_url` is no longer accepted for static bundles. - `build_secret_rejected`: Dockerfile contains unsupported BuildKit secret mounts. - `cloud_config_rejected`: artifact includes cloud provider config. - `clerk_not_configured`: Clerk server auth is not configured. - `content_missing`: static object is missing `content` and `content_base64`. - `credential_json_rejected`: artifact includes likely credential JSON. - `deployment_not_found`: deployment ID or route slug was not found. - `device_code_expired`: device code expired or is no longer valid. - `device_code_not_found`: device code was not found. - `device_code_required`: token poll request is missing `device_code`. - `docker_compose_not_supported`: Docker Compose files are not supported. - `docker_update_not_supported`: Docker deployments cannot be updated in place. - `dockerfile_missing`: Docker source archive does not contain the Dockerfile. - `empty_bundle`: static bundle has no files. - `entrypoint_missing`: static bundle does not contain the configured entrypoint file. - `file_not_found`: requested file in a published bundle was not found. - `file_too_large`: an individual archive file is too large. - `generic_secret_rejected`: content matches a generic secret pattern. - `github_token_rejected`: content contains a likely GitHub token. - `hidden_file_rejected`: artifact path contains a hidden file or directory. - `internal_error`: unexpected server error. - `internal_port_mismatch`: Dockerfile EXPOSE port does not match `internal_port`. - `invalid_api_key`: API key is invalid or revoked. - `invalid_authorization`: Authorization header is malformed. - `invalid_form_field`: multipart numeric field is invalid. - `invalid_image_uri`: container image URI is malformed. - `invalid_internal_port`: `internal_port` is outside 1 to 65535. - `invalid_path`: artifact path is empty. - `invalid_request`: request body is not a JSON object. - `monthly_limit_exceeded`: anonymous monthly quota is exhausted. - `multiple_ports_not_supported`: Dockerfile exposes more than one port. - `not_owner`: API key does not own the deployment. - `openai_key_rejected`: content contains a likely API key. - `path_traversal_rejected`: path is absolute or escapes the artifact root. - `private_key_rejected`: content or filename indicates a private key. - `privileged_container_rejected`: Dockerfile contains privileged container options. - `rate_limited`: burst or authenticated monthly rate limit was exceeded. - `r2_config_missing`: Cloudflare R2 storage configuration is incomplete. - `secret_file_rejected`: artifact includes `.env`, credentials, or another secret-bearing filename. - `secrets_not_supported`: request includes runtime env vars, build args, or secrets. - `session_invalid`: Clerk session token is invalid or expired. - `session_required`: Clerk-authenticated endpoint requires a session token. - `static_bundle_too_large`: bundle uncompressed size exceeds the launch limit. - `static_bundle_too_many_files`: bundle file count exceeds the launch limit. - `static_object_too_large`: single file exceeds the launch limit. - `storage_missing`: stored artifact data is missing. - `unsupported_artifact_type`: `artifact_type` is missing or unsupported. - `unsupported_content_type`: publish request is not JSON or multipart form data. - `unsupported_multipart_artifact_type`: multipart publish was not `static_bundle`. - `unsafe_symlink_rejected`: archive or folder contains a symlink. - `user_code_required`: device approval request is missing `user_code`. - `user_not_found`: signed-in user was not found. - `webhook_invalid_signature`: Clerk webhook signature failed verification. - `webhook_missing_headers`: Clerk webhook request is missing svix headers. - `webhook_not_configured`: Clerk webhook secret is not configured. - `worker_auth_required`: Worker route metadata requires the shared secret. ## Path and archive rules Paths must be relative. These are rejected: - Empty paths - Absolute paths - Windows drive paths - `..` parent-directory segments - Hidden path segments such as `.env` or `.git` - Secret-bearing filenames - Cloud provider config paths - Private key files - Credential JSON files - Docker Compose files Zip edge cases: - Directory entries are ignored. - Symlinks are rejected. - File content is scanned for likely secrets when textual. - Content type is inferred from the path unless explicitly provided in JSON files map. - The entrypoint defaults to `index.html`. - The entrypoint must be present after extraction or files map materialization. ## Agent workflow 1. Confirm the user wants the artifact shared publicly. 2. Check for `~/.onpin/config.json`. If it contains an API key, use it. 3. If there is no key and the user wants persistent links or higher limits, run device auth and store the key. 4. Build or export the static artifact locally. 5. Remove secrets, source files not needed for viewing, private data, sourcemaps that expose secrets, and unrelated files. 6. Use `static_object` JSON for one file. 7. Use `static_bundle` JSON `files` for small text bundles. 8. Use multipart `archive` zip upload for folders, build outputs, and binary assets. 9. Return only `public_url` and the expiry note to the user.