Skip to content
LifeLayer Apps
OneSweep NeverMiss PassVault AudioTale Canopus Support
Apps Support
Canopus

API Reference

Canopus · Your local AI agent

Last updated 23 October 2026

Canopus is just the interface. This is everything a server needs to answer for the app to work. We do not provide a server, or support for building or running one.

On this page

  1. 1 Basics
  2. 2 Health check
  3. 3 Models and settings
  4. 4 The agent
  5. 5 Job status
  6. 6 Media files
  7. 7 Backup
  8. 8 Voice mode
  9. 9 Limits

1.Basics

The user enters your server’s base URL in Settings, such as https://ai.example.com or http://192.168.1.20:8000. Every path below is under /v1 on it. Use whatever language, framework and models you like: the app only cares about the requests and responses on this page.

The AI agent runs on your server. The app sends each turn of a chat with the settings the user chose, and your server writes the prompts, calls its models, uses its tools, makes the media and streams back what it does (section 4). The app keeps the chats on the device and shows what comes back.

  • JSON everywhere, except file uploads, which are multipart/form-data.
  • Errors. Any response that is not 2xx should carry this body. The app shows message to the user, so write it for a person.
{
  "error": {
    "message": "The image model is not installed."
  }
}
  • Authentication. The app sends no credentials of its own. If your server sits behind Cloudflare Access, the user can enter a service token under General in Settings, and the app adds CF-Access-Client-Id and CF-Access-Client-Secret headers to every request to that host, voice mode included. Anything else, keep on a private network.
  • HTTPS is recommended for anything reachable from the internet. Plain http:// works, and the app warns the user that it is not encrypted.
  • Optional parts. A server can start with just the health check, the models list and the agent turn. Media and voice mode can come later; until then, have your agent tell the user what it can’t do.

2.Health check

GET /v1/health

Called when the user tests the connection in Settings, and before each agent turn. Any 2xx response counts as healthy, and the body is ignored.

{
  "ok": true
}

3.Models and settings

GET /v1/models

The app reads this when Settings opens and when it comes back to the foreground, so a model or a setting you add on the server shows up without an app update. It has two parts: models, the text models the user chats with, and catalog, the media models and their settings. The app builds its Models settings from it, and sends back what the user chose with every agent turn (section 4).

{
  "models": [
    {
      "id": "my-model",
      "name": "My Model",
      "params": [
        {
          "key": "temperature",
          "label": "Temperature",
          "type": "float",
          "default": 0.8,
          "min": 0,
          "max": 2,
          "step": 0.05
        },
        {
          "key": "thinking",
          "label": "Thinking",
          "type": "bool",
          "default": true
        }
      ]
    }
  ],
  "catalog": {
    "image": {
      "label": "Image",
      "models": [
        {
          "id": "image-a",
          "name": "Image A",
          "about": "Fast, keeps one face"
        },
        {
          "id": "image-b",
          "name": "Image B",
          "about": "Combines several pictures",
          "params": [
            {
              "key": "film_look",
              "label": "Film look",
              "type": "bool",
              "default": false,
              "ask": false
            }
          ]
        }
      ],
      "params": [
        {
          "key": "aspect",
          "label": "Aspect",
          "type": "enum",
          "default": "auto",
          "options": [
            "auto",
            "16:9",
            "1:1",
            "9:16"
          ]
        },
        {
          "key": "resolution",
          "label": "Megapixels",
          "type": "enum",
          "default": 2,
          "options": [
            1,
            2,
            4
          ]
        }
      ]
    },
    "video": {
      "label": "Video",
      "models": [
        {
          "id": "video-a",
          "name": "Video A"
        }
      ],
      "params": [
        {
          "key": "duration",
          "label": "Length (s)",
          "type": "int",
          "default": 5,
          "min": 1,
          "max": 30
        }
      ]
    },
    "music": {
      "label": "Music",
      "models": [
        {
          "id": "music-a",
          "name": "Music A"
        }
      ],
      "params": []
    },
    "speech": {
      "label": "Speech",
      "models": [
        {
          "id": "voice-a",
          "name": "Voice A"
        }
      ],
      "params": [
        {
          "key": "gender",
          "label": "Voice",
          "type": "enum",
          "default": "female",
          "options": [
            "female",
            "male"
          ]
        }
      ]
    }
  }
}

Text models (models), default first:

  • id (required) is what the app sends back as model. name is shown to the user.
  • params: settings the user can change for it (below), such as temperature and thinking.
  • Anything else you add, such as what a model can read or make, is for your own agent; the app ignores it.

Media catalog (catalog): one entry per kind, keyed image, video, music and speech. Each has a label, its models (default first) and the params every model of that kind takes. A model can add its own params, which apply only to what it makes. A kind you leave out has no page in the app’s settings.

A setting is an object with these fields:

  • key, label and type: int, float, bool or enum.
  • default, and for numbers min, max and step.
  • options for an enum: strings or numbers.
  • hint: an optional line of help shown under it.
  • ask: the user can leave a setting on Ask, and your agent should then ask them in the chat before making anything. "ask": false marks a setting that is never asked about; until the user sets it, it is its default.

4.The agent

Each time the user sends a message, the app first makes sure your server has the chat’s files, then sends the turn and reads the reply as it streams. A server without these paths should answer 404, and the app tells the user it doesn’t run the Canopus agent.

POST /v1/agent/files/missing

Before each turn, the app asks which of the chat’s files your server does not have yet: {"files": ["<name>", ...]}, answered with {"missing": ["<name>", ...]}. A name is the one the app keeps the file under (a UUID with an extension), and it never changes.

POST /v1/agent/files

The missing files then go up one per request, as multipart/form-data with one file part whose filename is that name. Keep them for later turns. Each name is checked again before every turn, so you may delete files you have not needed for a while.

POST /v1/agent/turn

{
  "model": "my-model",
  "media_mode": null,
  "instructions": "",
  "user_name": "Sam",
  "zone": "Europe/London",
  "now": "2026-10-16T18:30:00.000",
  "prefs": {
    "engines": {
      "image": "image-a"
    },
    "values": {
      "image": {
        "aspect": "16:9"
      },
      "my-model": {
        "temperature": 0.6
      }
    }
  },
  "history": [
    {
      "role": "user",
      "text": "Draw me as a pirate",
      "attachments": [
        {
          "kind": "image",
          "mime": "image/jpeg",
          "name": "IMG_0412.jpg",
          "file": "3f2a9c1e-7b4d-4e8a-9f61-2c5d8b0e4a17.jpg"
        }
      ]
    }
  ]
}
  • history: the newest 40 messages, oldest first, ending with the user’s newest. Each attachment (image, video, audio or pdf) names a file sent above.
  • model: the chat’s text model, or null for your default. Treat an unknown id as your default too. media_mode: what the user picked in the composer (image, video, or audio: speech or music, whichever fits), or null. Older apps send speech or music. instructions: the chat’s own system prompt, or empty.
  • now is the device’s local time and zone its time zone, so “now” is the user’s time wherever your server is. user_name is what the user asked to be called, or empty.
  • prefs: the user’s choices in Settings > Models. engines maps a media page (image, video, speech, voice) to a model id from your catalog; values maps a media page or a text model id to its settings. Anything missing is on Ask. Check them: drop keys you do not know and keep values within range.

Answer with server-sent events, one JSON object per data: line:

  • status while working, such as Searching: ...; the app shows it under the reply.
  • text as the reply is written, in Markdown. The app joins the pieces.
  • job for each picture, video, song or recording you start making, with its job_id, kind and a short label. The app follows it with section 5 and adds the files to the reply when they are done, even if the user has closed the app meanwhile.
  • {"type": "ping"} at least every 30 seconds while the agent works without saying anything.
  • Finish with {"type": "done"}, or {"type": "error", "message": "...", "status": 500}.

The app closes the connection when the user taps Stop.

data: {"type":"status","text":"Thinking…"}

data: {"type":"text","text":"Making it now."}

data: {"type":"job","job_id":"a1b2c3","kind":"image","label":"1 image(s)"}

data: {"type":"done"}

POST /v1/agent/enhance

The instructions screen’s Enhance: {"model": "my-model", "draft": "..."}, answered with {"text": "..."}, the draft rewritten into a full system prompt.

5.Job status

GET /v1/jobs/{job_id}

The app asks every few seconds while a job runs. Jobs should outlive the app: the user may close it and come back later for the result.

{
  "status": "done",
  "label": "2 image(s)",
  "progress": "Drawing 2/2",
  "result": {
    "attachments": [
      {
        "kind": "image",
        "mime": "image/png",
        "name": "beach-1.png",
        "url": "media/beach-1.png"
      },
      {
        "kind": "image",
        "mime": "image/png",
        "name": "beach-2.png",
        "url": "media/beach-2.png"
      }
    ]
  },
  "error": null
}
  • status: queued, running, done or failed.
  • progress: an optional line shown while it runs.
  • result, once it is done: the files it made. error, once it has failed: why, for the user.
  • Each attachment has a kind (image, video, audio or pdf), a mime type, a file name and a url. The URL can be absolute, or relative to the base URL.
  • Videos should be H.264 MP4, and sounds AAC (.m4a) or MP3, so the device can play them.

6.Media files

The app downloads each file as soon as its job is done and keeps its own copy, so a file only has to stay available until then.

DELETE /v1/media/{name}

Sent when the user deletes a conversation, for each file your server made for it, by its name. Delete the file. Any 2xx response is fine, and the body is ignored.

7.Backup

Under Data in Settings, Back up to server sends every chat, archived ones included, with its files, and Restore from server brings the last backup back. Keep one backup: each new one replaces the one before, so a chat the user deleted since the last backup is gone after the next, together with its files. File names are the ones the app keeps its files under (a UUID with an extension). A server without these paths answers 404, and the app shows the error.

POST /v1/backup/files/missing

First the app asks which of the chats’ files the backup does not have yet: {"files": ["<name>", ...]}, answered with {"missing": ["<name>", ...]}. A later backup only sends new files.

POST /v1/backup/files

The missing files then go up one per request, as multipart/form-data with one file part whose filename is that name.

PUT /v1/backup

{
  "version": 1,
  "created_at": 1792765800000,
  "conversations": [
    {
      "id": "<uuid>",
      "title": "Trip photos",
      "archived": 1,
      "created_at": 1792765000000,
      "updated_at": 1792765800000,
      "...": "...",
      "messages": [
        {
          "id": "<uuid>",
          "role": "user",
          "text": "make it brighter",
          "created_at": 1792765000000,
          "...": "...",
          "attachments": [
            {
              "id": "<uuid>",
              "kind": "image",
              "mime": "image/jpeg",
              "name": "IMG_1.jpg",
              "local_path": "<name>",
              "size_bytes": 1830211,
              "...": "..."
            }
          ]
        }
      ]
    }
  ]
}

Last come the chats, as the app keeps them: each conversation with its messages, each message with its attachments. Store the body as it is; the app may add fields. archived is 1 for a chat in the archive, and local_path names a file sent above. Answer 400 if a file is missing. Replace the old backup only once the new one is saved, then delete the files it no longer names.

GET /v1/backup

Restoring starts here: answer with the body of the last PUT, or 404 when there is no backup.

GET /v1/backup/files/{name}

Then each file the device does not have yet. The app downloads every file before it changes anything, puts each chat in the backup in place of its own copy (archived ones go back to the archive), and keeps chats that are not in it.

8.Voice mode

WS /v1/call

Voice mode is one WebSocket per conversation: the app sends what the user says as sound, and your server hears it, writes the reply and speaks it back sentence by sentence. Messages are JSON text frames, except the spoken reply, which comes as binary frames.

1. The app opens the call:

{
  "type": "start",
  "model": "my-model",
  "engine": "voice-a",
  "options": {
    "gender": "female"
  },
  "llm": {
    "temperature": 0.7
  },
  "language": "en"
}

model is the text model, engine and options the voice (from your speech catalog), llm the text model’s settings and language the language to speak at first. Get ready, then send {"type": "ready"}. To tell the user something, such as another voice standing in for the one they chose, send {"type": "note", "text": "..."} first.

2. Each time the user pauses, the app sends {"type": "audio", "data": "<base64 WAV>"}. Answer with {"type": "heard", "text": "...", "language": "en"}, with an empty text if it was not speech. Reply in the language the user last spoke.

A turn longer than 30 seconds comes in pieces: each one but the last carries "partial": true. Don’t answer a piece on its own. Send one heard for the whole turn, with the pieces’ text joined in order, once the last piece arrives.

3. The app then sends the turn: the chat’s newest 40 messages that have text, with the same now, zone, user_name and instructions as an agent turn. Write the call’s prompt from it. With tools, the model may search the web; send {"type": "searching"} while it does.

{
  "type": "turn",
  "tools": true,
  "voice": {
    "history": [
      {
        "role": "user",
        "text": "What is the weather like tomorrow?"
      }
    ],
    "now": "2026-10-16T18:30:00.000",
    "zone": "Europe/London",
    "user_name": "Sam",
    "instructions": ""
  }
}

4. While the reply is written and spoken, send:

  • {"type": "text", "delta": "..."} as the text comes in;
  • for each sentence, {"type": "say", "text": "..."}, then one binary frame holding that sentence as a WAV;
  • at the end, {"type": "done", "text": "<the whole reply>", "cancelled": false}.

5. When the user talks over the reply, the app sends {"type": "cancel"}, and a new turn cancels the one running too. Stop writing and speaking, and send done with "cancelled": true and the text so far.

6. A turn that fails sends {"type": "error", "message": "..."} and the call stays open. Closing the socket ends the call.

9.Limits

  • The app waits 15 seconds to connect and 120 seconds for a whole response that is not streamed.
  • During an agent turn it waits up to 3 minutes between two events.
  • Uploads are at most 100 MB for a video, 25 MB for a PDF or a sound, and 20 MB for a picture.
Back to Canopus
LifeLayer Apps

Small apps for the parts of the day that keep slipping.

lifelayer.apps@gmail.com

Apps

  • OneSweep
  • NeverMiss
  • PassVault
  • AudioTale
  • Canopus

Help

  • Support
  • Email us

Privacy

  • OneSweep
  • NeverMiss
  • PassVault
  • AudioTale
  • Canopus

© 2026 LifeLayer Apps. All rights reserved.