Partner Documentation Portal
Onboarding guide for B2B2B integration: External API (v1/external), Scopes, Webhooks, and White Label. Integrate with WellTwins using API keys and HMAC-verified webhooks.
B2B2B hierarchy
Your API key is bound to one organization. You can create child orgs and list users per org. Grail data is scoped by profiles.org_id.
Data flow: WellTwins → Partner
You register webhook URLs per org; we send events (grail.updated, user.registered) with a signed body so you can verify authenticity.
Authentication
All /api/v1/external/* routes require an API key.
Authorization: Bearer <your-api-key>or X-API-Key: <your-api-key>
API routes (v1/external)
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /organizations | orgs:read | List direct child organizations of the API key's org. |
| POST | /organizations | orgs:write | Create a sub-organization under the authenticated org. Body: { name, slug? }. |
| GET | /organizations/[orgId] | orgs:read | Get one organization (self or direct child). |
| PATCH | /organizations/[orgId] | orgs:write | Update org branding: primary_color, logo_url, custom_welcome_message. |
| GET | /organizations/[orgId]/users | users:read | List users belonging to the org (profiles.org_id). Returns id, email, created_at. |
| GET | /grail/aggregate | grail:read | Aggregated Grail scores for an org. Query: org_id (optional; default = API key's org). Dimension breakdown masked if member count < 3. |
Scopes
API keys are created with a set of scopes; each route requires a specific scope.
orgs:readList and read organizations (self and direct children).orgs:writeCreate sub-orgs and update org branding (primary_color, logo_url, custom_welcome_message).users:readList users (profiles) belonging to an org.users:writeReserved for future user management.grail:readRead aggregated Grail (Lazarus · AI) scores for an org.webhooks:readRead webhook registrations.webhooks:writeRegister or update outbound webhooks.
Webhooks (outbound)
Register URLs in external_webhooks per org and event. We POST JSON with headers:
X-WellTwins-Signature: sha256=<hmac-hex>– HMAC-SHA256 of raw body with your secretX-WellTwins-Event: <event>– e.g. grail.updated, user.registered
Events
grail.updatedSent when a user's Grail scores change (after profile PATCH or Ramune chat). Payload: { user_id, grail_data }.user.registeredSent when a new user joins the partner's sub-org (auth callback with org_id). Payload: { user_id, email }.subscription.statusSent on membership changes (e.g. upgrade to Plus). Reserved for future use.
Code: Verify HMAC signature
Use the same secret you registered; compute HMAC-SHA256 of the raw request body (before JSON parse) and compare with X-WellTwins-Signature.
// Validate X-WellTwins-Signature (Node/TypeScript)
import { createHmac } from "crypto";
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string
): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
return signatureHeader === expected;
}
// In your webhook handler (e.g. Express):
// const rawBody = req.rawBody; // use raw body, not parsed JSON
// const sig = req.headers["x-welltwins-signature"];
// if (!verifyWebhookSignature(rawBody, sig, process.env.WEBHOOK_SECRET)) return 401;
Code: Grail aggregate API
Optional query org_id; default is your API key's org. If org has fewer than 3 members with Grail data, dimension breakdown is masked (anonymity).
# Grail aggregate – cURL
curl -X GET "https://your-app.com/api/v1/external/grail/aggregate?org_id=YOUR_ORG_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json"
// Grail aggregate – TypeScript fetch
const res = await fetch(
`${BASE_URL}/api/v1/external/grail/aggregate?org_id=${orgId}`,
{
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
}
);
const data = await res.json();
// data.team.dimensions, data.team.memberCount, data.team.anonymityMasked
// data.global.dimensions, data.global.sampleCount
API base: /api/v1/external. For API key management and webhook registration, use the Simo AI Admin panel (Organizations → API Keys). White Label branding (X-Tenant-ID, logo, primary color) is resolved from the orgs table.