Legal (DPA / BAA)
Your agent cannot sign a contract. Mint a signing hand-off, give the URL to a person, and poll.
Before Atribu forwards a customer's conversion to an ad platform, the workspace has to have accepted a Data Processing Agreement. Healthcare Mode additionally needs a HIPAA Business Associate Agreement, which ships as one combined DPA & BAA click-wrap.
Neither is something a program can do. An API key is a machine credential — and
a delegated key is an agency's app acting inside someone else's workspace — so
an acceptance either of them could have produced would record nothing worth
recording. An MCP user token is refused too, and deliberately: it names a real
person, but it is the credential they minted for an agent, so accepting with
one is an agent signing on their behalf. Only a browser session says a person is
present. POST /api/v1/legal/{document}/accept refuses all three with a 403
that names the alternative.
The alternative is a hand-off: a session-less URL you give to a person, an id you poll, and an expiry.
Mint a signing hand-off
POST /api/v1/legal/dpa/handoff
POST /api/v1/legal/baa/handoffScope: exports:write · Body: none
curl -X POST -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://api.atribu.app/api/v1/legal/dpa/handoff"import { AtribuClient } from "@atribu/node";
const client = new AtribuClient({ apiKey: process.env.ATRIBU_API_KEY });
const handoff = await client.legal.signDpa();
if (handoff.status === "pending") {
// Send this to whoever can agree on the customer's behalf.
console.log("Please sign here:", handoff.url);
} else {
console.log("Already signed — nothing to do.");
}{
"data": {
"id": "0f2f6b0a-9a2c-4f4e-9a1e-6f7f2a5c3b21",
"kind": "sign_dpa",
"status": "pending",
"url": "https://www.atribu.app/h/9tQ2mB1x…",
"expires_at": "2026-09-05T11:30:00.000Z",
"created_at": "2026-09-05T10:45:00.000Z",
"completed_at": null,
"result": null
},
"meta": { "profile_id": "8f3d…" }
}The URL opens the same click-wrap the Atribu console shows — the full agreement, a scroll-to-the-bottom gate, and a name and email for the signer. Accepting records the signer's email, IP address, user agent, the timestamp and the document version.
Then poll GET /api/v1/handoffs/{id} until status leaves
pending, and read the record back from
GET /api/v1/conversion-sync/legal.
Already signed? You get completed, not an error
A hand-off minted for a document that is already accepted comes back settled, in the same call:
{
"data": {
"kind": "sign_dpa",
"status": "completed",
"url": null,
"result": {
"already_accepted": true,
"document": "dpa",
"document_version": "2026-09-02-v1",
"accepted_at": "2026-08-14T09:12:44.000Z"
}
}
}This is the property to build on: re-running a setup checklist must never ask a
customer to sign the same agreement twice, and it never will. url is null
because there is nothing for anyone to open.
The DPA and the BAA are separate
They write different columns, and that separation is the point: accepting the
DPA can never be mistaken for a BAA approval, which is what unlocks Healthcare
Mode. A signed DPA does not settle a BAA hand-off, and asking for /baa/handoff
on a profile that has only signed the DPA gives you a live URL, correctly.
Accept as a signed-in owner or admin
POST /api/v1/legal/dpa/accept
POST /api/v1/legal/baa/acceptScope: exports:write · Credential: a Supabase session bearer, held by a
workspace owner or admin
This is the Atribu console's own write, published so acceptance has one implementation rather than several. It records the agreement for the person making the call.
{ "document_version": "2026-09-02-v1" }The one field names which of the shipped texts you displayed — the full
agreement at /dpa, or the shorter platform-safe click-wrap that gates a
profile's privacy mode. It is checked against an allowlist and never recorded as
given: the stored version says which agreement a customer accepted, and it
cannot do that job if the customer picks the value. Omit it to accept the
current text.
{
"data": {
"document": "dpa",
"document_version": "2026-09-02-v1",
"accepted_at": "2026-09-05T10:45:12.000Z",
"already_accepted": false
},
"meta": { "profile_id": "8f3d…" }
}Idempotent at the document grain. Accepting an already-accepted document
writes nothing, answers already_accepted: true, and returns the ORIGINAL
timestamp — a reload is not a second agreement.
Why an API key gets a 403 here
{
"error": {
"code": "insufficient_scope",
"message": "A DPA or BAA can only be accepted by a signed-in workspace owner or admin. An API key cannot agree to a contract on a customer's behalf — mint a signing hand-off with POST /api/v1/legal/dpa/handoff and give the URL to a person.",
"status": 403
}
}A guest who reaches the profile through a profile membership alone is refused for the same reason, however wide their profile permissions: they are not the counterparty to Atribu's agreement.
Reading the result
GET /api/v1/conversion-sync/legal (scope exports:read) returns the full
compliance record: dpa_status, baa_status, signature_status, signed_at,
the document version, and every stored document with a time-limited signed URL.
That is where the signer's name, email, IP and user agent live — on the
evidence row rather than on the compliance columns, because the signatory
usually has no Atribu account for those columns' foreign keys to point at.