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
- What is Omen
- Accounts
- Developer Portal
- Organizations
- Applications (OAuth Clients)
- Login with Omen (OAuth2 Flow)
- Login with Omen Kit — button assets & quick-start guide
- Profile API
- App Data API
- Org Data API
- Badges API
- Referral API
- Notifications API
- Server-to-Server API
- Trait Entitlements API
- Family Accounts
- Family API (includes child inventory & parent notifications)
- Child Authentication
- Child Purchase Approvals
- PFP Editor
- Avatar Save API
- Avatar Templates API
- Items & Inventory
- Item Templates API
- Item Issue & Revoke API
- User Inventory API
- Item Lookup API
- Batch Item Issue API
- Webhooks
- Item Transfers
- Redeem Codes
- Creations (Omen Publish)
- Publish Pipeline
- omen-publish CLI
- Multiplayer / Realtime API
- Public Profile (OG images, accent color, section order)
- Vanity Domains
- Creation Viewer
- External Creations
- Content Reports
- Push Notifications
- Mood / Status API
- Dashboard API
- Friends & Social Graph
- Presence API
- Rate Limiting
- Support & Feature Requests
- Profile Themes
- Stars API
- Prestige Tiers
- Rarity Visuals
- Daemon API
- Checkout (Stripe Connect)
- Creation Comments
- XP & Levels
- Developer Agent Tokens
- Omen SDK (Creation Runtime)
- @omen.dog/sdk (NPM Package)
- Collections API (Structured Data)
- Developer Environments
- Creator Source Download
- Data Export & GDPR
- Version History & Rollback
- AI Asset Generation
- Asset Library API
- Content Moderation API
- Leaderboards
- Embeddable Creations
- Matchmaking
- Parties
- Voice Chat
- 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.
| Field | Description |
|---|---|
id | Client ID (public) |
secret | Client secret (keep private, used in token exchange) |
name | Display name shown to users during authorization |
redirectUris | Allowed redirect URIs (must match exactly) |
termsOfService | URL to your terms of service |
privacyPolicy | URL to your privacy policy |
organizationId | Required — 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=codeThe 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://..."
}
]
}| Field | Description |
|---|---|
email | User's email address (consented during authorization) |
image | User's default Omen profile image |
orgImage | Org-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. |
badges | User'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": "..." }
}| Field | Description |
|---|---|
orgData | Shared data across all apps in the org (only if app is in an org) |
appData | Data 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://..."
}| Field | Required | Description |
|---|---|---|
name | Yes | Badge display name |
description | No | Badge description |
image | No | Badge 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
- Your app builds an invite link containing the referrer's Omen user ID
- When the referred user clicks "Login with Omen", pass
&ref=REFERRER_USER_IDon the authorize URL - Omen records the referral on first authorization (deduped per referred user + client)
- If the referrer crosses a tier threshold, the corresponding badge is auto-awarded
- Your app polls
GET /api/referralsto 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_IDSelf-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": "..." } }
}| Field | Description |
|---|---|
count | Total referrals this user has made for this app |
referrals | List of referred user IDs and timestamps |
tiers | All configured referral tiers with achievement status |
nextTier | The 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>
| Param | Required | Description |
|---|---|---|
since | No | ISO 8601 timestamp — only return notifications created after this time |
limit | No | Max 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
| Type | Data fields | When |
|---|---|---|
badge_earned | badgeId, badgeName, reason, referralCount, threshold | User crosses a referral tier and a badge is auto-awarded |
badge_granted | badgeId, badgeName, clientId | An app grants a badge via POST /api/badges |
badge_revoked | badgeId, badgeName, clientId | An app revokes a badge via DELETE /api/badges |
Recommended Polling Pattern
- On first load, call
GET /api/notifications?limit=50 - Store the
createdAtof the most recent notification - On subsequent polls (page load or every 30–60 s):
GET /api/notifications?since=STORED_TIMESTAMP - 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" }
}| Field | Required | Description |
|---|---|---|
client_id | Yes | Your OAuth client ID |
client_secret | Yes | Your OAuth client secret |
appData | No* | Object to shallow-merge into the user's app data (scoped to your client) |
orgData | No* | 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
| Status | Cause |
|---|---|
| 401 | Missing or invalid client_id / client_secret |
| 404 | No user with that ID |
| 400 | Neither 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"
}| Field | Required | Description |
|---|---|---|
client_id | Yes | Your OAuth client ID |
client_secret | Yes | Your OAuth client secret |
projectId | Yes | PFP project ID |
categoryId | No | Filter 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'straitIds, OR its category is in a granted bundle'scategoryIds - 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:
- Click + New Bundle to create a bundle (e.g. "Elite Pack")
- Use the picker tabs to select individual traits, entire categories, bases, or variants
- Click Edit Default Traits to set which traits are free for all users
- 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:
| Field | Description |
|---|---|
id | Unique bundle ID (use this in grant/revoke calls) |
name | Display name (e.g. "Elite Pack") |
description | Optional description |
traitIds | Individual trait IDs unlocked by this bundle |
categoryIds | Category IDs — all traits in these categories are unlocked |
baseIds | Base template IDs unlocked |
variantIds | Variant trait IDs unlocked |
templateIds | Saved 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
| Limit | Value |
|---|---|
| Max family members (including owner) | 5 |
| Max child accounts | 4 |
| Invite link expiry | 7 days |
| QR login token expiry | 10 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
| Param | Required | Description |
|---|---|---|
childId | Yes | The child user's ID |
type | No | Filter by item type |
rarity | No | Filter by rarity |
cursor | No | Pagination cursor |
limit | No | Results 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.
| Type | Trigger | Data Fields |
|---|---|---|
child.item.received | An app issues an item to a child | childId, childName, itemId, itemName, appName |
child.item.expired | A 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
- Parent generates a QR token via
POST /api/family/children - Child scans the QR code which hits
POST /api/childauth/verify?token=TOKEN - Verify endpoint validates the token and redirects with an authorization code
- 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
- Child taps "buy" in your app — app detects child account via
scope: "child"in the token response - App writes a purchase request to the parent's app data via
PATCH /api/server/userdata/:parentId - Parent sees the request in Omen's family dashboard and approves or denies it
- Omen writes the result to the child's app data (
approvedPurchasesordeniedPurchases) - 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
| Field | Required | Description |
|---|---|---|
childName | Yes | Display name of the child |
childOmenId | Yes | Child's Omen user ID |
itemName | Yes | Display name of the item |
itemId | Yes | Your app's internal item identifier |
itemCost | Yes | Cost in your app's currency |
requestedAt | Yes | ISO 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
}| Field | Required | Description |
|---|---|---|
image | Yes | Base64-encoded PNG image (max 4MB) |
selections | Yes | Trait selections used to compose the avatar |
traits | No | Array of trait metadata |
base | No | Base template ID |
orgId | No | Save as org-specific avatar |
clientId | No | Save as app-specific avatar |
setAsDefault | No | Set 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>
| Param | Required | Description |
|---|---|---|
projectId | Yes | PFP project ID (from your org's PFP project settings) |
categoryId | No | Filter 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
| Field | Description |
|---|---|
categories | Array of template categories |
categories[].id | Category ID |
categories[].label | Display name for the category |
categories[].isDefault | Whether this is the default "Templates" category |
items[].id | Unique template ID |
items[].name | Template display name |
items[].image | URL path to the composite PNG image. Prepend your Omen origin (e.g. https://omen.dog) to get the full URL. |
items[].base | Base template ID used |
items[].selections | Trait selection map (categoryId → traitId) |
items[].traits | Array 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.dogfor full URLs - Each template includes
selectionswhich 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
privateby default. Users can set items topublicto show them on their profile. - Featured Wall — users can feature up to 20 public items for display on their profile.
Item Types
| Type | Description |
|---|---|
avatar_trait | Wearable trait for the avatar system |
collectible | Digital collectible |
achievement | Achievement or milestone award |
in_game_item | In-game item (weapon, power-up, etc.) |
physical_good | Redeemable for a physical product |
profile_decoration | Profile 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"]
}| Field | Required | Description |
|---|---|---|
type | Yes | One of the item types listed above |
name | Yes | Display name |
description | No | Description text |
rarity | No | Rarity tier |
maxSupply | No | Maximum number that can be issued (null = unlimited) |
transferable | No | Whether items can be transferred (default false) |
listable | No | Whether items can be listed on marketplace (default false) |
soulbound | No | Whether items are permanently bound to the owner (default false) |
ttlSeconds | No | Time-to-live in seconds — items expire after this duration |
imageUrl | No | Full-size image URL |
thumbnailUrl | No | Thumbnail image URL |
defaultMetadata | No | Default metadata object merged into issued items |
tags | No | Array 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>| Param | Description |
|---|---|
cursor | Pagination cursor from a previous response |
limit | Results per page (1–100, default 25) |
type | Filter by item type |
rarity | Filter by rarity |
active | true (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"
}| Field | Required | Description |
|---|---|---|
userId | Yes | The user to issue the item to |
templateId | No | Template to issue from (if omitted, creates ad-hoc) |
type | Ad-hoc only | Required when no templateId |
name | Ad-hoc only | Required when no templateId |
acquisitionType | No | purchase, transfer, gift, earn (default), redeem, mint |
metadata | No | Custom 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>| Param | Description |
|---|---|
cursor | Pagination cursor |
limit | Results per page (1–100, default 25) |
type | Filter by item type |
appId | Filter by issuing app |
rarity | Filter by rarity |
templateId | Filter 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
}| Field | Description |
|---|---|
visibility | public or private. Setting to private auto-unfeatures the item. |
featured | true or false. Cannot feature private items. |
featuredOrder | Non-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"
}
]
}| Field | Required | Description |
|---|---|---|
items | Yes | Array of item objects (max 100) |
items[].userId | Yes | Target user ID |
items[].templateId | No | Template to issue from. If omitted, name and type are required (ad-hoc). |
items[].metadata | No | Per-item metadata (merged with template defaults) |
items[].acquisitionType | No | Defaults 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
| Type | Fired when |
|---|---|
item.issued | An item is issued (single or batch) |
item.revoked | An item is revoked |
item.expired | An item's TTL expires (processed by cron worker) |
item.transferred | An item is transferred to another user (via QR transfer or gift) |
redeem.completed | A 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"]
}| Field | Required | Description |
|---|---|---|
url | Yes | HTTPS endpoint URL |
events | Yes | Array 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:
| Header | Description |
|---|---|
X-Omen-Event | Event type (e.g. item.issued) |
X-Omen-Signature | HMAC-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 childrenPOST /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.
| Param | Description |
|---|---|
batchId | Filter by batch |
status | Filter by status (active, redeemed, disabled) |
cursor | Pagination cursor |
limit | Page 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"
}| Field | Required | Description |
|---|---|---|
name | Yes | Display name (max 100 chars) |
slug | No | URL slug (auto-generated from name if omitted, max 60 chars) |
description | No | Short description |
category | No | game, art, music, animation, website, tool, other |
tags | No | Array of string tags |
visibility | No | public (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=gamePublic endpoint. Returns paginated list of a user's public, active creations.
Public: Get Pinned Creations
GET /api/v1/users/{userId}/creations/pinnedReturns 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
| Layer | What it checks | Blocking? |
|---|---|---|
| File Validation | Allowed extensions, max sizes (10MB/file, 50MB total, 500 files), no dotfiles/symlinks | Yes |
| Static Analysis | eval(), WebSocket, document.cookie, localStorage, iframes, crypto mining, etc. | Yes |
| AI Review | Malicious patterns, phishing, data exfiltration, inappropriate content | Yes |
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 eventsSend 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
- You enter your API key + a passphrase in Settings
- Your browser validates the key directly with the provider
- Your browser encrypts the key with PBKDF2(passphrase) → AES-256-GCM
- Only the encrypted blob is sent to the server for storage
- In Studio, you enter your passphrase to unlock — decryption happens in your browser
- 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:
- Detect your framework (Next.js, Vite, CRA, Vue, Svelte, Astro, or static)
- Run your build command (
npm run build) - Bundle the output directory into a zip
- Upload to Omen for 3-layer security scanning
- Display a QR code for mobile approval
- Deploy to
username-project.creations.omen.dog
Options
| Flag | Description |
|---|---|
-n, --name <name> | Project name (auto-detected from package.json) |
-t, --type <type> | Framework type override |
-o, --output <dir> | Build output directory override |
--no-build | Skip 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
| Framework | Detected by | Default output |
|---|---|---|
| Next.js | next in dependencies | out/ |
| Vite | vite in dependencies | dist/ |
| Create React App | react-scripts in dependencies | build/ |
| Vue CLI | @vue/cli-service in dependencies | dist/ |
| SvelteKit | @sveltejs/kit in dependencies | build/ |
| Astro | astro in dependencies | dist/ |
| Static | Fallback | ./ (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 / Method | Description |
|---|---|
omen.players | Array 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
| Tier | Concurrent Rooms | Authoritative Rooms |
|---|---|---|
| Free | 10 | 0 |
| Growth | 50 | 0 |
| Pro | 100 | 20 |
| Scale | Unlimited | Unlimited |
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
| Type | Who | Payload |
|---|---|---|
state_update | Host | { state: {...} } — full state replace |
state_patch | Host | { patch: {...} } — shallow merge |
deep_patch | Host | { patches: { "path.to.key": value } } — path-based |
message | Any | { data: ... } — broadcast to all |
direct_message | Any | { to: "userId", data: ... } |
team_message | Any | { team: "red", data: ... } — team-scoped |
assign_team | Host | { user_id: "...", team: "red" } |
start_game | Host | (no payload) |
end_game | Host | { results: { players: [{ userId, score, rank }], duration } } — triggers post-match screen |
kick | Host | { user_id: "...", reason: "..." } |
chat | Any | { text: "...", channel: "global"|"team"|"whisper", to: "userId" } |
chat_mute | Host | { userId: "...", duration: 300, reason: "..." } |
chat_unmute | Host | { userId: "..." } |
ping | Any | (no payload) → pong |
| custom type | Any | Forwarded to room-logic.js (authoritative) or broadcast (P2P) |
Server → Client Messages
| Type | When |
|---|---|
room_state | On join/rejoin — full snapshot (includes teams, metadata) |
player_joined | Someone joins |
player_left | Someone leaves or is kicked |
player_disconnected | Disconnect (rejoin window starts) |
player_reconnected | Rejoin within window |
host_changed | Host transferred |
team_changed | Player assigned to a team |
rate_limited | Your message was rate-limited |
chat | Chat message received (includes from, fromName, channel, text, ts) |
chat_echo | Your message echo (if shadow-muted: includes muted, muteExpiresIn) |
chat_muted | You were muted (includes duration, reason) |
chat_unmuted | You 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 daysIf 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" brandingAccent 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 nullSection 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
| URL | Content |
|---|---|
{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/creations | Full paginated list of all public creations |
{username}.omen.dog/items | Full 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 Type | Direction | Description |
|---|---|---|
omen:sparks:purchase | iframe → host | Request Sparks purchase (anti-spoofing overlay) |
omen:login:request | iframe → host | Request login flow (stub) |
omen:friend:invite | iframe → host | Request 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
| Feature | Omen-hosted | External |
|---|---|---|
| Hosting | *.creations.omen.dog | User's own server |
| Vanity URL behavior | Loads in sandboxed iframe | Shows “Leaving Omen” interstitial |
| Subdomain | Auto-generated | None |
| Security scanning | 3-layer pipeline | None (external content) |
| Publish pipeline | Zip upload + approval | N/A — just a URL |
| Profile badge | None | “External” badge overlay |
| Child viewer | CSP + sandboxing | Extra 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
| Category | Description |
|---|---|
malicious | Malware, crypto miners, or harmful code |
inappropriate | Age-inappropriate or offensive content |
phishing | Fake login pages or credential harvesting |
spam | Spam or unsolicited advertising |
copyright | Copyright or IP infringement |
other | Other 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"
}| Field | Required | Description |
|---|---|---|
token | Yes | Push token from FCM/APNs |
deviceId | Yes | Unique device identifier |
platform | No | fcm_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 } }
}
}| Field | Type | Description |
|---|---|---|
pushEnabled | 0 or 1 | Global push on/off |
quietHoursStart | HH:mm or null | Quiet hours start time |
quietHoursEnd | HH:mm or null | Quiet hours end time |
quietHoursTimezone | IANA timezone or null | Timezone for quiet hours |
appPreferences | Object | Per-app mute and category settings |
childPushDisabled | 0 or 1 | Parent: 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" }
}| Field | Required | Description |
|---|---|---|
userId | Yes | Target user ID |
title | Yes | Notification title |
body | Yes | Notification body text |
category | No | One of: general, items, transfers, badges, social, app, system |
actionUrl | No | URL to open on click |
data | No | Arbitrary JSON payload |
Quality Controls
Push delivery goes through several checks before sending:
| Check | Skip Reason | Description |
|---|---|---|
| Global push disabled | push_disabled | User turned off all push |
| App muted | app_muted | User muted notifications from this app |
| Category muted | category_muted | User muted this category for the app |
| Quiet hours | quiet_hours | Current time is within quiet hours |
| Child push disabled | child_push_disabled | Parent disabled push for child |
| Rate limit | rate_limited | App 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
| Category | Notification Types |
|---|---|
transfers | item.transfer.sent, item.transfer.received, child.transfer.pending, transfer.denied_by_parent |
items | child.item.received, child.item.expired, item.issued, item.revoked, redeem.completed |
badges | badge_earned, badge_granted, badge_revoked |
app | app.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"
}| Field | Max Length | Description |
|---|---|---|
moodEmoji | 4 chars | Emoji string (truncated if longer) |
moodText | 60 chars | Short 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": "..." }
]
}| Section | Description |
|---|---|
user | Identity: name, username, avatar, bio, mood, join date |
featuredItems | Up to 8 featured items (owner sees regardless of visibility) |
recentNotifications | Last 20 notifications across all orgs, newest first |
connectedApps | Deduplicated apps with active refresh tokens |
Rate Limiting
All /api/v1/ endpoints are rate limited. Limits are applied per authentication method:
| Auth type | Limit | Window |
|---|---|---|
| Bearer token (S2S) | 1,000 requests | 1 minute |
| Session / anonymous | 100 requests | 1 minute |
Rate limit headers are included in every response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests per window |
X-RateLimit-Remaining | Requests remaining |
X-RateLimit-Reset | Unix 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}/acceptOnly the receiver can accept. Returns 403 if the request needs parent approval first (pending_parent).
Decline Friend Request
POST /api/v1/friends/{friendshipId}/declineDeletes 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/ticketswith 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."
}| Field | Required | Description |
|---|---|---|
type | Yes | "support" or "feature_request" |
title | Yes | Brief summary of the issue or request |
description | No | Detailed description |
email | Anonymous only | Required 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.
| Theme | Style | Special Effect |
|---|---|---|
| void | Dark (default) | None |
| terminal | Green-on-black | Scanline overlay |
| sakura | Light pink | None |
| neon | Purple/magenta | Card glow |
| arctic | Light blue | None |
| sunset | Warm orange/brown | None |
| moss | Dark green | None |
| vapor | Purple glassmorphism | Glassmorphism 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}/starRequires 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_IDOnly 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.
| Tier | Stars Required | Avatar Ring |
|---|---|---|
| Common | 0 | Default border |
| Uncommon | 25 | Green ring |
| Rare | 100 | Purple ring + glow + pulse |
| Epic | 500 | Indigo ring + glow + rotating shimmer |
| Legendary | 2,000 | Gold 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
| Rarity | Effect |
|---|---|
| common | Default border |
| uncommon | Green border |
| rare | Purple border + glow + pulse animation |
| epic | Indigo border + glow + shimmer animation |
| legendary | Gold border + double glow + shimmer + sparkle |
Badge Tiers
| Tier | Effect |
|---|---|
| standard | Subtle border |
| rare | Purple ring + glow + pulse |
| legendary | Gold 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 Type | Effect |
|---|---|
| session_start | Increments totalSessions, may trigger "building" mood |
| session_end | Computes session duration, updates totalSessionMin + longestSessionMin |
| build_success | Increments totalBuildSuccesses |
| build_fail | Increments totalBuildFails |
| publish | Increments totalPublishes, may trigger "proud" mood |
| creation_try | Logged as activity |
| star_given | Logged as activity |
| badge_earned | May trigger "excited" mood |
Moods
Extended moods (event-driven) take priority over inactivity moods:
| Mood | Trigger |
|---|---|
| building | Session started < 60s ago |
| excited | Badge earned within 24h |
| proud | Published within 24h |
| energetic | Active within 1 day |
| happy | Active within 3 days |
| chill | Active within 7 days |
| sleepy | Active within 14 days |
| sleeping | Inactive 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.
| Trait | Condition |
|---|---|
| 🌙 Nocturnal | 60%+ sessions between 22:00-05:00 UTC |
| 🌅 Early Riser | 60%+ sessions between 05:00-09:00 UTC |
| 🎮 Game Dev | 70%+ creations are "game" category |
| 🔧 Tool Smith | 70%+ creations are "tool" category |
| 🎨 Artist | 70%+ creations are "art" category |
| 🐛 Stubborn Debugger | 3+ streaks of 3+ build fails before success |
| 🦋 Social Butterfly | 50+ accepted friendships |
| 🐺 Lone Wolf | 0 friends, 10+ sessions |
| 📦 Prolific Publisher | 10+ 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.
| Trait | Overrides | Effect |
|---|---|---|
| 🌙 Nocturnal | palette → 0 | Dark body |
| 🌅 Early Riser | palette → 2 | Warm golden |
| 🎮 Game Dev | pattern → 1 | Spotted/pixelated |
| 🔧 Tool Smith | build → 2 | Stocky frame |
| 🎨 Artist | pattern → 3 | Belly/gradient |
| 🐛 Stubborn Debugger | expression → 3 | Tongue out |
| 🦋 Social Butterfly | palette → 4 | Vibrant purple |
| 🐺 Lone Wolf | size → 2 | Large, imposing |
| 📦 Prolific Publisher | markings → 2 | Forehead 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.
| Badge | Condition | Accessory |
|---|---|---|
| 🐾 First Steps | 1+ sessions | — |
| 📅 Regular | 10+ sessions | — |
| ⚡ Veteran | 50+ sessions | Lightning pin |
| 🌙 Night Owl | 10+ night sessions (22-05 UTC) | Moon charm |
| ☀️ Early Bird | 10+ morning sessions (05-09 UTC) | Sun visor |
| 🚢 Ship Captain | 5+ publishes | Captain hat |
| ⚓ Fleet Admiral | 20+ publishes | — |
| 🐛 Bug Squasher | 20+ build fails | — |
| 🏃 Marathon Runner | 120+ min longest session | Coffee cup |
| 🌟 Social Star | 10+ accepted friendships | Scarf |
| 🎨 Prolific Creator | 5+ active creations | — |
| ⭐ Star Collector | 10+ stars received | Star 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).
| Accessory | Slot | Unlocked by |
|---|---|---|
| Lightning Bolt Pin | pin | Veteran |
| Moon Charm | charm | Night Owl |
| Sun Visor | hat | Early Bird |
| Captain Hat | hat | Ship Captain |
| Coffee Cup | held | Marathon Runner |
| Scarf | neck | Social Star |
| Star Pin | pin | Star 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 URL2. 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=xxx3. 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=xxx4. 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.
| Event | Description |
|---|---|
checkout.completed | One-time payment completed |
checkout.cancelled | Session expired or was cancelled |
checkout.denied | Parent denied child purchase |
subscription.created | New subscription activated |
subscription.renewed | Subscription renewed (invoice paid) |
subscription.cancelled | Subscription set to cancel at period end |
subscription.expired | Subscription ended |
subscription.payment_failed | Subscription 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 objectDelete 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 = enabledXP & 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
| Activity | XP | Category |
|---|---|---|
| Publish creation | 50 | Create |
| Star given | 5 | Social |
| Star received | 10 | Social |
| Comment posted | 5 | Social |
| Comment received | 10 | Social |
| Friend added | 15 | Social |
| Set mood | 5 | Social |
| Daemon interaction | 5 | Explore |
| Daily login | 10 | Uncapped |
| Level-up bonus | 100 | Uncapped |
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
| Permission | Default | Covers |
|---|---|---|
read | On | View orgs, apps, items, badges, tickets, data |
write | On | Create/edit apps, items, badges, redirect URIs, data |
delete | Off | Delete apps, items, badges, orgs |
credentials | Off | View/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() // → booleanStorage (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 slotCollections (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 itemsInput
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-clickDevice & 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
| Namespace | Methods | Description |
|---|---|---|
omen.users | get, friends | User profiles and friend lists |
omen.storage | get, set, merge | Per-user key-value storage |
omen.items | issue, issueBatch, revoke | Issue and manage items |
omen.collections | create, list, get, delete, insert, query, transaction | Structured data collections |
omen.webhooks | create, list, get, delete, verify | Webhook 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 charsList 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
| Context | Read | Write | Delete |
|---|---|---|---|
| SDK (client-side) | All docs in collection | Own docs only | Own docs only |
| S2S (Bearer token) | All docs | All docs | All 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 dataData 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}.zipAlso 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/zipPer-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
| Type | Default Size | Transparent |
|---|---|---|
| sprite | 64x64 | Yes |
| tile | 32x32 | No |
| background | 1280x720 | No |
| ui | 128x128 | Yes |
| item | 64x64 | Yes |
| icon | 512x512 | No |
| particle | 64x64 | Yes |
Rate Limits
| Limit | Value |
|---|---|
| Max concurrent | 1 (sequential) |
| Max per hour | 20 |
| Cooldown | 2 seconds between generations |
| Max queue | 5 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 gameAsset 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 promptGet 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, immutableDelete Asset
DELETE /api/v1/assets/{id}
Auth: Session cookie or Bearer token
→ 200 { "deleted": true } // soft delete, recoverable 30 daysAssign 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
| Tier | Assets | Total Size |
|---|---|---|
| Free | 100 | 50 MB |
| Plus Starter | 500 | 250 MB |
| Plus Pro | 2,000 | 1 GB |
| Plus Creator | 10,000 | 5 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
| Context | Behavior |
|---|---|
| chat | Standard filtering (profanity, hate, PII for children) |
| username | Stricter — also blocks impersonation (admin, omen, moderator, etc.) |
| content | Most 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
| Access | Limit |
|---|---|
| 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
| Scope | Max Boards |
|---|---|
| Per creation | 5 |
| 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
| Tier | Rating Range |
|---|---|
| Bronze | 0 – 1299 |
| Silver | 1300 – 1599 |
| Gold | 1600 – 1899 |
| Platinum | 1900 – 2199 |
| Diamond | 2200+ |
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
| Status | Meaning |
|---|---|
open | Accepting new members |
in_queue | Party is in matchmaking |
in_game | Party is in a game room |
disbanded | Party 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
| Type | Who | Payload |
|---|---|---|
voice_join | Any | (no payload) |
voice_leave | Any | (no payload) |
voice_mute | Any | (no payload) |
voice_unmute | Any | (no payload) |
voice_offer | Any | { to, sdp } |
voice_answer | Any | { to, sdp } |
voice_ice | Any | { to, candidate } |
voice_config | Host | { enabled, mode, maxSpeakers, teamOnly } |
voice_state | Any | Query 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 stateSpeaker Limits by Tier
| Tier | Max Speakers |
|---|---|
| Free | 4 |
| Growth | 8 |
| Pro | 16 |
| Scale | 64 |
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.