---
title: "The OpenVibe.Cam API and camera element"
canonical: "https://openvibe.cam/developers"
updated: null
---

# The OpenVibe.Cam API and camera element

HTML: [The OpenVibe.Cam API and camera element](https://openvibe.cam/developers)

Developers

# The Cam API and the camera element

The camera app is also one framework-free element: `<ov-camera>` runs the preview, the filters, the timer and the burst in the browser, and hands you the picture. The same page has the JSON API under `/api/v1`, with `application/problem+json` errors.

## OpenVibeCam: record on your site

One script opens Cam on the calling page. On a phone it fills the screen; on a computer it opens a centred stage. The person sees a plain permission prompt, records, reviews, trims and chooses *Use this*. Cam hands your page a `File` and metadata; your page decides where to upload it. Cancelling resolves to `null`.

For an in-place studio, the same SRI-pinned script exposes `OpenVibeCam.core`: `devices()`, `constraints({ facing, lens, mic, resolution })`, `open(options)`, `meter(stream, onLevel)`, `network()` and `stop(stream)`. Only `open()` asks for camera or microphone access; call it after the person's explicit action. Saved lens and mic ids are hints on another browser and fall back to its available devices.

~~~~
<script src="https://openvibe.cam/embed/cam-kit.js" integrity="sha384-6D72Zg6MS9RCQAiXgnP8R8iz/criDdCCgY48Jb87HRjzkVTvCDCCCmDs0E0RLE3s" crossorigin="anonymous" defer></script>
<button id="record">Record a clip</button>
<script>
  document.getElementById('record').addEventListener('click', async function () {
    const result = await OpenVibeCam.open({ mode: 'video', maxSeconds: 60 });
    if (result) { /* result.file is yours to upload or preview */ }
  });
</script>
~~~~

`OpenVibeCam.open({ mode: 'video' | 'photo' | 'screen', maxSeconds, timer, onDone })` resolves to `{ file, mode, mimeType, duration, width, height, maxSeconds }`. `onDone(result)` runs when Use this is pressed and may return a promise. `mode` defaults to `video`, `maxSeconds` defaults to 60, and `timer` is 0, 3 or 10 seconds. The screen option appears when the browser supports screen capture. Space starts or stops recording; Esc closes. Photos and recordings stay in the browser until your page handles the file. To upload, pass `result.file` to your site's uploader; use ov-upload's resumable path when it is available.

The demo keeps the file on this page.

## The <ov-camera> element

One script, one tag. Nothing starts until a person clicks or your code calls `start()`; with `upload="off"` (the default) the picture never leaves the browser. It uses a shadow root and two CSS custom properties, so a site's own styles cannot leak in.

~~~~
<script src="https://openvibe.cam/embed/ov-camera.js" async></script>
<ov-camera id="my-cam" facing="user" filters="none,mono,warm,cool,fade,vivid,blur" timer="0" mode="photo" upload="off"></ov-camera>

<script>
  document.getElementById('my-cam').addEventListener('ov-camera:capture', function (e) {
    // e.detail = { blob, width, height, filter }
    var img = new Image();
    img.src = URL.createObjectURL(e.detail.blob);
    document.body.appendChild(img);
  });
</script>
~~~~

Paste that once. `ov-camera:capture` fires for every photo (three in `mode="burst"`); `ov-camera:error` names what went wrong. The live demo below adds the photo to this page and nowhere else.

Take a photo; it stays in this page.

### Attributes

| Attribute | Values | What it does |
| --- | --- | --- |
| `facing` | `user` \| `environment` | The front camera (`user`, the default) or the back one (`environment`). |
| `filters` | `none`, `mono`, `warm`, `cool`, `fade`, `vivid`, `blur` | A comma list of the filters the strip offers. Unknown names are dropped; an empty list is `none`. |
| `timer` | `0` \| `3` \| `10` | A countdown in seconds before a capture, with the number over the preview. Anything else is `0`. |
| `mode` | `photo` \| `burst` | `photo` (the default) takes one; `burst` takes three, 450 ms apart, and fires three events. |
| `upload` | `off` \| `openvibe` | `off` (the default) never sends the picture. `openvibe` posts it to `POST /api/v1/photos` as the signed-in person, and only when the page is served from openvibe.cam. |

### Events

| Event | detail | When |
| --- | --- | --- |
| `ov-camera:capture` | `{ blob, width, height, filter }` | A photo is ready: a camera capture, a burst shot or a picture picked from disk. The event bubbles and crosses the shadow boundary. |
| `ov-camera:error` | `{ code, message }` | The camera or the upload failed. Codes: `camera_unavailable`, `permission_denied`, `not_started`, `capture_failed`, `file_unreadable`, `upload_unavailable`, `upload_denied`, `upload_failed`. |

### Methods

| Method | Returns | What it does |
| --- | --- | --- |
| `start()` | a promise | Asks for the camera; a click is the safe place to call it. It is never called on connect. |
| `stop()` | nothing | Stops every track and returns the viewfinder to the off card. |
| `capture()` | a promise of the detail (three in burst) | Takes a photo through the same path as the shutter, honouring `timer` and `mode`. |
| `flip()` | a promise of the new facing | Switches front/back and restarts the camera if it is on. |

Style it with `--ov-camera-accent` (default `#00c7d2`) and `--ov-camera-radius` (default `26px`). No camera permission? The element keeps its *Turn on the camera* button and the *Or pick a picture* picker, so a site still gets an image.

In an app with a build step, [OpenVibe.SDK](https://github.com/OpenVibers/OpenVibe.SDK) wraps the loader: `import { defineOvCamera } from 'openvibe-sdk/cam'; await defineOvCamera();` injects the script once and returns the element class. The typed attributes, events and methods are in the entry's `.d.ts`.

## The API

JSON under `/api/v1`, with `application/problem+json` errors. A person is the `cam_at` session cookie or a Network user token as a Bearer. No route needs an app or service token.

- `POST /api/v1/photos`: A person; the picture is the raw body (`image/jpeg`, `image/png` or `image/webp`), with optional `X-Filter`, `X-Caption` and `X-Camera` headers (URI-encoded). Cam uploads it to OpenVibe.Pics and stores the link.
- `GET /api/v1/photos`: Your photos, newest first.
- `GET /api/v1/photos/:id`: One of your photos.

## My cameras and viewer grants

- `GET /api/v1/me/camera-settings`: Your saved facing, lens, mic, resolution and mirror choices. Defaults are returned before you save.
- `PUT /api/v1/me/camera-settings`: `{ facing?, lens?, mic?, resolution?, mirror? }`. Partial updates preserve your other choices; a signed-in person only.
- `GET /api/v1/cameras`: Your paired cameras with their active grant count.
- `POST /api/v1/cameras`: `{ name, source, stream_id?, slot?, notes? }`. A stream id is read from Live and must be on your channel.
- `PATCH /api/v1/cameras/:id`: `{ name?, source?, status?, slot?, notes? }`; the owner only.
- `DELETE /api/v1/cameras/:id`: Unpair it; every grant on it goes. The owner only.
- `GET /api/v1/cameras/:id/wall`: The private wall: owner, or a viewer whose grant is active. A stranger gets 404, never a hint that it exists.
- `GET /api/v1/cameras/:id/grants`: The grants you gave, including expired and revoked ones. The owner only.
- `POST /api/v1/cameras/:id/grants`: `{ viewer_username }` or `{ viewer_subject }`, and `{ days }` (1–90, default 7). An existing active grant is extended, never duplicated.
- `DELETE /api/v1/cameras/:id/grants/:grantId`: Revoke at once: the next wall request is refused.

## The Live cam button

Live calls Cam with the viewer's own Network token — the token in the viewer's Live session — because the picture is saved to that person's OpenVibe.Pics. Cam needs no service token from Live.

1. Live receives `{ slot, stream_id, caption }` from its watch page and calls `POST https://openvibe.cam/api/v1/stills` with `Authorization: Bearer <viewer's Network token>`.
2. Cam reads `GET https://openvibe.media/live/:slot/frame.jpg?w=640`. A 404 means the slot is offline; SVG offline cards and HTML errors are never uploaded.
3. Cam calls `POST https://openvibe.pics/api/v1/images` with the viewer's token, the JPEG as the body, `X-Title` and `X-Filename`.
4. Cam answers `201 { still, share: { url, caption, text } }`. Live posts `share.text` to the stream's chat with its existing chat authority and shows the still.

~~~~
POST /api/v1/stills
{ "slot": "whip", "stream_id": 1234, "caption": "the exact second the robot fell over" }
~~~~

A still is `unlisted` by default: anyone with the link opens `/s/<id>`; it is not listed anywhere. Pass `"visibility": "private"` to keep it to yourself. Media's frame endpoint returns at most one frame every five seconds per slot and is rate-limited; a busy frame is `429 cam.frame_busy`.

## Stills

- `POST /api/v1/stills`: `{ slot, caption?, stream_id?, w?, visibility? }`. A person; fetches Media's frame and uploads it to Pics as them.
- `GET /api/v1/stills`: Your stills, newest first.
- `GET /api/v1/stills/:id`: One still; an unlisted one is readable by anyone with the link, a private one only by its owner.

## Privacy

A camera is private. Staff have no bypass; a global moderator without a grant is refused like anyone else. Cam stores no image bytes, no face template and no identity classification. It stores a camera's name and slot, a grant's two subjects and expiry, and a picture's Pics slug, links, filter name and caption. Account export and deletion cover cameras, grants, photos and stills; the picture itself is erased by OpenVibe.Pics on the same Network event.
