Client onboarding
Your customers become Prepared clients on your own site, before any contract exists. You put our sign-up form on a page; we give you a client ID for each buyer, which you send with their contracts. Everything here runs against the sandbox, https://api-sandbox.prepared.com.au.
Before you build
What to expect, so nothing here surprises you or your customer.
- The form is our full buyer intake, not a name-and-email box: about ten steps asking who is buying (up to four people), the property, the price and deposit, finance, and how the buyers will hold title. Allow your customer several minutes, and give the form room on your page.
- A person fills it in. Nothing can be pre-filled and code cannot submit it: our lawyers act on the answers. To test, type made-up details into the sandbox's form.
- Register your webhook endpoint, with both client events ticked, before anyone submits. An event goes only to an endpoint that is registered and has that event ticked at the moment it happens: clients.submitted when the form is submitted, clients.accepted when we accept it. Nothing is kept for an endpoint added, or an event ticked, afterwards. Without an endpoint, read the onboarding instead (step 3).
- Times are UTC to the microsecond and end in Z, the same wherever they appear. Parse them as times to compare them.
1. Open a sign-up from your server
With a token from POST /v1/token, as in Getting started. reference is your own ID for the sign-up. Sending one you have used before answers the same onboarding again, so the call is safe to repeat. The session_token is for the browser: it can show and submit this one form and read nothing. It does not expire.
POST https://api-sandbox.prepared.com.au/v1/clients/onboardings/sessions
Authorization: Bearer eyJ…
Content-Type: application/json
{ "reference": "MC-APP-1042" }
200 OK
{
"id": "onb_01j9z4a7k2m3n4p5q6r7s8t9v0",
"reference": "MC-APP-1042",
"state": "open",
"session_token": "onbs_CfDJ8…",
"created_at": "2026-10-08T01:10:02.418907Z"
}
2. Put the form on your page
One script and one call. The form is drawn in a frame we serve, so your page cannot read what your customer types, and onSubmitted is called with nothing. Load the script from the same base URL the session came from. The frame's own address is https://onboarding-sandbox.prepared.com.au in the sandbox and https://onboarding.prepared.com.au in production: a page with a content security policy needs it in frame-src. onSubmitted is called when your customer has sent the form, and we have normally recorded it by then, so read the onboarding at once. This submission is recorded if the onboarding's submitted_at is later than it was when you showed the form (it is null until the first); if it is not, we could not record it and its clients are from before. The form may be submitted more than once.
<div id="prepared-form"></div>
<script src="https://api-sandbox.prepared.com.au/onboarding.js"></script>
<script>
Prepared.onboarding.mount({
container: document.getElementById("prepared-form"),
sessionToken: "onbs_CfDJ8…",
onSubmitted: function () { /* thank them, move them on */ }
});
</script>
3. Hear about each client
Two events, each sent once per buyer on the form, each carrying the same client_id for that buyer. You can receive clients.submitted more than once for the same client_id: treat each one as that client's latest details.
- clients.submitted
- Sent as soon as the form is submitted, and again each time it is submitted. A buyer submitted again with the same email keeps their client_id.
- clients.accepted
- Sent once Prepared has accepted the sign-up. It can follow clients.submitted at once, or by minutes or days. The onboarding's state is then accepted. If the form is submitted again, expect both events again. The two are sent separately: do not rely on clients.submitted arriving first.
POST https://your-endpoint…
{
"type": "clients.submitted",
"timestamp": "2026-10-08T01:14:31.270115Z",
"test": false,
"data": {
"client_id": "cli_01j9z4c1x2y3z4a5b6c7d8e9f0",
"onboarding_id": "onb_01j9z4a7k2m3n4p5q6r7s8t9v0",
"reference": "MC-APP-1042",
"primary": true,
"first_name": "Dave",
"last_name": "Example",
"email": "dave@example.com"
}
}
Register a webhook endpoint in the portal, or through the MCP server, subscribed to both events. Every message is signed; the API reference shows how to verify it. Your endpoint must be reachable from the internet, so while you build on your own machine you can read the onboarding instead: GET /v1/clients/onboardings/{id} lists its clients once its state is submitted or accepted.
4. Read clients, and name them on a contract
GET /v1/clients/{id} answers one client; GET /v1/clients?reference=MC-APP-1042 lists the clients of one sign-up. To say who a contract is for, add clients to the request part when you send it: up to 10 IDs from the same environment. A client can be named as soon as it is submitted; it need not be accepted first. One unknown ID refuses the request (client_unknown) and nothing is sent.
POST https://api-sandbox.prepared.com.au/v1/contract-reviews
part "file": contract.pdf
part "request": { "external_reference": "MC-TEST-001", "product": "contract_viewer",
"clients": ["cli_01j9z4c1x2y3z4a5b6c7d8e9f0", "cli_01j9z4c1x2y3z4a5b6c7d8e9f1"] }
202 Accepted
{ "id": "crv_01j9z4a7…", …, "clients": ["cli_01j9z4c1x2y3z4a5b6c7d8e9f0", "cli_01j9z4c1x2y3z4a5b6c7d8e9f1"] }
The programs on Getting started send a contract; add clients to the JSON they put in the request part. Sending clients with a new version replaces the contract's list.
A complete example
Each of these is a whole program. It serves a page with the form at http://localhost:8000, and prints each client's ID once the form is submitted: by reading the onboarding when the page says it was sent, and from the webhook if you have registered this server's public address as an endpoint. Set PREPARED_API_KEY to a sandbox key first. To keep them short they do not verify the webhook's signature: do that before trusting a message.
Python (requests)
import json, os, uuid
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
import requests
API = "https://api-sandbox.prepared.com.au"
key = os.environ["PREPARED_API_KEY"] # prep_test_...
def token():
return requests.post(f"{API}/v1/token", headers={"Authorization": f"Bearer {key}"}).json()["access_token"]
PAGE = """<!doctype html><title>Sign up</title>
<div id="prepared-form"></div>
<script src="%(api)s/onboarding.js"></script>
<script>
Prepared.onboarding.mount({
container: document.getElementById("prepared-form"),
sessionToken: "%(session_token)s",
onSubmitted: function () { fetch("/submitted?onboarding=%(id)s"); }
});
</script>"""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
url = urlparse(self.path)
if url.path == "/":
# One sign-up per visit. The reference is your own ID for it.
session = requests.post(f"{API}/v1/clients/onboardings/sessions",
headers={"Authorization": f"Bearer {token()}"}, json={"reference": f"signup-{uuid.uuid4()}"}).json()
page = PAGE % {"api": API, "session_token": session["session_token"], "id": session["id"]}
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(page.encode())
elif url.path == "/submitted":
# The page says the form was sent: read who signed up.
onboarding = requests.get(f"{API}/v1/clients/onboardings/{parse_qs(url.query)['onboarding'][0]}",
headers={"Authorization": f"Bearer {token()}"}).json()
for client in onboarding["clients"]:
print(client["id"], client["first_name"], client["last_name"], client["email"])
self.send_response(204)
self.end_headers()
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
# Your webhook endpoint: one message per buyer.
event = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
if event["type"] == "clients.submitted":
print("webhook:", event["data"]["client_id"], event["data"]["email"])
self.send_response(204)
self.end_headers()
HTTPServer(("localhost", 8000), Handler).serve_forever()
Node 18 or later (no packages)
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
const API = "https://api-sandbox.prepared.com.au";
const key = process.env.PREPARED_API_KEY; // prep_test_...
const token = async () => (await (await fetch(`${API}/v1/token`, {
method: "POST", headers: { Authorization: `Bearer ${key}` },
})).json()).access_token;
createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost:8000");
if (req.method === "GET" && url.pathname === "/") {
// One sign-up per visit. The reference is your own ID for it.
const session = await (await fetch(`${API}/v1/clients/onboardings/sessions`, {
method: "POST",
headers: { Authorization: `Bearer ${await token()}`, "Content-Type": "application/json" },
body: JSON.stringify({ reference: `signup-${randomUUID()}` }),
})).json();
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(`<!doctype html><title>Sign up</title>
<div id="prepared-form"></div>
<script src="${API}/onboarding.js"></script>
<script>
Prepared.onboarding.mount({
container: document.getElementById("prepared-form"),
sessionToken: "${session.session_token}",
onSubmitted: () => fetch("/submitted?onboarding=${session.id}"),
});
</script>`);
} else if (req.method === "GET" && url.pathname === "/submitted") {
// The page says the form was sent: read who signed up.
const onboarding = await (await fetch(`${API}/v1/clients/onboardings/${url.searchParams.get("onboarding")}`, {
headers: { Authorization: `Bearer ${await token()}` },
})).json();
for (const client of onboarding.clients) console.log(client.id, client.first_name, client.last_name, client.email);
res.writeHead(204).end();
} else if (req.method === "POST") {
// Your webhook endpoint: one message per buyer.
let body = "";
for await (const chunk of req) body += chunk;
const event = JSON.parse(body);
if (event.type === "clients.submitted") console.log("webhook:", event.data.client_id, event.data.email);
res.writeHead(204).end();
} else {
res.writeHead(404).end();
}
}).listen(8000, "localhost");
C# (.NET 8 or later, as a console project's Program.cs)
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
const string Api = "https://api-sandbox.prepared.com.au";
var key = Environment.GetEnvironmentVariable("PREPARED_API_KEY"); // prep_test_...
var http = new HttpClient { BaseAddress = new Uri(Api) };
async Task<HttpRequestMessage> Authorised(HttpMethod method, string path)
{
using var exchange = new HttpRequestMessage(HttpMethod.Post, "/v1/token");
exchange.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
var token = (await (await http.SendAsync(exchange)).Content.ReadFromJsonAsync<JsonElement>()).GetProperty("access_token").GetString();
var request = new HttpRequestMessage(method, path);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return request;
}
var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8000/");
listener.Start();
while (true)
{
var context = await listener.GetContextAsync();
var (request, response) = (context.Request, context.Response);
if (request.HttpMethod == "GET" && request.Url?.AbsolutePath == "/")
{
// One sign-up per visit. The reference is your own ID for it.
var open = await Authorised(HttpMethod.Post, "/v1/clients/onboardings/sessions");
open.Content = JsonContent.Create(new { reference = $"signup-{Guid.NewGuid()}" });
var session = await (await http.SendAsync(open)).Content.ReadFromJsonAsync<JsonElement>();
var page = $$"""
<!doctype html><title>Sign up</title>
<div id="prepared-form"></div>
<script src="{{Api}}/onboarding.js"></script>
<script>
Prepared.onboarding.mount({
container: document.getElementById("prepared-form"),
sessionToken: "{{session.GetProperty("session_token").GetString()}}",
onSubmitted: () => fetch("/submitted?onboarding={{session.GetProperty("id").GetString()}}"),
});
</script>
""";
response.ContentType = "text/html; charset=utf-8";
await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(page));
}
else if (request.HttpMethod == "GET" && request.Url?.AbsolutePath == "/submitted")
{
// The page says the form was sent: read who signed up.
var read = await Authorised(HttpMethod.Get, $"/v1/clients/onboardings/{request.QueryString["onboarding"]}");
var onboarding = await (await http.SendAsync(read)).Content.ReadFromJsonAsync<JsonElement>();
foreach (var client in onboarding.GetProperty("clients").EnumerateArray())
Console.WriteLine($"{client.GetProperty("id")} {client.GetProperty("first_name")} {client.GetProperty("last_name")} {client.GetProperty("email")}");
response.StatusCode = 204;
}
else if (request.HttpMethod == "POST")
{
// Your webhook endpoint: one message per buyer.
var message = await JsonSerializer.DeserializeAsync<JsonElement>(request.InputStream);
if (message.GetProperty("type").GetString() == "clients.submitted")
Console.WriteLine($"webhook: {message.GetProperty("data").GetProperty("client_id")} {message.GetProperty("data").GetProperty("email")}");
response.StatusCode = 204;
}
else
{
response.StatusCode = 404;
}
response.Close();
}
Errors
- reference_required
- Send reference: your own ID for the sign-up.
- request_invalid
- The body isn't JSON this call reads. Field names are snake_case.
- clients_invalid
- clients must be up to 10 client IDs, each named once.
- client_unknown
- One of the IDs isn't a client of this environment. The message names it; nothing was sent.
Need help?
Stuck, or need a form of your own? Email integrations@prepared.com.au.