This guide walks you through building a complete native app on Orceum from scratch. We’ll build a Todo Manager that lets users create and list tasks via the assistant.

What We’re Building

A FastAPI app that:
  • Accepts action calls from Orceum (create_task, list_tasks, delete_task)
  • Receives an API key per user (injected automatically by Orceum)
  • Sends lifecycle webhook events on install/uninstall
  • Pushes a webhook event when a background job completes

Step 1: Plan Your App

Before writing code, define the actions your app will support:
ActionDescriptionParameters
create_taskCreate a new tasktitle (required), due_date, priority
list_tasksList the user’s tasksstatus filter, limit
delete_taskDelete a task by IDtask_id (required)

Step 2: Build Your App

# main.py
from fastapi import FastAPI, Request, Header, HTTPException
from typing import Optional
from datetime import datetime
import uuid

app = FastAPI(title="Todo Manager")

# Simple in-memory store (use a real DB in production)
task_store: dict[str, list] = {}

@app.post("/actions")
async def handle_action(
    request: Request,
    x_orceum_installation_id: Optional[str] = Header(None),
    authorization: Optional[str] = Header(None)
):
    if not x_orceum_installation_id:
        raise HTTPException(status_code=400, detail="Missing X-Orceum-Installation-Id")

    body = await request.json()
    event = body.get("event")
    event_data = body.get("event_data", {})
    installation_id = x_orceum_installation_id

    if event == "create_task":
        return await handle_create_task(installation_id, event_data)
    elif event == "list_tasks":
        return await handle_list_tasks(installation_id, event_data)
    elif event == "delete_task":
        return await handle_delete_task(installation_id, event_data)
    else:
        raise HTTPException(status_code=400, detail=f"Unknown action: {event}")


async def handle_create_task(installation_id: str, data: dict) -> dict:
    title = data.get("title")
    if not title:
        raise HTTPException(status_code=400, detail="title is required")

    task = {
        "task_id": str(uuid.uuid4())[:8],
        "title": title,
        "due_date": data.get("due_date"),
        "priority": data.get("priority", "normal"),
        "status": "open",
        "created_at": datetime.utcnow().isoformat()
    }
    task_store.setdefault(installation_id, []).append(task)
    return {"result": task}


async def handle_list_tasks(installation_id: str, data: dict) -> dict:
    tasks = task_store.get(installation_id, [])
    status_filter = data.get("status")
    if status_filter:
        tasks = [t for t in tasks if t["status"] == status_filter]
    limit = data.get("limit", 10)
    return {"result": {"tasks": tasks[:limit], "total": len(tasks)}}


async def handle_delete_task(installation_id: str, data: dict) -> dict:
    task_id = data.get("task_id")
    if not task_id:
        raise HTTPException(status_code=400, detail="task_id is required")
    tasks = task_store.get(installation_id, [])
    original_count = len(tasks)
    task_store[installation_id] = [t for t in tasks if t["task_id"] != task_id]
    if len(task_store[installation_id]) == original_count:
        raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
    return {"result": {"deleted": True, "task_id": task_id}}

Step 3: Handle Lifecycle Webhooks

# webhooks.py
import hmac
import hashlib
from fastapi import APIRouter, Request, Header, HTTPException
from typing import Optional

router = APIRouter()

ORCEUM_WEBHOOK_SECRET = "orc_sk_your_secret_from_registration"

@router.post("/webhooks/lifecycle")
async def lifecycle_webhook(
    request: Request,
    x_orceum_signature: Optional[str] = Header(None)
):
    body = await request.body()

    if not x_orceum_signature:
        raise HTTPException(status_code=401, detail="Missing signature")

    provided_sig = x_orceum_signature.removeprefix("sha256=")
    expected_sig = hmac.new(
        ORCEUM_WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(provided_sig, expected_sig):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = await request.json()
    event = payload["event"]

    if event == "INSTALLED":
        installation_id = payload["installation_id"]
        user_email = payload["user_email"]
        # In production: store installation_id → user mapping in your DB
        task_store[installation_id] = []
        print(f"New install: {installation_id} for {user_email}")

    elif event == "UNINSTALLED":
        installation_id = payload["installation_id"]
        task_store.pop(installation_id, None)
        print(f"Uninstalled: {installation_id}")

    return {"status": "ok"}

Step 4: Testing Locally (Optional)

If you are developing locally, Orceum won’t be able to reach your localhost server. You can use a tool like ngrok to expose your local server to the internet.
# Run your FastAPI app locally
uvicorn main:app --port 8000

# In a separate terminal, expose port 8000 with ngrok
ngrok http 8000
ngrok will give you a public URL (e.g., https://a1b2c3d4.ngrok-free.app). Use this URL as your Endpoint URL and Webhook URL in the next step when registering your app.

Step 5: Register Your App

Go to the Orceum Developer Studio, click Create App, and select Native App.
  1. Basic Info: Name it “Todo Manager” and provide a clear description.
  2. Endpoint URL: Enter your base URL (e.g., https://todo.yourapp.com).
  3. Authentication: Select API Key. Set the Header Name to Authorization and the Prefix to Bearer.
  4. Manifest: Paste the following JSON to define your actions:
[
  {
    "name": "create_task",
    "description": "Create a new task or to-do item. Use when the user wants to add something to their task list.",
    "endpoint": "/actions",
    "parameters": [
      {"name": "title", "type": "string", "description": "The task title", "required": true},
      {"name": "due_date", "type": "string", "description": "Due date in YYYY-MM-DD format", "required": false},
      {"name": "priority", "type": "string", "description": "Priority: low, normal, high, urgent", "required": false}
    ]
  },
  {
    "name": "list_tasks",
    "description": "List the user's tasks. Use when the user asks what they have to do.",
    "endpoint": "/actions",
    "parameters": [
      {"name": "status", "type": "string", "description": "Filter by status: open, completed, all", "required": false},
      {"name": "limit", "type": "integer", "description": "Max tasks to return. Default 10", "required": false}
    ]
  },
  {
    "name": "delete_task",
    "description": "Delete a task from the user's list. Requires the task_id.",
    "endpoint": "/actions",
    "parameters": [
      {"name": "task_id", "type": "string", "description": "The ID of the task to delete", "required": true}
    ]
  }
]
  1. Webhooks: Enter your installation_webhook_url. Toggle Events Enabled on and copy your generated webhook secret (whs_...) from the dashboard.
  2. Installation Guidance: Add hints for the user to find their API key (e.g., “Go to your Todo app account → Settings → API”).
Save your Orceum Webhook Secret! When you create the app, Orceum displays an orc_sk_... secret. Copy this into your backend immediately — it is only shown once and is required to verify signatures on lifecycle webhooks.

Step 6: Install and Test

  1. Navigate to your newly created app in the Orceum Store.
  2. Click Install.
  3. When prompted by the UI, enter your test API key (e.g., sk-myapikey123).
Now open Orceum and say: “Add a task: review the Q4 budget by Friday” The assistant will call create_task on your app, receive the result, and respond: “Done — I’ve added ‘Review the Q4 budget’ to your list, due Friday.”

Checklist

  • Action handler routes on event field
  • X-Orceum-Installation-Id extracted and used for user isolation
  • Lifecycle webhook handler signature verification implemented
  • orc_* secret stored in secrets manager
  • HTTPS on base_url and webhook URLs
  • Test action calls manually before publishing