Omen Documentation

Developer guide for integrating with Omen

New to Omen? Start with the Developer Guide for a step-by-step walkthrough of setup and features.

This page is the full API reference. The guide explains when and how to use each feature.

Contents

  1. What is Omen
  2. Accounts
  3. Developer Portal
  4. Organizations
  5. Applications (OAuth Clients)
  6. Login with Omen (OAuth2 Flow)
  7. Login with Omen Kit — button assets & quick-start guide
  8. Profile API
  9. App Data API
  10. Org Data API
  11. Badges API
  12. Referral API
  13. Notifications API
  14. Server-to-Server API
  15. Trait Entitlements API
  16. Family Accounts
  17. Family API (includes child inventory & parent notifications)
  18. Child Authentication
  19. Child Purchase Approvals
  20. PFP Editor
  21. Avatar Save API
  22. Avatar Templates API
  23. Items & Inventory
  24. Item Templates API
  25. Item Issue & Revoke API
  26. User Inventory API
  27. Item Lookup API
  28. Batch Item Issue API
  29. Webhooks
  30. Item Transfers
  31. Redeem Codes
  32. Creations (Omen Publish)
  33. Publish Pipeline
  34. omen-publish CLI
  35. Multiplayer / Realtime API
  36. Public Profile (OG images, accent color, section order)
  37. Vanity Domains
  38. Creation Viewer
  39. External Creations
  40. Content Reports
  41. Push Notifications
  42. Mood / Status API
  43. Dashboard API
  44. Friends & Social Graph
  45. Presence API
  46. Rate Limiting
  47. Support & Feature Requests
  48. Profile Themes
  49. Stars API
  50. Prestige Tiers
  51. Rarity Visuals
  52. Daemon API
  53. Checkout (Stripe Connect)
  54. Creation Comments
  55. XP & Levels
  56. Developer Agent Tokens
  57. Omen SDK (Creation Runtime)
  58. @omen.dog/sdk (NPM Package)
  59. Collections API (Structured Data)
  60. Developer Environments
  61. Creator Source Download
  62. Data Export & GDPR
  63. Version History & Rollback
  64. AI Asset Generation
  65. Asset Library API
  66. Content Moderation API
  67. Leaderboards
  68. Embeddable Creations
  69. Matchmaking
  70. Parties
  71. Voice Chat
  72. Content & Localization API

What is Omen

Omen is a unified login service and OAuth2 provider. It lets users sign in once and authorize third-party applications to access their profile, badges, and stored data. Developers can create applications that use "Login with Omen" instead of building their own auth systems.

Accounts

Users sign up and manage their account at /pound. New users go through an onboarding flow where they set a display name and profile avatar. An account can have multiple linked providers:

  • Solana wallets — connect via wallet adapter
  • Discord — link via Discord OAuth
  • Email — email/password or magic link

Users can set a display name and profile image from /pound/profile.

Child Accounts

Parents can create child accounts from the Family page at /pound/family. Child accounts are created by a parent and log in via QR code scan — they do not need an email or wallet. See Family Accounts and Child Authentication for details.

Developer Portal

The developer portal lives at /pound/developer. From here you can:

  • Create and manage organizations
  • Create and manage applications (OAuth clients)
  • View client IDs and secrets
  • Configure redirect URIs, terms of service, and privacy policy links

Organizations

Organizations group multiple applications together. They enable:

  • Shared user data — all apps in an org can read/write the same org-level data per user
  • Badges — defined at the org level, grantable by any app in the org
  • Shared PFP projects — org-level avatar editor configurations

An application does not need to belong to an organization. Standalone apps can still use app-level data and the profile API. Org features (badges, org data) require the app to be linked to an org.

Applications (OAuth Clients)

Each application gets a client ID and client secret. These are used in the OAuth2 flow to identify your app.

FieldDescription
idClient ID (public)
secretClient secret (keep private, used in token exchange)
nameDisplay name shown to users during authorization
redirectUrisAllowed redirect URIs (must match exactly)
termsOfServiceURL to your terms of service
privacyPolicyURL to your privacy policy
organizationIdRequired — the organization this app belongs to

You can regenerate your client secret from the developer portal if it is compromised.

Login with Omen (OAuth2 Flow)

Omen implements the standard OAuth2 authorization code flow. For ready-made button assets and copy-paste integration code, see the Login with Omen Kit.

Step 1: Redirect to Omen

Send the user to the authorization page:

GET /pound/authorize?client_id=YOUR_CLIENT_ID
                     &redirect_uri=https://yourapp.com/callback
                     &response_type=code

The user logs in (if needed) and approves your app. Omen redirects back to yourredirect_uri with a code query parameter.

Step 2: Exchange Code for Tokens

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Response:

{
  "accessToken": "...",
  "accessTokenExpiresAt": "2025-01-01T00:00:00.000Z",
  "refreshToken": "...",
  "refreshTokenExpiresAt": "2025-02-01T00:00:00.000Z",
  "tokenType": "Bearer"
}

Step 3: Refresh Tokens

When the access token expires, use the refresh token:

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Step 4: Use the Access Token

Include the access token as a Bearer token in API requests:

Authorization: Bearer YOUR_ACCESS_TOKEN

Profile API

GET /api/profile
Authorization: Bearer <access_token>

Response:

{
  "id": "user_abc123",
  "name": "DisplayName",
  "email": "user@example.com",
  "image": "https://...",
  "orgImage": "/avatars/user_abc123-org.png",
  "badges": [
    {
      "id": "badge_1",
      "name": "Early Adopter",
      "description": "Joined during beta",
      "image": "https://..."
    }
  ]
}
FieldDescription
emailUser's email address (consented during authorization)
imageUser's default Omen profile image
orgImageOrg-specific avatar from the PFP editor (only present if the user has saved one). Use this to show the user's org PFP, fall back to image if absent.
badgesUser's badges in the org (empty array if app is not in an org)

Including Data

Add ?include=data to also receive stored user data:

GET /api/profile?include=data

Additional fields in response:

{
  ...profile fields,
  "orgData": { "level": 5, "xp": 1200 },
  "appData": { "theme": "dark", "lastSeen": "..." }
}
FieldDescription
orgDataShared data across all apps in the org (only if app is in an org)
appDataData specific to this app only

App Data API

Store arbitrary JSON data per user, scoped to your application. All requests require a Bearer token.

GET /api/appdata

Read the current user's app data.

GET /api/appdata
Authorization: Bearer <access_token>

Response:

{ "data": { "theme": "dark", "preferences": {...} } }

PUT /api/appdata

Replace the user's app data entirely.

PUT /api/appdata
Authorization: Bearer <access_token>
Content-Type: application/json

{ "data": { "theme": "light" } }

PATCH /api/appdata

Shallow-merge into existing data. Existing keys not in the request body are preserved.

PATCH /api/appdata
Authorization: Bearer <access_token>
Content-Type: application/json

{ "data": { "theme": "light" } }

If the user had {"theme":"dark","lang":"en"}, the result would be {"theme":"light","lang":"en"}.

Org Data API

Same as App Data, but shared across all apps in the organization. Requires the app to be linked to an org.

GET /api/orgdata

GET /api/orgdata
Authorization: Bearer <access_token>

Response:

{ "data": { "level": 5, "xp": 1200 } }

PUT /api/orgdata

Replace the user's org data entirely.

PUT /api/orgdata
Authorization: Bearer <access_token>
Content-Type: application/json

{ "data": { "level": 6, "xp": 0 } }

PATCH /api/orgdata

Shallow-merge into existing org data.

PATCH /api/orgdata
Authorization: Bearer <access_token>
Content-Type: application/json

{ "data": { "xp": 1500 } }

Returns 400 if the app is not linked to an organization.

Badges API

Badges are defined at the organization level. Any app in the org can create, grant, revoke, and list badges. All requests require a Bearer token and the app must be linked to an org.

GET /api/badges

List the current user's badges in the org.

GET /api/badges
Authorization: Bearer <access_token>

Response:

{
  "badges": [
    {
      "id": "badge_1",
      "name": "Early Adopter",
      "description": "Joined during beta",
      "image": "https://...",
      "organizationId": "org_abc"
    }
  ]
}

PUT /api/badges

Create a new badge in the org.

PUT /api/badges
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "name": "Early Adopter",
  "description": "Joined during beta",
  "image": "https://..."
}
FieldRequiredDescription
nameYesBadge display name
descriptionNoBadge description
imageNoBadge image URL

Returns 201 with the created badge.

POST /api/badges

Grant a badge to the authenticated user.

POST /api/badges
Authorization: Bearer <access_token>
Content-Type: application/json

{ "badgeId": "badge_1" }

Granting the same badge twice is a no-op — no duplicate badges.

DELETE /api/badges

Revoke a badge from the authenticated user.

DELETE /api/badges
Content-Type: application/json

{ "badgeId": "badge_1" }

Referral API

Reward users who invite others to your app. When User A shares an invite link and User B signs up through it, User A earns referral credit. At configurable tier thresholds, badges are auto-awarded. Referral counting is per-app (per OAuth client). Requires the app to be linked to an org.

How It Works

  1. Your app builds an invite link containing the referrer's Omen user ID
  2. When the referred user clicks "Login with Omen", pass &ref=REFERRER_USER_ID on the authorize URL
  3. Omen records the referral on first authorization (deduped per referred user + client)
  4. If the referrer crosses a tier threshold, the corresponding badge is auto-awarded
  5. Your app polls GET /api/referrals to check counts and tier progress

Building Invite Links

Add the ref parameter to the Omen authorize URL:

GET /pound/authorize?client_id=YOUR_CLIENT_ID
                     &redirect_uri=https://yourapp.com/callback
                     &response_type=code
                     &ref=REFERRER_USER_ID

Self-referrals (where ref equals the authorizing user's ID) are silently ignored. Duplicate referrals for the same user + client combination are also ignored.

GET /api/referrals

Get the authenticated user's referral data for this app.

GET /api/referrals
Authorization: Bearer <access_token>

Response:

{
  "count": 5,
  "referrals": [
    { "referredId": "user_xyz", "createdAt": "2025-06-01T..." },
    ...
  ],
  "tiers": [
    { "threshold": 3, "achieved": true, "badge": { "id": "b1", "name": "Recruiter", "image": "..." } },
    { "threshold": 10, "achieved": false, "badge": { "id": "b2", "name": "Ambassador", "image": "..." } }
  ],
  "nextTier": { "threshold": 10, "achieved": false, "badge": { "id": "b2", "name": "Ambassador", "image": "..." } }
}
FieldDescription
countTotal referrals this user has made for this app
referralsList of referred user IDs and timestamps
tiersAll configured referral tiers with achievement status
nextTierThe next unachieved tier, or null if all tiers are achieved

Configuring Referral Tiers

Org owners configure referral tiers from the developer portal (Organization settings). Each tier maps a referral count threshold to a badge. When a referrer's count reaches a threshold, the badge is automatically added to their profile. Create badges first in the Badges section, then add referral tiers that reference those badges.

Notifications API

Poll for events that happened to a user in your org — badge awards, grants, revocations, and more. Omen creates notifications automatically when things change. Your app polls to detect them. Requires the app to be linked to an org.

GET /api/notifications

Fetch notifications for the authenticated user, scoped to the app's org.

GET /api/notifications
Authorization: Bearer <access_token>
ParamRequiredDescription
sinceNoISO 8601 timestamp — only return notifications created after this time
limitNoMax results (default 50, max 200)

Response (newest first):

{
  "notifications": [
    {
      "id": "notif_abc",
      "type": "badge_earned",
      "data": {
        "badgeId": "badge_1",
        "badgeName": "Recruiter",
        "reason": "referral_tier",
        "referralCount": 3,
        "threshold": 3
      },
      "createdAt": "2025-06-15T12:00:00.000Z"
    }
  ]
}

Notification Types

TypeData fieldsWhen
badge_earnedbadgeId, badgeName, reason, referralCount, thresholdUser crosses a referral tier and a badge is auto-awarded
badge_grantedbadgeId, badgeName, clientIdAn app grants a badge via POST /api/badges
badge_revokedbadgeId, badgeName, clientIdAn app revokes a badge via DELETE /api/badges

Recommended Polling Pattern

  1. On first load, call GET /api/notifications?limit=50
  2. Store the createdAt of the most recent notification
  3. On subsequent polls (page load or every 30–60 s): GET /api/notifications?since=STORED_TIMESTAMP
  4. Show a toast or banner for each new notification

Server-to-Server API

Update a user's app data or org data from your backend without a user session. Useful for webhook handlers (e.g. confirming a purchase from Shopify) where the user isn't actively logged in. Authenticates with your existing OAuth client credentials.

PATCH /api/server/userdata/:userId

Shallow-merge into a user's app data and/or org data. Requires yourclient_id and client_secret in the request body.

PATCH /api/server/userdata/USER_OMEN_ID
Content-Type: application/json

{
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "appData": { "purchased": true, "tier": "lifetime" },
  "orgData": { "vipStatus": "gold" }
}
FieldRequiredDescription
client_idYesYour OAuth client ID
client_secretYesYour OAuth client secret
appDataNo*Object to shallow-merge into the user's app data (scoped to your client)
orgDataNo*Object to shallow-merge into the user's org data (requires app linked to org)

* At least one of appData or orgData must be provided.

Response:

{
  "appData": { "purchased": true, "tier": "lifetime", "existingKey": "preserved" },
  "orgData": { "vipStatus": "gold", "level": 5 }
}

Only the stores you wrote to are included in the response. Existing keys not in the request are preserved (shallow merge).

Errors

StatusCause
401Missing or invalid client_id / client_secret
404No user with that ID
400Neither appData nor orgData provided, or orgData used but app not linked to an org

Example: Shopify Webhook

// In your webhook handler, after validating the Shopify signature:
await fetch(`${OMEN_HOST}/api/server/userdata/${omenUserId}`, {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_id: process.env.OMEN_CLIENT_ID,
    client_secret: process.env.OMEN_CLIENT_SECRET,
    appData: {
      purchased: true,
      purchaseTier: "lifetime",
      purchaseDate: new Date().toISOString(),
    },
  }),
});

No new secrets needed — use the same client credentials from the developer portal.

POST /api/server/avatar-templates

Fetch avatar templates from your backend without a user session. Useful for landing pages and public galleries where no user is logged in.

POST /api/server/avatar-templates
Content-Type: application/json

{
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "projectId": "YOUR_PROJECT_ID"
}
FieldRequiredDescription
client_idYesYour OAuth client ID
client_secretYesYour OAuth client secret
projectIdYesPFP project ID
categoryIdNoFilter to a single category. Omit to get all.

Returns the same { categories: [...] } response format as the Bearer-authenticated Avatar Templates API.

// Server-side: fetch templates for landing page
const res = await fetch('https://omen.dog/api/server/avatar-templates', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_id: process.env.OMEN_CLIENT_ID,
    client_secret: process.env.OMEN_CLIENT_SECRET,
    projectId: 'YOUR_PROJECT_ID',
  }),
})
const { categories } = await res.json()
const templates = categories.flatMap(c => c.items)

Trait Entitlements API

Control which PFP traits users can access. Org owners create trait bundles (groups of traits, categories, bases, and variants) from the PFP editor UI. Your app can then grant or revoke these bundles per user via the API. All requests require a Bearer token and the app must be linked to an org with a PFP project configured.

How Permissions Work

  • If no bundles or default traits are configured, all traits are free (backwards compatible)
  • Once the org owner creates any bundle or sets default traits, permissions activate
  • The org owner always sees everything unlocked
  • A trait is unlocked for a user if: it is in defaultTraitIds (free for everyone), OR it belongs to a granted bundle's traitIds, OR its category is in a granted bundle's categoryIds
  • Locked traits are visible with a lock icon, not hidden — users can see what's available to unlock
  • The server rejects avatar saves that include locked traits (403)

Setting Up Bundles

Bundle management is done by the org owner in the PFP editor UI (not via API). Open the PFP editor as the org owner, expand Trait Bundles & Permissions at the bottom, and:

  1. Click + New Bundle to create a bundle (e.g. "Elite Pack")
  2. Use the picker tabs to select individual traits, entire categories, bases, or variants
  3. Click Edit Default Traits to set which traits are free for all users
  4. Use the API below to grant bundles to users from your app

GET /api/trait-entitlements

Get the current user's granted bundle IDs for a project.

GET /api/trait-entitlements?projectId=PROJECT_ID
Authorization: Bearer <access_token>

Response:

{
  "entitlements": {
    "bundleIds": ["bundle_1", "bundle_2"]
  }
}

POST /api/trait-entitlements

Grant a bundle to the authenticated user.

POST /api/trait-entitlements
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "projectId": "PROJECT_ID",
  "bundleId": "bundle_1"
}

Granting the same bundle twice is a no-op — no duplicates.

Response:

{
  "message": "Bundle granted",
  "entitlements": { "bundleIds": ["bundle_1"] }
}

DELETE /api/trait-entitlements

Revoke a bundle from the authenticated user.

DELETE /api/trait-entitlements
Content-Type: application/json

{
  "projectId": "PROJECT_ID",
  "bundleId": "bundle_1"
}

Response:

{
  "message": "Bundle revoked",
  "entitlements": { "bundleIds": [] }
}

Bundle Object Reference

Bundles are created by the org owner in the UI. Each bundle contains:

FieldDescription
idUnique bundle ID (use this in grant/revoke calls)
nameDisplay name (e.g. "Elite Pack")
descriptionOptional description
traitIdsIndividual trait IDs unlocked by this bundle
categoryIdsCategory IDs — all traits in these categories are unlocked
baseIdsBase template IDs unlocked
variantIdsVariant trait IDs unlocked
templateIdsSaved template IDs unlocked

To list available bundles and their IDs, use the user permission endpoint (returns all bundles for the project):

GET /api/user/trait-entitlements?orgId=ORG_ID&projectId=PROJECT_ID

Response (session auth):

{
  "permissionsActive": true,
  "defaultTraitIds": ["trait-1", "trait-2"],
  "bundles": [ { "id": "...", "name": "Elite Pack", ... } ],
  "userBundleIds": ["bundle_1"],
  "isOwner": false
}

Family Accounts

Omen supports family groups where a parent can manage child accounts and control which apps children can access. Families are managed at /pound/family.

How It Works

  • A user creates a family and becomes the owner
  • The owner can create child accounts (up to 4 children per family)
  • The owner can invite other users to join the family via invite link
  • The owner can pre-approve apps so children skip the OAuth consent screen
  • Children log in via QR code generated by the parent — no email or wallet needed

Limits

LimitValue
Max family members (including owner)5
Max child accounts4
Invite link expiry7 days
QR login token expiry10 minutes

App Pre-Approval

Family owners can pre-approve OAuth applications. When a child account encounters a pre-approved app during the OAuth flow, the consent screen is skipped automatically. This is managed via the Family API or the family settings UI.

Parent Dashboard

The family dashboard at /pound/family lets parents manage each child with per-child actions: Activity (recent events grouped by day with stats), Settings (spending limits, purchase approval toggles, danger zone for account deletion), and QR Login (60-second countdown QR code). Family is also accessible from the Settings page and Menu Drawer for quick access.

Age-Based Safety Defaults

When adding a child, parents select an age range (Under 10, 10–12, 13–15, 16–17). Safety settings are pre-filled based on the age range, including daily Sparks spending limits, purchase approval requirements, friend request approval, publish approval, and external link access. Parents can customize these at any time.

Family API

All Family API endpoints require a user session. These are internal APIs used by the Omen family management UI.

POST /api/family

Create a new family. The authenticated user becomes the owner.

POST /api/family
Content-Type: application/json

{ "name": "My Family" }

GET /api/family

Get the current user's family details, including members, roles, and pre-approved apps. Returns null if the user is not in a family.

POST /api/family/children

Create a child account or generate a QR login token.

// Create child
POST /api/family/children
{ "name": "Alex", "birthYear": 2015 }

// Generate QR token for existing child
POST /api/family/children
{ "action": "qr", "childId": "child_id" }

Creating a child returns the child record and a QR token. The QR token contains a verifyUrl that the child scans to log in.

DELETE /api/family/children

Delete a child account (parent only). Cascades to all sessions and tokens.

DELETE /api/family/children
{ "childId": "child_id" }

POST /api/family/invite

Generate a family invite link (owner only). Valid for 7 days.

POST /api/family/invite

Response:

{ "token": "...", "expiresAt": "..." }

POST /api/family/join

Join a family using an invite token.

POST /api/family/join
{ "token": "INVITE_TOKEN" }

GET /api/family/apps

List pre-approved apps for the family.

POST /api/family/apps

Pre-approve an app for the family (owner only).

POST /api/family/apps
{ "clientId": "app_client_id" }

DELETE /api/family/apps

Revoke an app pre-approval (owner only).

DELETE /api/family/apps
{ "clientId": "app_client_id" }

GET /api/family/child-items

View a child's item inventory. Only the child's parent can access this endpoint. Returns paginated items with template and app info. Revoked items are excluded.

GET /api/family/child-items?childId=CHILD_ID&type=collectible&limit=25
Cookie: session
ParamRequiredDescription
childIdYesThe child user's ID
typeNoFilter by item type
rarityNoFilter by rarity
cursorNoPagination cursor
limitNoResults per page (1–100, default 25)

Returns 403 if the authenticated user is not the child's parent.

GET /api/family/child-activity

View a child's recent activity timeline and weekly stats. Only the child's parent can access this. Returns events grouped by today, yesterday, and earlier.

GET /api/family/child-activity?childId=CHILD_ID
Cookie: session

Response:

{
  "child": { "id": "...", "name": "Alex", "username": "alex" },
  "activity": {
    "today": [{ "id": "...", "type": "creation_published", "data": {...}, "createdAt": "..." }],
    "yesterday": [...],
    "earlier": [...]
  },
  "stats": {
    "creationsThisWeek": 3,
    "friendsCount": 5,
    "totalStars": 12
  }
}

Parent Notification Types

Parents automatically receive notifications when items are issued to or expire for their children. These appear in the parent's notification feed.

TypeTriggerData Fields
child.item.receivedAn app issues an item to a childchildId, childName, itemId, itemName, appName
child.item.expiredA child's item expires (TTL)childId, childName, itemId, itemName

Child Authentication

Child accounts use a QR-based login flow instead of wallets or email. The parent generates a QR code, the child scans it, and a session is created.

Flow

  1. Parent generates a QR token via POST /api/family/children
  2. Child scans the QR code which hits POST /api/childauth/verify?token=TOKEN
  3. Verify endpoint validates the token and redirects with an authorization code
  4. The code is exchanged for access/refresh tokens via POST /api/childauth/token

POST /api/childauth/verify

Verify a QR token. Redirects to the auth callback with a code parameter. Tokens expire after 10 minutes.

POST /api/childauth/token

Exchange an authorization code for tokens.

POST /api/childauth/token
Content-Type: application/json

{
  "clientId": "YOUR_CLIENT_ID",
  "clientSecret": "YOUR_CLIENT_SECRET",
  "code": "AUTHORIZATION_CODE",
  "type": "childauth"
}

Response:

{
  "ok": true,
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "bearer",
  "expires_at": "...",
  "scope": "child"
}

POST /api/childauth/identity

Get the child's user ID from an access token.

POST /api/childauth/identity
Content-Type: application/json

{
  "clientId": "YOUR_CLIENT_ID",
  "clientSecret": "YOUR_CLIENT_SECRET",
  "access_token": "ACCESS_TOKEN",
  "type": "childauth"
}

Response:

{ "id": "token_id", "childId": "user_id" }

Child Purchase Approvals

Apps can implement parental approval for in-app purchases made by child accounts. When a child tries to buy something, the app writes a purchase request to the parent's app data. The parent reviews and approves or denies it in Omen. The result is written to the child's app data for the app to pick up.

Flow

  1. Child taps "buy" in your app — app detects child account via scope: "child" in the token response
  2. App writes a purchase request to the parent's app data via PATCH /api/server/userdata/:parentId
  3. Parent sees the request in Omen's family dashboard and approves or denies it
  4. Omen writes the result to the child's app data (approvedPurchases or deniedPurchases)
  5. App polls the child's app data, grants the item if approved, and clears the entry

Step 1: Write Purchase Request to Parent

PATCH /api/server/userdata/<parentOmenId>
Content-Type: application/json

{
  "client_id": "YOUR_CLIENT_ID",
  "client_secret": "YOUR_CLIENT_SECRET",
  "appData": {
    "childPurchaseRequests": [
      {
        "childName": "Timmy",
        "childOmenId": "child_omen_id",
        "itemName": "Double Dice",
        "itemId": "double_dice",
        "itemCost": 15,
        "requestedAt": "2026-02-26T12:00:00Z"
      }
    ]
  }
}

Note: childPurchaseRequests is shallow-merged at the top level. To append to an existing array, read the current data first, then write the full array.

Step 2: Parent Approves/Denies in Omen

The parent sees pending requests in their family dashboard at /pound/family. Apps can also build their own UI using GET /api/family/purchases and POST /api/family/purchases.

Step 3: Read Result from Child's App Data

Poll GET /api/appdata with the child's Bearer token. On approval, the child's app data will contain:

{
  "approvedPurchases": [
    {
      "itemId": "double_dice",
      "itemName": "Double Dice",
      "itemCost": 15,
      "approvedAt": "2026-02-26T12:05:00Z",
      "approvedBy": "parent_omen_id"
    }
  ]
}

On denial:

{
  "deniedPurchases": [
    {
      "itemId": "double_dice",
      "itemName": "Double Dice",
      "itemCost": 15,
      "deniedAt": "2026-02-26T12:05:00Z",
      "deniedBy": "parent_omen_id"
    }
  ]
}

Step 4: Grant Item and Clear

After processing, clear the entries from the child's app data via PATCH /api/appdata (set approvedPurchases and/or deniedPurchases to []).

Purchase Request Fields

FieldRequiredDescription
childNameYesDisplay name of the child
childOmenIdYesChild's Omen user ID
itemNameYesDisplay name of the item
itemIdYesYour app's internal item identifier
itemCostYesCost in your app's currency
requestedAtYesISO 8601 timestamp

PFP Editor

Omen includes a built-in avatar/PFP editor that your app can integrate. PFP projects can be configured at the app or org level. The editor consumes a separate API that serves trait images, categories, and bases.

For the full PFP Editor API specification, see Avatar Editor API docs.

To save a composed avatar image, see the Avatar Save API. To fetch saved templates for display in your app, see the Avatar Templates API.

Trait Permissions

Org owners can gate traits behind permissions using trait bundles. When permissions are active, users see locked traits with a lock icon and cannot select them until granted access. Your app grants bundles to users via the Trait Entitlements API.

Avatar Save API

Save a composed avatar image from the PFP editor. The image is stored server-side and can optionally be set as the user's default profile image.

POST /api/user/saveAvatar

Requires a user session.

POST /api/user/saveAvatar
Content-Type: application/json

{
  "image": "data:image/png;base64,...",
  "selections": { "hair": "style_1", "eyes": "blue" },
  "traits": [...],
  "base": "base_id",
  "orgId": "org_id",
  "clientId": "client_id",
  "setAsDefault": true
}
FieldRequiredDescription
imageYesBase64-encoded PNG image (max 4MB)
selectionsYesTrait selections used to compose the avatar
traitsNoArray of trait metadata
baseNoBase template ID
orgIdNoSave as org-specific avatar
clientIdNoSave as app-specific avatar
setAsDefaultNoSet as user's default profile image

Response:

{ "imageUrl": "/avatars/user_abc123-org.png" }

Avatar Templates API

Fetch saved avatar templates from an organization's PFP project. Templates are pre-composed avatars created by the org owner in the PFP editor. Use this to display avatars in your app (e.g. landing page, galleries, selection screens).

GET /api/avatar-templates

Requires a Bearer token (OAuth access token).

GET /api/avatar-templates?projectId=PROJECT_ID
Authorization: Bearer <access_token>
ParamRequiredDescription
projectIdYesPFP project ID (from your org's PFP project settings)
categoryIdNoFilter to a single category. Omit to get all categories.

Response

{
  "categories": [
    {
      "id": "templates",
      "label": "Templates",
      "isDefault": true,
      "items": [
        {
          "id": "abc-123",
          "name": "Cool Cat",
          "image": "/avatars/org-xxx-proj-abc123.png",
          "base": "yogicat",
          "selections": { "variant": "yogicat-variant-snow", ... },
          "traits": [
            { "id": "trait-id", "name": "Trait Name", "categoryId": "hair" }
          ],
          "createdAt": "2026-02-28T12:00:00.000Z"
        }
      ]
    }
  ]
}

Response Fields

FieldDescription
categoriesArray of template categories
categories[].idCategory ID
categories[].labelDisplay name for the category
categories[].isDefaultWhether this is the default "Templates" category
items[].idUnique template ID
items[].nameTemplate display name
items[].imageURL path to the composite PNG image. Prepend your Omen origin (e.g. https://omen.dog) to get the full URL.
items[].baseBase template ID used
items[].selectionsTrait selection map (categoryId → traitId)
items[].traitsArray of trait metadata with id, name, categoryId

Example: Fetch templates for display

const res = await fetch(
  'https://omen.dog/api/avatar-templates?projectId=YOUR_PROJECT_ID',
  { headers: { Authorization: 'Bearer ' + accessToken } }
)
const { categories } = await res.json()

// Get all template images across categories
const allTemplates = categories.flatMap(c => c.items)
allTemplates.forEach(t => {
  console.log(t.name, 'https://omen.dog' + t.image)
})

Notes

  • Templates are created by the org owner in the PFP editor under "Save to Category"
  • The default "Templates" category is created automatically for every PFP project
  • Image URLs are relative paths — prepend https://omen.dog for full URLs
  • Each template includes selections which can be passed to the PFP editor to load that template for further customization
  • For server-to-server calls without a user session (e.g. landing pages), use POST /api/server/avatar-templates with client credentials instead

Items & Inventory

The Items system lets apps define item templates, issue items to users, and lets users view and manage their inventory. Items can represent collectibles, achievements, in-game items, avatar traits, and more.

Key Concepts

  • Item Template — a reusable blueprint that defines an item's type, rarity, supply limit, and default properties. Created by apps via S2S API.
  • Item — a concrete instance issued to a user, either from a template or ad-hoc. Tracks ownership, visibility, and display settings.
  • Visibility — items are private by default. Users can set items to public to show them on their profile.
  • Featured Wall — users can feature up to 20 public items for display on their profile.

Item Types

TypeDescription
avatar_traitWearable trait for the avatar system
collectibleDigital collectible
achievementAchievement or milestone award
in_game_itemIn-game item (weapon, power-up, etc.)
physical_goodRedeemable for a physical product
profile_decorationProfile decoration or flair

Rarities

Optional rarity tiers: common, uncommon, rare, epic, legendary, unique.

Authentication

Template and item issuance endpoints require Bearer token auth (S2S). User inventory endpoints support both session auth and Bearer token auth. The single item lookup endpoint supports session, Bearer, or unauthenticated access (with visibility restrictions).

Item Templates API

Manage reusable item blueprints for your app. All endpoints require Bearer token auth where the token's client matches the appId in the URL.

POST /api/v1/apps/{appId}/item-templates

Create a new item template.

POST /api/v1/apps/{appId}/item-templates
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "collectible",
  "name": "Golden Trophy",
  "description": "Awarded to tournament winners",
  "rarity": "legendary",
  "maxSupply": 100,
  "transferable": true,
  "imageUrl": "https://example.com/trophy.png",
  "defaultMetadata": { "season": 1 },
  "tags": ["tournament", "season-1"]
}
FieldRequiredDescription
typeYesOne of the item types listed above
nameYesDisplay name
descriptionNoDescription text
rarityNoRarity tier
maxSupplyNoMaximum number that can be issued (null = unlimited)
transferableNoWhether items can be transferred (default false)
listableNoWhether items can be listed on marketplace (default false)
soulboundNoWhether items are permanently bound to the owner (default false)
ttlSecondsNoTime-to-live in seconds — items expire after this duration
imageUrlNoFull-size image URL
thumbnailUrlNoThumbnail image URL
defaultMetadataNoDefault metadata object merged into issued items
tagsNoArray of string tags

GET /api/v1/apps/{appId}/item-templates

List templates with cursor pagination. Returns active templates by default.

GET /api/v1/apps/{appId}/item-templates?type=collectible&limit=10
Authorization: Bearer <access_token>
ParamDescription
cursorPagination cursor from a previous response
limitResults per page (1–100, default 25)
typeFilter by item type
rarityFilter by rarity
activetrue (default), false, or all

Response:

{
  "items": [ { "id": "...", "type": "collectible", "name": "Golden Trophy", ... } ],
  "pagination": { "hasMore": true, "nextCursor": "eyJpZCI6..." }
}

GET /api/v1/apps/{appId}/item-templates/{id}

Get a single template by ID.

PATCH /api/v1/apps/{appId}/item-templates/{id}

Update a template. Cannot change type. Cannot update a deactivated template. Cannot set maxSupply below the current issuedCount.

POST /api/v1/apps/{appId}/item-templates/{id}/deactivate

Deactivate a template. No new items can be issued from a deactivated template. Existing items are unaffected. This action is idempotent.

Item Issue & Revoke API

Issue items to users and revoke them. All endpoints require Bearer token auth.

POST /api/v1/apps/{appId}/items/issue

Issue an item to a user. You can issue from a template or create an ad-hoc item.

Template-based issuance

POST /api/v1/apps/{appId}/items/issue
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "userId": "user_abc",
  "templateId": "template_xyz",
  "acquisitionType": "earn",
  "metadata": { "score": 9500 }
}

When using a template, the item inherits the template's type, name, rarity, and flags. Supply is checked and incremented atomically. The item's metadata is merged with the template's defaultMetadata. If the template has a ttlSeconds, the item's expiresAt is calculated automatically. Returns 409 if supply is exhausted.

Ad-hoc issuance

POST /api/v1/apps/{appId}/items/issue
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "userId": "user_abc",
  "type": "achievement",
  "name": "First Login",
  "rarity": "common",
  "acquisitionType": "earn"
}
FieldRequiredDescription
userIdYesThe user to issue the item to
templateIdNoTemplate to issue from (if omitted, creates ad-hoc)
typeAd-hoc onlyRequired when no templateId
nameAd-hoc onlyRequired when no templateId
acquisitionTypeNopurchase, transfer, gift, earn (default), redeem, mint
metadataNoCustom metadata object

POST /api/v1/apps/{appId}/items/{id}/revoke

Soft-revoke an item. The item remains in the database but is excluded from inventory listings. Can only revoke items issued by your app. This action is idempotent.

POST /api/v1/apps/{appId}/items/{id}/revoke
Authorization: Bearer <access_token>
Content-Type: application/json

{ "reason": "Refund processed" }

User Inventory API

View and manage a user's item inventory. Supports both session auth and Bearer token auth. Visibility rules determine which items are returned.

Visibility Rules

  • Owner (session auth): sees all own items regardless of visibility
  • App (Bearer auth): sees items it issued (any visibility) + public items from other apps
  • Other users / unauthenticated: only sees public items

GET /api/v1/users/{userId}/items

List a user's inventory with cursor pagination. Revoked items are always excluded.

GET /api/v1/users/{userId}/items?type=collectible&limit=10
Authorization: Bearer <access_token>
ParamDescription
cursorPagination cursor
limitResults per page (1–100, default 25)
typeFilter by item type
appIdFilter by issuing app
rarityFilter by rarity
templateIdFilter by template

Response includes template and originApp relations:

{
  "items": [
    {
      "id": "item_1",
      "type": "collectible",
      "name": "Golden Trophy",
      "rarity": "legendary",
      "editionNumber": 42,
      "editionTotal": 100,
      "visibility": "public",
      "featured": true,
      "acquiredAt": "2026-03-01T...",
      "template": { "id": "tpl_1", "name": "Golden Trophy", ... },
      "originApp": { "id": "app_1", "name": "My Game", "image": "..." }
    }
  ],
  "pagination": { "hasMore": false, "nextCursor": null }
}

GET /api/v1/users/{userId}/items/featured

Get a user's featured items wall. Returns up to 20 public, featured items ordered by featuredOrder (ascending), then acquiredAt (descending). No auth required.

PATCH /api/v1/users/{userId}/items/{id}

Update an item's display settings. Session auth only, owner only.

PATCH /api/v1/users/{userId}/items/{id}
Content-Type: application/json

{
  "visibility": "public",
  "featured": true,
  "featuredOrder": 1
}
FieldDescription
visibilitypublic or private. Setting to private auto-unfeatures the item.
featuredtrue or false. Cannot feature private items.
featuredOrderNon-negative integer controlling display order (lower = first), or null.

Item Lookup API

GET /api/v1/items/{itemId}

Look up a single item by ID. Supports session auth, Bearer auth, or unauthenticated access.

  • Private items return 404 (not 403) to non-owners to avoid leaking existence
  • Metadata is hidden from non-owner, non-issuer viewers
  • Revoked items return 404

Response includes template and originApp relations.

Batch Item Issue API

Issue up to 100 items in a single atomic request. All items succeed or all fail together. Auth: S2S via Bearer token (same as single issue).

POST /api/v1/apps/{appId}/items/issue-batch

POST /api/v1/apps/{appId}/items/issue-batch
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "items": [
    { "userId": "user_1", "templateId": "tpl_abc" },
    { "userId": "user_2", "templateId": "tpl_abc", "metadata": { "color": "gold" } },
    {
      "userId": "user_3",
      "name": "Custom Trophy",
      "type": "achievement",
      "acquisitionType": "earn"
    }
  ]
}
FieldRequiredDescription
itemsYesArray of item objects (max 100)
items[].userIdYesTarget user ID
items[].templateIdNoTemplate to issue from. If omitted, name and type are required (ad-hoc).
items[].metadataNoPer-item metadata (merged with template defaults)
items[].acquisitionTypeNoDefaults to earn

Response (201):

{
  "items": [ ...created items ],
  "count": 3
}

Atomicity

The entire batch runs in a database transaction. If any item fails validation (invalid user, inactive template, supply exhausted), the entire batch is rejected and no items are created. Supply is checked in aggregate — if a template has 5 remaining and you try to issue 6 from it in one batch, the whole batch fails with 409.

Webhooks

Receive real-time notifications when items are issued, revoked, or expire. Register HTTPS endpoints and Omen will POST event payloads to them. Auth: S2S via Bearer token.

Event Types

TypeFired when
item.issuedAn item is issued (single or batch)
item.revokedAn item is revoked
item.expiredAn item's TTL expires (processed by cron worker)
item.transferredAn item is transferred to another user (via QR transfer or gift)
redeem.completedA redeem code is used and an item is issued

POST /api/v1/apps/{appId}/webhooks

Register a new webhook endpoint.

POST /api/v1/apps/{appId}/webhooks
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "url": "https://yourapp.com/webhooks/omen",
  "events": ["item.issued", "item.revoked"]
}
FieldRequiredDescription
urlYesHTTPS endpoint URL
eventsYesArray of event types to subscribe to

Response (201) includes the endpoint details and the signing secret. The secret is only shown once — store it securely.

GET /api/v1/apps/{appId}/webhooks

List all webhook endpoints for this app. Secret is excluded.

GET /api/v1/apps/{appId}/webhooks/{id}

Get a single webhook endpoint. Secret is excluded.

DELETE /api/v1/apps/{appId}/webhooks/{id}

Delete a webhook endpoint and all its queued events.

Payload Format

Omen POSTs a JSON payload to your URL with these headers:

HeaderDescription
X-Omen-EventEvent type (e.g. item.issued)
X-Omen-SignatureHMAC-SHA256 hex digest of the payload using your webhook secret
// Example payload for item.issued
{
  "item": {
    "id": "item_abc",
    "name": "Gold Trophy",
    "type": "achievement",
    "ownerId": "user_123",
    ...
  }
}

Verifying Signatures

import crypto from 'crypto';

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Retry Behavior

  • Your endpoint must respond with a 2xx status within 10 seconds
  • On failure: retries up to 3 times with exponential backoff (1 min, 5 min, 30 min)
  • After 3 failed attempts the event is marked as failed

Item Transfers

Transfer items between users via QR codes. Only transferable, non-soulbound items can be transferred. Child accounts require parent approval before a transfer completes.

POST /api/v1/items/{itemId}/transfer

Create a transfer intent. Returns a signed token and QR URL. Auth: Bearer token (item owner only).

POST /api/v1/items/{itemId}/transfer
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "transfer"   // or "gift" (7-day expiry vs 15-min)
}

Response (201):

{
  "intentId": "clx...",
  "qrUrl": "https://omen.dog/transfer/{signedToken}",
  "signedToken": "...",
  "expiresAt": "2026-03-03T12:15:00.000Z",
  "type": "transfer"
}

POST /api/v1/transfers/{intentId}/claim

Claim a transfer (S2S). Auth: Bearer token. Atomically transfers item ownership.

POST /api/v1/transfers/{intentId}/cancel

Cancel a transfer. Auth: Bearer token (original sender only).

GET /api/v1/transfers/verify?token=...

Verify a signed transfer token and return intent + item + sender info. Auth: session (optional).

POST /api/v1/transfers/claim-web

Claim a transfer from the browser. Auth: session. Body: { "signedToken": "..." }

Web Pages

  • /transfer/{token} — view and accept a transfer

Family Transfer Approvals

Parents can view and approve/deny pending transfers for their children.

  • GET /api/family/transfers — list pending transfers for children
  • POST /api/family/transfers — approve or deny: { "action": "approve", "intentId": "..." }

Transfer Expiration

Transfers expire automatically. Default expiry: 15 minutes for transfers, 7 days for gifts. A cron worker at GET /api/cron/transfer-expiration cleans up expired intents.

Redeem Codes

Partners can generate batch redeem codes (physical QR cards) that users scan to receive items. Codes use XXXX-XXXX-XXXX format with unambiguous characters.

POST /api/v1/apps/{appId}/redeem-codes/batch

Generate a batch of redeem codes. Auth: S2S Bearer token.

POST /api/v1/apps/{appId}/redeem-codes/batch
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "templateId": "clx...",
  "count": 100,
  "expiresAt": "2026-12-31T00:00:00.000Z"  // optional
}

Response (201) includes batchId and array of codes.

GET /api/v1/apps/{appId}/redeem-codes

List redeem codes with optional filters. Auth: S2S Bearer token.

ParamDescription
batchIdFilter by batch
statusFilter by status (active, redeemed, disabled)
cursorPagination cursor
limitPage size (max 100)

POST /api/v1/redeem/{code}

Redeem a code. Auth: session or Bearer token. Issues the item to the authenticated user.

GET /api/v1/redeem/verify?code=...

Verify a redeem code and return template info. Auth: session (optional).

Web Pages

  • /redeem/{code} — view and redeem a code

Creations (Omen Publish)

Omen Publish lets users upload and host static web creations (games, art, websites) on their Omen profile. Each creation gets its own subdomain at username-project.creations.omen.dog.

Create a Creation

POST /api/v1/creations
Cookie: __Secure-next-auth.session-token=<token>
Content-Type: application/json

{
  "name": "My Cool Game",
  "slug": "my-cool-game",
  "description": "A platformer built with Pixi.js",
  "category": "game",
  "tags": ["pixel-art", "platformer"],
  "visibility": "public"
}
FieldRequiredDescription
nameYesDisplay name (max 100 chars)
slugNoURL slug (auto-generated from name if omitted, max 60 chars)
descriptionNoShort description
categoryNogame, art, music, animation, website, tool, other
tagsNoArray of string tags
visibilityNopublic (default), private, unlisted

Returns the created creation with liveUrl and subdomain.

List Own Creations

GET /api/v1/creations
GET /api/v1/creations?status=archived&category=game

Requires session auth. Returns paginated list of the authenticated user's creations. Defaults to status=active.

Get One Creation

GET /api/v1/creations/{id}

Requires session auth. Owner sees all their creations; others only see public/unlisted active creations.

Update a Creation

PATCH /api/v1/creations/{id}
Content-Type: application/json

{ "name": "Updated Name", "visibility": "unlisted" }

Owner only. Updatable fields: name, description, thumbnailUrl, category, tags, visibility.

Delete (Archive) a Creation

DELETE /api/v1/creations/{id}

Owner only. Soft-deletes by setting status to archived.

Pin a Creation

PATCH /api/v1/creations/{id}/pin
Content-Type: application/json

{ "pinnedOrder": 1 }

Owner only. Sets a pin position (1–6) to feature a creation on the profile. Send {"pinnedOrder": null} to unpin. Maximum 6 pinned creations.

Public: List User Creations

GET /api/v1/users/{userId}/creations
GET /api/v1/users/{userId}/creations?category=game

Public endpoint. Returns paginated list of a user's public, active creations.

Public: Get Pinned Creations

GET /api/v1/users/{userId}/creations/pinned

Returns up to 6 pinned creations ordered by pinnedOrder. Only public, active creations are returned.

Publish Pipeline

The publish pipeline handles uploading, scanning, and deploying static web creations. Files are uploaded as a zip, scanned for security issues (file validation, static analysis, AI review), and then approved via a mobile-friendly QR approval page before going live.

Upload & Stage

POST /api/v1/publish/stage
Authorization: Bearer <access_token>   (or session cookie)
Content-Type: multipart/form-data

Fields:
  file: <zip file>           (required, max 50MB)
  projectName: "My Project"  (required, max 100 chars)
  projectType: "static"      (optional: "static", "vite", etc.)
  creationId: "clu..."       (optional: link to existing creation)
  changelog: "Fixed bugs"    (optional: version changelog, max 2000 chars)

Response 201:
{
  "sessionId": "clu...",
  "status": "scanning",
  "previewUrl": "https://preview.creations.omen.dog/{sessionId}",
  "expiresAt": "2026-03-03T...",
  "approvalUrl": "https://omen.dog/publish/{sessionId}"
}

Uploads a zip of static files, extracts them, and kicks off a 3-layer security scan. Returns immediately with a session ID for polling. The approvalUrl can be displayed as a QR code for mobile approval.

Poll Session Status

GET /api/v1/publish/session/{sessionId}
Authorization: Bearer <access_token>   (or session cookie)

Response 200:
{
  "id": "clu...",
  "projectName": "My Project",
  "status": "pending_approval",
  "securityStatus": "passed",
  "fileCount": 12,
  "totalSize": 245760,
  "previewUrl": "https://preview.creations.omen.dog/...",
  "scanResult": {
    "fileValidationPassed": 1,
    "staticAnalysisPassed": 1,
    "aiReviewSafe": 1,
    "overallStatus": "passed",
    "staticAnalysisWarnings": [...],
    "staticAnalysisFlags": [...]
  }
}

Poll this endpoint to track scan progress. Status flow: scanning pending_approval (or scan_failed) → published / denied.

Approve & Deploy

POST /api/v1/publish/session/{sessionId}/approve
Authorization: Bearer <access_token>   (or session cookie)

Response 200:
{
  "status": "published",
  "creationId": "clu...",
  "version": 2,
  "liveUrl": "https://user.omen.dog/project",
  "vanityUrl": "https://user.omen.dog/project",
  "subdomain": "user-project"
}

Approves the session and deploys files to the live subdomain. Only the session owner (or parent for child accounts) can approve. Session must be in pending_approval status.

Deny

POST /api/v1/publish/session/{sessionId}/deny
Authorization: Bearer <access_token>   (or session cookie)
Content-Type: application/json

{ "reason": "Contains inappropriate content" }

Response 200:
{ "status": "denied", "deniedReason": "Contains inappropriate content" }

Denies the session and cleans up staged files. Reason is optional.

QR Approval Page

Visit https://omen.dog/publish/{sessionId} for a mobile-friendly approval page. This URL can be encoded as a QR code for phone-based approval. The page shows project details, scan results, a preview link, and approve/deny buttons.

Security Scan Layers

LayerWhat it checksBlocking?
File ValidationAllowed extensions, max sizes (10MB/file, 50MB total, 500 files), no dotfiles/symlinksYes
Static Analysiseval(), WebSocket, document.cookie, localStorage, iframes, crypto mining, etc.Yes
AI ReviewMalicious patterns, phishing, data exfiltration, inappropriate contentYes

Version History & Rollback

Every publish creates a new version. Creators can view version history with changelogs and roll back to any previous version instantly.

Changelog on Publish

Include an optional changelog field (max 2000 chars) when staging a publish. This appears in the creation viewer and version history.

POST /api/v1/publish/stage
Content-Type: multipart/form-data

Fields:
  file: <zip>
  projectName: "My Game"
  changelog: "Added multiplayer support and fixed save bug"
  creationId: "clu..."   (for updates to existing creations)

Get Version History

GET /api/v1/creations/{id}/versions

Response 200:
{
  "creationId": "clu...",
  "currentVersion": 3,
  "versions": [
    {
      "id": "clv...",
      "version": 3,
      "isLive": true,
      "changelog": "Added multiplayer support",
      "totalSize": 245760,
      "publishedAt": "2026-03-14T..."
    },
    {
      "id": "clu...",
      "version": 2,
      "isLive": false,
      "changelog": "Bug fixes",
      "totalSize": 230400,
      "publishedAt": "2026-03-13T..."
    }
  ]
}

Public endpoint. Returns all versions in descending order. The isLive flag indicates which version is currently being served.

Rollback to Version

POST /api/v1/creations/{id}/versions/{version}/rollback
Authorization: Bearer <access_token>   (or session cookie)

Response 200:
{
  "status": "rolled_back",
  "version": 2,
  "changelog": "Bug fixes"
}

Instantly rolls back to a previous version by swapping the live symlink. Only the creation owner can roll back. The creation immediately serves the target version's files.

Dev Tools (Visual Editing)

Creators can visually edit their creation's colors, gameplay values, text, and assets through a panel in the creation viewer. Overrides are stored server-side and baked into the source at publish time. Requires creations to follow the Code Conventions (CSS vars, CONFIG, data-omen, ASSETS).

GET /api/v1/creations/{id}/overrides

GET /api/v1/creations/{id}/overrides
Authorization: session cookie (creation owner only)

→ { overrides: [{ id, category, key, value, updatedAt }] }

Lists all dev tool overrides for a creation. Categories: css, config, text, asset.

PUT /api/v1/creations/{id}/overrides

PUT /api/v1/creations/{id}/overrides
Content-Type: application/json
{ "overrides": [{ "category": "css", "key": "--color-primary", "value": "#ff0000" }] }

→ { overrides: [{ id, category, key, value, updatedAt }] }

Upsert overrides (max 200 per request). Upserts by creation + category + key.

DELETE /api/v1/creations/{id}/overrides/{overrideId}

DELETE /api/v1/creations/{id}/overrides/{overrideId}

→ { deleted: true }

Remove a single override. Creation owner only.

Studio (AI Creation Builder)

Browser-based AI tool at /studio that generates interactive creations from natural language. Chat with Haiku, Sonnet, Opus (Anthropic BYOK), GPT-4o/4o Mini/o3-mini (OpenAI BYOK), or Gemini Flash/Pro (Google BYOK). Preview live and publish directly to your profile. BYOK users stream AI responses directly from their browser using zero-knowledge encryption — the server never sees their API keys.

POST /api/v1/studio

POST /api/v1/studio
Content-Type: application/json
{ "title": "My Game" }

→ { id, title, model, files, messages, ... }

Create a new Studio project.

GET /api/v1/studio

GET /api/v1/studio

→ { projects: [{ id, title, model, sparksUsed, published, updatedAt }] }

List all your Studio projects.

POST /api/v1/studio/{id}/message

POST /api/v1/studio/{id}/message
Content-Type: application/json
{ "message": "Build me a space shooter" }

→ SSE stream: text, file_write, file_delete, done, error events

Send a message to the AI. For platform models (Haiku/Sonnet), streams via Server-Sent Events and costs 1 Spark (Haiku) or 3 Sparks (Sonnet). BYOK models stream directly from the browser to the AI provider — the server is not involved. Use POST /api/v1/studio/{id}/byok-save to persist results. The AI uses write_file and delete_file tools to create project files.

POST /api/v1/studio/{id}/byok-save

POST /api/v1/studio/{id}/byok-save
Content-Type: application/json
{
  "userMessage": "make the background blue",
  "assistantMessage": "I changed the background color...",
  "files": { "index.html": "..." }
}

→ { ok: true }

Save results from a BYOK browser-direct AI call. Used after the browser streams a response directly from the AI provider — persists messages and file changes to the project.

POST /api/v1/studio/{id}/publish

POST /api/v1/studio/{id}/publish
Content-Type: application/json
{ "name": "Space Shooter" }

→ { ok: true, creationId, liveUrl, vanityUrl }

Publish a Studio project as a live Creation on your profile.

API Keys (BYOK) — Zero-Knowledge Encryption

Add your own AI provider API keys to use premium models in Studio without spending Sparks. Supported providers: Anthropic (Opus), OpenAI (GPT-4o, 4o Mini, o3-mini), and Google (Gemini Flash, Gemini Pro).

Zero-knowledge architecture: Keys are encrypted client-side with your passphrase using PBKDF2 + AES-256-GCM before being sent to the server. The server stores only the encrypted blob and can never decrypt your key. When you use Studio, your browser decrypts the key locally and calls AI providers directly — the server is never involved in BYOK API calls.

How It Works

  1. You enter your API key + a passphrase in Settings
  2. Your browser validates the key directly with the provider
  3. Your browser encrypts the key with PBKDF2(passphrase) → AES-256-GCM
  4. Only the encrypted blob is sent to the server for storage
  5. In Studio, you enter your passphrase to unlock — decryption happens in your browser
  6. AI requests go directly from your browser to the provider (Anthropic/OpenAI/Google)

GET /api/v1/api-keys

GET /api/v1/api-keys

→ { keys: [{
    id, provider, keyHash, isValid, lastUsed, createdAt,
    encrypted: { cipher, iv, tag, salt }  // base64, for client-side decryption
  }] }

List your stored API keys with encrypted blobs. The encrypted data is useless without your passphrase.

POST /api/v1/api-keys

POST /api/v1/api-keys
Content-Type: application/json
{
  "provider": "anthropic|openai|google",
  "keyHash": "sk-ant-...abcd",
  "encrypted": { "cipher": "base64", "iv": "base64", "tag": "base64", "salt": "base64" }
}

→ { id, provider, keyHash, isValid, createdAt, encrypted }

Store a client-encrypted API key. The server never sees the plaintext key. One key per provider per user.

DELETE /api/v1/api-keys/{id}

DELETE /api/v1/api-keys/{id}

→ { ok: true }

Permanently delete an API key.

omen-publish CLI

The omen-publish CLI bundles, uploads, and deploys static web projects to Omen directly from your terminal. It handles build detection, zip bundling, security scanning, and QR-based mobile approval.

Setup

1. Generate a CLI token at /pound/developer/cli-token

2. Authenticate:

npx omen-publish login --token <your-token>

Credentials are saved to ~/.omen/credentials.json.

Publishing

From your project directory:

npx omen-publish

The CLI will:

  1. Detect your framework (Next.js, Vite, CRA, Vue, Svelte, Astro, or static)
  2. Run your build command (npm run build)
  3. Bundle the output directory into a zip
  4. Upload to Omen for 3-layer security scanning
  5. Display a QR code for mobile approval
  6. Deploy to username-project.creations.omen.dog

Options

FlagDescription
-n, --name <name>Project name (auto-detected from package.json)
-t, --type <type>Framework type override
-o, --output <dir>Build output directory override
--no-buildSkip the build step
--creation-id <id>Update an existing creation

Other Commands

npx omen-publish status    # Check last publish status
npx omen-publish --help    # Full help

Project Config

After your first publish, the CLI saves a .omen/config.json in your project with the creation ID and last session. This is auto-added to .gitignore. Subsequent publishes will update the same creation automatically.

Framework Detection

FrameworkDetected byDefault output
Next.jsnext in dependenciesout/
Vitevite in dependenciesdist/
Create React Appreact-scripts in dependenciesbuild/
Vue CLI@vue/cli-service in dependenciesdist/
SvelteKit@sveltejs/kit in dependenciesbuild/
Astroastro in dependenciesdist/
StaticFallback./ (current dir)

Multiplayer / Realtime API

Omen provides two tiers of multiplayer: P2P (free) for casual games and Server-Authoritative (Pro+) for competitive games with anti-cheat. Both tiers share the same room infrastructure, WebSocket protocol, and SDK.

Platform-Managed Multiplayer (Recommended)

For multiplayer creations, Omen handles lobby, matchmaking, and room management automatically outside your iframe. Your creation just receives a clean player list and events. At publish, Omen auto-detects omen.rooms usage and sets up multiplayer config.

Property / MethodDescription
omen.playersArray of current players: { id, username, avatar, team, isHost }
omen.ready(fn)Register callback for when the game starts (all players ready)
omen.on('playerJoin', fn)Player joined mid-game
omen.on('playerLeave', fn)Player left
omen.on('playerReconnect', fn)Player reconnected after disconnect

Concepts

  • Room: A multiplayer session identified by a 6-char code (e.g. A3BK7N)
  • Persistent code: Each user+creation pair always gets the same room code
  • Host: The room creator — manages game state, kicks players, starts/ends games
  • Rejoin window: Disconnected players can rejoin within a configurable window (default 120s, max 600s)
  • Teams: Built-in team assignment and team-scoped broadcasting
  • Rate limiting: Per-message-type rate limits, enforced server-side
  • Room discovery: Public rooms are queryable via API
  • Authoritative mode: (Pro+) Server runs developer-uploaded room-logic.js in a sandboxed V8 isolate

Room Limits per Tier

TierConcurrent RoomsAuthoritative Rooms
Free100
Growth500
Pro10020
ScaleUnlimitedUnlimited

REST Endpoints

All endpoints require authentication (Bearer token or session).

Create Room

POST /api/v1/multiplayer/rooms
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "creation_id": "clx...",
  "app_id": "clx...",
  "max_players": 8,
  "allow_spectators": true,
  "rejoin_window_seconds": 120,
  "visibility": "public",
  "authoritative": false,
  "tick_rate": 20,
  "metadata": { "mode": "deathmatch", "map": "arena_1" },
  "rate_limits": {
    "default": { "max": 30, "windowMs": 1000 },
    "playerMove": { "max": 60, "windowMs": 1000 }
  },
  "chat": {
    "enabled": true,
    "channels": ["global", "team", "whisper"],
    "maxLength": 200,
    "rateLimits": { "max": 5, "windowMs": 3000 },
    "allowSpectatorChat": false
  }
}

Discover Rooms

GET /api/v1/multiplayer/rooms?app_id=clx...&visibility=public&limit=20
Authorization: Bearer <access_token>

Response: { "rooms": [...], "total": 12 }

Get / Join / Leave / Close Room

GET    /api/v1/multiplayer/rooms/{id}
POST   /api/v1/multiplayer/rooms/{id}/join   { "role": "player" }
POST   /api/v1/multiplayer/rooms/{id}/leave
DELETE /api/v1/multiplayer/rooms/{id}        (host only)
GET    /api/v1/multiplayer/join/{code}       (lookup by room code)

Online Player Count

GET /api/v1/multiplayer/online-count?creationId={id}

// Response: { "count": 5 }
// Returns count of players with active presence in this creation (last 5 min).

Platform Matchmaking (Session Auth)

For platform-managed creations, session-authenticated matchmaking creates or joins public rooms automatically.

POST /api/v1/multiplayer/matchmaking/ticket
  { "creationId": "clx..." }
  // Finds or creates a public matchmaking room
  // Response: { "ticketId": "room-id", "roomCode": "A3BK7N", "status": "searching" }

GET /api/v1/multiplayer/matchmaking/ticket?id={ticketId}
  // Poll: status "searching" or "matched" (when 2+ players in room)

DELETE /api/v1/multiplayer/matchmaking/ticket?id={ticketId}
  // Cancel matchmaking (closes room if host)

Managed Multiplayer Config (Developer Suite)

External apps can enable platform-managed multiplayer via REST API. When managed: true, Omen handles lobby UI, matchmaking, voice chat, and post-match screens automatically.

PUT /api/v1/apps/{appId}/multiplayer-config
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "managed": true,
  "maxPlayers": 8,
  "soloSupported": true,
  "allowSpectators": true,
  "voiceEnabled": false,
  "voiceMode": "open-mic"  // "open-mic" | "push-to-talk"
}

GET /api/v1/apps/{appId}/multiplayer-config
// Response: { "multiplayerConfig": { ... } }

WebSocket Protocol

Connect: wss://realtime.omen.dog/room/{code}?token={bearerToken}&role=player

Client → Server Messages

TypeWhoPayload
state_updateHost{ state: {...} } — full state replace
state_patchHost{ patch: {...} } — shallow merge
deep_patchHost{ patches: { "path.to.key": value } } — path-based
messageAny{ data: ... } — broadcast to all
direct_messageAny{ to: "userId", data: ... }
team_messageAny{ team: "red", data: ... } — team-scoped
assign_teamHost{ user_id: "...", team: "red" }
start_gameHost(no payload)
end_gameHost{ results: { players: [{ userId, score, rank }], duration } } — triggers post-match screen
kickHost{ user_id: "...", reason: "..." }
chatAny{ text: "...", channel: "global"|"team"|"whisper", to: "userId" }
chat_muteHost{ userId: "...", duration: 300, reason: "..." }
chat_unmuteHost{ userId: "..." }
pingAny(no payload) → pong
custom typeAnyForwarded to room-logic.js (authoritative) or broadcast (P2P)

Server → Client Messages

TypeWhen
room_stateOn join/rejoin — full snapshot (includes teams, metadata)
player_joinedSomeone joins
player_leftSomeone leaves or is kicked
player_disconnectedDisconnect (rejoin window starts)
player_reconnectedRejoin within window
host_changedHost transferred
team_changedPlayer assigned to a team
rate_limitedYour message was rate-limited
chatChat message received (includes from, fromName, channel, text, ts)
chat_echoYour message echo (if shadow-muted: includes muted, muteExpiresIn)
chat_mutedYou were muted (includes duration, reason)
chat_unmutedYou were unmuted
state_patch_auto(Authoritative) Auto-diff patches from tick

Server-Authoritative (Pro+)

Upload a bundled JS file (room-logic.js) that runs in a sandboxed V8 isolate on Omen's server. The sandbox validates all player actions — no client is trusted.

Deploy Room Logic

PUT /api/v1/apps/{appId}/room-logic
Authorization: Bearer <app-secret>
Content-Type: application/javascript
X-Deploy-Message: "v2 - added dash cooldown"

<bundled JS file body>

Response: { "version": 2, "size": 48192, "deployedAt": "..." }

GET  /api/v1/apps/{appId}/room-logic/versions    (list versions)
POST /api/v1/apps/{appId}/room-logic/rollback     { "version": 1 }

Room Logic Hooks

// room-logic.js (bundled with esbuild/webpack)
module.exports = {
  onMessage(room, player, type, data) { /* validate & apply */ },
  onTick(room, deltaTime) { /* physics, timers, AI */ },
  onJoin(room, player) { /* spawn player */ },
  onLeave(room, player) { /* cleanup */ },
  onDisconnect(room, player) { /* mark AFK */ },
  onReconnect(room, player) { /* restore */ },
  onChat(player, message, channel) { /* filter/modify/block */ },
  onClose(room) { /* persist final state */ },
};

Sandbox APIs

// State & Broadcasting
room.state                     // mutable game state (auto-diffed each tick)
room.broadcast(type, data)     // send to all players
room.broadcastPatch(patches)   // path-based: { "players.abc.health": 75 }
room.broadcastToTeam(team, type, data)
room.sendTo(playerId, type, data)

// Timers
room.setTimeout(fn, ms)  / room.clearTimeout(id)
room.setInterval(fn, ms) / room.clearInterval(id)

// Teams
room.assignTeam(playerId, "red")
room.getTeam("red")  // → [playerId, ...]

// Player actions
room.kick(playerId, reason)
room.warn(playerId, type)

// Collections (async)
await room.db.query(collection, opts)
await room.db.insert(collection, data)
await room.db.update(collection, docId, data)
await room.db.delete(collection, docId)

Resource Limits

  • Bundle size: 1 MB max
  • Memory: 64 MB (Pro), 256 MB (Scale) per room
  • Tick budget: 50ms per tick
  • Errors: 100 in 60s kills the room (strike system)
  • No filesystem, network, or cross-room access

SDK (Client-Side)

// Create a room
const room = await omen.rooms.create({
  maxPlayers: 8, public: true,
  metadata: { mode: "ranked" },
  rateLimits: { default: { max: 30, windowMs: 1000 } }
});

// Join a room
const room = await omen.rooms.join("ABC123");

// Discover rooms
const { rooms } = await omen.rooms.list({ appId: "...", public: true });

// Send messages
omen.rooms.broadcast("playerMove", { x: 100, y: 200 });
omen.rooms.sendTo(playerId, { damage: 25 });
omen.rooms.teamMessage("red", { text: "flank left" });

// Listen for events
omen.rooms.on("player_joined", (e) => { ... });
omen.rooms.on("state_patch_auto", (e) => { ... });
omen.rooms.on("*", (e) => { ... }); // wildcard

// Host controls
omen.rooms.setState({ round: 1 });
omen.rooms.patchState({ timeLeft: 42 });
omen.rooms.assignTeam(playerId, "blue");
omen.rooms.startGame();
omen.rooms.kick(playerId, "AFK");

// In-Game Chat
omen.rooms.chat("nice shot!");
omen.rooms.chat("flank left", { channel: "team" });
omen.rooms.chat("gg", { to: "userId", channel: "whisper" });
omen.rooms.on("chat", (msg) => {
  // msg: { from, fromName, channel, text, ts, filtered }
});
omen.rooms.mute(playerId, 300, "spam");
omen.rooms.unmute(playerId);

Room Lifecycle Webhooks

Register webhook endpoints with room.started and room.ended events. The room.ended payload includes the final game state, all players, and room metadata. Useful for persisting game results to an external database.

Public Profile

Every Omen user can claim a unique username and have a public profile page at omen.dog/@username (also /u/username). Profiles show the user's avatar, display name, bio, pinned creations, featured items, and badges.

Profile API

Fetch public profile data for any user. The userId param accepts either a cuid (user ID) or a username string.

GET /api/v1/users/{userId}/profile

Response:
{
  "user": {
    "id": "clx...",
    "username": "alice",
    "name": "Alice",
    "image": "/avatars/alice.png",
    "bio": "Building cool things",
    "createdAt": "2024-01-15T..."
  },
  "isOwner": false,
  "stats": {
    "creationsCount": 3,
    "itemsCount": 12,
    "badgesCount": 2
  },
  "pinnedCreations": [...],
  "featuredItems": [...],
  "badges": [...]
}

Username Management

These endpoints require session authentication (logged-in user).

Check availability

GET /api/user/checkUsername?username=alice

Responses:
→ { "available": true }
→ { "available": false, "reason": "taken" }
→ { "available": false, "reason": "reserved", "availableAt": "2026-04-02T..." }

When a user changes their username, the old name is reserved for 30 days. During this period, reason: "reserved" is returned with the date the name becomes available. The original owner can reclaim their own old username at any time.

Set username

POST /api/user/setUsername
Body: { "username": "alice" }
→ { "status": "success", "username": "alice" }

Rules:
- 3-20 characters, lowercase letters, numbers, underscores
- No leading/trailing underscores
- Must be unique (case-insensitive)
- 30-day cooldown between changes (first-time set is exempt)
- Recently released usernames are reserved for 30 days

If the user has changed their username within the last 30 days, a 429 response is returned with { "error": "...", "nextChangeAt": "..." }.

Update bio

POST /api/user/updateBio
Body: { "bio": "Building cool things" }
→ { "status": "success", "bio": "Building cool things" }

Max 280 characters.

OG Image

Every profile has a branded Open Graph image card (1200×630 PNG) for rich previews on Discord, Twitter, iMessage, etc. Generated server-side via sharp.

GET /api/v1/og/profile?username={username}

Returns: 1200×630 PNG image
Headers: ETag, Cache-Control: public, max-age=600

Card includes:
- Circular avatar with prestige tier glow ring
- Display name + @username
- Bio (truncated to 80 chars)
- Stats pills (stars, creation count)
- Tier badge (if uncommon+)
- Daemon sprite (if exists)
- Accent color highlights (if set)
- "omen.dog" branding

Accent Color

Users can set a custom accent color that overrides their theme's default accent. 8 preset colors plus a custom hex color picker. The color cascades through CSS var(--accent) to all profile elements.

POST /api/user/updateTheme
Body: { "accentColor": "#ef4444" }   // set
Body: { "accentColor": null }        // reset to theme default

Accepts: #RRGGBB hex format or null

Section Order

Profile owners can drag-to-reorder their 6 profile sections. The order is persisted and visible to all visitors.

GET /api/v1/profile/layout
→ { "sectionOrder": ["daemon","creations","collectibles","badges","activity","friends"],
    "default": [...] }

PATCH /api/v1/profile/layout
Body: { "sectionOrder": ["creations","daemon","badges","collectibles","friends","activity"] }
→ { "status": "success", "sectionOrder": [...] }

Must include all 6 sections, no duplicates.

Vanity Domains

Every Omen user with a username automatically gets a vanity domain at {username}.omen.dog. No claiming flow is needed — set a username and the domain works immediately.

URL Structure

URLContent
{username}.omen.dog/Profile page (same as omen.dog/@{username})
{username}.omen.dog/{slug}Host page (bar + sandboxed iframe loading from *.creations.omen.dog)
{username}.omen.dog/creationsFull paginated list of all public creations
{username}.omen.dog/itemsFull paginated list of all public items

How It Works (V2 Iframe Architecture)

Caddy handles *.omen.dog with wildcard TLS and proxies all requests to Next.js. When a request comes in for alex.omen.dog/my-game, Next.js renders a host page containing the creation viewer and a sandboxed iframe that loads the creation content from alex-my-game.creations.omen.dog. This iframe architecture provides browser-enforced origin isolation — creation code cannot access or manipulate the bar, and cannot spoof Omen UI elements.

Username Change Redirects

When a user changes their username, the old vanity domain automatically redirects to the new one for 30 days. For example, if alice changes to alicenew, visiting alice.omen.dog or omen.dog/@alice will redirect to the new username. Creation files remain accessible via both old and new URLs during this period through filesystem symlinks. After 30 days, the redirects expire and the old username becomes available for others to claim.

Creation Viewer

Every creation viewed on *.omen.dog uses a three-phase viewer experience: a cinematic intro panel during load, a minimal floating pill during play, and a tappable rich panel (bottom sheet) for full details and actions.

Iframe Architecture

The viewer lives in a host page on *.omen.dog that Omen controls. Creation content loads in a sandboxed iframe from *.creations.omen.dog. Different origins means browser-enforced isolation — creation code cannot access or manipulate the viewer UI, overlay Omen elements, or phish users. Viewer data is fetched server-side.

The iframe uses sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-modals" and allow="fullscreen; autoplay; gamepad; camera; microphone".

Viewer Phases

  • Intro panel — creator avatar with prestige border, @username, creation name, stats (stars, views, comments), action buttons (Star/Edit, Comment, Share), loading progress bar with animated messages
  • Floating pill — 30px pill at top-center with creator avatar + star count. Tap to open the rich panel. Always visible during play.
  • Rich panel — bottom sheet with creator card, creation info, stats, action buttons (Star, Comment, Share, Report), navigation between creator's creations (‹ N of M ›), report form, comment thread

postMessage Protocol

The host page listens for messages from the iframe (origin-validated against *.creations.omen.dog).

Message TypeDirectionDescription
omen:sparks:purchaseiframe → hostRequest Sparks purchase (anti-spoofing overlay)
omen:login:requestiframe → hostRequest login flow (stub)
omen:friend:inviteiframe → hostRequest friend invite (stub)

External Creations

Users can list externally-hosted creations (Itch.io, Vercel, Roblox, etc.) on their Omen profile. External creations get an {username}.omen.dog/{slug} URL that shows a “Leaving Omen” interstitial instead of loading in an iframe.

Creating an External Creation

POST /api/v1/creations
Content-Type: application/json

{
  "name": "My Itch.io Game",
  "hostingType": "external",
  "externalUrl": "https://example.itch.io/my-game",
  "category": "game",
  "visibility": "public"
}

Set hostingType to "external" and provide an externalUrl. The URL must start with https:// and cannot point to omen.dog or any of its subdomains. External creations do not get a subdomain or liveUrl.

Key Differences from Omen-Hosted

FeatureOmen-hostedExternal
Hosting*.creations.omen.dogUser's own server
Vanity URL behaviorLoads in sandboxed iframeShows “Leaving Omen” interstitial
SubdomainAuto-generatedNone
Security scanning3-layer pipelineNone (external content)
Publish pipelineZip upload + approvalN/A — just a URL
Profile badgeNone“External” badge overlay
Child viewerCSP + sandboxingExtra safety warning on interstitial

Updating External URL

PATCH /api/v1/creations/{id}
Content-Type: application/json

{ "externalUrl": "https://new-url.example.com/game" }

Only external creations can update externalUrl. The hostingType cannot be changed after creation.

Content Reports

Users can report creations hosted on *.creations.omen.dog for policy violations. Reports are submitted via the creation viewer panel or directly via API. Each user can submit one report per creation.

POST /api/v1/creations/{id}/report

Submit a report for a creation. Supports CORS from *.creations.omen.dog origins. Authentication is optional — logged-in users get deduplication, anonymous reports are also accepted.

POST /api/v1/creations/{creationId}/report
Content-Type: application/json

{
  "category": "malicious",
  "description": "Optional description of the issue"
}

Report Categories

CategoryDescription
maliciousMalware, crypto miners, or harmful code
inappropriateAge-inappropriate or offensive content
phishingFake login pages or credential harvesting
spamSpam or unsolicited advertising
copyrightCopyright or IP infringement
otherOther policy violation

Response

// 201 Created
{ "ok": true }

// 409 Conflict (duplicate)
{ "error": "You have already reported this creation" }

Strike System

Creations with excessive confirmed reports may be automatically suspended. Owners receive publish strikes: 1 strike = warning, 2 strikes = manual review, 3+ strikes = publishing disabled.

Push Notifications

Omen provides push notification infrastructure on top of the existing polling-based notification system. Apps can register push tokens, manage notification preferences, and send push notifications to users. The existing GET /api/notifications polling endpoint remains as a fallback.

Push delivery is currently stubbed (logged server-side and marked as delivered) — when FCM/APNs credentials are configured, the sendPush() function will be swapped to real delivery with no API changes.

Push Tokens

Register and manage push notification tokens per device. Bearer auth required (self or parent of child).

Register a Push Token

POST /api/v1/users/{userId}/push-tokens
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "token": "fcm-token-abc123...",
  "deviceId": "dev_browser_1",
  "platform": "fcm_web"
}
FieldRequiredDescription
tokenYesPush token from FCM/APNs
deviceIdYesUnique device identifier
platformNofcm_web (default), fcm_android, fcm_ios, apns

Upserts on (userId, deviceId) — re-registering updates the token.

Remove a Push Token

DELETE /api/v1/users/{userId}/push-tokens
Authorization: Bearer <access_token>
Content-Type: application/json

{ "deviceId": "dev_browser_1" }

List Push Tokens

GET /api/v1/users/{userId}/push-tokens
Authorization: Bearer <access_token>

Returns registered tokens (token value excluded for security). Includes id, platform, deviceId, createdAt, lastUsedAt.

Notification Preferences

Users can configure push notification preferences. Bearer auth required (self or parent).

Get Preferences

GET /api/v1/users/{userId}/notification-preferences
Authorization: Bearer <access_token>

Returns current preferences (creates defaults if none exist).

Update Preferences

PATCH /api/v1/users/{userId}/notification-preferences
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "pushEnabled": 1,
  "quietHoursStart": "22:00",
  "quietHoursEnd": "07:00",
  "quietHoursTimezone": "America/New_York",
  "appPreferences": {
    "APP_ID": { "muted": false, "categories": { "items": true, "social": false } }
  }
}
FieldTypeDescription
pushEnabled0 or 1Global push on/off
quietHoursStartHH:mm or nullQuiet hours start time
quietHoursEndHH:mm or nullQuiet hours end time
quietHoursTimezoneIANA timezone or nullTimezone for quiet hours
appPreferencesObjectPer-app mute and category settings
childPushDisabled0 or 1Parent: block push for this child account

Send Notification (S2S)

Apps can send push notifications to users via the server-to-server API. Bearer auth required, token must match the app.

POST /api/v1/apps/{appId}/notifications/send
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "userId": "user-id-here",
  "title": "New Achievement!",
  "body": "You earned the Gold Badge",
  "category": "badges",
  "actionUrl": "/pound/inventory",
  "data": { "badgeId": "badge-123" }
}
FieldRequiredDescription
userIdYesTarget user ID
titleYesNotification title
bodyYesNotification body text
categoryNoOne of: general, items, transfers, badges, social, app, system
actionUrlNoURL to open on click
dataNoArbitrary JSON payload

Quality Controls

Push delivery goes through several checks before sending:

CheckSkip ReasonDescription
Global push disabledpush_disabledUser turned off all push
App mutedapp_mutedUser muted notifications from this app
Category mutedcategory_mutedUser muted this category for the app
Quiet hoursquiet_hoursCurrent time is within quiet hours
Child push disabledchild_push_disabledParent disabled push for child
Rate limitrate_limitedApp exceeded 10 notifications/user/day

Family Push Controls

Parents can disable push notifications for child accounts. Session auth required (parent).

GET /api/family/push-settings     — List child push settings
PATCH /api/family/push-settings   — Toggle child push

PATCH body: { "childId": "child-user-id", "childPushDisabled": 1 }

Safety-critical notifications (e.g. child.transfer.pending, child.item.received) always bypass child push blocking.

Notification Categories

CategoryNotification Types
transfersitem.transfer.sent, item.transfer.received, child.transfer.pending, transfer.denied_by_parent
itemschild.item.received, child.item.expired, item.issued, item.revoked, redeem.completed
badgesbadge_earned, badge_granted, badge_revoked
appapp.notification (S2S sent)

Polling Fallback

The existing GET /api/notifications endpoint continues to work for clients that don't support push. All notifications are stored in the database regardless of push delivery status.

Mood / Status API

Users can set a mood emoji and short status text on their profile. The mood is publicly readable and editable only by the owner via session auth.

GET /api/v1/users/{userId}/mood

Public endpoint, no auth required. The userId param accepts a cuid or username.

GET /api/v1/users/{userId}/mood

Response:
{
  "userId": "clx...",
  "moodEmoji": "😊",
  "moodText": "feeling great today"
}

PUT /api/v1/users/{userId}/mood

Session auth required, owner only. Updates the user's mood. Both fields are optional.

PUT /api/v1/users/{userId}/mood
Content-Type: application/json

{
  "moodEmoji": "🎉",
  "moodText": "shipping features"
}
FieldMax LengthDescription
moodEmoji4 charsEmoji string (truncated if longer)
moodText60 charsShort status message

Dashboard API

First-party endpoint that aggregates all data needed for the user's home dashboard. Session auth required.

GET /api/v1/dashboard

GET /api/v1/dashboard
Cookie: __Secure-next-auth.session-token=...

Response:
{
  "user": {
    "id": "clx...",
    "name": "Alice",
    "username": "alice",
    "image": "/avatars/alice.png",
    "bio": "Building cool things",
    "moodEmoji": "😊",
    "moodText": "feeling great",
    "createdAt": "2024-01-15T..."
  },
  "featuredItems": [
    {
      "id": "clx...",
      "name": "Gold Sword",
      "imageUrl": "...",
      "thumbnailUrl": "...",
      "rarity": "legendary",
      "type": "collectible",
      "originApp": { "id": "clx...", "name": "Arcade", "image": "..." }
    }
  ],
  "recentNotifications": [
    {
      "id": "clx...",
      "type": "badge_earned",
      "title": "You earned Early Adopter",
      "body": null,
      "actionUrl": null,
      "data": {},
      "createdAt": "2024-03-01T...",
      "organization": { "id": "clx...", "name": "Yogicats", "image": "..." }
    }
  ],
  "connectedApps": [
    { "id": "clx...", "name": "Arcade", "image": "..." }
  ]
}
SectionDescription
userIdentity: name, username, avatar, bio, mood, join date
featuredItemsUp to 8 featured items (owner sees regardless of visibility)
recentNotificationsLast 20 notifications across all orgs, newest first
connectedAppsDeduplicated apps with active refresh tokens

Rate Limiting

All /api/v1/ endpoints are rate limited. Limits are applied per authentication method:

Auth typeLimitWindow
Bearer token (S2S)1,000 requests1 minute
Session / anonymous100 requests1 minute

Rate limit headers are included in every response:

HeaderDescription
X-RateLimit-LimitMax requests per window
X-RateLimit-RemainingRequests remaining
X-RateLimit-ResetUnix timestamp when window resets

When rate limited, you receive a 429 response with a Retry-After header (seconds until the window resets):

HTTP/1.1 429 Too Many Requests
Retry-After: 45

{ "error": "Rate limit exceeded", "retryAfter": 45 }

Friends & Social Graph

Omen provides friend requests, blocking, and child friend approval. All endpoints require session authentication.

Send Friend Request

POST /api/v1/friends/request
Content-Type: application/json

{ "userId": "TARGET_USER_ID" }

Returns 201 with { friendship: { id, status } }. Status is pending for normal users, pending_parent if either user is a child account.

List Pending Requests

GET /api/v1/friends/requests

Returns { incoming, outgoing } arrays of pending friend requests with sender/receiver user info.

Accept Friend Request

POST /api/v1/friends/{friendshipId}/accept

Only the receiver can accept. Returns 403 if the request needs parent approval first (pending_parent).

Decline Friend Request

POST /api/v1/friends/{friendshipId}/decline

Deletes the friend request record.

List Friends

GET /api/v1/friends

Returns accepted friends with presence info:

{
  "friends": [
    {
      "user": { "id": "...", "name": "...", "username": "...", "image": "..." },
      "presence": { "status": "online" },
      "acceptedAt": "2026-03-03T..."
    }
  ]
}

Unfriend

DELETE /api/v1/friends/{friendshipId}

Removes the friendship. Either party can unfriend.

Block User

POST /api/v1/friends/block
Content-Type: application/json

{ "userId": "TARGET_USER_ID" }

Creates a block and auto-removes any existing friendship. Blocked users cannot send friend requests or view presence.

Unblock User

DELETE /api/v1/friends/block/{userId}

List Blocked Users

GET /api/v1/friends/blocked

Returns { blocked: [{ id, user, createdAt }] }.

Parent Friend Approval (Child Accounts)

When a child account sends or receives a friend request, the status is set to pending_parent and the parent is notified. Parents manage these via:

GET /api/family/friend-requests
POST /api/family/friend-requests
Content-Type: application/json

{ "friendshipId": "...", "action": "approve" }  // or "deny"

Presence API

Polling-based presence system. Supports session auth and OAuth Bearer tokens (for SDK use).

Update Presence

PUT /api/v1/presence
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "status": "online",     // "online", "away", or "offline"
  "appId": "clx...",      // optional: current app
  "presenceText": "Level 5 — Boss Fight",  // optional: rich presence (max 80 chars)
  "visible": true         // optional: show presence to non-friends
}

Upserts the user's presence row. Send periodically as a heartbeat. Users auto-offline after 5 minutes without an update. presenceText is filtered for safety and shown to friends below "playing [creation]". Send null to clear.

Get Presence

GET /api/v1/presence?userId=TARGET_USER_ID
Authorization: Bearer <access_token>

Returns the target user's presence status. Blocked users receive a 404. Non-visible presence is only shown to friends.

{
  "presence": {
    "userId": "...",
    "status": "online",
    "currentAppId": "clx...",
    "presenceText": "Level 5 — Boss Fight",
    "lastSeenAt": "2026-03-03T..."
  }
}

SDK (Creations)

// Set rich presence text — shows in Friends Online
omen.presence("Level 5 — Fighting the Dragon Boss");

// Clear custom text (shows just "playing [creation]")
omen.presence(null);

Max 80 characters. Filtered for safety. Updates with the 30-second heartbeat. Live-only — no feed events for presence text.

Support & Feature Requests

Omen provides a support ticket system for users and app visitors to submit support requests and feature requests. Organizations can link their users to a pre-scoped form.

Public Support Form

Anyone can submit a ticket at /support. Logged-in users are identified automatically. Anonymous visitors must provide an email address.

App-Scoped Tickets

Link your users to /support?app=YOUR_CLIENT_ID to associate submitted tickets with your app and organization. The form will display your app's name. Tickets submitted this way appear in your developer portal.

Viewing Tickets

  • Developers: View tickets for your organizations at /pound/developer/tickets
  • API: GET /api/tickets with a session or Bearer token returns tickets scoped to your role

Creating Tickets via API

Apps can submit tickets on behalf of users using a Bearer token:

POST /api/tickets
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "type": "support",
  "title": "Cannot load inventory",
  "description": "The inventory screen shows a blank page after the update."
}
FieldRequiredDescription
typeYes"support" or "feature_request"
titleYesBrief summary of the issue or request
descriptionNoDetailed description
emailAnonymous onlyRequired when no auth is provided

Bearer token requests automatically associate the ticket with the user, app, and organization.

Profile Themes

Users can choose from 8 curated CSS themes for their public profile. Themes control colors, backgrounds, borders, and special visual effects.

ThemeStyleSpecial Effect
voidDark (default)None
terminalGreen-on-blackScanline overlay
sakuraLight pinkNone
neonPurple/magentaCard glow
arcticLight blueNone
sunsetWarm orange/brownNone
mossDark greenNone
vaporPurple glassmorphismGlassmorphism blur

Update Theme

POST /api/user/updateTheme
Content-Type: application/json

{ "theme": "neon" }

Requires session auth. Theme must be one of the 8 valid values. Returns { status: "success", theme: "neon" }.

Stars API

Authenticated users can star creations. Stars are unique per user per creation (toggle on/off). Star counts are denormalized on both the Creation and the creator's User record.

Toggle Star

POST /api/v1/creations/{id}/star

Requires session auth. Toggles the star on or off. Returns:

{ "starred": true, "starCount": 42 }

List Stars (Creator Only)

GET /api/v1/creations/{id}/stars?limit=20&cursor=CURSOR_ID

Only the creation's owner can call this endpoint. Returns paginated list of users who starred:

{
  "items": [
    { "id": "...", "createdAt": "...", "user": { "id": "...", "username": "...", "name": "...", "image": "..." } }
  ],
  "nextCursor": "..." | null
}

Visit Tracking

Unique visitor counts are tracked automatically when authenticated users view a creation. The uniqueUserCount field on each creation is incremented on first visit only. Repeat visits update lastVisitedAt and visitCount but don't increase the unique count.

Prestige Tiers

Creator prestige is based on total stars received across all creations. The tier determines the visual treatment of the creator's avatar ring on their profile.

TierStars RequiredAvatar Ring
Common0Default border
Uncommon25Green ring
Rare100Purple ring + glow + pulse
Epic500Indigo ring + glow + rotating shimmer
Legendary2,000Gold ring + double glow + shimmer + sparkle

Prestige tier is recalculated automatically when stars are added or removed. Profile owners see a progress bar showing how many stars they need to reach the next tier.

Rarity Visuals

Items and badges on user profiles display animated visual effects based on their rarity or tier.

Item Rarity Levels

RarityEffect
commonDefault border
uncommonGreen border
rarePurple border + glow + pulse animation
epicIndigo border + glow + shimmer animation
legendaryGold border + double glow + shimmer + sparkle

Badge Tiers

TierEffect
standardSubtle border
rarePurple ring + glow + pulse
legendaryGold ring + glow + shimmer

All animations respect prefers-reduced-motion — when enabled, static borders and colors are preserved but animations are disabled. Rarity colors are theme-aware: light themes (Sakura, Arctic) use darker rarity colors for better contrast.

Daemon API

Each user has a virtual daemon that reacts to their activity. Daemons have moods, stats, and say contextual lines based on events.

GET /api/v1/daemon

Returns the authenticated user's daemon with mood, appearance, avatar URL, and cumulative stats.

POST /api/v1/daemon

Creates a daemon for the authenticated user (one per user). Accepts optional name (1-24 chars). Auto-generates appearance from user ID.

PATCH /api/v1/daemon

Renames the daemon. Body: { "name": "NewName" }.

POST /api/v1/daemon/activity

Logs an activity event. Rate limited to 30 events per hour per user. Body: { "type": "session_start" }. Returns updated mood and a contextual daemon line.

Activity TypeEffect
session_startIncrements totalSessions, may trigger "building" mood
session_endComputes session duration, updates totalSessionMin + longestSessionMin
build_successIncrements totalBuildSuccesses
build_failIncrements totalBuildFails
publishIncrements totalPublishes, may trigger "proud" mood
creation_tryLogged as activity
star_givenLogged as activity
badge_earnedMay trigger "excited" mood

Moods

Extended moods (event-driven) take priority over inactivity moods:

MoodTrigger
buildingSession started < 60s ago
excitedBadge earned within 24h
proudPublished within 24h
energeticActive within 1 day
happyActive within 3 days
chillActive within 7 days
sleepyActive within 14 days
sleepingInactive 14+ days

Stats

Daemon responses include a stats object with: totalSessions, totalPublishes, totalBuildFails, totalBuildSuccesses, longestSessionMin, totalSessionMin, daemonsMet. Non-zero stats display as pills on the profile daemon card.

Personality Traits

Daemon responses include a traits array. Traits are computed daily from 90 days of activity data. Each trait has name, label, icon, and confidence (0.5-1.0). Only traits with confidence >= 0.5 appear.

TraitCondition
🌙 Nocturnal60%+ sessions between 22:00-05:00 UTC
🌅 Early Riser60%+ sessions between 05:00-09:00 UTC
🎮 Game Dev70%+ creations are "game" category
🔧 Tool Smith70%+ creations are "tool" category
🎨 Artist70%+ creations are "art" category
🐛 Stubborn Debugger3+ streaks of 3+ build fails before success
🦋 Social Butterfly50+ accepted friendships
🐺 Lone Wolf0 friends, 10+ sessions
📦 Prolific Publisher10+ publishes in 90 days

Note: Time-based traits use UTC hours.

Appearance Evolution

Traits gradually change how the daemon looks. Each trait overrides one appearance property. Changes are limited to one new visual per day; removals (trait lost) apply immediately. Responses include an evolvedAppearance object with the current overrides. The avatar endpoint merges base + evolved automatically.

TraitOverridesEffect
🌙 Nocturnalpalette → 0Dark body
🌅 Early Riserpalette → 2Warm golden
🎮 Game Devpattern → 1Spotted/pixelated
🔧 Tool Smithbuild → 2Stocky frame
🎨 Artistpattern → 3Belly/gradient
🐛 Stubborn Debuggerexpression → 3Tongue out
🦋 Social Butterflypalette → 4Vibrant purple
🐺 Lone Wolfsize → 2Large, imposing
📦 Prolific Publishermarkings → 2Forehead stripe

When two traits target the same property, highest confidence wins.

Badges

Permanent milestone achievements earned through usage. Computed by the daily cron. Once earned, badges are never revoked. Some badges unlock equippable cosmetic accessories.

BadgeConditionAccessory
🐾 First Steps1+ sessions
📅 Regular10+ sessions
Veteran50+ sessionsLightning pin
🌙 Night Owl10+ night sessions (22-05 UTC)Moon charm
☀️ Early Bird10+ morning sessions (05-09 UTC)Sun visor
🚢 Ship Captain5+ publishesCaptain hat
Fleet Admiral20+ publishes
🐛 Bug Squasher20+ build fails
🏃 Marathon Runner120+ min longest sessionCoffee cup
🌟 Social Star10+ accepted friendshipsScarf
🎨 Prolific Creator5+ active creations
Star Collector10+ stars receivedStar pin

Accessories

Cosmetic SVG elements that render on the daemon avatar. Unlocked by earning the associated badge. Equip via PATCH /api/v1/daemon with { equippedAccessories: ["captain_hat"] }. Only one accessory per slot (hat, pin, charm, neck, held).

AccessorySlotUnlocked by
Lightning Bolt PinpinVeteran
Moon CharmcharmNight Owl
Sun VisorhatEarly Bird
Captain HathatShip Captain
Coffee CupheldMarathon Runner
ScarfneckSocial Star
Star PinpinStar Collector

Omen Sparks

Omen Sparks is a virtual currency for in-creation purchases. Users start with 100 Sparks. Creators earn 70% of every sale; the remaining 30% platform fee is split: 80% burned, 10% treasury, 10% Creator Fund.

Wallet API

GET /api/v1/wallet — Returns the user's wallet balance and lifetime stats. Auth: session or bearer token. Creates wallet with 100 starter Sparks on first call.

// Response
{
  "balance": 100,
  "lifetimeEarned": 100,
  "lifetimeSpent": 0,
  "lifetimeCreatorEarnings": 0,
  "isVerified": false
}

Transaction History

GET /api/v1/wallet/transactions?cursor=X&limit=20&type=spend — Paginated transaction list. Filter by type: spend, creator_earning, transfer_in, transfer_out, starter_grant, creator_fund, burn.

SparksProduct CRUD

Creators define purchasable items inside their creations. Price range: 1–10,000 Sparks.

// Create product (creator only)
POST /api/v1/creations/{id}/sparks-products
{ "name": "Sword", "priceSparks": 50, "description": "A cool sword" }

// List products (public)
GET /api/v1/creations/{id}/sparks-products

// Update product (creator only)
PATCH /api/v1/creations/{id}/sparks-products/{productId}
{ "priceSparks": 75 }

// Deactivate product (creator only)
DELETE /api/v1/creations/{id}/sparks-products/{productId}

Purchase API

POST /api/v1/sparks/purchase — Buy a product. Validates balance, prevents self-purchase, enforces child spending limits. If the product has an itemTemplateId, an Item is issued to the buyer.

// Request
{ "productId": "..." }

// Response
{
  "transaction": { "id": "...", "type": "spend", "amount": -50, ... },
  "item": null,
  "newBalance": 50
}

Gift API

POST /api/v1/sparks/gift — Send Sparks to another user. No platform fee on gifts. Min 1, max 10,000 per gift. Cannot gift to self.

// Request
{ "recipientId": "...", "amount": 25 }

// Response
{ "ok": true, "newBalance": 75 }

postMessage Bridge (In-Creation Purchases)

Creations running in the iframe can request purchases via postMessage. The host page shows an anti-spoofing confirmation overlay outside the iframe.

// From creation iframe:
window.parent.postMessage({
  type: 'omen:sparks:purchase',
  productId: 'PRODUCT_ID'
}, '*');

// Listen for response:
window.addEventListener('message', (e) => {
  if (e.data.type === 'omen:sparks:purchase:response') {
    if (e.data.success) {
      // Purchase succeeded
      // e.data.item — issued item (if any)
      // e.data.newBalance — buyer's new balance
    } else {
      // e.data.error — 'cancelled', 'Insufficient Sparks', etc.
    }
  }
});

Creator Fund

10% of platform fees go to the Creator Fund pool. Each week, the pool is distributed proportionally based on verified-user play time. Creators are paid directly to their Sparks wallet and notified. Only verified users (who have purchased Sparks with real money) generate play credits.

Family Controls

Parents can set daily spending limits and per-purchase approval requirements for child accounts.

// Set child Sparks settings (parent only)
PATCH /api/v1/family/children/{childId}/sparks-settings
{ "spendingLimit": 50, "requiresApproval": true }

Checkout (Stripe Connect)

Omen Checkout lets external apps sell products and subscriptions using Stripe Connect. Developers connect their own Stripe account via the Developer Portal — payments go directly to their account. Omen takes 0% platform fee (configurable later).

1. Connect Stripe (Developer Portal)

In the Developer Portal, open your app and click Connect Stripe Account in the Payments section. You'll be redirected to Stripe to complete onboarding. Once done, your app can accept payments.

// Check connect status
GET /api/v1/apps/{appId}/stripe-connect
→ { connected, accountId, chargesEnabled, payoutsEnabled, onboardingComplete }

// Start or resume onboarding (session auth, app owner)
POST /api/v1/apps/{appId}/stripe-connect
→ { url }  // Redirect user to this URL

2. Register Products (S2S)

Create products that users can purchase. Products are created on your connected Stripe account automatically.

// Create product (S2S auth via Bearer token)
POST /api/v1/apps/{appId}/products
{
  "name": "Premium Pass",
  "type": "one_time",        // or "subscription"
  "priceCents": 499,
  "currency": "usd",
  "billingInterval": "month", // required for subscription
  "trialDays": 7,             // optional, subscription only
  "maxPurchasesPerUser": 1,   // optional
  "templateId": "clx..."      // optional — auto-issue item on purchase
}

// List products
GET /api/v1/apps/{appId}/products?active=true

// Update product
PATCH /api/v1/apps/{appId}/products?productId=xxx
{ "name": "New Name", "active": false }

// Deactivate product
DELETE /api/v1/apps/{appId}/products?productId=xxx

3. Create Checkout Session (OAuth2)

When a user wants to buy something, create a checkout session. The user will be redirected to Stripe to pay. If the user is a child, the session requires parent approval first.

// Create checkout session (OAuth2 Bearer auth)
POST /api/v1/checkout/sessions
{ "productId": "clx...", "redirectUrl": "https://myapp.com/store" }
→ {
    "sessionId": "clx...",
    "checkoutUrl": "https://checkout.stripe.com/...",
    "status": "pending",
    "requiresParentApproval": false
  }

// For child users:
→ {
    "sessionId": "clx...",
    "checkoutUrl": null,
    "status": "awaiting_parent_approval",
    "requiresParentApproval": true
  }

// Poll session status
GET /api/v1/checkout/sessions?sessionId=xxx

4. Subscriptions

Users can view and manage their app subscriptions.

// List user's app subscriptions (OAuth2 Bearer auth)
GET /api/v1/checkout/subscriptions

// Cancel at period end
POST /api/v1/checkout/subscriptions?subscriptionId=xxx&action=cancel

// Resume cancelled subscription
POST /api/v1/checkout/subscriptions?subscriptionId=xxx&action=resume

5. Family Purchase Approval

When a child creates a checkout session, their parent is notified via email and in-app notification. The parent can approve or deny the purchase.

// Parent approves (session auth)
POST /api/v1/family/purchases/{sessionId}?action=approve
→ { success: true, checkoutUrl: "https://checkout.stripe.com/..." }

// Parent denies
POST /api/v1/family/purchases/{sessionId}?action=deny
→ { success: true, status: "denied" }

6. Webhook Events

Register webhook endpoints to receive checkout and subscription events.

EventDescription
checkout.completedOne-time payment completed
checkout.cancelledSession expired or was cancelled
checkout.deniedParent denied child purchase
subscription.createdNew subscription activated
subscription.renewedSubscription renewed (invoice paid)
subscription.cancelledSubscription set to cancel at period end
subscription.expiredSubscription ended
subscription.payment_failedSubscription payment failed

Creation Comments

SoundCloud-style comments on creations. Each comment can include a screenshot captured at the moment of commenting. Supports replies (1 level), likes, pinning, and creator moderation.

List Comments

GET /api/v1/creations/{id}/comments
Auth: Optional (enables likedByMe field)
Query: ?sort=newest|oldest|top&cursor=xxx&limit=20

Response: {
  comments: [{
    id, text, screenshotUrl, likeCount, pinned,
    createdAt, parentCommentId,
    user: { id, username, name, image, prestigeTier },
    isCreator: boolean,
    likedByMe: boolean,
    replies: [...]
  }],
  total: number,
  nextCursor: string | null
}

Create Comment

POST /api/v1/creations/{id}/comments
Auth: Required
Body: {
  text: string,          // max 120 chars
  screenshot: string,    // base64 JPEG data URL (optional)
  parentCommentId: string // optional, for replies
}

Response: created comment object

Delete Comment

DELETE /api/v1/creations/{id}/comments/{commentId}
Auth: Required (comment owner or creation owner)

Response: { ok: true }

Pin / Unpin Comment

PATCH /api/v1/creations/{id}/comments/{commentId}/pin
Auth: Required (creation owner only)
Body: { pinned: boolean }

Response: { id, pinned }
Note: Max 1 pinned comment per creation.

Toggle Comment Like

POST /api/v1/creations/{id}/comments/{commentId}/like
Auth: Required

Response: { liked: boolean, likeCount: number }

Toggle Comments

Creators can disable comments on a creation by including commentsEnabled: 0 in a PATCH update:

PATCH /api/v1/creations/{id}
Auth: Required (creation owner)
Body: { commentsEnabled: 0 }  // 0 = disabled, 1 = enabled

XP & Levels

Omen includes a personal progression system. Users earn XP from activities like publishing creations, adding friends, daily logins, and daemon interactions. XP accumulates toward 25 levels, each with increasing thresholds.

XP Sources

ActivityXPCategory
Publish creation50Create
Star given5Social
Star received10Social
Comment posted5Social
Comment received10Social
Friend added15Social
Set mood5Social
Daemon interaction5Explore
Daily login10Uncapped
Level-up bonus100Uncapped

Daily Caps

To prevent grinding, XP is capped per category per day: Create (200), Social (100), Explore (50). Daily login and level-up bonuses are uncapped.

Level Thresholds

25 levels with names: Newcomer (1-4), Builder (5-9), Creator (10-14), Veteran (15-19), Master (20-24), Legend (25). Thresholds range from 0 XP (Lv1) to 100,000 XP (Lv25).

Milestones

One-time achievements like "First Creation", "10 Friends", "7-Day Streak" award bonus XP and appear on the Journey Map. 16 milestones in total.

Get XP Summary (Private)

GET /api/v1/xp
Auth: Session required

Response: {
  level, levelName, totalXP,
  xpInLevel, xpToNextLevel, progressPct,
  currentStreak, longestStreak,
  recentEvents: [{ id, type, amount, metadata, createdAt }],
  milestones: [{ id, name, description, icon, completed, progress?, target? }]
}

Get User Level (Public)

GET /api/v1/users/{userId}/level

Response: { level: number, title: string }

Developer Agent Tokens

Scoped tokens for AI coding agents to manage your Omen integration. Each token is tied to one organization with specific permissions (read, write, delete, credentials).

Token Format

Tokens follow the format dev_{orgslug}_{random}. The org slug prefix makes tokens human-identifiable. Tokens are shown once on generation — only the bcrypt hash is stored.

Permissions

PermissionDefaultCovers
readOnView orgs, apps, items, badges, tickets, data
writeOnCreate/edit apps, items, badges, redirect URIs, data
deleteOffDelete apps, items, badges, orgs
credentialsOffView/regenerate client secrets and tokens

Generate Token

POST /api/developer/token
Auth: Session required (isDeveloper)

{
  "organizationId": "org_id",
  "permissions": { "read": true, "write": true, "delete": false, "credentials": false }
}

Response: {
  "id": "token_db_id",
  "token": "dev_yogicats_abc123...",  // shown ONCE
  "tokenPrefix": "dev_yogicats_abc123..",
  "organizationId": "...",
  "organizationName": "Yogicats",
  "permissions": { ... },
  "createdAt": "..."
}

List Active Tokens

GET /api/developer/token
Auth: Session required

Response: [
  {
    "id": "...",
    "tokenPrefix": "dev_yogicats_abc1...",
    "permissions": { "read": true, "write": true, ... },
    "createdAt": "...",
    "lastUsedAt": "...",
    "organization": { "id": "...", "name": "Yogicats" }
  }
]

Revoke Token

DELETE /api/developer/token
Auth: Session required

{ "tokenId": "token_db_id", "confirm": true }

Response: { "message": "Token revoked" }

Verify Token (Agent Use)

GET /api/v1/developer/verify
Authorization: Bearer dev_yogicats_abc123...

Response: {
  "valid": true,
  "user": { "username": "pistolphoenix", "display_name": "Pistol Phoenix" },
  "organization": { "id": "...", "name": "Yogicats" },
  "permissions": { "read": true, "write": true, "delete": false, "credentials": false },
  "apps": [{ "id": "...", "name": "Arcade", "image": "..." }]
}

Using Dev Tokens with APIs

Pass the token as a Bearer token to existing endpoints. The token scopes access to its organization. Missing permissions return 403 insufficient_scope.

// Example: list apps in the org
GET /api/oauthclients
Authorization: Bearer dev_yogicats_abc123...

// 403 if missing permission
{ "error": "insufficient_scope", "message": "This token does not have 'delete' permission." }

Skill File

The developer skill file is available at omen.dog/skill/dev.md. AI agents can fetch this to learn the auth flow and available endpoints.

Omen SDK (Creation Runtime)

The Omen SDK provides platform features to creations hosted on Omen. Creators include it as a script tag — it communicates with the parent frame via a postMessage bridge. Works standalone for local development with graceful fallbacks (localStorage for saves, null for identity).

<script src="https://sdk.omen.dog/v1/omen.js"></script>

Identity

const user = await omen.me()
// → { id, username, displayName, avatar, tier, level } or null

await omen.login()       // prompt login
await omen.isLoggedIn()  // → boolean

Storage (Cloud Saves)

Per-user save slots. Max 10 slots, 5MB each. Falls back to localStorage outside Omen.

await omen.save({ score: 100, level: 5 })           // default slot
await omen.save({ score: 100 }, 'checkpoint-1')     // named slot
const data = await omen.load()                       // → saved object or null
const data = await omen.load('checkpoint-1')         // named slot
const slots = await omen.saves()                     // → [{ slot, updatedAt }]
await omen.delete('checkpoint-1')                    // delete slot

Collections (Structured Data)

Document storage for leaderboards, match history, user-generated content. Auto-creates on first write. 5 collections max, 10K documents each. Users can only update/delete their own documents.

// Insert a document
await omen.put('scores', { username: 'Rex', score: 12450, level: 5 })

// Query with filters and sorting
const { documents } = await omen.query('scores', {
  where: { level: 5 },                   // exact match
  sort: { score: -1 },                   // descending
  limit: 10
})

// Full API
await omen.db.update('scores', docId, { score: 13000 })
await omen.db.delete('scores', docId)
const count = await omen.db.count('scores', { level: 5 })

// Filter operators: $gt, $gte, $lt, $lte, $ne, $in
const hard = await omen.query('scores', {
  where: { score: { $gt: 10000 }, level: { $in: [4, 5] } }
})

Economy (Sparks)

const result = await omen.buy('product-id')  // → { success, item, newBalance }
const owned = await omen.owns('item-id')     // → boolean
const items = await omen.inventory()         // → array of owned items

Input

Unified input — works on desktop + mobile. WASD/arrows/joystick mapped automatically.

omen.input.isDown('action')   // true while held
omen.input.pressed('action')  // true for one frame on press
omen.input.released('action') // true for one frame on release
omen.input.on('action', fn)   // callback on press
omen.input.pointer            // { x: 0-1, y: 0-1, down: boolean }

// Virtual joystick (mobile)
if (omen.device.touch) omen.input.joystick()
omen.input.stick              // { x: -1..1, y: -1..1 }

// Actions: 'action' | 'secondary' | 'up' | 'down' | 'left' | 'right'
// action = Space / click / tap, secondary = Shift / right-click

Device & Haptics

omen.device.type        // 'mobile' | 'tablet' | 'desktop'
omen.device.touch       // boolean
omen.device.keyboard    // boolean
omen.device.width       // viewport px
omen.device.height      // viewport px
omen.device.orientation // 'portrait' | 'landscape'
omen.device.on('resize', fn)

omen.vibrate('light')   // 'light' | 'medium' | 'heavy' | 'success' | 'error'

Social, Items & Daemon

const friends = await omen.friends()    // → [{ id, username, avatar, status }]
await omen.hasItem('item-id')           // → boolean
await omen.hasBadge('badge-id')         // → boolean
const buddy = await omen.daemon()    // → { name, mood, traits }

// Feature detection
await omen.has('storage')      // → true
await omen.has('collections')  // → true
await omen.has('economy')      // → true

@omen.dog/sdk (NPM Package)

TypeScript server-side SDK for external app developers. Zero dependencies, Node 18+, native fetch. For server-to-server API calls — not for creations (use the Creation Runtime SDK above).

npm install @omen.dog/sdk

Quick Start

import { OmenClient } from '@omen.dog/sdk';

const omen = new OmenClient({
  token: process.env.OMEN_TOKEN,  // dev_yourorg_abc123...
  appId: 'your-app-id',          // from the Developer Portal
});

// Get a user profile
const profile = await omen.users.get('pistolphoenix');

// Store data for a user
await omen.storage.set({ highScore: 9001 });

// Issue an achievement
await omen.items.issue({
  userId: profile.user.id,
  name: 'Dragon Slayer',
  type: 'achievement',
  rarity: 'legendary',
});

Namespaces

NamespaceMethodsDescription
omen.usersget, friendsUser profiles and friend lists
omen.storageget, set, mergePer-user key-value storage
omen.itemsissue, issueBatch, revokeIssue and manage items
omen.collectionscreate, list, get, delete, insert, query, transactionStructured data collections
omen.webhookscreate, list, get, delete, verifyWebhook management + HMAC verification

Error Handling

import { OmenNotFoundError, OmenRateLimitError } from '@omen.dog/sdk';

try {
  await omen.users.get('nonexistent');
} catch (err) {
  if (err instanceof OmenNotFoundError) {
    // 404 — resource not found
  }
  if (err instanceof OmenRateLimitError) {
    console.log('Retry after', err.retryAfter, 'seconds');
  }
}

Creation Runtime Types

For creation developers — get autocomplete for the omen.* global inside your creation code. Types only, zero runtime cost.

/// <reference types="@omen.dog/sdk/creation" />

Collections API (Structured Data)

Server-to-server API for structured document storage. Requires Bearer token auth. Developers define collections with schemas; Omen provides JSONB queries with GIN indexes.

Tier 2 (Growth+): Collections + queries. 10 collections, 100K docs/collection, 300 queries/min.
Tier 3 (Pro+): Atomic transactions with SERIALIZABLE isolation. Max 5 ops, 5s timeout.

Create Collection

POST /api/v1/apps/{appId}/collections
Authorization: Bearer <token>
Content-Type: application/json

{
  "name": "scores",
  "schema": { "user_id": "string", "score": "number", "level": "number" },
  "indexedFields": ["user_id", "score"]
}

// Schema types: "string" | "number" | "boolean" | "date"
// Name: lowercase, a-z start, a-z0-9_ only, max 50 chars

List Collections

GET /api/v1/apps/{appId}/collections
→ { "collections": [{ "name", "schema", "indexedFields", "documentCount", "maxDocuments", "createdAt" }] }

Insert Documents

POST /api/v1/apps/{appId}/collections/{name}/documents

// Single document
{ "data": { "user_id": "abc", "score": 12450, "level": 5 } }

// Batch (max 100)
{ "documents": [{ "user_id": "abc", "score": 100 }, { "user_id": "def", "score": 200 }] }

Query Documents

GET /api/v1/apps/{appId}/collections/{name}/documents
  ?where={"level":5,"score":{"$gt":1000}}
  &sort={"score":-1}
  &limit=10
  &offset=0
  &count=1

// Filter operators: $gt, $gte, $lt, $lte, $ne, $in
// Sort: 1 ascending, -1 descending
// Max limit: 200

→ { "documents": [{ "id", "data", "ownerId", "createdAt", "updatedAt" }], "total": 42 }

Update & Delete

PATCH /api/v1/apps/{appId}/collections/{name}/documents/{docId}
{ "data": { "score": 13000 } }

DELETE /api/v1/apps/{appId}/collections/{name}/documents/{docId}

// Count
GET /api/v1/apps/{appId}/collections/{name}/count?where={"level":5}

Transactions (Tier 3, Pro+)

Atomic operations — all succeed or all roll back. SERIALIZABLE isolation. Server-to-server only (not available from client SDK). Max 5 operations, 5 second timeout.

POST /api/v1/apps/{appId}/collections/{name}/transaction

{
  "operations": [
    { "type": "get", "id": "doc-a" },
    { "type": "update", "id": "doc-a", "data": { "coins": 500 } },
    { "type": "update", "id": "doc-b", "data": { "coins": -500 }, "merge": true },
    { "type": "insert", "data": { "type": "trade", "from": "a", "to": "b" } },
    { "type": "delete", "id": "doc-c" }
  ]
}

// Operation types: "get" | "insert" | "update" | "delete"
// merge: true (default) merges with existing data, false replaces
→ { "results": [{ "id", "data" }, { "id", "data" }, ...] }

Delete Collection

DELETE /api/v1/apps/{appId}/collections/{name}
// Deletes collection and ALL documents (cascade)

Security Model

ContextReadWriteDelete
SDK (client-side)All docs in collectionOwn docs onlyOwn docs only
S2S (Bearer token)All docsAll docsAll docs

Data is isolated per app. A token for App A cannot access App B's collections.

Developer Environments

Environments let developers test against isolated data without touching production. Each environment is a linked app with its own client ID, secret, and data store. Max 2 environments per production app.

Create Environment

POST /api/v1/developer/environments
Body: {
  "appId": "prod_app_id",
  "type": "development",       // "development" | "staging" | "custom"
  "name": "My QA Env",         // optional, defaults to type name
  "copyTemplates": true,       // copy item templates from production
  "copyRedirects": true        // copy redirect URIs
}

→ {
  "id": "env_app_id",
  "clientId": "env_app_id",
  "clientSecret": "abc123...",   // shown once
  "environmentType": "development",
  "copiedTemplates": 5
}

List Environments

GET /api/v1/developer/environments?appId={productionAppId}

→ {
  "productionAppId": "...",
  "environments": [{
    "id", "name", "type", "environmentName",
    "templateCount", "dataCount", "createdAt"
  }]
}

Promote Templates to Production

POST /api/v1/developer/environments?appId={prodId}&action=promote
Body: {
  "sourceEnvironmentId": "dev_app_id",
  "promote": ["item_templates"]
}

→ { "promoted": [{ "name": "Diamond Axe", "action": "created" }] }

Delete Environment

DELETE /api/v1/developer/environments?envId={envAppId}
// Deletes environment app and ALL associated data

Data Isolation

Each environment has completely separate: per-app storage (KV), structured collections, item/badge grants, leaderboard data, and notifications. Users log in with their real Omen account but see a clean slate in each environment.

Switch environments by switching which client ID your app uses — same as switching between Stripe test and live keys.

Creator Source Download

Creation owners can download their deployed source files as a ZIP archive. Omen-injected files (bridge script, manifest, child sentinel) are automatically excluded — the download contains only the creator's original files.

GET /api/v1/creations/{creationId}/download
Authorization: session cookie (must be creation owner)

→ ZIP file (Content-Type: application/zip)
→ Filename: {slug}-v{version}.zip

Also available via the "Download Source" button in the creation viewer (owner view only).

Data Export & GDPR

Export all data for your app — users, storage, collections, items, badges, tickets — in JSON or CSV format. Omen also provides per-user endpoints for GDPR data portability and erasure requests.

Bulk Export

POST /api/v1/developer/apps/{appId}/export
Authorization: Bearer dev_yourorg_token (requires read permission)
Content-Type: application/json

{
  "format": "json",         // "json" or "csv"
  "include": ["users", "data", "collections", "items", "badges", "tickets"]
  // omit "include" for everything
}

→ 202 {
  "export_id": "clx...",
  "status": "pending",
  "message": "Export queued. Poll GET /exports/{id} for status."
}
GET /api/v1/developer/exports/{exportId}
Authorization: Bearer dev_yourorg_token

→ 200 {
  "export_id": "clx...",
  "status": "complete",        // "pending" | "processing" | "complete" | "failed"
  "format": "json",
  "size_bytes": 8400000,
  "download_url": "/api/v1/developer/exports/clx...?download=1",
  "expires_at": "2026-03-15T..."
}

# Download the ZIP:
GET /api/v1/developer/exports/{exportId}?download=1
→ application/zip

Per-User Export (GDPR Article 20)

GET /api/v1/developer/apps/{appId}/users/{userId}/export
Authorization: Bearer dev_yourorg_token (requires read permission)

→ 200 {
  "user_id": "...",
  "app_data": [...],
  "items": [...],
  "collection_documents": [...],
  "badges": [...],
  "tickets": [...]
}

Per-User Deletion (GDPR Article 17)

DELETE /api/v1/developer/apps/{appId}/users/{userId}/data
Authorization: Bearer dev_yourorg_token (requires delete permission)

→ 200 {
  "deleted": true,
  "user_id": "...",
  "summary": {
    "app_data": 3,
    "collection_documents": 12,
    "items_revoked": 2,
    "tickets": 1,
    "tokens_revoked": 1
  }
}

Items are soft-revoked (audit trail preserved). OAuth tokens are deleted (user disconnected from app). This only affects data scoped to YOUR app — the user's Omen account and data in other apps are untouched.

Migration Notice

Send a one-time migration notification to all users of your app through Omen's notification system. Reaches every user regardless of login method (email, Discord, wallet).

POST /api/v1/developer/apps/{appId}/migrate
Authorization: Bearer dev_yourorg_token (requires write permission)

{
  "migrationUrl": "https://yourapp.com/migrate",
  "message": "We're moving to a new platform. Visit the link to keep your account."
}

→ 200 { "sent": true, "users_notified": 142 }

Migration notices can only be sent once per app to prevent abuse.

AI Asset Generation

Generate sprites, tiles, backgrounds, icons, and other game assets using AI (Google Gemini). Smart helpers construct optimized prompts. Transparency pipeline produces clean PNG assets.

Free tier: 5 generations/day, 2 Sparks each after. Plus subscribers: 15-100 free/day, 1 Spark each after.

Generate Image

POST /api/v1/generate/image
Auth: Session cookie or Bearer token

{
  "prompt": "fire wizard",
  "type": "sprite",          // sprite, tile, background, ui, item, icon, particle
  "style": "pixel-16bit",    // pixel-8bit, pixel-16bit, pixel-32bit, cartoon,
                              // flat, hand-drawn, painterly, isometric
  "view": "side",            // side, top-down, front, three-quarter (optional)
  "palette": "nes",          // auto, nes, snes, gameboy, pastel, neon, earth,
                              // monochrome (optional)
  "colorCount": 4,           // 4, 8, 16, 32 (optional)
  "width": 64,
  "height": 64,
  "transparent": true,
  "variations": 1            // 1-4
}

→ 200 {
  "images": [{
    "id": "uuid",
    "url": "/api/v1/assets/{id}/file",
    "name": "fire-wizard",
    "filename": "fire-wizard.png",
    "width": 64, "height": 64,
    "transparent": true,
    "fileSize": 2400
  }],
  "cost": 2,
  "freeUsed": 0,
  "balance": 43,
  "dailyFreeUsed": 3,
  "dailyFreeLimit": 5
}

Asset Types

TypeDefault SizeTransparent
sprite64x64Yes
tile32x32No
background1280x720No
ui128x128Yes
item64x64Yes
icon512x512No
particle64x64Yes

Rate Limits

LimitValue
Max concurrent1 (sequential)
Max per hour20
Cooldown2 seconds between generations
Max queue5 items

SDK Usage

const result = await omen.generate({
  prompt: 'fire wizard',
  type: 'sprite',
  style: 'pixel-16bit',
  view: 'side',
  size: 64
});
// Shows consent overlay. Cannot auto-generate.
// result.images[0].url → use in game

Asset Library API

Every user has a personal asset library. Generated assets auto-save to it. Browse at /create/assets.

List Assets

GET /api/v1/assets?type=sprite&style=pixel-16bit&q=wizard&limit=20&offset=0
Auth: Session cookie or Bearer token

→ 200 { "assets": [...], "total": 47 }

Search Assets

GET /api/v1/assets/search?q=fire+wizard
Auth: Session cookie or Bearer token

→ 200 { "assets": [...] }  // fuzzy match on name and prompt

Get Asset Detail

GET /api/v1/assets/{id}
Auth: Session cookie or Bearer token

→ 200 {
  "id": "uuid", "name": "fire-wizard", "filename": "fire-wizard.png",
  "url": "/api/v1/assets/{id}/file",
  "width": 64, "height": 64, "fileSize": 2400,
  "type": "sprite", "style": "pixel-16bit", "palette": "nes",
  "transparent": true, "aiGenerated": true,
  "prompt": "fire wizard", "generationParams": {...},
  "createdAt": "2026-03-15T...",
  "usedIn": [{ "creationId": "...", "creationName": "Space Blaster", "filePath": "assets/fire-wizard.png" }]
}

Serve Asset File

GET /api/v1/assets/{id}/file
→ 200 (image/png, image/jpeg, etc.) — CDN-cacheable, immutable

Delete Asset

DELETE /api/v1/assets/{id}
Auth: Session cookie or Bearer token

→ 200 { "deleted": true }  // soft delete, recoverable 30 days

Assign to Creation

POST /api/v1/assets/{id}/assign
Auth: Session cookie or Bearer token

{ "creationId": "...", "filePath": "assets/fire-wizard.png" }

→ 200 { "assigned": true }

Upload Manual Asset

POST /api/v1/assets/upload
Auth: Session cookie or Bearer token
Content-Type: multipart/form-data

Fields: file (image), name (optional), type (optional, default "sprite")
Formats: PNG, JPG, SVG, GIF — max 10 MB

→ 200 { "asset": {...} }

Storage Limits

TierAssetsTotal Size
Free10050 MB
Plus Starter500250 MB
Plus Pro2,0001 GB
Plus Creator10,0005 GB

Content Moderation API

Server-side text filtering for child safety and community health. Two access methods: SDK for creations, REST API for developer apps.

SDK (Creations)

// Clean mode (default) — replaces blocked words with ***
const cleaned = await omen.filter("user input here");
// Returns: "user *** here" (string)

// With context — username checks also block impersonation
const cleaned = await omen.filter("admin_user", { context: "username" });

// Check mode — returns { allowed, reason } without modifying
const result = await omen.filter("some text", { mode: "check" });
if (!result.allowed) {
  console.log(result.reason); // "Contains inappropriate language"
}

REST API (Developer Suite)

POST /api/v1/moderation/filter
Authorization: Bearer <developer-token>
Content-Type: application/json

{
  "text": "user input to filter",
  "context": "chat",    // "chat" | "username" | "content"
  "mode": "clean"       // "clean" | "check"
}

Responses

// Clean mode response
{ "text": "user ***", "filtered": true }

// Check mode response
{ "allowed": false, "reason": "Contains inappropriate language" }

// Rate limited (fail-open)
{ "text": "original text", "filtered": false, "rateLimited": true }

Contexts

ContextBehavior
chatStandard filtering (profanity, hate, PII for children)
usernameStricter — also blocks impersonation (admin, omen, moderator, etc.)
contentMost permissive — allows milder words for creative contexts

Rules

  • Whole-word matching only — "assessment" passes, "shitake" passes
  • Child-aware — automatically applies stricter filters when user isChild
  • PII detection for children — phone numbers and email addresses stripped
  • 1000 character limit per request
  • Fail-open on rate limit — text returned unmodified

Rate Limits

AccessLimit
SDK (per user)30 requests/min
REST API (per app)300 requests/min

Leaderboards

Per-creation and per-app leaderboards with auto-creation, time-scoped views, friends filter, and anti-cheat. SDK and REST API access.

SDK (Creations)

// Submit a score (auto-creates "default" board)
const result = await omen.score(9500);
// Returns: { rank: 1, score: 9500, isNewBest: true, previousRank: null }

// Submit to a named board with metadata
await omen.score(42.5, { board: "speedrun", metadata: { level: "hard" } });

// Query leaderboard data (for custom UI)
const data = await omen.leaderboard({ board: "default", period: "week" });
// Returns: { entries: [...], board: { name, direction, entryCount }, userRank: 5 }

// Query friends-only
const friends = await omen.leaderboard({ friends: true });

// Show pre-built overlay (renders in parent frame)
omen.showLeaderboard();
omen.showLeaderboard("speedrun");
omen.showLeaderboard({ board: "speedrun", friends: true, period: "week" });

REST API (Developer Suite)

// Submit score
POST /api/v1/creations/{creationId}/leaderboards/{name}/scores
Authorization: Bearer <token>
{ "score": 9500, "metadata": { "level": "hard" } }

// Query leaderboard
GET /api/v1/creations/{creationId}/leaderboards/{name}?mode=top&period=all&limit=10
Authorization: Bearer <token>

// Mode: "top" | "around" (centered on user) | "friends"
// Period: "all" | "week" | "month"

Score Submission Response

{
  "rank": 1,
  "score": 9500,
  "isNewBest": true,
  "previousRank": 3
}

Leaderboard Query Response

{
  "entries": [
    { "rank": 1, "userId": "...", "username": "alice", "displayName": "Alice",
      "avatar": "...", "score": 9500, "metadata": {...}, "submittedAt": "..." }
  ],
  "board": { "name": "default", "direction": "desc", "entryCount": 47 },
  "userRank": 5
}

Anti-Cheat

  • Rate limiting: 10 submissions per minute per user per board
  • Bounds validation: optional min/max score per board
  • Session verification: login required to submit
  • One-best-per-user by default (keepAll option available)
  • Scores rounded to 3 decimal places server-side

Social Integration

  • Top-3 ranks appear in feed events (boards with 20+ entries)
  • #1 on 50+ entry board triggers a celebration popup
  • Dethroned notification when someone takes your #1 (max 3/day)
  • Profile "Leaderboard Rankings" section shows top positions
  • Creation viewer "Scores" section with period/friends toggles

Limits

ScopeMax Boards
Per creation5
Per app (Growth)25
Per app (Pro+)100

Embeddable Creations

Any public Omen-hosted creation can be embedded on external websites via iframe. The embed includes a compact Omen bar with creation name, star button, and a "Join Omen" link.

Embed Code

<iframe
  src="https://embed.omen.dog/c/{creationId}"
  width="800"
  height="450"
  frameborder="0"
  allowfullscreen
></iframe>

Size options: 800x450 (default), 640x360, or use CSS width:100%; aspect-ratio:16/9 for responsive.

Rules

  • All embeds are free — no tier gating
  • The Omen bar is always visible (branding, non-removable)
  • External creations (hosted outside Omen) cannot be embedded
  • Anonymous viewers can play but cannot star, comment, or earn items
  • Logged-in viewers (with omen.dog session) get full features

Embed code is available via the "Embed" button in the creation viewer panel.

Matchmaking

Queue-based matchmaking with Glicko-2 skill ratings. Auto-created queues, expanding rating windows, snake draft team balancing, and dodge penalties.

Create Queue

POST /api/v1/apps/{appId}/matchmaking/queues
Authorization: Bearer <token>

{
  "name": "ranked_1v1",
  "teamSize": 1,
  "teamsPerMatch": 2,
  "rated": true
}

// Queues auto-create on first join if they don't exist (casual, unrated)

Join Queue

POST /api/v1/apps/{appId}/matchmaking/queues/{name}/join
Authorization: Bearer <token>

{ "userId": "cmm0tzco8...", "properties": { "map": "desert" } }

// Response:
{
  "id": "ticket_id",         // poll this
  "status": "searching",     // searching → matched → cancelled
  "rating": 1500,
  "roomId": null,
  "roomCode": null,
  "createdAt": "..."
}

Poll Ticket

GET /api/v1/apps/{appId}/matchmaking/tickets/{ticketId}

// When matched:
{
  "id": "ticket_id",
  "status": "matched",
  "roomId": "room_id",       // connect to this room
  "roomCode": "ABC123",
  "matchedAt": "...",
  "estimatedWaitSeconds": 15  // only while searching
}

Cancel / Leave Queue

DELETE /api/v1/apps/{appId}/matchmaking/tickets/{ticketId}

// If ticket was matched → dodge penalty (escalating: 2min → 5min → 15min)
// Response: { "cancelled": true, "dodgePenaltySeconds": 120 }

Report Match Result

POST /api/v1/apps/{appId}/matchmaking/results
Authorization: Bearer <token>

// Team/1v1 — use score (higher = better)
{
  "roomId": "room_id",
  "results": [
    { "userId": "player1", "score": 1 },
    { "userId": "player2", "score": 0 }
  ]
}

// FFA — use placement (1 = 1st place)
{
  "roomId": "room_id",
  "results": [
    { "userId": "player1", "placement": 1 },
    { "userId": "player2", "placement": 2 },
    { "userId": "player3", "placement": 3 }
  ]
}

// Response (rated queues only):
{
  "updates": [{
    "userId": "player1",
    "previousRating": 1500,
    "newRating": 1516,
    "ratingChange": 16,
    "tier": "gold"
  }],
  "seasonId": "season_id"  // if active season
}

Seasons

Seasonal ratings reset periodically. When a season activates, player ratings are compressed toward 1500 using the resetFactor (0 = full reset, 1 = keep all). Match results during an active season update the season rating. All-time ratings are unaffected.

// Create season
POST /api/v1/apps/{appId}/matchmaking/seasons
Authorization: Bearer <token>

{
  "queueName": "ranked",
  "name": "Season 1",
  "startDate": "2026-04-01T00:00:00Z",
  "endDate": "2026-07-01T00:00:00Z",
  "resetFactor": 0.5
}

// List seasons
GET /api/v1/apps/{appId}/matchmaking/seasons?queueName=ranked

// Get season with leaderboard
GET /api/v1/apps/{appId}/matchmaking/seasons/Season%201?queueName=ranked&limit=20

// Activate season (seeds ratings from all-time)
PATCH /api/v1/apps/{appId}/matchmaking/seasons/Season%201
{ "queueName": "ranked", "status": "active" }

// End season
PATCH /api/v1/apps/{appId}/matchmaking/seasons/Season%201
{ "queueName": "ranked", "status": "ended" }

Rating Tiers

TierRating Range
Bronze0 – 1299
Silver1300 – 1599
Gold1600 – 1899
Platinum1900 – 2199
Diamond2200+

SDK (Creations)

// Join matchmaking — resolves when matched
const match = await omen.match();
// → { roomCode, roomId, team, opponents }

// Named queue with properties
const match = await omen.match({ queue: 'ranked', properties: { map: 'desert' } });

// Cancel
await omen.matchCancel();

// Pre-built search overlay (elapsed time, estimated wait, cancel button)
omen.match.search();

// Get player rating (returns active season or all-time)
const stats = await omen.rating();
// → { rating: 1650, tier: 'gold', wins: 42, losses: 18, season: 'Season 1' }

// Rating for specific queue
const stats = await omen.rating('ranked');

How It Works

  • Expanding windows: starts at ±50 rating, widens by 25 every 10 seconds (max ±500)
  • Snake draft: sorts by rating descending, assigns T1, T2, T2, T1... for balanced teams
  • Dodge penalties: cancelling a matched ticket gives escalating cooldowns (2 → 5 → 15 min)
  • Glicko-2: rating deviation increases with inactivity, so returning players have wider windows
  • FFA placement: placement-based results are converted to pairwise comparisons — 1st beats all, 2nd beats 3rd+, etc.
  • Seasons: seasonal ratings reset with a configurable compression factor. Leaderboards per season.
  • Auto-create: first join to a non-existent queue creates a casual 1v1 queue (unrated)

Parties

Pre-game party system. Players form a group, then enter matchmaking or join rooms together. Parties persist across creations until disbanded. Max 8 members per party.

REST API (Developer Suite)

Create Party

POST /api/v1/multiplayer/parties
Cookie: session

{ "maxSize": 4 }

// Response:
{
  "party": {
    "id": "cuid",
    "code": "ABCD12",
    "leaderId": "user_id",
    "maxSize": 4,
    "status": "open",
    "members": [{ "userId": "...", "username": "...", "name": "...", "image": "..." }]
  }
}

Get Current Party

GET /api/v1/multiplayer/parties
Cookie: session

// Response: { "party": { ... } }  or  { "party": null }

Join Party

POST /api/v1/multiplayer/parties/join
Cookie: session

{ "code": "ABCD12" }

// Response: { "party": { ... } }

Leave Party

POST /api/v1/multiplayer/parties/leave
Cookie: session

// If leader leaves, leadership transfers to next member.
// If last member leaves, party is disbanded.
// Response: { "left": true }

Kick Member (Leader Only)

POST /api/v1/multiplayer/parties/kick
Cookie: session

{ "memberId": "user_id" }

// Response: { "kicked": true }

Disband Party (Leader Only)

DELETE /api/v1/multiplayer/parties
Cookie: session

// Response: { "disbanded": true }

Party + Matchmaking

Pass a partyId when creating a matchmaking ticket to queue the whole party together. The party status changes to in_queue and all members are placed in the same room when matched.

POST /api/v1/multiplayer/matchmaking/ticket
{ "creationId": "...", "partyId": "party_id" }

SDK (Creations)

// Get current party
const party = await omen.party();
// → { id, code, leaderId, maxSize, status, members: [...] } or null

// Create a party
const party = await omen.party.create({ maxSize: 4 });

// Join by code
const party = await omen.party.join("ABCD12");

// Leave
await omen.party.leave();

// Kick a member (leader only)
await omen.party.kick(memberId);

// Disband (leader only)
await omen.party.disband();

// Queue with party
const match = await omen.match({ partyId: party.id });

Party Statuses

StatusMeaning
openAccepting new members
in_queueParty is in matchmaking
in_gameParty is in a game room
disbandedParty has been disbanded

Voice Chat

WebRTC voice chat in multiplayer rooms. Two modes: P2P mesh (adults only, up to 8 speakers) andSFU relay (when any child account is present, up to 64 speakers). Push-to-talk by default, open-mic optional.

No recording. Ever. Omen never records voice. No server-side recording, no client-side recording API.

How It Works

P2P mode (adults): Voice signaling (SDP offers/answers, ICE candidates) relayed through the room WebSocket. Audio flows directly peer-to-peer. STUN/TURN via coturn for NAT traversal.

SFU mode (children present): When any child account joins a room, voice automatically switches to SFU (mediasoup). All audio routes through the server — no direct peer connections. Supports 20+ speakers. Children need per-friend parent approval to use voice.

Child Voice Approval (Parent API)

// List approved friends for voice chat
GET /api/family/voice-approvals?childId=child_id

// Approve a friend for voice with your child
POST /api/family/voice-approvals
{ "childId": "child_id", "friendId": "friend_id" }

// Revoke approval
DELETE /api/family/voice-approvals
{ "childId": "child_id", "friendId": "friend_id" }

WebSocket Voice Messages

TypeWhoPayload
voice_joinAny(no payload)
voice_leaveAny(no payload)
voice_muteAny(no payload)
voice_unmuteAny(no payload)
voice_offerAny{ to, sdp }
voice_answerAny{ to, sdp }
voice_iceAny{ to, candidate }
voice_configHost{ enabled, mode, maxSpeakers, teamOnly }
voice_stateAnyQuery current voice state

REST API

PATCH /api/v1/multiplayer/rooms/{code}/voice
{
  "enabled": true,
  "mode": "push-to-talk",   // "push-to-talk" | "open-mic"
  "maxSpeakers": 8,
  "teamOnly": false
}

Host only. Voice config is applied in real-time via WebSocket — this endpoint validates and returns the config.

SDK Controls (Creation SDK)

omen.voice.mute()         // Mute self
omen.voice.unmute()       // Unmute self
omen.voice.setChannel('team')  // Team-only voice
omen.voice.disable()      // Disable voice for this creation
omen.voice.state()        // Query voice state

Speaker Limits by Tier

TierMax Speakers
Free4
Growth8
Pro16
Scale64

Content & Localization API

Key-value content store with locale support. Store text strings per namespace/key/locale. Use for UI labels, marketing copy, game text, or any content that needs translation.

Get All Keys

GET /api/v1/apps/{appId}/content/{locale}/{namespace}

// Response:
{
  "namespace": "default",
  "locale": "en",
  "content": {
    "welcome": "Welcome to our app!",
    "play_button": "Play Now"
  },
  "count": 2
}

Get Single Key

GET /api/v1/apps/{appId}/content/{locale}/{namespace}/{key}

Upsert Key

PUT /api/v1/apps/{appId}/content/{locale}/{namespace}/{key}

Body: { "value": "Welcome!", "format": "text" }
// format: "text" (default) or "markdown"

Bulk Upsert

POST /api/v1/apps/{appId}/content/bulk

Body: {
  "locale": "en",
  "namespace": "default",
  "keys": {
    "welcome": "Welcome!",
    "play_button": "Play Now",
    "game_over": "Game Over"
  }
}

// Response: { "ok": true, "created": 2, "updated": 1, "total": 3 }

Delete Key

DELETE /api/v1/apps/{appId}/content/{locale}/{namespace}/{key}

List Locales

GET /api/v1/apps/{appId}/content/locales

// Response: { "locales": ["en", "de", "es"] }

SDK (Creations)

// Get localized string
const label = await omen.t('play_button');
const labelDE = await omen.t('play_button', { locale: 'de', fallback: 'en' });

Auth: Bearer token (OAuth) or Developer Token. Max 500 keys per bulk request. Values up to 50,000 characters (for long-form content). Responses cached for 5 minutes.