other · Listed · eip155:8453 · Found · 346 endpoints · Gateway-eligible
api.jubjubapp.com
jubjubapp
X402 endpoints at api.jubjubapp.com.
Indexed from this operator's public /.well-known/x402.json. Found is not operator-owned and is not attested. Claim or opt out.
Agent Read · Cleared Index
ROUTE
Route when you need other at published x402 prices.
confidence
78%
source
signal
Index before you pay. Same payload for agents:
GET /api/cleared/agent-read?slug=cat-api-jubjubapp-com
When to call
- Need other via x402 and want Cleared-indexed payTo with a live scorecard.
- X402 endpoints at api.jubjubapp.com.
Risks
- Found — not operator-owned; claim status unknown.
- No Cleared settlement receipt on file yet.
- No Gateway traffic yet — market share unproven.
Price posture
346 endpoints — confirm price on manifest before pay.
Category · Gateway
other · no Gateway routes yet — early / unproven on Cleared market share.
Endpoint hints
GET /healthGET /health at $Metered.
GET /v2/gpt-actions-schemaServe the GPT Actions OpenAPI schema (no auth required). Returns JSON by default (for ChatGPT "Import from URL"). Add ?format=yaml to get the raw YAML instead.
POST /profilesPOST /profiles at $Metered.
GET /profiles/currentGET /profiles/current at $Metered.
POST /complete-registrationCompletes user profile after Firebase registration and migrates temporary session data. **Flow**: 1. User registers via Firebase Auth (createUserWithEm
GET /notificationsList notifications for the authenticated user. **Query Parameters:** - `type`: Filter by notification type (e.g., "team_invite", "launch_success") - `read`: Fi
GET /notifications/unread-countGet the count of unread notifications for the authenticated user. **Returns:** - `unread_count`: Number of unread notifications
GET /notifications/team-unread-countsUnread notification counts bucketed by team for the authenticated user. **Returns:** - `counts`: map of team_id -> unread count. Teams with no unread notific
Evidence (Cleared)
- → Intake verified · Gateway-eligible
- → Trust 70/100 · pass · tier listed
- → Protocol x402 · eip155:8453
- → Manifest reachable · schema valid
- → Found listing — indexed from public x402.json, not operator-attested.
Endpoints
Health
$MeteredGET https://api.jubjubapp.com/healthGpt Actions Schema
$MeteredGET https://api.jubjubapp.com/v2/gpt-actions-schemaServe the GPT Actions OpenAPI schema (no auth required). Returns JSON by default (for ChatGPT "Import from URL"). Add ?format=yaml to get the raw YAML instead.
Create Profile
Not used$MeteredPOST https://api.jubjubapp.com/profilesGet Profile
$MeteredGET https://api.jubjubapp.com/profiles/currentComplete User Profile After Firebase Registration
$MeteredPOST https://api.jubjubapp.com/complete-registrationCompletes user profile after Firebase registration and migrates temporary session data. **Flow**: 1. User registers via Firebase Auth (createUserWithEmailAndPassword) 2. Frontend calls this endpoint with Firebase token + additional data 3. System creates enriched profile in Firestore 4. Migrates temporary session data (if exists) 5. Records tracking events **Required Headers**: - Authorization: Bearer {firebase_jwt_token} **Data Collected**: - How they found the platform (try_now, google_search, referral, etc.) - Professional information (company, role) - Marketing consents - Referral code (if applicable)
List Notifications
$MeteredGET https://api.jubjubapp.com/notificationsList notifications for the authenticated user. **Query Parameters:** - `type`: Filter by notification type (e.g., "team_invite", "launch_success") - `read`: Filter by read status (true/false) - `limit`: Maximum number of notifications to return (1-100, default 50) - `offset`: Number of notifications to skip for pagination (default 0) **Returns:** - List of notifications with pagination info - Total count of matching notifications - Unread count
Get Unread Notifications Count
$MeteredGET https://api.jubjubapp.com/notifications/unread-countGet the count of unread notifications for the authenticated user. **Returns:** - `unread_count`: Number of unread notifications
Get Team Unread Counts Endpoint
$MeteredGET https://api.jubjubapp.com/notifications/team-unread-countsUnread notification counts bucketed by team for the authenticated user. **Returns:** - `counts`: map of team_id -> unread count. Teams with no unread notifications are omitted (treat a missing key as 0).
Get Notification Stats Endpoint
$MeteredGET https://api.jubjubapp.com/notifications/statsGet notification counts for the surface's filter tabs. **Returns:** - `total`: total notifications - `unread`: unread count - `by_category`: per-category counts (invites, teams, publishing, social, alerts, system). Unfiltered, so the pills show global totals.
Mark Notification Read
$MeteredGET https://api.jubjubapp.com/notifications/{notification_id}/readMark a specific notification as read. **Path Parameters:** - `notification_id`: The notification ID **Returns:** - Success status with notification ID
Mark All Notifications Read
$MeteredGET https://api.jubjubapp.com/notifications/mark-all-readMark all notifications as read for the authenticated user. **Returns:** - Number of notifications marked as read
Delete Notification Endpoint
$MeteredGET https://api.jubjubapp.com/notifications/{notification_id}Delete a specific notification. **Path Parameters:** - `notification_id`: The notification ID **Returns:** - Success status
Websocket Stats
$MeteredGET https://api.jubjubapp.com/ws/statsGet WebSocket connection statistics (for monitoring/debugging). **Returns:** - Total active connections - Number of connected users - List of connected user profile IDs
List Agents
$MeteredGET https://api.jubjubapp.com/v2/agentsList all agents owned by the current user.
Create Agent
$MeteredPOST https://api.jubjubapp.com/v2/agentsCreate a new agent key. Returns the raw secret once — caller must store it securely. Publish scopes are off by default and require a paid plan.
Update Agent
$MeteredGET https://api.jubjubapp.com/v2/agents/{agent_id}Update an agent's scopes, name, or status. Only the owner can update their agents.
Revoke Agent
$MeteredGET https://api.jubjubapp.com/v2/agents/{agent_id}Revoke an agent (soft-delete), or permanently remove with ?permanent=true.
List Workspaces
$MeteredGET https://api.jubjubapp.com/v2/workspacesList workspaces with optional filtering. **Filters:** - role: "owner" (created by), "member" (invited to), or omit for all - folder_id: Filter by folder - include_progress: Include progress calculation (default: true) **Legacy filters:** - owner_id: Filter by owner (deprecated, use role=owner instead) Returns lightweight card data for efficient list rendering. Stats (total_contents, etc.) are calculated dynamically from the database. **Examples:** - No params → All workspaces the authenticated user has access to - `?role=owner` → Workspaces created by the authenticated user - `?role=member` → Workspaces the authenticated user was invited to - `?include_progress=false` → Skip progress calculation for performance
Create Workspace
$MeteredPOST https://api.jubjubapp.com/v2/workspacesCreate a new workspace. The authenticated user becomes the owner of the workspace.
Get Workspace
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}Get full details of a workspace. Access: Owner, direct member, or team member with WORKSPACE_VIEW permission. Stats are calculated dynamically from the database.
Update Workspace
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}Update a workspace's details. Only provided fields will be updated. Access: Owner, direct member with edit rights, or team member with WORKSPACE_EDIT permission.
Delete Workspace
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}Delete a workspace. This will also delete all contents and media within the workspace. Access: Owner, direct member with admin rights, or team member with WORKSPACE_DELETE permission.
Set Workspace Team
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/teamAtomically set (or clear) a workspace's team in one request. Replaces the client-side unlink-then-link pattern, which could leave a workspace orphaned from every team if the second call failed. Validates the target team up front, then unlinks from the old team and links to the new one, saving the workspace once. team_id=None unlinks only. Access: workspace owner only (matches the link endpoint).
Archive Workspace
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/archiveArchive a workspace: hide it from listings without deleting anything. Reversible via POST /{workspace_id}/unarchive. The workspace remains fetchable by id — the archive view needs to read it — and nothing inside it is modified. Access: same as DELETE (owner, admin member, or team member with WORKSPACE_DELETE).
Unarchive Workspace
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/unarchiveRestore an archived workspace to normal visibility. Clears the archive fields entirely — see Workspace.unarchive() for why the timestamp is not kept as history. Access: same as DELETE.
Delete Workspaces Batch
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/batch/deleteDelete multiple workspaces at once. Returns which workspaces were deleted and which were not found. Maximum 100 workspaces per request. Access: Must have WORKSPACE_DELETE permission for each workspace.
List Members
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/membersList all mentionable members of a workspace. Returns a deduplicated list of members from: 1. The workspace owner 2. Direct workspace members 3. Team members (if workspace is linked to a team) Used by the frontend for @mention autocomplete. Access: Owner, direct member, or team member with WORKSPACE_VIEW permission.
Add Member
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/membersAdd a member to a workspace. Access: Owner, direct member with admin rights, or team member with MEMBERS_INVITE permission.
Update Member
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/members/{member_profile_id}Update a workspace member's role or expiration. Cannot update the owner. Access: Owner, direct member with admin rights, or team member with MEMBERS_EDIT_ROLE permission.
Remove Member
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/members/{member_profile_id}Remove a member from a workspace. Cannot remove the owner. Access: Owner, direct member with admin rights, or team member with MEMBERS_REMOVE permission.
Get Workspace Activity
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/activityActivity for a single workspace, drawn from its team's audit trail. ⚠️ A SEPARATE ENDPOINT, NOT A `workspace_id` PARAMETER ON `GET /v2/teams/{team_id}/activity`. Three reasons, and the first is the one that matters: 1. THE TEAM ENDPOINT 403s A NON-TEAM-MEMBER (team_routes.py, its `team.is_member` guard). A workspace OWNER need not be in the team — and is exactly who opens a workspace page. Reusing that route would 403 the owner on their own workspace. This route is guarded by `require_workspace_context(WORKSPACE_VIEW)`, which resolves all four access paths (owner / direct / team / dual) and lets the owner in. 2. The caller has a workspace id and may not know the team id — or there may not be a team at all. 3. The resource being described is the workspace. ⚠️ FILTERED IN MEMORY, NOT IN THE QUERY. Deliberate, with a known ceiling: * `TeamAuditEvent.team_id` is required and every repository query filters on it; there is no query path by workspace, so a query-side filter needs a new composite index AND a field that most existing rows lack. * `list_by_team` ALREADY streams the whole team's events and paginates in memory (firestore_team_audit_repository.py `list_by_team`), so filtering here adds no new read pattern — it reuses the one that exists. * The 90-day TTL bounds how large a team's trail can get. * It also catches legacy rows that predate `workspace_id`, which a query on that field could not. The trade is real: we fetch the team's events to keep a subset. If a team ever accumulates thousands of events in 90 days, this should become a `list_by_workspace` query with an index on (workspace_id, created_at). Access: anyone who can view the workspace.
Check Storage Health
$MeteredGET https://api.jubjubapp.com/v2/workspaces/health/storageCheck if workspace storage is properly configured.
Get Workspace Progress
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/progressGet detailed progress information for a workspace. Access: Owner, direct member, or team member with WORKSPACE_VIEW permission. Returns: - percentage: Overall progress (0-100) - total_contents: Number of contents in workspace - breakdown: Detailed breakdown by state (published, scheduled, ready, in_progress, pending) - summary: Simple 3-category summary (published, in_progress, pending) - contents: Per-content progress details Progress states and their weights: - EMPTY (0%): Content created but no media - DRAFT (20%): Has media but no platform config - INCOMPLETE (35%): Has config but missing required fields - READY (50%): Config complete, ready to publish - SCHEDULED (75%): Has a pending launch with scheduled_for - PUBLISHING (85%): Currently being published - PARTIAL_SUCCESS (90%): Some platforms published successfully - PUBLISHED (100%): All target platforms published - FAILED (40%): Last publish attempt failed
Ownership Opt Out
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/ownership/opt-outOpt out of revenue splits for this workspace. Adds caller's profile_id to workspace.member_opt_outs. Idempotent: already opted out returns 200 OK. Caller must be a member of the workspace's team.
Ownership Opt In
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/ownership/opt-inOpt back into revenue splits for this workspace. Removes caller's profile_id from workspace.member_opt_outs.
Create Invite Link
$MeteredPOST https://api.jubjubapp.com/v2/workspaces/{workspace_id}/invite-linksCreate an invite link for a workspace. Access: Owner or admin (requires MEMBERS_INVITE permission). Link expires in 7 days.
Revoke Invite Link
$MeteredGET https://api.jubjubapp.com/v2/workspaces/{workspace_id}/invite-links/{token}Revoke an invite link. Access: Owner or admin (requires MEMBERS_INVITE permission).
Get Workspace Share Preview
$MeteredGET https://api.jubjubapp.com/v2/workspace-shares/{token}Get a public preview of a workspace via invite link. NO authentication required. Returns workspace name, content titles/thumbnails, and member names. Does NOT return private content URLs, download links, or sensitive data.
Join Via Invite Link
$MeteredPOST https://api.jubjubapp.com/v2/workspace-shares/{token}/joinJoin a workspace via invite link. Requires authentication. If auto_accept=true: user is added immediately as viewer. If auto_accept=false: user is added to pending_requests for owner approval.
Approve Join Request
$MeteredPOST https://api.jubjubapp.com/v2/workspace-shares/{token}/approve/{profile_id}Approve a pending join request. Access: Workspace owner or admin.
Request a signed URL for upload
$MeteredPOST https://api.jubjubapp.com/v2/media/upload/requestStart the upload process by requesting a signed URL. The frontend should: 1. Call this endpoint with filename and optional content_type 2. Use the returned signed_url to PUT the file directly to GCS 3. Call /v2/media/{media_id}/upload/complete when done The signed URL is valid for 10 minutes.
Confirm upload completion
$MeteredPOST https://api.jubjubapp.com/v2/media/{media_id}/upload/completeCall this after successfully uploading the file to GCS. This will: 1. Validate the Media exists and is in 'uploading' status 2. Make the file publicly accessible 3. Extract video metadata (width, height, fps, codec, etc.) for Smart Tags 4. Update Media status to 'ready' 5. Return the complete Media with public URL and video_metadata
Ingest media from a public URL
$MeteredPOST https://api.jubjubapp.com/v2/media/ingest-urlDownload a file from a public URL and ingest it into JubJub. The backend downloads the file server-side and uploads it to GCS. Useful for GPT Actions and MCP clients that cannot send binary data. The URL must be publicly accessible. Max download timeout is 120 seconds.
Search media by extracted metadata
$MeteredGET https://api.jubjubapp.com/v2/media/searchStructured search over AI-extracted media metadata (media_search_index_v1). include_unanalysed (discoverable scope only): also return minted pieces indexed with no extracted metadata, registered imports. A viewer surface can send people to the platform; an agent buying metadata cannot use them, so the default leaves them out. Free for human users — pure indexed filters, no inference. scope: - "mine" (default): your own catalogue, scoped to created_by. - "discoverable": cross-corpus, only content whose owner opted in. cursor: opaque bookmark from a previous discoverable response's next_cursor — pass it back verbatim to continue. Ignored for scope=mine (owner corpora fit one page). Returns content cards (content_id, title, thumbnail_id, thumbnail_url, facets) matching ALL supplied filters. Topic matches if it appears in the content's topics.
Opt a content in/out of cross-corpus search
$MeteredGET https://api.jubjubapp.com/v2/media/discoverabilitySet whether a content you own is discoverable in the (free) cross-corpus metadata search. Default is ON (opt-out — see backfill_discoverable_default.py); this endpoint is how an owner opts a piece out, or back in. Owner only.
Get media details
$MeteredGET https://api.jubjubapp.com/v2/media/{media_id}Get details of a specific media file. Access: Must have CONTENT_VIEW permission on the media's workspace.
Rename a media file
$MeteredGET https://api.jubjubapp.com/v2/media/{media_id}Rename a media file by updating its filename. This only updates the display name (filename) in the database. The actual file in GCS is not renamed (it uses media_id as identifier).
Delete a media file
$MeteredGET https://api.jubjubapp.com/v2/media/{media_id}Delete a media file and its record. This will also delete the file from GCS.
List media in a workspace
$MeteredGET https://api.jubjubapp.com/v2/mediaList all media in a workspace. Supports filtering by type and status. Access: Must have CONTENT_VIEW permission on the workspace.
Rename multiple media files
$MeteredGET https://api.jubjubapp.com/v2/media/bulk/renameRename multiple media files in a single request. Each item in the request specifies a media_id and the new filename. The operation processes all items and returns individual results. Partial success is possible - some items may succeed while others fail.
Reprocess video metadata
$MeteredPOST https://api.jubjubapp.com/v2/media/{media_id}/reprocess-metadataRe-extract video metadata (width, height, fps, codec, etc.) for an existing media. Useful for: - Videos uploaded before metadata extraction was implemented - Videos where metadata extraction failed during upload - Forcing a refresh of metadata This downloads the video from GCS, extracts metadata via FFprobe, and updates the record.
Check storage connection status
$MeteredGET https://api.jubjubapp.com/v2/media/health/storageCheck if GCS storage and Firestore are properly configured and connected.
Test signed URL generation
$MeteredGET https://api.jubjubapp.com/v2/media/health/signed-urlTest that GCS can generate a valid signed URL. Generates a test signed URL for a dummy path to verify that credentials and bucket access are working correctly.
Public summary of the JubJub media library
$MeteredGET https://api.jubjubapp.com/v2/catalogue/manifestPublic, unauthenticated summary of the discoverable metadata library. Aggregates media_search_index_v1 (discoverable == true) in Python — no new indexes. Cached in catalogue_manifest_v1/current with a 1-hour TTL. Best-effort: an empty index returns zeroes, never 404.
List Contents
$MeteredGET https://api.jubjubapp.com/v2/contentsList contents in a workspace. - workspace_id: Required - the workspace to list contents from - status: Optional filter by content status - folder_id: Optional filter by folder Access: Must have CONTENT_VIEW permission on the workspace.
Create Content
$MeteredPOST https://api.jubjubapp.com/v2/contentsCreate a new content. The content will be in 'draft' status initially. After configuring platforms, it can be marked as 'ready' for publishing. Access: Must have CONTENT_CREATE permission on the workspace.
List Visible Contents
$MeteredGET https://api.jubjubapp.com/v2/contents/visibleEverything this profile MAY SEE, across every workspace it can reach. 🔑 THE VAULT ASKS THE WRONG QUESTION TODAY. It answers "what did I put here" — the dashboard queries `onchain_publish_events` by `contributors_jubjub_profile_ids array-contains me`, so a teammate's piece in a workspace I fully share is invisible to me even though I can open it and play it through the workspace door. This endpoint answers "what may I see" instead, which is the question a shared workspace implies. ⚠️ THE ACCESS RULE IS NOT REIMPLEMENTED HERE. Every candidate workspace is put through `check_workspace_access` — the SAME function `require_workspace_context` and `verify_workspace_permission` use, and therefore the same one that already governs playback. A second copy of an access rule is how a leak happens: the copies drift, one of them forgets a case, and the forgetful one is the one serving media. There is exactly one rule and this calls it. Candidates are gathered widely and then FILTERED by that rule, never trusted from the gathering step: * workspaces owned by, or directly listing, this profile (`list_by_profile` — which covers owner + direct member only) * workspaces linked to any team this profile belongs to, which `list_by_profile` does NOT return: that omission is precisely the gap being closed. Membership is checked per workspace even for team-linked candidates, because team membership is necessary but not sufficient — the permission still has to hold.
Get Content
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}Get full details of a content. Access: Must have CONTENT_VIEW permission on the parent workspace. Metadata-commerce tiers: the owner sees full gated fields; everyone else sees free preview fields only (purchase to unlock).
Update Content
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}Update a content's details. Only provided fields will be updated. Access: Must have CONTENT_EDIT permission on the parent workspace.
Delete Content
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}Delete a content and all associated resources (cascade). Deletes: Content + PlatformConfigs + Media (GCS + DB) + cancels active Launches. If the content has pending/scheduled launches: - Without `?force=true`: returns 409 Conflict - With `?force=true`: cancels all active launches (including Cloud Tasks) and proceeds Access: Must have CONTENT_DELETE permission on the parent workspace.
Set Content Thumbnail From Frame
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/thumbnailSet this piece's thumbnail to a frame at the given timestamp. A NEW media_v2 image is created every time and thumbnail_id is repointed at it. The previous image is left exactly where it is, still public, still at its URL. That is not tidiness — one of them is already embedded in a published Farcaster cast, and cast previews resolve the image at render time, so overwriting the object would change what a historical post shows. Orphaned images are the cheaper problem. NO `if not content.thumbnail_id` GUARD. That guard belongs to create_content, where a supplied thumbnail must not be overwritten. Here the piece always has one already — that is what re-picking means — so the guard would make this endpoint a permanent no-op. ON FAILURE THIS RETURNS AN ERROR, NOT A SILENT 200. create_content swallows extraction failures and continues, which is right at ingest: the upload should not fail because ffmpeg timed out. Here the thumbnail IS the request, so a 200 with the old image still attached would be indistinguishable from success — the same invisible failure that let a Farcaster cast publish under a camera filename with nothing to explain it. Access: CONTENT_EDIT on the parent workspace.
Register imported content for on-chain revenue
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/registerPromote an OAuth-imported (`source="historical_import"`) content row from `historical` to `minting`, kicking off the on-chain ownership deployment state machine. Guards: - 404 if content not found. - 400 if not a historical import or already past historical state. - 403 if the original platform credential is no longer ACTIVE or no longer belongs to current_user.
Name the host a piece plays from so JubJub meters it without serving it
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/playback-sourceWrite `source_video_url` (and optionally the price) on a piece the caller created. For an import this turns "watched elsewhere" into a sellable piece: `is_watched_elsewhere` (media_search_service) reads the field and the streaming rail's refusal keys on it. For an upload it moves delivery off JubJub: the tier decision (playback_url_signer.is_gated_playback, leg 2) prefers a fetchable source over the GCS object, so the SDK leaves the page's own src alone and the bucket serves nothing. In both cases the public playback route emits it as a Tier 1 source. It is a money gate, so the URL is validated with the same predicate the tier decision uses; a watch page, a URL the page cannot play, or JubJub's own bucket is refused, never stored. Was import-only until 2026-09-09. The reason given ("a piece published through JubJub already has its file") was backwards: JubJub having the file is why JubJub pays to serve it. Guards: - 404 if content not found. - 400 if the URL is not a playable file on another host, or the price is above the settlement ceiling. - 403 if the caller is not the creator.
List Targets
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}/targetsList all platform targets for a content. Returns the PlatformConfigs with credential info. Access: Must have CONTENT_VIEW permission on the parent workspace.
Add Targets
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/targetsAdd platform targets (destinations) to a content. This creates PlatformConfig entries for each credential. The user can then configure platform-specific settings for each target. - credential_ids: List of credential IDs to add as targets Access: Must have CONTENT_EDIT permission on the parent workspace, AND each credential must be the caller's own or actively shared with the workspace's team. A credential that exists and is neither is reported as `not_accessible` with nothing about it — until 2026-09-03 this route returned any credential's account name and username for any id, and persisted the config so a launch published through its tokens.
Remove Target
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}/targets/{credential_id}Remove a platform target from a content. This deletes the PlatformConfig for the specified credential. Access: Must have CONTENT_EDIT permission on the parent workspace.
Get Content With Configs
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}/fullGet content with all platform configurations. This is useful for the publish/edit screen where you need both content details and all platform configs. **Query params:** - `expand=media`: Include video and thumbnail details with video_metadata Access: Must have CONTENT_VIEW permission on the parent workspace.
Validate Configs
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/validate-configsValidate all platform configurations for a content. Checks each config against platform-specific requirements. Returns validation results for each config. Use this before allowing user to proceed to publish step. Access: Must have CONTENT_VIEW permission on the parent workspace.
Apply General Settings
$MeteredPOST https://api.jubjubapp.com/v2/contents/{content_id}/apply-general-settingsApply content's general settings to platform configurations. Copies specified fields from Content to PlatformConfig.settings. Useful for "Apply to All" button in the UI. Field mapping: - title → title (YouTube, TikTok, Facebook, LinkedIn, Vimeo) - description → description, caption (Instagram) - tags → tags (YouTube, Vimeo) Access: Must have CONTENT_EDIT permission on the parent workspace.
Get Content Playback
$MeteredGET https://api.jubjubapp.com/v2/contents/{content_id}/playbackResolve Mux playback metadata for a content item. Public endpoint — no auth required. Used by /watch and /embed routes on the dashboard to load the player before the viewer authenticates. Returns mux_playback_id, playback URL, price, and ownership info. Tier-2 gating: for gated content the durable Mux URL and playback_id are withheld (null) and `gated=true` is returned, so the client runs the paid streaming-session flow instead of playing the durable URL. `gated` is DERIVED here (is_gated_playback), never read from the document.
Check Storage Health
$MeteredGET https://api.jubjubapp.com/v2/contents/health/storageCheck if content storage is properly configured.
List Launches
$MeteredGET https://api.jubjubapp.com/v2/launchesList launches with optional filters. Filter by workspace, content, or status. Access: Must have CONTENT_VIEW permission on the workspace/content.
Create Launch
$MeteredPOST https://api.jubjubapp.com/v2/launchesCreate a new multi-platform launch. This endpoint allows publishing content to multiple platforms at once. If `scheduled_for` is provided, the launch will be scheduled for that time. Otherwise, it will execute immediately. **Request body:** - `content_id`: ID of the content to publish - `platform_config_ids`: List of PlatformConfig IDs to publish to - `scheduled_for`: (optional) When to publish - `timezone`: (optional) Timezone for scheduled_for Access: Must have PUBLISH_EXECUTE permission on the content's workspace. Agents: Require ``publish:execute`` scope.
Validate Launch
$MeteredPOST https://api.jubjubapp.com/v2/launches/validateValidate a launch before execution. This endpoint checks if all platforms can accept the content without actually publishing anything. Access: Must have CONTENT_VIEW permission on the content's workspace.
Get Launch
$MeteredGET https://api.jubjubapp.com/v2/launches/{launch_id}Get details of a specific launch including history. Access: Must have CONTENT_VIEW permission on the launch's workspace.
Get Launch Summary
$MeteredGET https://api.jubjubapp.com/v2/launches/{launch_id}/summaryGet a summary of launch results. Useful for displaying in UI after launch completes. Access: Must have CONTENT_VIEW permission on the launch's workspace.
Cancel Launch
$MeteredPOST https://api.jubjubapp.com/v2/launches/{launch_id}/cancelCancel a pending or scheduled launch. Only works for launches that haven't started yet. Access: Must have PUBLISH_CANCEL permission on the launch's workspace. Agents: Require ``publish:schedule`` scope.
Cancel Platform
$MeteredGET https://api.jubjubapp.com/v2/launches/{launch_id}/platforms/{platform_config_id}Cancel a single platform within a launch. Only works for platforms that are still PENDING or IN_PROGRESS. If all platforms become terminal after this, the launch status updates accordingly. Access: Must have PUBLISH_CANCEL permission on the launch's workspace. Agents: Require ``publish:schedule`` scope.
Retry Failed Platforms
$MeteredPOST https://api.jubjubapp.com/v2/launches/{launch_id}/retryRetry publishing to failed platforms. Only works for launches with status 'partial_failure'. Access: Must have PUBLISH_EXECUTE permission on the launch's workspace.
Reschedule Launch
$MeteredGET https://api.jubjubapp.com/v2/launches/{launch_id}/rescheduleReschedule a pending launch to a new time. Only works for scheduled launches that haven't started yet. Access: Must have PUBLISH_SCHEDULE permission on the launch's workspace.
Internal Execute Launch
$MeteredPOST https://api.jubjubapp.com/v2/launches/internal/execute/{launch_id}Execute a launch asynchronously. Called by Cloud Tasks, not by users. This endpoint receives a callback from Google Cloud Tasks after ``create_launch`` dispatches the work. It loads the launch from Firestore and runs the orchestrator, which may take several minutes (e.g. Instagram video processing). **Authentication:** verified Google OIDC only (``require_cron_oidc``, SEC-07) — the same guard as ``process_scheduled_launches`` below, and the same one ``purchase_sweep_dispatcher`` targets. This docstring previously claimed the Cloud Tasks header ``X-CloudTasks-QueueName`` was checked "to prevent direct invocation from external clients". No such check existed anywhere in the function: the route executed a launch — publishing to creators' connected social accounts — for anything that could reach the URL. A header would not have been worth much either, since any caller can send one; that is why SEC-07 replaced presence-of-header authorisation across the cron routes. When ``platform_config_id`` is provided, only that single platform executes (per-platform schedules, Phase 4 Option B). The parent launch finalises only once every platform has reached a terminal state, so multiple per-platform tasks can fire independently.
Process Scheduled Launches
$MeteredPOST https://api.jubjubapp.com/v2/launches/process-scheduledProcess all scheduled launches that are due for execution. This endpoint is designed to be called by Cloud Scheduler periodically (e.g., every 5 minutes). It finds all launches with: - status IN ['pending', 'scheduled'] - scheduled_for <= now And executes them via the LaunchOrchestrator. **Authentication:** This endpoint should be protected by Cloud Scheduler's OIDC authentication at the infrastructure level, not by user authentication. **Returns:** - processed: number of launches executed - results: list of {launch_id, status, error?}
Recover Stuck Launches
$MeteredPOST https://api.jubjubapp.com/v2/launches/recover-stuckReset launches stuck in in_progress for more than 10 minutes. Designed to be called alongside process-scheduled by Cloud Scheduler. Catches launches that were abandoned due to Cloud Run timeout or unhandled exceptions in the orchestrator.
Check Storage Health
$MeteredGET https://api.jubjubapp.com/v2/launches/health/storageCheck if launch storage is working correctly.
Initiate Oauth
$MeteredGET https://api.jubjubapp.com/v2/credentials/{platform}/auth-urlInitiate OAuth flow for a platform. Returns an auth URL to redirect the user for authorization. Optional query params for post-callback navigation: return_context_flow: Flow identifier (e.g. "try_now_onboarding") return_context_workspace_id: Workspace ID for try-now flows
Oauth Callback
$MeteredPOST https://api.jubjubapp.com/v2/credentials/{platform}/callbackHandle OAuth callback and store credentials. This endpoint is called by the frontend after the user authorizes.
Connect Token
$MeteredPOST https://api.jubjubapp.com/v2/credentials/{platform}/connect-tokenConnect a token-based platform (Mux, Vimeo OTT). For Mux: token_id = Mux Token ID, token_secret = Mux Token Secret For Vimeo OTT: token_id = API key, token_secret = API secret profile_id comes from auth cookie only — never from request body.
List Credentials
$MeteredGET https://api.jubjubapp.com/v2/credentialsList all credentials for the authenticated user. Pass check_tokens=true to include live token_valid checks (adds latency).
List Credentials By Platform
$MeteredGET https://api.jubjubapp.com/v2/credentials/by-platformList all credentials grouped by platform. Useful for the frontend to display credentials organized by platform. Pass check_tokens=true to include live token_valid checks (adds latency).
Get Credential
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}Get details of a specific credential.
Update Credential
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}Update a credential (currently only nickname can be updated).
Delete Credential
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}Delete a credential. By default, performs a soft delete (marks as inactive). Use hard_delete=true to permanently remove.
Set Tokenise Destination
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}/tokenise-destinationSet where a connected channel's imported history tokenises. Imported channel history has no workspace — nothing is downloaded, so there is no container — and registration refuses until a destination exists (content_routes.register_content_for_revenue, 409 `tokenise_destination_unset`). This is the only thing that clears that refusal, and the last gate before a permanent on-chain allocation, so every fact is checked HERE rather than assumed from the client having rendered a filtered list of teams. Changing a destination applies FORWARD ONLY. Content already registered carries its own copy of the destination it was minted under (`contents_v2.tokenise_destination_mode` / `tokenise_team_id`), and `resolve_registration_destination` reads that copy in preference to the credential. Nothing here touches contents_v2 — that copy is the record, and the split derived from it is already on-chain.
Revalidate Credential Endpoint
$MeteredPOST https://api.jubjubapp.com/v2/credentials/{credential_id}/revalidateAsk the platform whether the STORED credential still works, whatever `status` says, and act on the answer. This is what the Reconnect button calls. THREE OUTCOMES: alive the platform accepted the stored credential. Status is `active` again, the auth notification is cleared. No form. dead the platform answered 401. Status is `needs_reauth`; `needs_input` says what the creator must now supply (for Mux only the secret — the Token ID is `account_id`). undetermined the platform could not be asked (timeout, 5xx, 429, no probe). NOTHING changed except the evidence stamp; the caller may simply try again. Why this exists: the health endpoint below refused non-active rows, the list endpoint's `token_valid` returned False before decrypting, and the sweep skipped flagged rows — so a `needs_reauth` written by a refresher that never contacted the platform could never be disproved. Loading the credential regardless of status is the whole point.
Check Credential Health
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}/healthCheck if a credential's access token is still valid by making a lightweight API call to the platform. Refreshes first where the platform has a refresher and the stored token is stale. Same check as POST /{id}/revalidate, kept for the callers of this shape. It no longer marks a credential dead because a platform had no probe, and no longer refuses a credential that is already flagged. Returns: status: "healthy" | "refreshed" | "needs_reauth" | "unknown" platform, account_name, detail "unknown" is the third outcome: the platform could not be asked and nothing was changed.
List Facebook Pages
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}/facebook-pagesList Facebook Pages the user manages for a given credential. Returns page_id, name, and category for each Page. The page_id is needed when creating a PlatformConfig for Facebook. Supports shared credentials: if the caller is not the owner, they can pass ?team_id=xxx to check access via SharedCredential.
Search Locations
$MeteredGET https://api.jubjubapp.com/v2/credentials/{credential_id}/location-searchSearch for locations via Meta Pages Search API. Returns locations that can be used as `location_id` in Instagram PlatformConfig. Works with both Facebook and Instagram credentials (both use Meta Graph API). The frontend should call this with a debounced search input and display results in an autocomplete dropdown.
Cron Credential Health Sweep
$MeteredPOST https://api.jubjubapp.com/v2/credentials/cron/health-sweepProbe every active credential and mark the ones the platform says are dead. Same auth as every other scheduled job in this service: Cloud Scheduler OIDC, verified by require_cron_oidc against CRON_OIDC_AUDIENCE and the CRON_SERVICE_ACCOUNTS allowlist (cron_auth.py:145-156). Nothing else can call it. NIGHTLY, not hourly. These tokens die on a scale of months — the oldest dead one here expired in April — so an hourly sweep is 26 API calls an hour to learn nothing, against third-party rate limits, to shorten a detection window nobody is watching by 23 hours. Nightly also means a marked credential is excluded from the NEXT import cron run rather than the current one. `dry_run=true` classifies and reports without writing, so the first sweep can be read before it marks anything. Marks ONLY on a definite platform answer. A timeout, a 429 or a 5xx is recorded as undetermined and changes nothing — see credential_health_policy for why that distinction is the point of this job.
List Platform Configs
$MeteredGET https://api.jubjubapp.com/v2/platform-configsList all platform configurations for a content. Optionally filter by platform type. Pass check_tokens=true to include live token_valid checks (adds latency). When a token is invalid, a notification is written to the user's feed. Access: Must have CONTENT_VIEW permission on the content's workspace.
Create Platform Config
$MeteredPOST https://api.jubjubapp.com/v2/platform-configsCreate a new platform configuration for a content. Each config represents how to publish to a specific platform/account. A content can have multiple configs for the same platform with different credentials. Access: Must have CONTENT_EDIT permission on the content's workspace.
Bulk Create Platform Configs
$MeteredPOST https://api.jubjubapp.com/v2/platform-configs/bulkCreate multiple platform configurations for a content at once. Useful when the user selects multiple platforms/accounts in a single step. Each item is processed independently — partial success is possible. Access: Must have CONTENT_EDIT permission on the content's workspace.
Bulk Update Platform Configs
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/bulkUpdate multiple platform configurations at once. Useful when the user configures settings for multiple platforms and clicks "Save All". Each config in the request will be updated independently - failures on one config won't affect others. Access: Must have CONTENT_EDIT permission on each config's content's workspace.
Get All Platform Schemas
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/schemasGet the settings schema for all platforms. Returns JSON schemas and field definitions for each platform, which the frontend can use to dynamically generate forms.
Get Platform Schema
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/schemas/{platform}Get the settings schema for a specific platform. Returns JSON schema and field definitions for the platform, which the frontend can use to dynamically generate the settings form.
Get Platform Requirements
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/schemas/{platform}/requirementsGet validation requirements for a specific platform. Returns: - required_fields: Fields that must be filled - limits: Character/count limits for fields - inherits_from_content: Fields that can use Content values as fallback - notes: Platform-specific tips Used by frontend for: - Real-time validation as user types - Showing required field indicators - Character counters
Get Platform Config
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/{config_id}Get full details of a platform configuration. Access: Must have CONTENT_VIEW permission on the content's workspace.
Update Platform Config
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/{config_id}Update a platform configuration. Only provided fields will be updated. Access: Must have CONTENT_EDIT permission on the content's workspace.
Delete Platform Config
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/{config_id}Delete a platform configuration. Access: Must have CONTENT_EDIT permission on the content's workspace.
Mark Config Ready
$MeteredPOST https://api.jubjubapp.com/v2/platform-configs/{config_id}/readyMark a platform configuration as ready for publishing. Validates that required settings are present for the platform. Access: Must have CONTENT_EDIT permission on the content's workspace.
Check Storage Health
$MeteredGET https://api.jubjubapp.com/v2/platform-configs/health/storageCheck if platform config storage is properly configured.
Get Platform Default
$MeteredGET https://api.jubjubapp.com/v2/platform-defaults/{credential_id}Get saved default settings for a credential. Returns empty settings object if no defaults have been saved yet.
Save Platform Default
$MeteredGET https://api.jubjubapp.com/v2/platform-defaults/{credential_id}Save (create or update) default settings for a credential. Only the credential owner can save defaults. The settings dict is partial — include only the fields you want as defaults.
Delete Platform Default
$MeteredGET https://api.jubjubapp.com/v2/platform-defaults/{credential_id}Remove saved defaults for a credential. Only the credential owner can delete defaults.
List Teams
$MeteredGET https://api.jubjubapp.com/v2/teamsList all teams the user is a member of. Returns lightweight card data for efficient list rendering. **Query params:** - `include=workspaces` - Include workspaces inline (avoids N+1 requests)
Create Team
$MeteredPOST https://api.jubjubapp.com/v2/teamsCreate a new team. The authenticated user becomes the owner of the team.
Search Teams
$MeteredGET https://api.jubjubapp.com/v2/teams/searchSearch teams by name. Only searches teams the user is a member of. Returns matching teams with highlights.
Get Team
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}Get full details of a team. User must be a member of the team.
Update Team
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}Update a team's details. Requires TEAM_EDIT permission.
Delete Team
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}Delete a team. Only the owner can delete a team. This does NOT delete associated workspaces.
Get Team Stats
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/statsGet aggregated statistics for a team. Calculates total_contents, total_launches, etc. by summing from all team workspaces. This provides real-time stats without requiring denormalized counters. User must be a member of the team.
Transfer Ownership
$MeteredPOST https://api.jubjubapp.com/v2/teams/{team_id}/transferTransfer team ownership to another admin. Only the current owner can transfer ownership. The new owner must be an Admin of the team.
List Members
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/membersList all members of a team. User must be a member of the team.
Update Member Role
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/members/{member_profile_id}Update a member's role. Requires MEMBERS_MANAGE permission. Cannot change the owner's role (use transfer ownership instead).
Remove Member
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/members/{member_profile_id}Remove a member from the team. Requires MEMBERS_MANAGE permission. Cannot remove the owner (use delete team instead).
Leave Team
$MeteredPOST https://api.jubjubapp.com/v2/teams/{team_id}/leaveLeave a team. The owner cannot leave (must transfer ownership first).
Get My Permissions
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/my-permissionsGet the current user's permissions in a team. Useful for UI to determine what actions to show.
List Team Workspaces
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/workspacesList all workspaces linked to this team. User must be a member of the team. Response includes progress information for each workspace: - progress: Overall percentage (0-100) - progress_summary: Breakdown by category (published, in_progress, pending)
Link Workspace To Team
$MeteredPOST https://api.jubjubapp.com/v2/teams/{team_id}/workspaces/{workspace_id}Link a workspace to a team. Requirements: - User must have WORKSPACE_CREATE permission in the team - User must be the workspace owner Note: Linking a workspace to a team gives all team members access.
Unlink Workspace From Team
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/workspaces/{workspace_id}Unlink a workspace from a team. Requirements: - User must have WORKSPACE_CREATE permission in the team OR be workspace owner Note: Unlinking removes team access but doesn't delete the workspace.
Get Team Activity
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/activityGet the team activity timeline. Returns audit events ordered by most recent first. Can filter by action type (e.g., member_joined, credential_shared). User must be a member of the team.
List Team Invites
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/invitesList all invites for a team. Requires MEMBERS_VIEW permission. Can filter by status: pending, accepted, rejected, expired, cancelled.
Create Invite
$MeteredPOST https://api.jubjubapp.com/v2/teams/{team_id}/invitesCreate an invitation to join the team. Requires MEMBERS_INVITE permission. Cannot invite someone who is already a member. Cannot create duplicate pending invites for the same email.
Cancel Invite
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/invites/{invite_id}Cancel a pending invitation. Requires MEMBERS_INVITE permission. Can only cancel pending invites.
List My Pending Invites
$MeteredGET https://api.jubjubapp.com/v2/invites/pendingList all pending invites for the current user. Used to show a notification badge or list of pending team invitations.
Preview Invite
$MeteredGET https://api.jubjubapp.com/v2/invites/{invite_id}/previewPublic, UNAUTHENTICATED preview of an invite. NO auth dependency, deliberately: the invitee opens this link before they have an account or a session, so any `get_current_user` here would break the whole email flow. The invite_id itself is the credential. Returns an explicit allowlist (InvitePreviewResponse) — see that model for what is withheld and why. A dead or consumed invite answers 200 with `actionable: false` and NO team or inviter detail, so the page can say "this invite has expired" instead of a generic error, while a guessed or reused id reveals nothing. Only an unknown id is a 404.
Accept Invite
$MeteredPOST https://api.jubjubapp.com/v2/invites/{invite_id}/acceptAccept a team invitation. Adds the user as a member of the team with the invited role.
Reject Invite
$MeteredPOST https://api.jubjubapp.com/v2/invites/{invite_id}/rejectReject a team invitation.
List Shared Credentials
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/credentialsList all credentials shared with the team. Requires CREDENTIALS_VIEW permission. By default, only shows active credentials.
Share Credential
$MeteredPOST https://api.jubjubapp.com/v2/teams/{team_id}/credentialsShare one of your credentials with the team. IMPORTANT: Only the credential owner can share it. Requires CREDENTIALS_SHARE permission. Cannot share the same credential twice with the same team.
List Available Credentials
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/credentials/availableList all credentials available to the user in team context. Returns both: - Personal credentials owned by the user - Shared credentials from other team members Useful for credential selection dropdown when creating a PlatformConfig.
Update Shared Credential
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/credentials/{shared_credential_id}Update a shared credential's nickname. Only the owner can update the nickname.
Revoke Shared Credential
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/credentials/{shared_credential_id}Revoke a shared credential. Only the owner of the credential or team admin can revoke it.
List My Shared Credentials
$MeteredGET https://api.jubjubapp.com/v2/teams/{team_id}/credentials/mineList credentials I have shared with this team. Useful for "My Contributions" view.
Get Profiles Batch
$MeteredGET https://api.jubjubapp.com/v2/profilesBatch lookup profiles by IDs. Returns profile summaries (display_name, email, avatar_url) for the given IDs. Useful for resolving names in member lists, activity feeds, etc. **Usage:** ``` GET /v2/profiles?ids=prof_abc,prof_xyz,prof_123 ``` **Note:** Maximum 50 IDs per request to prevent abuse.
Search Profile By Email
$MeteredGET https://api.jubjubapp.com/v2/profiles/searchSearch for a profile by exact email match. Used by the "add member" flow to resolve an email to a profile_id. **Usage:** ``` GET /v2/profiles/search?email=user@example.com ```
Get Wallet Rails
$MeteredGET https://api.jubjubapp.com/v2/profiles/wallet-railsGet wallet rails for the authenticated user. Returns the wallet_rails object if registered, null otherwise.
Register Wallet Rails
$MeteredPOST https://api.jubjubapp.com/v2/profiles/wallet-railsRegister wallet rails (Privy wallet) for the authenticated user. Writes wallet_rails structured metadata and appends the address to wallet_addresses for fast array-contains lookups. Both fields are written in a single Firestore update. Idempotent: returns 200 if already registered with the same privy_user_id. Returns 409 if wallet_rails.evm exists with a different privy_user_id.
Update My Profile
$MeteredGET https://api.jubjubapp.com/v2/profiles/meUpdate the authenticated user's own profile. PATCH semantics: only fields explicitly provided are written. Mutable fields: display_name, avatar_url, bio. Email is SET-ONCE: written only when the profile has no email yet (lets a wallet-only viewer add an email to publish & get paid); ignored if an email already exists. No verification / Privy / Firebase user is involved. Returns the updated profile.
Update My Handle
$MeteredGET https://api.jubjubapp.com/v2/profiles/me/handleSet or change the authenticated user's @handle. Validation rules (src/utils/handle.py): - 3 to 30 chars - Lowercase letters, numbers, underscores only - Must start with a letter - Cannot be on the reserved list Uniqueness: case-insensitive. Returns 409 if another profile holds the handle (the current user holding it is a no-op success).
List Mentionable Profiles
$MeteredGET https://api.jubjubapp.com/v2/profiles/mentionableReturn profiles matching `prefix` for @mention autocomplete. Ranking: 1. `workspace_member` — the workspace's people: owner, direct members, the members of the team the workspace is linked to (`team_id`), and the caller. The same union as GET /v2/workspaces/{id}/members; a team member has access to the workspace, so they are a person in it (2026-09-09: a confirmed team member could not be tagged from a workspace their team owns). Matched on handle, display name OR email, so a member with no handle is found by name and a member with no name yet is found by email. With an empty prefix this is the whole list, which is what a bare "@" shows. Sorted by handle/name. 2. `recent_collaborator` — profiles whose handle starts with `prefix` and who share a workspace with the caller where activity occurred within the last 90 days. Sorted by `last_collaborated_at` desc, then alphabetical by handle. Skipped for an empty prefix. The caller is included. A solo workspace has exactly one person in it, and that person could not tag themself (2026-09-05: "No matching profiles" on the owner's own workspace). Recency gate ("90-day window"): if no shared workspace has any activity (content, launch, or workspace-scope comment) within the last 90 days, the candidate is dropped. v0 simplification documented in `_latest_workspace_activity`.
Get Profile
$MeteredGET https://api.jubjubapp.com/v2/profiles/{profile_id}Get a single profile by ID. Returns profile summary (display_name, email, avatar_url).
Add Wallet
$MeteredPOST https://api.jubjubapp.com/v2/profiles/{profile_id}/walletsAdd a wallet address to a profile. Validates EVM format (0x + 40 hex chars). Appends if not already present. Only the profile owner can modify their wallets.
Remove Wallet
$MeteredGET https://api.jubjubapp.com/v2/profiles/{profile_id}/wallets/{wallet_address}Remove a wallet address from a profile. Only the profile owner can modify their wallets.
Claim Agent Wallet
$MeteredPOST https://api.jubjubapp.com/v2/profiles/wallets/claim-agentBind a wallet-only agent profile to the signed-in person. Proof is SIWE over the agent's wallet: whoever holds that key made the agent, and signing from it says so. Refused for a wallet that is not on an `agent_viewer` profile (a person's own wallet is docked through wallet-rails, not claimed), and for an agent already owned by someone else. Idempotent for the same owner. Only humans may own agents: an agent-key session cannot claim one.
Release My Payouts
$MeteredPOST https://api.jubjubapp.com/v2/profiles/me/payouts/releaseRe-check the caller's payout identity and release any held tokens. Safe to call at any time: the on-chain registration is idempotent and the identity check is a read. Returns the reason either way, so a surface can say "held: verify your email" rather than nothing.
List Messages
$MeteredGET https://api.jubjubapp.com/v2/communication/Create Message
$MeteredPOST https://api.jubjubapp.com/v2/communication/List Inbox
$MeteredGET https://api.jubjubapp.com/v2/communication/inboxMessages relevant to the authenticated user: those that mention them, or those in a workspace they can reach. Server-side scope (no client N+1). Sorted newest-first, soft-deleted excluded, offset-paginated. OPEN ONLY, BY DEFAULT. The inbox is the list of things still to deal with; a message marked handled (PATCH /{id}/resolve) leaves it. Without this the list was append-only and a 193-day-old test comment sat at the top of the home page with no way off.
Get Message
$MeteredGET https://api.jubjubapp.com/v2/communication/{message_id}Edit Message
$MeteredGET https://api.jubjubapp.com/v2/communication/{message_id}Delete Message
$MeteredGET https://api.jubjubapp.com/v2/communication/{message_id}Resolve Decision
$MeteredGET https://api.jubjubapp.com/v2/communication/{message_id}/resolveMark a message handled. A DECISION resolves with a decision_status, as before. A COMMENT resolves with an empty body: it is stamped resolved_at/resolved_by and leaves the inbox. Resolved is not read — a comment someone has seen but not acted on stays open; this is the "handled" action, on the message itself. Anyone who can write the message's scope may resolve it.
List Legacy Media Comments
$MeteredGET https://api.jubjubapp.com/v2/communication/legacy-media-commentsRead legacy Firestore comments from /media/{mediaId}/comments and return them in the v2 MessageListResponse shape. Returned messages have ``origin='legacy'`` and ``event_version=0`` so the dashboard can render them identically to v2 messages without needing to know the source.
List Notifications
$MeteredGET https://api.jubjubapp.com/v2/notifications/Get Unread Count
$MeteredGET https://api.jubjubapp.com/v2/notifications/unread-countMark Notification Read
$MeteredGET https://api.jubjubapp.com/v2/notifications/{notification_id}/readMark All Notifications Read
$MeteredGET https://api.jubjubapp.com/v2/notifications/mark-all-readDelete Notification
$MeteredGET https://api.jubjubapp.com/v2/notifications/{notification_id}Create an upload session link
$MeteredPOST https://api.jubjubapp.com/v2/uploads/linkAgent-authenticated. Creates a short-lived upload link for a user.
Get upload session status
$MeteredGET https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}Session-token authenticated. Returns full session state for agent polling.
Request a signed upload URL for a file
$MeteredPOST https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}/request-uploadSession-token authenticated. Creates a media_v2 record and returns a signed PUT URL.
Mark a file upload as complete
$MeteredPOST https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}/completeSession-token authenticated. Makes file public, extracts metadata, marks asset ready.
Auto-infer video-thumbnail groupings
$MeteredPOST https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}/infer-groupingsSession-token authenticated. Runs heuristic pairing on uploaded assets.
Confirm video-thumbnail groupings
$MeteredPOST https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}/confirm-groupingsSession-token authenticated. Stores confirmed groupings. Does NOT create contents.
Close an upload session
$MeteredPOST https://api.jubjubapp.com/v2/uploads/sessions/{upload_session_id}/closeSession-token authenticated. Marks the session as CLOSED and extends expires_at to 24 hours so the agent can continue polling. Idempotent — calling on an already-closed session returns 200.
Upload page (no login required)
$MeteredGET https://api.jubjubapp.com/upload/{upload_session_id}Returns a self-contained HTML upload page. Validate via ?token= query parameter.
Mcp Get
$MeteredGET https://api.jubjubapp.com/v2/mcpSSE streaming not supported.
Mcp Post
$MeteredPOST https://api.jubjubapp.com/v2/mcpMCP Streamable HTTP endpoint. Accepts a JSON-RPC 2.0 message and returns a JSON-RPC response. Supports agent key auth and OAuth 2.1 Bearer token auth.
Mcp Delete
$MeteredGET https://api.jubjubapp.com/v2/mcpSession termination not supported.
Decide Approval
$MeteredPOST https://api.jubjubapp.com/v2/approvals/{approval_id}/decideApprove or reject an approval request. Only the designated approver (credential owner) can make this decision. **Path parameters:** - `approval_id`: The approval request to decide on **Request body:** - `decision`: "approved" or "rejected" - `reason`: Optional reason (recommended for rejections) **Responses:** - 200: Decision recorded successfully - 400: Invalid decision - 403: Not the designated approver - 404: Approval request not found - 410: Approval request has expired - 503: Approval service unavailable
Get Pending Approvals
$MeteredGET https://api.jubjubapp.com/v2/approvals/pendingGet all pending approval requests for the current user. Returns approval requests where the current user is the designated approver and the status is still "pending". **Responses:** - 200: List of pending approvals - 503: Approval service unavailable
Get Approvals For Launch
$MeteredGET https://api.jubjubapp.com/v2/approvals/launch/{launch_id}Get all approval requests for a specific launch. Returns all approval requests (any status) associated with the given launch ID. Useful for checking whether all approvals have been resolved. **Responses:** - 200: List of approvals for this launch - 503: Approval service unavailable
Cleanup Expired Workspaces
$MeteredPOST https://api.jubjubapp.com/v2/internal/cleanup-expired-workspacesDelete EXPIRED ANONYMOUS TRIAL workspaces. Never a creator's. 🔴 THIS CRON MAY NOT DELETE OWNED WORKSPACES, AND CANNOT. Creator media is inventory: it is what x402 agents buy through media_search / purchase_metadata / purchase_content. A timer that deletes it destroys saleable stock and a creator's archive at once. Owned workspaces are permanent (decision 2026-09-01, workspace_routes.py:521); only anonymous "Try Now" sandboxes expire. Two independent things keep that true, and only the second is load-bearing: 1. the query below filters `owner_id == None`, so an owned workspace is not even considered. This is an efficiency and a statement of intent. 2. `_delete_trial_workspace_cascade` RE-READS each document and RAISES on anything owned. This is the guarantee. Widening the query cannot widen what gets deleted. A refusal is logged and counted, never swallowed: if this cron ever selects an owned workspace, that is a bug in the selection and the evidence must survive the run. Scheduled daily; trials live TrialConfig.SESSION_DURATION_HOURS (72h), so a daily tick removes them within a day of expiry. Processes up to batch_size per invocation.
Cleanup Orphaned Media
$MeteredPOST https://api.jubjubapp.com/v2/internal/cleanup-orphaned-mediaDelete media records stuck in 'uploading' status for longer than threshold. These are uploads that were started but never completed (user cancelled, browser closed, network error, etc). The frontend only clears local state, leaving orphaned records in Firestore and partial files in GCS. Called by Cloud Scheduler (e.g. every hour) or manually.
Cleanup Expired Members
$MeteredPOST https://api.jubjubapp.com/v2/internal/cleanup-expired-membersRemove workspace members whose expires_at has passed. Iterates over all workspaces, filters out expired members, and saves the updated member list. Called by Cloud Scheduler or manually.
Cleanup Expired Try Now
$MeteredPOST https://api.jubjubapp.com/v2/internal/cleanup-expired-try-nowDelete old try-now session documents. - Expired/abandoned sessions: deleted after retention_days (default 30) - Claimed sessions: deleted after claimed_retention_days (default 7) since data is already in profiles/{profileId}.try_now_metadata Workspace + media are already cleaned by cleanup-expired-workspaces. Called by Cloud Scheduler weekly (e.g., Sunday 4 AM AEST).
Internal Run Qa
$MeteredPOST https://api.jubjubapp.com/v2/internal/qa/runRun the proactive QA test suite. Called by Cloud Scheduler at 2am AEST.
Farcaster Webhook Health
$MeteredGET https://api.jubjubapp.com/v2/webhooks/farcasterHealth check endpoint for Neynar webhook probe.
Farcaster Webhook
$MeteredPOST https://api.jubjubapp.com/v2/webhooks/farcasterHandle Neynar cast events for JubJub-connected Farcaster creators. Signature verification fails closed and propagates 403 on mismatch. Processing failures are swallowed and return 200 so Neynar does not retry on our internal errors; genuine 400/403 responses still propagate.
Start Try Now
$MeteredPOST https://api.jubjubapp.com/v2/try-now/startStart a new try-now session. Creates a real V2 workspace (owner_id=None, is_trial=True) and a TryNowSession for analytics tracking. Returns a trial_token that the frontend must include as X-Trial-Token in all subsequent calls.
Get Trial Workspace
$MeteredGET https://api.jubjubapp.com/v2/try-now/{workspace_id}Get trial workspace data, plus everything a client needs to RESUME. `resume` is what the website's `?tn_resume=` handler (and a reload on the same device) rebuilds its local state from: the uploaded media with a signed playback URL and its probed metadata, the content id, the thumbnail, the captured email and chosen platforms. Without it a resume link from another device would land on an empty drop zone.
Create Trial Content
$MeteredPOST https://api.jubjubapp.com/v2/try-now/{workspace_id}/contentsCreate content in trial workspace. Enforces MAX_CONTENTS limit.
Request Trial Upload
$MeteredPOST https://api.jubjubapp.com/v2/try-now/{workspace_id}/media/upload-urlRequest a signed URL for media upload in trial workspace.
Confirm Trial Upload
$MeteredPOST https://api.jubjubapp.com/v2/try-now/{workspace_id}/media/{media_id}/confirmConfirm upload completion for trial media.
Set Trial Platform Configs
$MeteredPOST https://api.jubjubapp.com/v2/try-now/{workspace_id}/contents/{content_id}/platform-configsSet platform configs for trial content. credential_id is None — credentials are bound after claim. Enforces MAX_PLATFORMS limit.
Update Pre Profile
$MeteredGET https://api.jubjubapp.com/v2/try-now/{workspace_id}/pre-profileUpdate pre-profile data (micro-survey, behavioral timing, email capture). Only non-null fields are applied.
Get Trial Progress
$MeteredGET https://api.jubjubapp.com/v2/try-now/{workspace_id}/progressGet trial progress and funnel data.
Claim Try Now
$MeteredPOST https://api.jubjubapp.com/v2/try-now/claimClaim a trial workspace after Firebase signup. Requires: - X-Trial-Token header (trial authentication) - Authorization: Bearer <firebase_token> (Firebase authentication)
Get Try Now Copy
$MeteredGET https://api.jubjubapp.com/v2/public/try-now/copyThe drop zone's headline/subline/CTA overrides, editable in Firestore (`website_config/try_now_copy`, shape `{pages: {...}, variants: {...}}`) so messaging can change without a website deploy. An absent doc is an empty override, not an error — the site's built-in copy applies.
Metrics Overview
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/overviewHigh-level totals: total sessions, conversion rate, status breakdown.
Metrics Funnel
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/funnelStep-by-step drop-off: how many sessions reached each step.
Metrics By Source
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/by-sourceBreakdown per source (landing page / UTM source).
Metrics By Feature
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/by-featureBreakdown per feature_interest.
Metrics By Geo
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/by-geoBreakdown per country.
Metrics By Device
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/by-deviceBreakdown per device_type (desktop, mobile, tablet).
Metrics Timeline
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/timelineTime series of sessions created and claimed.
Metrics Enrichment
$MeteredGET https://api.jubjubapp.com/v2/admin/try-now/metrics/enrichmentPreProfile fill rates: what percentage of sessions filled each field.
Create Streaming Session
$MeteredPOST https://api.jubjubapp.com/v2/streaming/sessionsCreate a streaming session for watching content. Requires the viewer to have a registered wallet for USDC payments. Rate-limited per profile AND per IP. This is DoS-critical, not merely abuse control: `create_session` issues a ~300k-gas OPERATOR transaction, and operator txs serialise on the operator nonce — so flooding this endpoint stalls minting, catalogue deploys and settlement platform-wide, not just streaming. The check must therefore sit BEFORE create_session.
Record Streaming Segment
$MeteredPOST https://api.jubjubapp.com/v2/streaming/sessions/{session_id}/segmentRecord a viewed segment (heartbeat from player every 6 seconds). Auto-settles when 1 minute of unsettled segments accumulates.
Get Session Playback Url
$MeteredPOST https://api.jubjubapp.com/v2/streaming/sessions/{session_id}/playback-urlTier-2 gated playback: issue a short-lived, session-scoped signed URL. THIS is the paywall. Before issuing anything we require, in order: (a) the session exists, (b) it belongs to THIS viewer (verified via the streaming-session token), (c) the session is live and paid (status == "active"). Only then do we mint a signed, expiring URL via the per-host signer. If no signer can sign the content's host, we 422 — we never return a non-expiring durable URL. There is no public / pre-payment path to this endpoint.
Close Streaming Session
$MeteredPOST https://api.jubjubapp.com/v2/streaming/sessions/{session_id}/closeClose a streaming session and settle any remaining balance. Optional body: {"playback_seconds": <float>} — if provided, the final settlement uses precise playback seconds instead of segment rounding.
Beacon Close Session
$MeteredPOST https://api.jubjubapp.com/v2/streaming/sessions/{session_id}/beacon-closeUnauthenticated close endpoint for navigator.sendBeacon on tab close. sendBeacon cannot carry custom headers, so we verify ownership by matching the viewer_wallet in the request body against the session's stored viewer_wallet. The session_id is a UUID (hard to guess) and the close operation is non-destructive (settles money TO the creator). THE BODY IS PARSED BY HAND, AND THAT IS THE POINT. This route used to bind `request: BeaconCloseRequest`, which makes FastAPI require `Content-Type: application/json` — and a JSON Blob is not CORS-simple, so `sendBeacon` had to preflight it and the POST was dropped during unload. Clients now send `text/plain` to avoid the preflight, which a Pydantic body model answers with 422. `parse_beacon_body` accepts both shapes (and form-encoding) so the mini-app's vendored SDK snapshot keeps working unchanged while the new bundle rolls out. An unreadable DURATION never blocks the close — it is telemetry, and `settle_session` bills from segment_count alone. An unreadable WALLET does, because on an unauthenticated endpoint it is the only authorisation there is; that is a 400, never a 500.
Get Streaming Session
$MeteredGET https://api.jubjubapp.com/v2/streaming/sessions/{session_id}Get the current state of a streaming session.
Settle Idle Streaming Sessions
$MeteredPOST https://api.jubjubapp.com/v2/streaming/cron/settle-idleSettle all active sessions that haven't had a segment in 60+ seconds. Designed to be called by Cloud Scheduler every 60 seconds.
Cron Settle Crossings
$MeteredPOST https://api.jubjubapp.com/v2/pool/cron/settle-crossingsCompose at most one new crossing and submit everything pending. Safe to over-schedule: the ledger lease single-flights concurrent runs and the on-chain replay guard makes a double submission a gas-only no-op.
Cron Reconcile
$MeteredPOST https://api.jubjubapp.com/v2/pool/cron/reconcileBooks-vs-chain verification + alerts M-a..M-f. Reads only.
Accrual Detail
$MeteredGET https://api.jubjubapp.com/v2/pool/accrual/{pot_address}Ledger-side money states for one catalogue pot. AUTH: authenticated users only (this shipped without a dependency — caught in audit; it was the only unauthenticated money read in the codebase). The full authorisation rule this surface ultimately needs is holder-or-owner-scoped and id-keyed: dashboard and MCP speak content/team ids, pots are catalogue addresses, so the user-facing reads belong on an id-keyed sibling endpoint that resolves the pot via the entity doc and checks membership the way catalogue_get does. This address-keyed read stays as the low-level tool: authenticated, and exposing only aggregate, non-PII ledger positions. States 1-3 of the five-state display ("earning now" is per-viewer and summed here; "held for your catalogue" is pending_at_hub). Everything here is NON-spendable by definition — states 4-5 (ready to claim / in wallet) come from the catalogue contract via the existing claim surfaces and are deliberately not blended in.
Get Content Analytics
$MeteredGET https://api.jubjubapp.com/v2/analytics/content/{content_id}Get analytics for a specific content item over a date range.
Get Portfolio Analytics
$MeteredGET https://api.jubjubapp.com/v2/analytics/portfolioGet analytics across all content owned by the authenticated user.
Get Content Snapshot
$MeteredGET https://api.jubjubapp.com/v2/analytics/content/{content_id}/snapshotGet a single day's analytics snapshot for a content item.
Get Content Totals
$MeteredGET https://api.jubjubapp.com/v2/analytics/content/{content_id}/totalsGet all-time totals for a content item.
Fetch Content Analytics Now
$MeteredPOST https://api.jubjubapp.com/v2/analytics/content/{content_id}/fetchForce fetch analytics for a content item right now.
Cron Daily Analytics Fetch
$MeteredPOST https://api.jubjubapp.com/v2/analytics/cron/daily-fetchDaily analytics fetch for all published content. Designed to be called by Cloud Scheduler once per day. Works inside a wall-clock budget and queues a continuation of itself when rows remain.
Cron Quota Watch
$MeteredPOST https://api.jubjubapp.com/v2/quota/cron/watchAssess platform API consumption against each ceiling and emit the [QUOTA-APPROACH] alert line for anything worth acting on. Designed for a daily Cloud Scheduler job, AFTER the daily analytics fetch (so last_ok_at reflects today's run, not yesterday's).
Cron Trial Watch
$MeteredPOST https://api.jubjubapp.com/v2/quota/cron/trial-watchEmit the [TRIAL-FUNNEL-RISK] line when the try-now funnel is heading for a 429 (an IP nearing the per-IP session cap) or when a trial cleanup cron has stopped succeeding (which is what lets sessions pile up against that cap). Lives beside the platform-quota watcher for the same reason the AI-credential watcher does: same detector shape (approach-a-ceiling and absence-of-expected-outcome), same alert-token contract, same 20-hour dedupe, same dry_run-as-status. Different subject, so a separate route on a separate schedule.
Cron Trial Recovery Emails
$MeteredPOST https://api.jubjubapp.com/v2/quota/cron/trial-recovery-emailsHourly: send "your video is waiting" (~2h after an abandoned email capture) and "expires soon" (final window) to ACTIVE, idle trial sessions with a captured email — never to a claimed one. Both latched once. See trial_watch_worker.sweep_recovery_emails for why hourly.
Cron Ai Credential Watch
$MeteredPOST https://api.jubjubapp.com/v2/quota/cron/ai-credential-watchEmit the [AI-CREDENTIAL-STALE] line for any connected AI credential that has not succeeded in AI_CREDENTIAL_STALE_DAYS while something has tried and failed. Lives beside the platform-quota watcher because it is the same detector — absence of an expected outcome — and shares its alert-token contract, its 20-hour dedupe and its dry_run-as-status behaviour. It is a different subject (creator AI keys, not platform API ceilings), so it is a separate route on a separate schedule rather than another `kind` in that sweep.
List Platforms
$MeteredGET https://api.jubjubapp.com/v2/platformsList all supported platforms grouped by category with connection type info. Used by frontend platform connect page and MCP.
Release Pending Payouts Cron
$MeteredPOST https://api.jubjubapp.com/v2/ownership/release-pending-payoutsBackstop for the payout identity gate (payout_identity.py). Identity can attach where no backend call sees it — an email verified through the dashboard's Firebase reset link flips `email_verified` on the Auth user only. This sweep re-checks every profile whose brand tokens are held for identity and releases the ones that now qualify. Idempotent; a profile still lacking identity is left held and counted. Cloud Scheduler hourly is plenty: nothing is lost while held.
Recover Stuck Minting
$MeteredPOST https://api.jubjubapp.com/v2/ownership/recover-stuck-mintingResume content stuck in ownership_status="minting" for more than 10 minutes. Designed to be called by Cloud Scheduler every 10 minutes. Uses the state machine's ownership_step field to resume from the last completed checkpoint. Idempotent: checks on-chain state before retrying each step.
Recover Stuck Spine
$MeteredPOST https://api.jubjubapp.com/v2/ownership/recover-stuck-spineRetry onchain_publish_events stuck in status="staged" for 10+ minutes. These events were staged (Firestore doc written) but the on-chain JubJubPublishLedger.recordLaunch() tx was never submitted — typically because the Cloud Run container was terminated mid-task. Designed to be called by Cloud Scheduler every 10 minutes (same cadence as recover-stuck-minting).
Accept Proposal
$MeteredPOST https://api.jubjubapp.com/v2/catalogue/{catalogue_id}/proposals/acceptAccept a brand token proposal — transfers tokens to caller's wallet.
Reject Proposal
$MeteredPOST https://api.jubjubapp.com/v2/catalogue/{catalogue_id}/proposals/rejectReject a brand token proposal — redistributes bp to remaining members.
Get Proposals
$MeteredGET https://api.jubjubapp.com/v2/catalogue/{catalogue_id}/proposalsGet full allocation state with per-member status. Team members only.
Repropose
$MeteredPOST https://api.jubjubapp.com/v2/catalogue/{catalogue_id}/proposals/reproposeModify splits and re-notify — creates a new version.
Harvest Status
$MeteredGET https://api.jubjubapp.com/v2/catalogue/{team_id}/harvest/{content_contract_address}Read-only status for a content contract's harvest path. Returns whether auto-forward is enabled (content.catalogueAddress is set) and how much USDC is currently pending to be swept to the catalogue on the next router.settle(). This endpoint does NOT write to chain — harvest via claimFor() would orphan funds because the catalogue's notifyDistribution is onlyRegisteredContent. Auth: team owner only.
Get Holdings
$MeteredGET https://api.jubjubapp.com/v2/catalogue/{catalogue_id}/holdingsGet content contracts held, revenue, and brand token distribution. Team members only.
Cron Ensure Holders Minted
$MeteredPOST https://api.jubjubapp.com/v2/catalogue/cron/ensure-holders-mintedMint any brand-token holder that is still missing, across every catalogue we know about. THE BACKSTOP, not the primary mechanism. Minting is ensured at catalogue deploy, on the already-deployed path, and immediately before registerContent — all three inside the window where no revenue can yet have arrived. This pass exists because that used to be a single fire-and-forget side effect of deploy whose one transient failure was permanent and invisible: eight of nine mainnet catalogues had never minted a holder, two of them while actively earning. Cheap and safe to run often: a fully-minted catalogue costs one hasMinted view call per split and sends no transaction. Catalogues with no code on the active chain are reported, never minted into. Deploy as a Cloud Scheduler job (suggested: hourly) once verified.
Get Unified Balance
$MeteredGET https://api.jubjubapp.com/v2/wallet/unified-balanceAggregate the user's spendable USDC: smart wallet balance + every catalogue's claimable amount. Used by the frontend to render a single balance number and to drive silent auto-claim before a spend.
Get Streaming Earnings
$MeteredGET https://api.jubjubapp.com/v2/wallet/streaming-earningsNet-to-creator STREAMING earnings for today and the last 30 days. Streaming-only approximation: aggregates streaming_sessions_v2 gross settled USDC, attributes the creator's ownership share, and subtracts the 3% platform fee. Excludes catalogue/licensing/x402 revenue. Never raises on the no-data path — a creator with no streaming sessions gets zeros, and any unexpected read failure degrades to zeros (logged) so the wallet hero never breaks on a non-critical stat.
Register Content
$MeteredPOST https://api.jubjubapp.com/v2/platform/register-contentRegister content via platform API key. Full orchestration: resolve/create profile -> resolve/create workspace -> fingerprint the file at media_url (streamed, never stored) -> create content doc with the developer's URL as its playback source (triggers ownership deployment in background) -> optional spine entry. JubJub holds no bytes for the piece. The page's own <video src> plays and the SDK meters it (Tier 1); playback-info answers gated=False. Auth: X-JubJub-Platform-Key header.
Get Playback Info
$MeteredGET https://api.jubjubapp.com/v2/public/contents/{content_id}/playback-infoPublic endpoint — no auth required. Returns chain config and content contract for SDK Player integration. Also mints a short-lived `playback_grant` (K1-2). The grant is what makes `content_id` server-chosen at session-create time. When the caller happens to present a bearer token we bind the grant to their profile, which also kills cross-viewer replay; an anonymous fetch yields an unbound grant.
Get Content By Cast Hash
$MeteredGET https://api.jubjubapp.com/v2/public/contents/by-cast-hash/{cast_hash}Resolve a Farcaster cast hash to a JubJub content item for the Mini App paywall. Public endpoint — no auth required. URL priority (3-step): 1. source_video_url — direct playable embed URL (mp4 / CDN / Mux stream) written by the auto-tokenise service. This is the canonical playable URL for the Mini App <video> tag. 2. platform_video_url — legacy fallback for pre-fix docs (currently same as farcaster_cast_url for Farcaster-native content but may carry a real video URL for SDK-registered content). 3. farcaster_cast_url — last resort (cast permalink page, usually not directly playable). Here as a defensive fallback for pre-fix auto-tokenise docs with no source_video_url. 4. None of the above → 404 with retry hint. The launches_v2 fallback from FarcasterAdapter._resolve_video_url is deliberately skipped here — the Mini App viewer paywall is Farcaster-native content only. Tier-2 gating: if the content is marked gated_playback=true, the durable URL is paywalled and is NEVER emitted here (video_url is null). The Mini App falls through to the normal gated flow — GET playback-info (sees gated=true) → pay → resolve a short-lived signed URL via the AUTHED POST /v2/streaming/sessions/{id}/playback-url endpoint — exactly like any other Tier-2 piece. Note: this endpoint intentionally treats video_url as an opaque string — see content_routes.py for the legacy Mux-specific path that will be replaced in the /watch/[contentId] migration (TODO.md:59-66).
List Platform Keys
$MeteredGET https://api.jubjubapp.com/v2/platform/keysList the authenticated user's platform keys — metadata only. The raw key is hashed at issue and can never be shown again; this returns name/scope/status/created_at so a dashboard can show what exists and what each key may do. Auth: Firebase Bearer (normal user auth, NOT platform key).
Create Platform Key
$MeteredPOST https://api.jubjubapp.com/v2/platform/keysGenerate a new platform API key for the authenticated user. Auth: Firebase Bearer (normal user auth, NOT platform key). The key is shown ONCE in the response — user must save it.
Get Platform Key Payout Status
$MeteredGET https://api.jubjubapp.com/v2/platform/keys/{key_id}/payout-statusWhether this platform key has a payout wallet behind it. Auth: Firebase Bearer, same as POST /v2/platform/keys — and the same refusal for platform-key auth, so a key cannot be used to interrogate itself or enumerate a profile's other keys. Read-only. Resolves the owner's wallet LIVE from their profile; nothing is stored on the key and nothing is written here. A key whose owner attaches a wallet becomes eligible with no action on the key itself.
Create Viewer Session
$MeteredPOST https://api.jubjubapp.com/v2/public/viewer-sessionCreate a lightweight session token for an SDK viewer. Public endpoint — no auth required. Creates or finds a viewer profile, returns a jj_ session token usable for streaming API calls. Rate-limited per wallet AND per IP: unauthenticated, and each call can provision a profile. The wallet here is attacker-controlled input, so the IP bucket is the one that actually bounds provisioning.
Developer Signup
$MeteredPOST https://api.jubjubapp.com/v2/public/developer-signupPublic endpoint — no auth required. Creates a developer profile and returns a platform API key with a ready-to-paste code snippet. Rate limited to 3 keys per day.
Register Viewer
$MeteredPOST https://api.jubjubapp.com/v2/auth/register-viewerRegister a streaming viewer or agent — creates Firebase user + JubJub profile. PUBLIC: No Firebase auth required (viewer doesn't have Firebase yet). The caller proves control of `wallet_address` with a SIWE signature over a nonce this server issued; nothing below runs until that proof holds. Idempotent: if a profile already holds this wallet, returns the existing profile without modification — legitimately, because the signature just proved the caller controls that wallet.
Exchange Token
$MeteredPOST https://api.jubjubapp.com/v2/auth/exchange-tokenPUBLIC. Exchange a firebase_custom_token (from register-viewer) for a Firebase ID token + refresh token, SERVER-SIDE — so the browser never calls the referrer-restricted Google Identity Toolkit endpoint directly.
Refresh Token
$MeteredPOST https://api.jubjubapp.com/v2/auth/refresh-tokenPUBLIC. Refresh an expiring Firebase ID token SERVER-SIDE via the Secure Token API, keeping all auth same-origin to api.jubjubapp.com (the browser's referrer-restricted key can't call securetoken.googleapis.com either).
Exchange Firebase Token
$MeteredPOST https://api.jubjubapp.com/v2/auth/exchange-firebase-tokenPUBLIC. Verify a Firebase ID token and mint a jj_ session for the EXISTING profile keyed off that Firebase uid (the dashboard account). Session-only — never writes wallet_addresses, never touches wallet_rails, never registers on-chain.
Login Password
$MeteredPOST https://api.jubjubapp.com/v2/auth/login-passwordPUBLIC. Server-side email+password sign-in via Google Identity Toolkit (accounts:signInWithPassword). Server-side requests carry no Referer, so the referrer-restricted web key works (same rationale as /exchange-token). On success mints a jj_ session for the existing profile. Session-only; no wallet / on-chain writes.
Firebase Config
$MeteredGET https://api.jubjubapp.com/v2/auth/firebase-configPUBLIC. Client-safe Firebase Web config so the static mini-app can init the Firebase JS SDK and sign in CLIENT-SIDE (the web apiKey is HTTP-referrer -restricted; these are the NEXT_PUBLIC_* values, safe to expose). Reads the same env the OAuth consent page uses (FIREBASE_WEB_*), falling back to NEXT_PUBLIC_FIREBASE_* / PROJECT_ID so it works regardless of which naming the Secret Manager mount uses.
Get Wallet Nonce
$MeteredGET https://api.jubjubapp.com/v2/auth/wallet-nonceIssue a fresh SIWE nonce + the canonical message to sign. The agent must sign the returned `message_to_sign` exactly as returned (whitespace and line endings included) and POST the signature plus this nonce to /v2/auth/register-viewer.
Get Intelligence Profile
$MeteredGET https://api.jubjubapp.com/v2/intelligence/profileGet the content intelligence profile for the current user. If not yet analysed, triggers analysis and returns result.
Analyse Intelligence
$MeteredPOST https://api.jubjubapp.com/v2/intelligence/analyseForce a fresh intelligence analysis for the current user. Rate limited to once per hour.
Get Recommendations
$MeteredGET https://api.jubjubapp.com/v2/intelligence/recommendationsGet publish-time recommendations based on the creator's historical performance patterns.
Get Platform Preferences
$MeteredGET https://api.jubjubapp.com/v2/intelligence/platform-preferencesGet the user's platform preferences for intelligence analysis. Returns current preference and all available platforms.
Set Platform Preferences
$MeteredGET https://api.jubjubapp.com/v2/intelligence/platform-preferencesSet preferred platforms for intelligence analysis. Any user can set any number of platforms — personal intelligence has no tier restrictions. Triggers fresh analysis with new preferences.
Cron Refresh Intelligence
$MeteredPOST https://api.jubjubapp.com/v2/intelligence/cron/refreshNightly intelligence refresh for all profiles. Called by Cloud Scheduler at 4:30am Sydney (after analytics fetch at 3am).
Get Ai Credentials
$MeteredGET https://api.jubjubapp.com/v2/ai-credentialsGet AI credentials for the current user. Never returns encrypted API keys.
Connect Ai Provider
$MeteredPOST https://api.jubjubapp.com/v2/ai-credentials/{provider}Connect an AI provider by validating and storing an API key.
Disconnect Ai Provider
$MeteredGET https://api.jubjubapp.com/v2/ai-credentials/{provider}Disconnect an AI provider — clears the encrypted key.
List Provider Models
$MeteredGET https://api.jubjubapp.com/v2/ai-credentials/{provider}/modelsModels this profile's key can see, plus the authoritative default. WHY A SEPARATE ENDPOINT rather than widening the connect response. The validator runs once, at connect time, before anything is stored — but the user needs this list every time they open the form, including long after connecting, and when editing a model on an already-connected provider. A connect-time-only payload cannot serve that, and widening `tuple[bool, str]` across all four validators would burden every connect with a list most of them do not need. NEVER RETURNS ANOTHER USER'S DATA. The caller supplies no key and no profile id: `profile_id` comes from the authenticated session, and the key is the one already encrypted under THAT profile's document. There is no input through which another profile could be named. ALWAYS 200, NEVER BLOCKS THE USER. The model field is free text and stays that way, so an unobtainable list is a missing convenience, not an error: available=false, reason="no_key" nothing connected yet available=false, reason="unavailable" provider refused or is unreachable (Claude's /v1/models has 404'd before — see _validate_claude_key's fallback path) `default_model` is returned in every case, including with no key, because the dashboard needs it for the placeholder before anything is connected. It is read from core.ai_models, which is env-overridable — the backend is the single source of truth and the dashboard must not keep its own copy.
Set Active Provider
$MeteredGET https://api.jubjubapp.com/v2/ai-credentials/activeSet the active AI provider. Must be connected first.
Fill Field
$MeteredPOST https://api.jubjubapp.com/v2/ai/fill-fieldSiwn Config
$MeteredGET https://api.jubjubapp.com/v2/farcaster/siwn/configReturn the public NEYNAR_CLIENT_ID the dashboard SIWN widget needs.
Siwn Callback
$MeteredPOST https://api.jubjubapp.com/v2/farcaster/siwn/callbackComplete a SIWN connect: verify the approved signer server-side and persist the Farcaster credential for the logged-in profile.
Siwn Connect Url
$MeteredGET https://api.jubjubapp.com/v2/farcaster/siwn/connect-urlReturn a dashboard deeplink that mounts the SIWN widget. Used by the MCP/agent surface to hand the human a clickable connect link.
Connect Fid
$MeteredPOST https://api.jubjubapp.com/v2/farcaster/connect-fidConnect a Farcaster FID for INBOUND auto-tokenise, directly from the mini-app's SIWE-verified context — no SIWN redirect, no signer. Cast delivery depends only on the FID being present in the Neynar webhook's author_fids filter (sync_farcaster_webhook_authors) + an active credential row (fid -> profile_id); SIWN's signer is only needed for OUTBOUND publishing. This endpoint creates the inbound-only credential and syncs the filter. Auth is the mini-app jj_ session token (get_current_user), NOT the Firebase cookie the SIWN callback uses.
Farcaster Connection
$MeteredGET https://api.jubjubapp.com/v2/farcaster/connectionWhether the caller has an ACTIVE Farcaster credential (drives the mini-app's pre-open connected/not-connected state). Read-only. Single-field query on profile_id (auto-indexed) + in-memory platform/status filter, so no composite index is needed.
Reconcile Webhook
$MeteredPOST https://api.jubjubapp.com/v2/farcaster/webhook/reconcileRebuild the Neynar inbound-webhook author_fids from all active Farcaster credentials and PUT it (idempotent). Heals drift from failed connect/disconnect updates or manual dashboard edits. Cron: hourly.
Get Creator Earnings
$MeteredGET https://api.jubjubapp.com/v2/creator/earningsSpendable balance + per-content earnings for the signed-in creator. Calls the SAME service functions the chat's get_wallet_balance + get_my_claimable use, then converts raw units to decimals and renames the per-content field to earned_usdc (never 'claimable').
Chat Message
$MeteredPOST https://api.jubjubapp.com/v2/chat/messageSend a message to the user's active AI provider. Returns the AI reply as JSON. Uses the creator's own API key from ai_credentials_v1.
Start Platform Import
$MeteredPOST https://api.jubjubapp.com/v2/import/{platform}Start a historical import for one platform. Runs in background — returns immediately. Rate limited: once per 24 hours per platform.
Start All Imports
$MeteredPOST https://api.jubjubapp.com/v2/import/allStart historical imports for ALL connected platforms. Runs in background — returns immediately.
Get Import Status Endpoint
$MeteredGET https://api.jubjubapp.com/v2/import/{platform}/statusGet current import job status.
Cron Nightly Import
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/nightlyNightly historical import for all users with active credentials. Called by Cloud Scheduler once per day at 4am Sydney time.
Cron Media Analysis
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/media-analysisNightly media metadata analysis for unanalysed content. Called by Cloud Scheduler. Uses each creator's own AI provider.
Cron Transcripts
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/transcriptsLayer 1 backstop — transcribe eligible content that has none. THE ONLY PATH for reference-only content. Farcaster auto-tokenised pieces never pass through upload/complete, so no upload trigger can reach them; `run_pending` existed for exactly this and had zero callers until now. Also the net under the create-content trigger, for a piece whose upload finished after its content row was written, and for any piece marked `pending_ai_key` whose creator has since connected a key. BYOK-only, like every AI surface: `extract()` hard-stops without a connected audio-capable provider and marks the row rather than spending anything. Thumbnail-only historical imports are excluded by `is_transcript_candidate` — a JPEG has no audio and attempting one would only consume the run's budget.
Cron Reindex Reconcile
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/reindex-reconcileLayer-3 backstop: re-index content that SHOULD be searchable but is missing from media_search_index_v1. The search index is written at three points — end of analysis (media_metadata_service:upsert at analysis completion), mint completion (ownership_recovery_worker after ownership flips active), and here. This sweep exists because the first two are event-triggered and can be missed: analysis running before a mint completes leaves the piece un-indexed until *something* re-runs upsert. This makes listing self-healing, not dependent on a single trigger. Selects contents_v2 rows that are media_analysed==True AND minted (ownership_contract_address set AND ownership_status=='active') AND published (discoverable OR has an external URL) BUT absent from the index, and upserts them. upsert_search_index self-gates on minted + metadata presence, so a row that no longer qualifies is skipped (and any stale index doc deleted) inside the upsert — this sweep only decides *what to check*. Called by Cloud Scheduler (needs a new job — see TODO). Best-effort, batched.
Cron Farcaster Spine Reconcile
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/farcaster-spine-reconcileBackstop for the Farcaster vault-visibility gap. handle_cast writes the vault-visible spine row (onchain_publish_events, keyed farcaster_<content_id>) best-effort with no backstop — a failed write leaves a source="farcaster_auto" cast owned/earning but INVISIBLE in the creator's vault (the vault filters onchain_publish_events by contributors_jubjub_profile_ids). This sweep finds such orphans and (re)writes the spine row via record_launch_onchain + _LaunchProxy, reconstructing the launch from the content doc's own fields. Idempotent + safe: - The staged doc is written with .set(merge=True) keyed by launch_id, so a double write merges harmlessly. - We SKIP any row that already has contributors_jubjub_profile_ids. That field is only set once record_launch_onchain reaches step 5 (the full staged write), which is BEFORE its on-chain ledger tx (step 6). So "no contributors" ⟹ the on-chain tx never fired ⟹ running record_launch_onchain fires it exactly once (no double-submit); and any already-staged/confirmed row (contributors present) is left untouched. NOTE: the generic spine-recovery worker (recover_stuck_spine_events) CANNOT heal these — it loads the launch from launches_v2, which Farcaster casts do not have. Hence this dedicated sweep. Called by Cloud Scheduler (needs a NEW job — see TODO). Best-effort, batched.
Cron Platform Data Retention
$MeteredPOST https://api.jubjubapp.com/v2/import/cron/platform-data-retentionRefresh or redact platform-supplied text on imported content. YouTube's Developer Policies require stored titles, channel names and descriptions to be DELETED or REFRESHED within 30 calendar days. Rows are selected on AGE (`platform_data_fetched_at`, absent = stale), refreshed via videos.list where the creator still has an active credential, and REDACTED where they do not — a row nothing can refresh must lose the data, and skipping it is how it stays non-compliant forever. DAILY, not hourly. The threshold is 23 days against a 30-day limit, so a daily run leaves seven days of margin: a week of failures, a deploy freeze or a quota exhaustion is survivable without falling out of policy. Hourly would buy nothing and re-stream the corpus 24x. `dry_run=true` reports what it WOULD do and writes nothing. Redaction is irreversible, so the first pass over the backlog is worth reading before it is trusted. `limit` overrides the per-run cap (0 = the default). Returns counts for every outcome — examined, selected, skipped_fresh, refreshed, redacted, deferred, capped — so a run can be checked rather than assumed. `deferred` is the honest one: rows left alone because a transient API or credential error made them unreadable this time. Those are NOT redacted; destroying a creator's titles over a brief outage is unrecoverable.
Get Content Ownership
$MeteredGET https://api.jubjubapp.com/v2/tokens/content/{content_id}/ownershipGet on-chain ownership details for a content item.
Get Content Revenue
$MeteredGET https://api.jubjubapp.com/v2/tokens/content/{content_id}/revenueGet streaming revenue stats from the smart contract.
Get Profile Claimable
$MeteredGET https://api.jubjubapp.com/v2/tokens/profile/claimableWhat the current user can claim, in USDC. Reads the CATALOGUE contracts, not the content contracts. Every live content contract holds its full 10,000 units at the catalogue, so a creator wallet's content-contract claimable is structurally zero — this endpoint returned 0.0 for everyone partly because it was asking the wrong contract. The catalogue is where the money is, and it is what `/v2/wallet/unified-balance` reads, so the two surfaces now agree by construction rather than by coincidence. `total_claimable_usdc` is the catalogue-pending leg ONLY. It deliberately excludes USDC already sitting in the wallet — that is already the creator's, not something they can claim.
Get Profile Portfolio
$MeteredGET https://api.jubjubapp.com/v2/tokens/profile/portfolioGet all content where the current user holds ownership tokens.
Get Wallet Balance
$MeteredGET https://api.jubjubapp.com/v2/tokens/wallet/balanceGet ETH and USDC balance of the user's connected wallet.
Admin Get User Credentials
$MeteredGET https://api.jubjubapp.com/v2/admin/users/{profile_id}/credentialsAll platform + AI credentials for a user.
Admin Get User Notifications
$MeteredGET https://api.jubjubapp.com/v2/admin/users/{profile_id}/notificationsLast 50 notifications for a user.
Admin Get User Workspaces
$MeteredGET https://api.jubjubapp.com/v2/admin/users/{profile_id}/workspacesAll workspaces owned by a user.
Admin Get User Summary
$MeteredGET https://api.jubjubapp.com/v2/admin/users/{profile_id}/summaryCombined user investigation — single call for everything.
Admin List Users
$MeteredGET https://api.jubjubapp.com/v2/admin/usersList all users with health signals.
Admin Trigger Deploy
$MeteredPOST https://api.jubjubapp.com/v2/admin/deployTrigger Cloud Build backend-push-to-main on main branch.
Admin Rollback
$MeteredPOST https://api.jubjubapp.com/v2/admin/rollbackRoll back Cloud Run traffic to the previous revision.
Admin Seed Agent Configs
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/seedSeed agent_config_v1 with defaults. Idempotent — skips existing.
Admin List Errors
$MeteredGET https://api.jubjubapp.com/v2/admin/errorsList error events with optional filters.
Admin Update Error
$MeteredGET https://api.jubjubapp.com/v2/admin/errors/{event_id}Update an error event's status.
Admin Run Watcher
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/watcher/runRe-trigger the watcher on stale error events (status=ingested, older than 5 min).
Admin List Agents
$MeteredGET https://api.jubjubapp.com/v2/admin/agentsList all agents with config + status.
Admin Get Agent Logs
$MeteredGET https://api.jubjubapp.com/v2/admin/agents/logsRecent agent logs.
Admin Update Agent Config
$MeteredGET https://api.jubjubapp.com/v2/admin/agents/{agent_name}/configUpdate agent config fields.
Admin Reset Budget
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/{agent_name}/reset-budgetReset an agent's daily token budget counter to zero.
Admin Approve Job
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/approvals/{approval_id}/approveApprove a pending agent job.
Admin Deny Job
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/approvals/{approval_id}/denyDeny a pending agent job.
Admin Clear Approvals
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/{agent_name}/clear-approvalsReject all pending approvals for an agent.
Admin Get Agent Approvals
$MeteredGET https://api.jubjubapp.com/v2/admin/agents/{agent_name}/approvalsList pending approvals for an agent.
Admin Clear Queue
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/clear-queueClear stuck error events (ingested, fixing, investigating) by marking them ignored.
Admin Queue Status
$MeteredGET https://api.jubjubapp.com/v2/admin/agents/queue-statusCount error events by status for the admin dashboard.
Admin Refresh Stats
$MeteredPOST https://api.jubjubapp.com/v2/admin/stats/refreshRefresh the accumulated platform stats (full recount). Called nightly by the `refresh-platform-stats` Cloud Scheduler job at 20:00 UTC, and on demand by an admin.
Admin Get Stats
$MeteredGET https://api.jubjubapp.com/v2/admin/statsRead the current accumulated platform stats.
Admin Run Support Investigator
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/support-investigator/runManually trigger the support investigator on one or all needs_admin events.
Admin Run Product Listener
$MeteredPOST https://api.jubjubapp.com/v2/admin/agents/product-listener/runManually trigger the product listener agent.
Admin Claim Error
$MeteredPOST https://api.jubjubapp.com/v2/admin/errors/{event_id}/claimClaim an error event for manual investigation. Agents will stop touching it.
Admin Unclaim Error
$MeteredPOST https://api.jubjubapp.com/v2/admin/errors/{event_id}/unclaimRelease a claimed error event back to agents.
Admin Run Qa
$MeteredPOST https://api.jubjubapp.com/v2/admin/qa/runRun the proactive QA test suite. Returns test results.
Admin Get Qa Results
$MeteredGET https://api.jubjubapp.com/v2/admin/qa/resultsGet recent QA run results from qa_results_v1.
Admin Run Extraction
$MeteredPOST https://api.jubjubapp.com/v2/admin/media/run-extractionTrigger L0/L1/L2 metadata extraction for a creator's content (or all content when profile_id is null, capped at 200). Admin-only. BYOK only — each layer runs on the content owner's connected AI key. JubJub never pays. Returns immediately; extraction runs as background tasks. Skip logic (force=false): a layer already done for a piece is skipped — l0 done = `provenance` key in media_metadata_v1/{content_id} l1 done = `transcript` key in media_metadata_v1/{content_id} l2 done = intelligence_status == "complete" in media_metadata_v1 force=true queues every requested layer regardless. Counts are per (content, layer): `queued` is layer-tasks fired, `skipped` is layer-tasks skipped (already done, or no owner to bill).
Report Error
$MeteredPOST https://api.jubjubapp.com/v2/errors/reportEndpoint for users AND client surfaces to report issues into the agent pipeline (ingest -> error_events_v1 -> watcher/support investigator). OPTIONAL auth: a valid Bearer token attaches the reporter's profile_id; without one the report is still accepted anonymously (profile_id=None) so PRE-LOGIN crashes reach the agent instead of being rejected with 401. Accepts any JSON body — normalised by the ingestor. Hardened (see docs/security-posture.md): rate-limited per profile/IP, and the attacker-controllable fields are size-capped at ingress. The trust gate that stops anonymous reports before the paid RCA call lives downstream in the watcher (only authenticated reports reach RCA). Nothing downstream writes code: the fixer was retired 2026-09-02 (CLAUDE.md, AGENT-01).
List Client Errors
$MeteredGET https://api.jubjubapp.com/v2/errors/clientAdmin read of client error reports. Pagination + filter by user_id and date range. Index-free: pulls the most recent FETCH_CAP rows (single-field created_at order) and filters/paginates in Python — fine for an admin debug surface (the admin UI itself is held for Phase 19).
Report Client Error
$MeteredPOST https://api.jubjubapp.com/v2/errors/clientRecord an automatic client error. High-volume + anonymous-capable, so it writes straight to error_reports_v1 and does NOT route through the agent system. Fields are truncated to keep docs well under Firestore's 1MB limit.
List Reports
$MeteredGET https://api.jubjubapp.com/v2/admin/reportsList report documents. live=true: query Firestore fresh for the latest document per report type. live=false: return stored documents ordered by week_ending descending.
Get Report
$MeteredGET https://api.jubjubapp.com/v2/admin/reports/{report_id}Return a single report document by report_id.
Generate Reports
$MeteredPOST https://api.jubjubapp.com/v2/admin/reports/generateTrigger all four report generations + narratives. Writes to admin_reports_v1 and returns the four reports.
List Signals
$MeteredGET https://api.jubjubapp.com/v2/admin/signalsList product signals with optional filters.
Get Signal
$MeteredGET https://api.jubjubapp.com/v2/admin/signals/{signal_id}Get a single signal by ID.
Update Signal
$MeteredGET https://api.jubjubapp.com/v2/admin/signals/{signal_id}Update a signal's status or add notes.
Run Listener
$MeteredPOST https://api.jubjubapp.com/v2/admin/signals/run-listenerTrigger the product listener agent. Returns immediately.
Paid Media Search
$MeteredPOST https://api.jubjubapp.com/v2/paid/media_searchCross-corpus metadata search — ALWAYS FREE, no payment gate. Returns discoverable content with PREVIEW fields only. Agents use purchase_metadata / purchase_content to unlock full fields on a piece. 100%-margin: structured Firestore filters + in-memory facet match, zero inference at query time.
Cron Recover Settlements
$MeteredPOST https://api.jubjubapp.com/v2/paid/cron/recover-settlementsRun the x402 settle-retry worker. Designed to be called by Cloud Scheduler every 5 minutes. Protected by Cloud Scheduler's OIDC authentication at the infrastructure level; no user auth required. See the Manual Steps section of the feature PR for the exact `gcloud scheduler jobs create` command.
Cron Sweep Purchase Distributions
$MeteredPOST https://api.jubjubapp.com/v2/paid/cron/sweep-purchase-distributionsDistribute collected purchase revenue 97/3 through JubJubPaymentRouter. Two-phase by necessity: router.settle enforces a $0.20/min rate cap, so a session is opened on one tick and settled on a later one once enough time has elapsed for the accrued amount. Idempotent and safe to over-schedule — a tick with nothing due is a no-op. Protected by Cloud Scheduler OIDC at the infrastructure level, matching /v2/paid/cron/recover-settlements. 🔴 Behind POOL_PURCHASE_SOURCE this endpoint NO-OPS: hub crossings distribute purchases once the flag is on, and the scheduler job stays live until the owner pauses it — running the old sweep in that window would pay the same purchase out twice (plan: double-distribution hazard). One flag gates both the new writer and this endpoint, atomically.
Paid Purchase Metadata
Not used$MeteredPOST https://api.jubjubapp.com/v2/paid/purchase_metadatax402 — unlock all structured metadata fields for one content piece. Idempotent within 24h (valid prior purchase → full fields, no new charge).
Paid Purchase Content
Not used$MeteredPOST https://api.jubjubapp.com/v2/paid/purchase_contentx402 — metadata_access PLUS full transcript + rights-holder info, with a logged intended_use licence declaration. Idempotent within 24h.
Purchase Metadata
$MeteredPOST https://api.jubjubapp.com/v2/content/{content_id}/purchase/metadataLegacy alias for POST /v2/paid/purchase_metadata.
Purchase Content
$MeteredPOST https://api.jubjubapp.com/v2/content/{content_id}/purchase/contentLegacy alias for POST /v2/paid/purchase_content.
Watch Content
$MeteredGET https://api.jubjubapp.com/watch/{content_id}Return HTML unfurl page with fc:miniapp meta tags for Farcaster.
Protected Resource Metadata
$MeteredGET https://api.jubjubapp.com/.well-known/oauth-protected-resourceRFC 9728 — Protected Resource Metadata.
Authorization Server Metadata
$MeteredGET https://api.jubjubapp.com/.well-known/oauth-authorization-serverRFC 8414 — OAuth Authorization Server Metadata.
Gpt Actions Openapi Schema
$MeteredGET https://api.jubjubapp.com/openapi-gpt.yamlGPT Actions OpenAPI 3.1 schema (public, cached 1 hour).
Register Client
$MeteredPOST https://api.jubjubapp.com/oauth/registerRFC 7591 — Dynamic Client Registration.
Authorize
$MeteredGET https://api.jubjubapp.com/oauth/authorizeOAuth 2.1 Authorization Endpoint. Renders a self-contained consent page with embedded Firebase login. After login, JS POSTs the Firebase ID token to /oauth/authorize/complete.
Authorize Complete
$MeteredPOST https://api.jubjubapp.com/oauth/authorize/completeExchange a Firebase ID token for an authorization code. Called by the consent page JS after successful Firebase login.
Token
$MeteredPOST https://api.jubjubapp.com/oauth/tokenOAuth 2.1 Token Endpoint. Supports: grant_type=authorization_code (code + code_verifier → tokens) grant_type=refresh_token (refresh_token → rotated tokens)
Check Messaging Health
$MeteredGET https://api.jubjubapp.com/health/messagingCheck notification system health (Firestore + WebSocket).
Whoami
$MeteredGET https://api.jubjubapp.com/health/whoamiGet current user info from Authorization token
Checks
reachable
valid
2026-10-08T07:15:52.521Z
No settlement evidence found in chain signals.
Gateway routing
Score ≥70/100 — Cleared attestation pass. Route via Gateway before pay.
Claim this listing to upgrade to Cleared attestation.
Claim listing