Capture SDK
@capturly/capture-sdk records on your user’s own device, from inside your own
product. It is the browser half of the
capture request flow: your backend creates the request
and mints a per-person link, and the SDK records against it.
npm install @capturly/capture-sdkPublic package with no dependencies. The recording engine is bundled in.
Where the credentials live
Your secret key never reaches a browser. It grants your whole organization.
What the browser gets is the per-person capability from
POST /v1/capture-requests/{id}/contacts — the contact id and the k query
parameter of the returned URL. That capability is scoped to one person,
revocable by removing the contact, and dead once the request’s deadline passes.
For a link anyone can use, take the request’s openLink.url and pass
{ mode: 'open', campaignId, key } instead.
Recording
import { CaptureClient } from '@capturly/capture-sdk';
if (!CaptureClient.isSupported()) {
// No amount of retrying fixes this. Offer another browser.
}
const client = new CaptureClient({
access: { mode: 'contact', contactId, key },
});
const stream = await client.preview(); // triggers the permission prompt
videoEl.srcObject = stream;
await client.start();
// …
const take = await client.stop(); // nothing has left the browser yet
previewEl.src = take.previewUrl; // let them watch it back
await client.submit({
releaseAccepted: true,
onProgress: (fraction) => setBar(fraction),
});
client.destroy();preview() is separate from start() so people can see themselves before
anything records. Between stop() and submit() the file sits in the
browser’s own storage, so a take can be reviewed and re-recorded with
discard() without anything reaching a server.
Call destroy() on unmount, or the camera light stays on after the person has
navigated away.
Consent
If the request requires a release, read its release.text from the API and
show it before recording, then pass releaseAccepted: true only if the person
actually accepted it.
A consent record is written only when the request requires a release and you
pass true. Passing true without showing them anything records an agreement
that never happened, which is worse than none. Passing false on a request
that requires a release refuses the submission rather than storing an
unconsented take.
Errors
Every failure is a CaptureError with a code. What matters is whether
retrying can work, and who has to act:
| Code | Meaning | Retry? |
|---|---|---|
unsupported_browser | No MP4 recording in this browser | No — offer another browser |
permission_denied | Camera or microphone refused | No — your user must allow it |
no_devices | No camera or microphone found | No — your user must connect one |
not_recording, nothing_to_submit | Called out of order | No — fix the call sequence |
invalid_request | The call or its payload was rejected — missing release, missing name, take too long | No — fix the input |
link_expired | Unknown link, or a key that no longer matches | No |
request_closed | The capture request is closed, past its deadline, revoked, or the workspace has lapsed | No |
superseded | Another take for this person landed first | No — their other submission is the live one |
quota_exceeded | No room right now: take too large, or the request is at its cap | No — the workspace owner must act |
rate_limited | Too many requests | Yes, after a pause |
upload_failed | Storage or the API failed server-side | Yes |
network | The request never reached Capturly | Yes |
Only the last three are worth retrying as-is. Retrying anything above them re-uploads the whole take to fail the same way.
What it does not do
Multi-participant sessions. One person at a time. A room where several people record at once needs signaling, TURN and a join ceremony, and is not part of this package. Use the sessions API and the hosted recorder for that.
UI. You get a MediaStream and state events; you render the interface.
Transcription. Capturly delivers masters; transcription is your pipeline’s.
Learning that a take landed
The SDK is not told the recording id, and your backend should not trust the
browser for it. Use the capture_request.submission_received
webhook, or read
GET /v1/capture-requests/{id}/submissions, then download through
GET /v1/recordings/{id}.
Browser support
Chrome and Edge 124+, or Safari 16.4+, on a secure context. Firefox cannot
record: it has neither MP4 MediaRecorder nor MediaStreamTrackProcessor, so
CaptureClient.isSupported() is always false there.
CaptureClient.isSupported() is the check to run before showing a record
button.