SDKs
The Overturo client libraries: install, first calls, request shapes, errors — and the Forms entry points.
Choose your integration: Server-side REST API → Ruby SDK. Browser-side consent flow embed → JavaScript SDK.
JavaScript SDK
Browser-side SDK for embedding Overturo consent flows in your application's UI. For server-side REST API access (creating applications, managing flows, handling webhooks), see the Ruby SDK.
Don't need programmatic control? An HTML-only embed (<a> redirect or <iframe>) covers most integrations without npm or build tooling — see Embedding.
Installation
npm install @overturo/js
yarn add @overturo/js
<script type="module">
import { Overturo } from "https://esm.sh/@overturo/js"
// initialise as shown in "Initialization" below
</script>
Source: github.com/overturo/js
Initialization
Initialize the SDK with your publishable key. Use pk_test_* for sandbox and pk_live_* for production.
import { Overturo } from "@overturo/js"
const overturo = new Overturo({
publishableKey: "pk_test_abc123"
})
The constructor also accepts:
apiKey— required for preview and token-exchange. Never expose in browser code shipped to end users.baseUrl— API host. Defaults to the production endpoint.flowBaseUrl— consent-flow host. Same value asbaseUrltoday; will diverge when the platform splits API and flow hosts.timeout— request timeout in milliseconds.
Core concepts
A handful of distinctions that affect every call:
- Publishable key vs. API key. The publishable key (
pk_*) is safe in browser code — it can only mint consent sessions. The API key (sk_*) reads internal state (previews, token exchange, server-mints-the-session flows) and must never appear in client-side bundles. - Two host accessors today, one host in production.
baseUrl(API host) andflowBaseUrl(consent-flow host) return the same value today — you can ignore the distinction. They'll diverge when the API and flow hosts split; using both accessors now means your code already handles that day. - Origin model. The SDK pins
postMessageevents to the flow origin and refuses to send to a blank target. The hosting page's origin must appear on your Application's Allowed Origins list or the browser blocks the SDK's API calls at preflight time.
Embed a Consent Flow (high-level)
Embed a consent flow in the browser. The SDK creates the session internally — pass flowId, attach to a container, listen for onComplete. Need the session minted server-side? Skip ahead to Consent flow (low-level).
import { Overturo } from "@overturo/js"
const overturo = new Overturo({ publishableKey: "pk_test_abc123" })
const handle = await overturo.consent.open({
flowId: "flw_abc123",
container: "#consent-container",
onComplete: (outcome) => {
console.log("Consent granted:", outcome.consentToken)
}
})
typeof handle.destroy
Consent flow (low-level)
When you need to mint the session server-side (so the API key stays off the client) or when you want to switch delivery mode dynamically, use createSession() + embed() / popup() / redirect(). The Ruby SDK has a matching server-side session example.
import { Overturo } from "@overturo/js"
const overturo = new Overturo({ publishableKey: "pk_test_abc123" })
// Server mints the session; client just embeds with the token.
const session = await overturo.createSession({
flowId: "flw_abc123",
deliveryMode: "embed",
embedOrigin: window.location.origin,
})
const widget = overturo.embed({
sessionToken: session.sessionToken,
container: "#consent",
onComplete: (r) => console.log("granted:", r.consentToken),
})
// Or open in a popup:
// const popup = overturo.popup({ sessionToken: session.sessionToken })
// Or full-page redirect:
// overturo.redirect({ sessionToken: session.sessionToken })
Handle Consent Events
The embed communicates via postMessage events prefixed with overturo: (Trust Surface postMessage v2). Always verify the origin:
window.addEventListener("message", (event) => {
if (event.origin !== "https://overturo.com") return
switch (event.data.type) {
case "overturo:ready":
console.log("Decision flow loaded; kind:", event.data.kind)
break
case "overturo:resize":
iframe.style.height = event.data.height + "px"
break
case "overturo:state":
// Visible state changed; payload is kind-specific.
break
case "overturo:complete":
// Consent: outcome is "granted" or "denied".
// Approval: outcome is "approve", "deny", or "counter".
console.log("Decision outcome:", event.data.outcome)
// Hand event.data.exchange_token to your backend; it's the source of truth.
break
case "overturo:cancel":
console.log("User cancelled:", event.data.reason)
break
case "overturo:error":
console.error("Flow error:", event.data.code, event.data.message)
break
case "overturo:deadline":
// Server-pushed deadline tick (T-30s, T=0); update countdown UI.
console.log("Remaining:", event.data.remaining_seconds, "seconds")
break
}
})
| Event | Description | Payload |
|---|---|---|
| overturo:ready | Iframe loaded and ready | kind, deadline, session_token, branding |
| overturo:resize | Content height changed | height |
| overturo:state | Visible state changed (kind-specific payload) | kind-specific |
| overturo:complete | User decided (granted/denied for consent; approve/deny/counter for approval) | outcome, exchange_token |
| overturo:cancel | User declined or dismissed | reason |
| overturo:error | An irrecoverable error | code, message |
| overturo:deadline | Server-pushed deadline tick (T−30s and T=0) | remaining_seconds |
Complete Example
Full integration with session creation, iframe, event handling, and auto-resizing:
async function embedDecisionFlow(containerId, flowId) {
// 1. Mint a decision token from your backend.
const res = await fetch("/api/decision-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind: "consent", flow_id: flowId })
})
const { decision_url, session_token } = await res.json()
// 2. Drop the custom element (or create an iframe pointed at decision_url).
const iframe = document.createElement("iframe")
iframe.src = decision_url
iframe.style.cssText = "width:100%; border:none; min-height:400px;"
document.getElementById(containerId).appendChild(iframe)
// 3. Listen for postMessage v2 events.
window.addEventListener("message", (event) => {
if (event.origin !== "https://overturo.com") return
switch (event.data.type) {
case "overturo:resize":
iframe.style.height = event.data.height + "px"
break
case "overturo:complete":
iframe.remove()
// Hand exchange_token to your backend; verify via /api/v1/decisions/:token/exchange.
onDecisionComplete(event.data.outcome, event.data.exchange_token)
break
case "overturo:cancel":
iframe.remove()
onDecisionCancelled(event.data.reason)
break
}
})
}
await embedDecisionFlow("decision-container", "flw_abc123")
Error Handling
import { Overturo, OverturoApiError } from "@overturo/js"
const overturo = new Overturo({ publishableKey: "pk_test_abc123" })
await overturo.createSession({ flowId: "flw_doesnt_exist" })
| Status | Meaning | Action |
|---|---|---|
| 401 | Unauthorized | Verify publishable key |
| 422 | Validation Failed | Check error.details |
| 429 | Rate Limited | Exponential backoff |
Security: Publishable keys are safe for client-side code. They can only create consent sessions — never expose API tokens in the browser.
Preview tokens
Preview an unpublished template or in-progress setup session inside your own UI before publishing it. Returns a handle that controls device/theme/visibility.
Internal tooling only. previewTemplate() needs the secret API key in the same execution context that mounts the iframe. Use it in staging dashboards or admin UIs you trust — never in code shipped to end users.
import { Overturo } from "@overturo/js"
// ⚠ INTERNAL TOOLING ONLY. previewTemplate() reads internal state and
// requires the secret API key in the same execution context that mounts
// the iframe. Run this in a staging dashboard or admin UI you trust —
// never in code shipped to end users.
const overturo = new Overturo({
publishableKey: "pk_test_abc123",
apiKey: process.env.OVERTURO_API_KEY,
})
const preview = await overturo.previewTemplate({
templateId: "tpl_abc123",
container: "#preview",
})
// preview.setDevice("desktop" | "mobile")
// preview.setTheme("light" | "dark")
// preview.destroy()
Token exchange
The consentToken you receive on completion is a single-use receipt — not the data. Exchange it server-side (with your API key) for the resolved claims, third-party attestations, and any verifiable credentials issued by the flow.
import { Overturo } from "@overturo/js"
// Token exchange must run server-side — it requires the secret API key.
// (Use `pk_*` alone on the client; reach for `sk_*` only in trusted contexts.)
const overturo = new Overturo({
publishableKey: "pk_test_abc123",
apiKey: process.env.OVERTURO_API_KEY,
})
// Server-side flow:
// 1. createSession() → hand the returned sessionToken to the browser
// 2. browser embeds the flow and receives a consentToken on completion
// 3. POST the consentToken back to your server, then exchange() it here
const result = await overturo.exchange({
sessionToken: "ct_session_token_from_step_1",
consentToken: "ct_consent_token_from_step_2",
})
result.claims
result.attestations
result.credentials
When not to use this SDK
If you only need to authenticate users — no consent UI, no claims exchange — you don't need this SDK at all. Your Application is a standards OAuth 2.0 / OIDC client. Use any compliant library: openid-client (Node), Authlib (Python), oidc-client-ts (browser SPA).
Pre-filled OIDC snippets for your Application live on Connect → your Application → Integration → Authentication only. The auth-vs-consent gate on the Embedding docs page also walks through the decision.
Consent in your own form
Two entry points: overturo.forms.attach captures your own control at submit (the capture snippet); the field entry renders the Overturo field inside your form. Both hand your server a capture token to commit. Full guide: Forms.
import { Overturo } from "@overturo/js"
const overturo = new Overturo({ publishableKey: "pk_live_…" })
// Your existing form keeps every input. Map each consent control to its purpose,
// name the email field, and let the snippet capture the decision at submit.
overturo.forms.attach({
flowId: "flw_…",
form: "#signup-form",
map: { marketing: "#marketing_opt_in", analytics: "#analytics_opt_in" },
subjectField: "#email",
externalIdField: "#customer_id", // optional: your own identifier
labels: { marketing: "#marketing_label" }, // the label the person reads: its digest is attested (match / drift in the record)
// wording: { marketing: "#marketing_label" }, // or: write the published wording into the label
onCaptured: (r) => console.log("capture token", r.token, "wording", r.wordingVerdict),
onError: (e) => console.warn("capture failed; the form submitted anyway", e.message),
})
const overturo = new Overturo({ publishableKey: "pk_live_…" })
// The field is a form control: at submit it captures from its own frame and
// hands the capture token to your form (a hidden input and the element's value).
overturo.forms.attach({
flowId: "flw_…",
form: "#signup-form",
field: "overturo-consent-field", // the element inside the form (one per flow)
subjectField: "#email",
externalIdField: "#customer_id", // optional: your own identifier
onCaptured: (r) => console.log("capture token", r.token),
onError: (e) => console.warn("capture failed; the form submitted anyway", e.message),
})
// Then, on your server, commit the token with your application credential —
// exactly as for the snippet. The receipt says the control was rendered by Overturo.
Also in the package, not on this page:
createSetupEmbed,
createSetupPopup, and
setupRedirect
embed the flow setup wizard. They remain callable for internal partners, but the setup-wizard surface is not yet generally available — a follow-up spec will promote them once it ships.
Ruby SDK
Server-side gem for managing Overturo from your backend (Rails, Sinatra, or any Ruby app). For browser-side consent flow embedding in your application's UI, see the JavaScript SDK.
Configuration
Create a client with an API token from Credentials.
require "overturo"
client = Overturo::Client.new(api_key: ENV.fetch("OVERTURO_API_KEY"))
Security: Store API tokens in environment variables or Rails credentials, never in source code.
Get Your Application Credentials
Applications are created in the dashboard. After signing up, create an application and copy its id and publishable_key — they're the first argument to every nested-resource call below.
# Applications are created in the Overturo dashboard, not via the API.
# Sign up at https://overturo.com, create an application, then copy the
# id (e.g. "app_abc123") and publishable key (e.g. "pk_live_...") from
# the application's settings page. Generate an API key from the API
# tokens page and set it in your environment as OVERTURO_API_KEY.
#
# The SDK does not expose POST /api/v1/applications — application
# creation is a dashboard-only flow. The SDK manages resources nested
# under an existing application: flows, webhooks, connections, etc.
# The application_id from your dashboard is the first argument to every
# nested-resource call (see the next snippet).
application_id = "app_abc123" # from your dashboard
publishable_key = "pk_live_demo123" # from your dashboard
puts "Configured for application: #{application_id} (#{publishable_key})"
Create a Consent Flow
Flows are nested under the application that owns them. Replace app_abc123 with the id from the previous step.
flow = client.connect.flows.create(
"app_abc123", # application_id (positional — from your dashboard)
name: "Privacy Policy",
pattern: "standard",
purposes: ["data_sharing", "analytics"]
)
flow.id
flow.status
Start a Consent Session (server-side)
Mint the consent session server-side so your API key never reaches the browser. Hand the resulting session_token to the client, then call
overturo.embed({ sessionToken }) from the JavaScript SDK's
low-level consent section.
session = client.consent.consent_sessions.create(
flow_id: "flw_xyz789",
delivery_mode: "embed",
embed_origin: "https://myapp.com"
)
session.session_token
session.embed_url
Verify a Consent Token
After the user completes the flow, verify the consent token your frontend received before trusting it.
result = client.consent.consent_sessions.verify(
"ct_abc123",
consent_token: "ct_abc123"
)
result.valid
result.status
Handle Webhooks
Webhook deliveries carry three identifying headers and an HMAC-SHA256 signature. The signed message is
"<timestamp>.<raw body>" —
include both when reconstructing the signature.
| Header | Description |
|---|---|
| X-Overturo-Signature | HMAC-SHA256, format v1=<hex> |
| X-Overturo-Timestamp | Unix epoch seconds; reject if older than 5 min |
| X-Overturo-Event-ID | Unique per delivery; use for idempotency |
| X-Overturo-Event-Type | e.g. consent.granted |
class WebhooksController < ActionController::API
TOLERANCE_SECONDS = 5 * 60
def receive
body = request.body.read
signature = request.headers["X-Overturo-Signature"].to_s
timestamp = request.headers["X-Overturo-Timestamp"].to_i
return head :unauthorized if (Time.now.to_i - timestamp).abs > TOLERANCE_SECONDS
expected = "v1=" + OpenSSL::HMAC.hexdigest(
"SHA256", ENV.fetch("OVERTURO_WEBHOOK_SECRET"), "#{timestamp}.#{body}"
)
unless signature.bytesize == expected.bytesize &&
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
return head :unauthorized
end
event = JSON.parse(body)
case event["event"]
when "consent.granted" then handle_consent_granted(event)
when "consent.revoked" then handle_consent_revoked(event)
when "flow.activated" then handle_flow_activated(event)
end
head :ok
end
private
def handle_consent_granted(_event); end
def handle_consent_revoked(_event); end
def handle_flow_activated(_event); end
end
Manage webhook subscriptions programmatically via
client.connect.webhooks.create(application_id, url:, events:).
Verify the Audit Chain
Every audit event is hash-chained and Merkle-anchored every 15 minutes. Pull anchors to prove the integrity of any audit record:
anchor = client.comply.anchors.retrieve("anc_1")
anchor.merkle_root
receipts = client.comply.anchors.receipts(anchor.id)
receipts.first.witness
Error Handling
The SDK raises a typed subclass of Overturo::Error for every non-2xx response — rescue the specific class you care about, or the base class as a catch-all.
caught = begin
client.connect.flows.create("app_abc123", name: "")
rescue Overturo::InvalidRequestError => e
e
end
caught.http_status
HTTP Error Responses
| Status | SDK Class | Action |
|---|---|---|
| 400 | InvalidRequestError | Check request body |
| 401 | AuthenticationError | Verify API token |
| 403 | ForbiddenError | Insufficient permissions |
| 404 | NotFoundError | Resource doesn't exist |
| 422 | InvalidRequestError | Check validation errors in e.json_body |
| 429 | RateLimitError | Respect Retry-After header |
| 5xx | ApiError | Retry with exponential backoff |
Consent records
Create a consent record your server captured, commit a capture token from the snippet or the field, import historical consents with a dry run, and read a record's status by your own subject reference. Guide: Forms.
client = Overturo::Client.new(api_key: ENV["OVERTURO_API_KEY"])
record = client.consent.consent_records.create(
flow_id: "flw_...",
subject: {email: "[email protected]", external_id: "crm-42"},
decisions: {marketing: {decision: "granted", default_checked: false, mandatory: false}},
captured_at: Time.now.utc.iso8601,
idempotency_key: "signup-42"
)
record.status
record.purposes
# Your form handler receives the hidden field overturo_capture_token with the submission.
client = Overturo::Client.new(api_key: ENV["OVERTURO_API_KEY"])
decision = client.consent.decisions.authorize(
params[:overturo_capture_token],
subject: {email: params[:email], external_id: current_customer.id},
suppress_receipt: false
)
decision.status
decision.record_id # the consent record; its receipt is at /api/v1/consent_receipts/{record_id}
Capture consent in your own form
You keep your sign-up, lead, intake, booking or checkout form. Overturo keeps the record: the versioned wording, the signed receipt, the withdrawal path and the audit evidence.
The full guide, with the rules a captured consent must follow, is in the documentation: Forms documentation
Your own form
The Overturo field: the control rendered by Overturo, inside your form
Place one element where the checkbox used to be. Overturo renders the published wording, shows the control unticked and observes the person's change in a frame your page cannot script; at submit the field hands your form the capture token and your server commits it as for the snippet. The receipt says the control was rendered by Overturo.
<!-- Two scripts: the core and the field entry (loaded explicitly, never fetched behind your back) -->
<script src="https://overturo.com/sdk/v1/overturo.js"></script>
<script src="https://overturo.com/sdk/v1/overturo-field.js"></script>
<form id="signup-form" action="/signup" method="post">
<input id="email" name="email" type="email" required>
<!-- Overturo renders the consent control here: the published wording, shown unticked, in your form -->
<overturo-consent-field name="consent" flow="flw_…" locale="en"
appearance='{"fontFamily":"system","accent":"#2563eb","radius":"6px","density":"regular"}'></overturo-consent-field>
<label><input name="terms" type="checkbox" required> I accept the terms</label>
<button type="submit">Create account</button>
</form>
const overturo = new Overturo({ publishableKey: "pk_live_…" })
// The field is a form control: at submit it captures from its own frame and
// hands the capture token to your form (a hidden input and the element's value).
overturo.forms.attach({
flowId: "flw_…",
form: "#signup-form",
field: "overturo-consent-field", // the element inside the form (one per flow)
subjectField: "#email",
externalIdField: "#customer_id", // optional: your own identifier
onCaptured: (r) => console.log("capture token", r.token),
onError: (e) => console.warn("capture failed; the form submitted anyway", e.message),
})
// Then, on your server, commit the token with your application credential —
// exactly as for the snippet. The receipt says the control was rendered by Overturo.
Appearance is a bounded vocabulary applied inside the frame (fontFamily: system, inter, serif, mono; fontSize; text; muted; accent; border; radius; density: compact, regular, relaxed), plus theme and locale attributes. The field renders no required control: your terms checkbox stays your own. One field per flow per form; the application needs at least one allowed origin.
The snippet: your own control, captured at submit
Add the SDK, map each consent checkbox to its purpose and name the email field. At submit, the snippet captures the decision and hands your server a capture token; your server commits it with the application key.
import { Overturo } from "@overturo/js"
const overturo = new Overturo({ publishableKey: "pk_live_…" })
// Your existing form keeps every input. Map each consent control to its purpose,
// name the email field, and let the snippet capture the decision at submit.
overturo.forms.attach({
flowId: "flw_…",
form: "#signup-form",
map: { marketing: "#marketing_opt_in", analytics: "#analytics_opt_in" },
subjectField: "#email",
externalIdField: "#customer_id", // optional: your own identifier
labels: { marketing: "#marketing_label" }, // the label the person reads: its digest is attested (match / drift in the record)
// wording: { marketing: "#marketing_label" }, // or: write the published wording into the label
onCaptured: (r) => console.log("capture token", r.token, "wording", r.wordingVerdict),
onError: (e) => console.warn("capture failed; the form submitted anyway", e.message),
})
# Your form handler: the capture token arrived in the submission.
client = Overturo::Client.new(api_key: ENV["OVERTURO_API_KEY"])
decision = client.consent.decisions.authorize(
params[:overturo_capture_token],
subject: {email: params[:email], external_id: current_customer.id}
)
decision.status
decision.record_id # the consent record (its receipt lives at /api/v1/consent_receipts/{record_id})
decision.channel
Typeform
Connect a Typeform form once: name the form and the flow, map its fields to the flow's purposes, and let Typeform's signed submission webhook commit each consent.
Alternatives: a Typeform that redirects on completion can hand off to the hosted flow with the email recalled, and a server that receives Typeform's webhook can call the Record API. The documentation says when to prefer each.
Mailchimp: the withdrawal that travels
Connect an audience once and map a flow's purposes to its marketing permissions and tags. Every opt-in and every withdrawal — on the records page, by expiry, by "this was not me" — is written to the audience, and a daily check reads it back. The audience's own unsubscribes and permission changes come back the other way (read back before anything is withdrawn), and every member carries a merge field with the person's records link for your footer.
Server and import
Record a consent your server captured in one call, or import the consents you already hold with their original capture time. A dry run reports what would be recorded and refused.
curl -X POST https://overturo.com/api/v1/consent_records \
-H "Authorization: Bearer $OVERTURO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow_id": "flw_...",
"subject": { "email": "[email protected]", "external_id": "crm-42" },
"decisions": { "marketing": { "decision": "granted", "default_checked": false, "mandatory": false } },
"captured_at": "2026-10-12T09:01:30Z",
"idempotency_key": "signup-42"
}'
client = Overturo::Client.new(api_key: ENV["OVERTURO_API_KEY"])
record = client.consent.consent_records.create(
flow_id: "flw_...",
subject: {email: "[email protected]", external_id: "crm-42"},
decisions: {marketing: {decision: "granted", default_checked: false, mandatory: false}},
captured_at: Time.now.utc.iso8601,
idempotency_key: "signup-42"
)
record.status
record.purposes
import { OverturoConsentRecords } from "@overturo/sdk"
const records = new OverturoConsentRecords({ baseUrl: "https://overturo.com", apiToken: process.env.OVERTURO_API_KEY })
const { consent_record } = await records.create({
flow_id: "flw_...",
subject: { email: "[email protected]", external_id: "crm-42" },
decisions: { marketing: { decision: "granted", default_checked: false, mandatory: false } },
captured_at: new Date().toISOString(),
idempotency_key: "signup-42",
})
console.log(consent_record.status, consent_record.purposes)
# Dry run first: what would be recorded, what would be refused — nothing is written.
curl -X POST https://overturo.com/api/v1/consent_records/imports \
-H "Authorization: Bearer $OVERTURO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow_id": "flw_...",
"dry_run": true,
"records": [
{ "subject": { "email": "[email protected]" },
"decisions": { "marketing": { "decision": "granted" } },
"captured_at": "2025-03-01T10:00:00Z",
"idempotency_key": "crm-row-1" }
]
}'
# Then read the outcome: GET /api/v1/consent_records/imports/{id}
Overturo proves when the decision was recorded, which wording was in force, who attested it and that nothing has changed since; for a record an organisation captured in its own form, Overturo does not vouch that the control was shown unticked; for a record captured through the Overturo field, Overturo rendered the control, showed it unticked and recorded the person's own change, and does not vouch for what the page placed around it.
Ready to build?
Get an API token