Embed the recorder
Put a record button on a page you run. Pressing it opens the script.rip recorder in its own window, signed in as the person using your page. When they stop, the recording is transcribed with the speakers separated, and the transcript is sent back to your page. Your page never holds an API key, and it does not need one: the person's own script.rip session does the work.
This is for a page a person is looking at. A program with no person present uses the runs API instead.
Why a popup and not an iframe
A browser only gives a page the microphone in a secure context — https, or localhost. An https
frame inside a plain-http page is not a secure context, so a recorder framed into a LAN app
cannot hear anything. A top-level script.rip window can. The recorder therefore always opens as a
popup, and it refuses to be framed at all (frame-ancestors 'none').
Before you start: allow your page
Sign in to script.rip and open Account → Embedding. Add the exact origin your page is served from — scheme, host and port, no path:
http://192.168.0.100:8081
Plain http is accepted only for a private-network address (your LAN, localhost, a .local
name, Tailscale's 100.x addresses). A public address must be https. Only listed pages can use the
recorder; any other page that opens it receives nothing — not a transcript, not an error.
When your page is on a public domain
Any https origin can be allowed — https://app.example.com works exactly like the LAN address
above, and is the safer choice because nobody on the network can pose as it. Two headers on
your page decide whether the recorder works there:
Cross-Origin-Opener-Policy.same-originsevers the link between your page and any cross-origin window it opens, so the recorder has nowhere to send the transcript and your page receives onlyonClosed. Sendsame-origin-allow-popups, or no header at all.Content-Security-Policy. If you send one,script-srcmust allow the script.rip origin:
Content-Security-Policy: script-src 'self' https://<host>
Nothing else about your page needs to change: the recorder is a separate window, so your
frame-src, connect-src and media-src are never consulted.
The snippet
<script src="https://<host>/embed/v1.js"></script>
<button id="record">Record this call</button>
<script>
document.getElementById('record').addEventListener('click', function () {
ScriptRip.record({
title: 'Call — Fox Den Store-It (HWY 51)',
context: { app: 'harvest-moon', providerId: 'k17' },
consent: 'all-party',
jurisdiction: 'IL',
onResult: function (r) { document.getElementById('notes').value += r.text; },
onError: function (e) { console.warn(e.message); },
});
});
</script>
Call ScriptRip.record inside the click handler itself. A popup opened after an await, a
timer or a network request is blocked by every browser, and onError reports popup_blocked.
ScriptRip.record(options) → { cancel() }
| Option | Default | Means |
|---|---|---|
| title | none | Shown in the recorder and stored as the run's title. At most 200 characters. |
| context | null | Any JSON value up to 4 KB. Stored with the run and handed back untouched in the result, so you know which record to fill. |
| consent | 'all-party' | 'all-party', 'one-party' or 'none' — which recording reminder to show. |
| jurisdiction | none | A short label such as 'IL', shown in the reminder and stored with it. |
| format | 'speakers' | Which rendering text carries: prose, speakers, timestamped, srt, vtt or json. |
| onStatus(s) | — | Progress: capture state while recording, then the run's status. |
| onResult(r) | — | The finished transcript. |
| onError(e) | — | Something failed; e.message is a sentence for a person. |
| onClosed(c) | — | The window closed. c.runId is set once a recording exists. |
cancel() asks the recorder to close. It closes at once if nothing has been recorded; while a
recording is in progress it keeps what was recorded and asks the person to press stop.
The reminder is a reminder, not legal advice. With all-party the recorder shows a blocking
dialog — a line to say aloud and a required The other party agreed checkbox — and capture cannot
start until it is checked. With one-party it shows a short note. What was shown, the jurisdiction
and the time of the acknowledgement are stored with the run and returned in result.consent. Your
page decides which mode applies; script.rip does not know the law of any place.
Collecting a result later
If the person closes the window before the transcript is ready, the run continues in their
account and onClosed carries its runId. Keep it, and collect the result later — again from a
click:
ScriptRip.collect({ runId: savedRunId, onResult: fill, onError: report });
A run is delivered only to the page that recorded it.
The result
{ "source": "script.rip", "v": 1, "nonce": "3f9c…", "type": "result",
"runId": "0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
"runUrl": "https://<host>/runs/0195c8e4-8d02-7c19-b3e7-5a1f9d20c68b",
"durationSec": 412.4, "language": "en", "format": "speakers",
"speakers": [ { "id": "SPEAKER_00", "label": "Trevor", "name": "Trevor" },
{ "id": "SPEAKER_01", "label": "Speaker 2" } ],
"segments": [ { "speaker": "SPEAKER_00", "startSec": 0.0, "endSec": 3.9, "text": "Hi, this is Trevor." } ],
"text": "Trevor: Hi, this is Trevor.\n\nSpeaker 2: Fox Den, how can I help?",
"consent": { "mode": "all-party", "jurisdiction": "IL", "acknowledged": true,
"acknowledged_at": "2026-09-15T15:00:00.000Z", "text_shown": "…",
"surface": "embed", "origin": "http://192.168.0.100:8081" },
"context": { "app": "harvest-moon", "providerId": "k17" } }
A speaker who introduced themselves is named; anyone else is numbered. Speakers are separated for the first 60 minutes of a recording; anything later is transcribed without speaker names, and the recorder says so while it is recording.
The raw protocol
For a page that does not load v1.js. Open
https://<host>/embed/record?v=1&origin=<your origin>&nonce=<16–64 random URL-safe characters>
(add &run=<id> to collect) with window.open from a click, then listen for messages.
Popup → your page. Every message is posted with your origin as its target and carries
source: "script.rip", v: 1 and your nonce. Accept a message only when event.origin is the
script.rip origin, event.source is the window you opened, and the nonce is yours.
| type | When | Carries |
|---|---|---|
| ready | The recorder verified your origin | — reply with init |
| status | Capture changed, or the run moved | status (a run status, or null before the run exists), capture, elapsedSec, step, runId, runUrl |
| result | The transcript is ready | the fields above |
| error | Something failed | code (an API error code) or error_class (a run failure class), message, and runId when there is one |
| closed | The window is closing | reason (user, canceled, done), runId, runUrl |
Your page → popup, posted to the script.rip origin, with source: "script.rip:opener", v: 1
and your nonce: init (the options above, minus the callbacks) in answer to ready, and cancel.
If no init arrives within three seconds the recorder proceeds on the defaults.