A signature API in three lines.
One POST to send a document for signature. Same per-signer pricing, same OTP signing — now from your own code.
The whole API
Send, track, and a webhook — the three you'll reach for. No SDK required.
Send a document to one or more signatories. Charges per signer, returns each signer's signing link.
Check who has signed and who's still pending, plus the sealed PDF when complete. Look up by your own ref with /by-ref.
Subscribe once and get a callback the instant every signatory is done — plus signatory.signed and more.
Embedded signing
Sign inline, as a step in your own flow, instead of sending a link. Remote parties still get the hosted /sign link — this is for the signer who is already in your app.
1. Create the request with an embed signatory
Set embed: true on a signatory in the usual POST /v1/requests body. Omit phone to skip the OTP step for a signer you have already verified yourself (SSO, session, etc.) — but then you must include assertedIdentity, since there is no other verification of who is signing.
POST https://api.onetapsign.com/v1/requests
{
"title": "Mutual NDA",
"document": { "url": "https://acme.com/nda.pdf" },
"signatories": [
{
"name": "Jordan Lee",
"embed": true,
"assertedIdentity": {
"subject": "jordan@acme.com",
"method": "sso"
}
}
]
}{
"id": "req_9fa2kx",
"status": "pending",
"title": "Mutual NDA",
"signatories": [
{
"id": "sig_1a2b3c",
"name": "Jordan Lee",
"status": "pending",
"embedToken": "emb_5xkq...",
"embedUrl":
"https://sign.onetapsign.com/embed/emb_5xkq..."
}
]
}2. Drop in the iframe — zero JS required from us
Point an iframe at embedUrl (or build it yourself from embedToken). An optional ?accent= query param recolors the ceremony to match your brand. Listen for postMessage events to size the iframe and react to completion.
<iframe
id="ots-embed"
src="https://sign.onetapsign.com/embed/EMBED_TOKEN?accent=4F46E5"
style="width:100%;border:0"
></iframe>
<script>
const iframe = document.getElementById("ots-embed");
addEventListener("message", (e) => {
if (e.origin !== "https://sign.onetapsign.com") return; // origin check
const m = e.data || {};
if (m.type === "onetapsign:resize") iframe.style.height = m.height + "px";
if (m.type === "onetapsign:signer.completed") {
// advance your multi-step form here, e.g. yourForm.next();
}
});
</script>For guaranteed cross-browser delivery (e.g. Firefox with a strict referrer policy), append &parentOrigin=<your-page-origin> to the iframe src so the widget can target your origin directly.
3. Optional helper (copy-paste, self-hosted)
Do not want to hand-wire the resize and origin check yourself? Paste this into your own codebase and host it as your own file — it is documentation, not a script we serve or version. There is no embed.js asset to load from us.
// From the OneTapSign docs — paste into your app, host it yourself.
function onetapsignEmbed(el, { token, accent, onSigned, onDeclined, onError }) {
const ORIGIN = "https://sign.onetapsign.com";
const iframe = document.createElement("iframe");
iframe.src = `${ORIGIN}/embed/${token}${accent ? `?accent=${accent}` : ""}`;
iframe.style.cssText = "width:100%;border:0";
document.querySelector(el).appendChild(iframe);
addEventListener("message", (e) => {
if (e.origin !== ORIGIN || e.source !== iframe.contentWindow) return;
const m = e.data || {};
if (m.type === "onetapsign:resize") iframe.style.height = m.height + "px";
if (m.type === "onetapsign:signer.completed") onSigned?.(m);
if (m.type === "onetapsign:declined") onDeclined?.(m);
if (m.type === "onetapsign:error") onError?.(m);
});
}4. Event contract
Every message is namespaced under onetapsign:and carries only enough data to correlate — never legal proof. That is the webhook's job (see the callout below). Internal ceremony phases (review, OTP, signing) are not surfaced.
| Event | Payload | Fires when |
|---|---|---|
| onetapsign:ready | — | The ceremony has loaded and rendered its first view. |
| onetapsign:resize | { height } | The content height changes — resize the iframe to avoid inner scrollbars. |
| onetapsign:signer.completed | { requestId, signatoryId, signedAt } | The present signer finishes signing or approving. |
| onetapsign:declined | { requestId, signatoryId } | The signer declines to sign. |
| onetapsign:error | { code } | An action fails (e.g. OTP verification, a network error). |
5. Register your origins
Tell OneTapSign which of your https:// origins are allowed to frame the ceremony. This drives the Content-Security-Policy: frame-ancestors header the embed page sends, so only pages you have listed can embed it.
PUT https://api.onetapsign.com/v1/embed-origins
Authorization: Bearer $OTS_KEY
{ "origins": ["https://app.acme.com", "https://acme.com"] }This is defense-in-depth, not the real gate. The embed token is what actually authorizes access to the signing content; the origin allowlist only restricts where a browser will render it in a frame. If the allowlist is empty or unset — or the lookup is unreachable — the page is served without a frame-restricting header at all. Configure your production origins before you rely on embedding.
signer.completed is not proof of signature. The browser message only tells your UI to advance immediately for the signer in front of it — it is sent by the page, not signed or verifiable, and it never waits on other parties. Before you grant anything of value (unlocking content, marking an order paid, releasing a document), wait for the request.completed or signatory.signed webhook — that is the source of truth.
Start building today
Create an account and grab your API keys. Test keys are free — you're only charged on a real send, $1 a signer.
Tell us what you're building and we'll get you set up.