Using DBLU to build end user apps
Become a partner
Calling DBLU APIs on behalf of users is reserved to vetted partners. Tell us about your company and your integration; we review every application and get back to you within 48 hours.
Note
DBLU partnerships are granted to companies (not individuals).
Once approved, you receive a partner_id and client_secret,
and can start the flow described in the next section.
How to get the DBLU key to call the APIs on behalf of the user
The DBLU key (technically request_token) is what your backend
sends on every DBLU API call made on behalf of a specific end-user. You obtain it once,
after the user has explicitly approved your integration from inside the DoubleU mobile app.
Note
The request_token is scoped to the personas,
bundles and duration the user approved.
It expires when that grant expires.
Overview
- Your app opens the DBLU login page in a browser (full page or popup).
- The user signs in to DoubleU.
- The user picks which doubles / personas to share and submits.
- DBLU sends a push notification to the user's iPhone DoubleU app.
- The user approves the bundles and chooses an access duration in the app.
- The browser tab navigates to a success screen with the
request_token— visible on the page and present in the URL. - You save the token on your backend and use it on subsequent partner API calls.
Prerequisites
- You have a registered DBLU partner integration with a
partner_idand aclient_secret. - The end-user has installed the DoubleU iOS app, signed in there at least once, and granted push-notification permission.
Step 1 — Open the DBLU login page from your app
From your client (web app, mobile app or desktop app) point the user to:
https://api.dblu.ai/login-page?partner_id=<your_partner_id>
For a partner with id 7865571 the URL becomes:
https://api.dblu.ai/login-page?partner_id=7865571
You have two integration shapes:
- Full-page redirect — simplest. After approval the browser lands on
the DBLU success page; your app reads the
request_tokenfrom the URL. - Popup — recommended for SPAs. Open a popup pointing at the same URL
and listen for a
postMessage; the popup auto-closes on success.
Popup example
const popup = window.open(
'https://api.dblu.ai/login-page?partner_id=7865571',
'dblu-connect',
'width=560,height=720'
);
window.addEventListener('message', (event) => {
const msg = event.data || {};
if (msg.type === 'DOUBLEU_CONNECTION_SUCCESS') {
const requestToken = msg.data.request_token;
// -> send requestToken to your backend and store it
}
if (msg.type === 'DOUBLEU_CONNECTION_ERROR') {
console.error('DBLU connect error:', msg.error);
}
if (msg.type === 'DOUBLEU_CONNECTION_CANCELLED') {
console.warn('User closed the DBLU window');
}
});
Important
If partner_id does not match an active row in DBLU's clients
table, the login page returns an access-denied view and no flow can start.
Step 2 — The user signs in
The user authenticates with their DoubleU credentials. Once signed in, they pick which persona(s) from their vault they want to expose to your integration, then submit.
Step 3 — A push notification is sent to the user's device
On submit, DBLU:
- Records a
partner_requestfor this grant. - Sends an APNs push to the user's DoubleU iOS app, with payload
type: "partner_request".
The user sees a notification similar to:
Partner <your-app-name> is requesting access to your selected Doubles
Meanwhile, the browser tab parks on a “Partner request sent!” waiting screen that polls in the background for approval.
Step 4 — The user approves bundles and duration in the DoubleU app
Inside the DoubleU iOS app the user picks:
- The bundles to share for the selected persona.
- The access duration:
1 hour,24 hours, orIndefinitely.
When they tap Approve, DoubleU calls
POST https://api.dblu.ai/partner-request/accept on their behalf and the
browser tab is unblocked.
Step 5 — The token is returned to your app
The success page appears at:
https://api.dblu.ai/access-granted?request_token=<TOKEN>
The token is rendered on the page and is one-click copy-pasteable. If you opened DBLU
in a popup (see Step 1) you'll instead receive a postMessage of type
DOUBLEU_CONNECTION_SUCCESS carrying the same request_token;
the popup will auto-close shortly after.
Tip
Persist the request_token on your backend, indexed by your internal user
record. Treat it as a secret — anyone holding it can act on behalf of that user
within the approved scope until the grant expires.
Terminology
| Term | What it is | Where it lives |
|---|---|---|
partner_id |
Your integration's public identifier. | Sent in the login URL; safe in clients. |
client_secret |
Your integration's server credential. | Server-only; sent as X-Client-Secret. |
request_token DBLU key |
Per-user grant token issued after approval. | Returned on the success page and via postMessage. |
request_token is not the same as a user session token, an MCP token, or a
partner_key. It is the value stored on
client_access.request_token in DBLU.
/partner/llm - Augmented LLM with User's Data
Once you hold a request_token for a user, you call
POST https://api.dblu.ai/partner/llm
to send a prompt that DBLU automatically enriches with the personas, bundles, and
governance rules the user approved for your integration.
Important
This is a server-side only endpoint. The
X-Client-Secret header is your private credential — never expose it
to a browser, mobile app, or any client-side code.
Prerequisites
- You are an active DBLU partner with a
partner_idandclient_secret. - You hold a valid
request_tokenissued for the target user (see the previous chapter).
Request
Endpoint:
POST https://api.dblu.ai/partner/llm
Required headers:
Content-Type: application/json
X-Client-Secret: <your_client_secret>
JSON body:
{
"prompt": "<the user-facing prompt>",
"request_token": "<saved DBLU key for this user>",
"model": "gpt-3.5-turbo",
"temperature": 0.7,
"max_tokens": 500,
"system_message": "You are a helpful AI assistant."
}
| Field | Required | Default | Notes |
|---|---|---|---|
prompt |
Yes | — | The user-facing instruction. Cannot be empty. |
request_token |
Yes | — | The DBLU key for this user. Must be active and not expired. |
model |
No | gpt-3.5-turbo |
OpenAI model identifier. |
temperature |
No | 0.7 |
Sampling temperature, 0.0 — 2.0. |
max_tokens |
No | 500 |
Maximum tokens in the completion. |
system_message |
No | "You are a helpful AI assistant." |
System role content prepended to the conversation. |
What DBLU does behind the scenes
For every call, DBLU:
- Authenticates your partner integration from
X-Client-Secret. - Resolves
request_tokento aclient_accessgrant — user, allowed personas, allowed bundles, expiry. - Refreshes
last_access_atfor the grant. - Decrypts the user's vault and pulls only the traits inside the approved bundles for the approved persona(s).
- Appends those traits and any active governance constraints to your prompt as additional context.
- Calls the underlying LLM with your
system_message+ the enriched user message. - Logs the request for billing and governance audit (no plaintext vault data is stored in logs).
- Returns the model's response to you.
Note
You do not need to fetch persona data yourself. Just write the prompt as if the user were typing it; DBLU injects the consented context for you and enforces the user's governance rules on the model.
Responses
DBLU separates partner authentication (X-Client-Secret) from
application outcome (prompt validation, request_token / grant
state, LLM provider errors). Bad client_secret yields HTTP 401
with a FastAPI detail JSON body. Valid partner auth almost always returns HTTP
200 with a PartnerLLMResponse — inspect success and
error. An expired or inactive user grant is
therefore still 200 with success: false, not 401.
HTTP 401 — partner secret
Returned before the route handler runs if X-Client-Secret is missing or does not
match an active row in clients. Body is FastAPI's default error JSON (not
PartnerLLMResponse):
{"detail":"Client secret is required"}— header absent or treated as empty.{"detail":"Invalid client secret"}— no matching active client.
HTTP 422 — request body validation
If the JSON body does not satisfy PartnerLLMRequest (wrong types, invalid
structure for Pydantic), FastAPI returns 422 Unprocessable Entity with a
validation error payload. Fix the body and retry.
HTTP 200 — PartnerLLMResponse
When X-Client-Secret is accepted, the handler responds with HTTP 200 and this shape:
{
"success": true,
"response": "<model output>",
"error": null,
"llm_model": "gpt-3.5-turbo",
"tokens_used": 412
}
success |
error |
response |
llm_model / tokens_used |
Meaning |
|---|---|---|---|---|
true |
null |
Completion text from the model. | llm_model: requested model id; tokens_used: total tokens when the provider returns usage (otherwise may be null). |
Grant resolved; prompt enriched; LLM call completed successfully. |
false |
"Prompt cannot be empty" |
null |
Both null. |
prompt was blank after trim. |
false |
"Request token is required" |
null |
Both null. |
request_token missing or empty in the JSON body. |
false |
"Invalid or inactive request token" |
null |
Both null. |
No active client_access row for this token (unknown token, revoked grant, or is_active false). |
false |
"Access has expired" |
null |
Both null. |
Grant exists but expires_at is in the past. User must re-approve to obtain a fresh request_token. |
false |
"Error processing LLM request: …" |
null |
Both null. |
Unhandled exception during enrichment or the LLM call; error includes the exception message after the fixed prefix. |
Optional: many integrations also send Authorization: Bearer <request_token>
for consistency; the API does not require it for this route — the token in the JSON body is
authoritative.
Examples
curl
curl -X POST https://api.dblu.ai/partner/llm \
-H "Content-Type: application/json" \
-H "X-Client-Secret: $DBLU_CLIENT_SECRET" \
-d '{
"prompt": "Suggest a weekend plan that fits this user.",
"request_token": "<saved_request_token>",
"model": "gpt-4o-mini",
"max_tokens": 600
}'
Node.js (server-side)
const res = await fetch('https://api.dblu.ai/partner/llm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Secret': process.env.DBLU_CLIENT_SECRET,
},
body: JSON.stringify({
prompt: 'Suggest a weekend plan that fits this user.',
request_token: savedRequestToken,
model: 'gpt-4o-mini',
max_tokens: 600,
}),
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'DBLU LLM call failed');
}
console.log(data.response);
Python (server-side)
import os, requests
resp = requests.post(
"https://api.dblu.ai/partner/llm",
headers={
"Content-Type": "application/json",
"X-Client-Secret": os.environ["DBLU_CLIENT_SECRET"],
},
json={
"prompt": "Suggest a weekend plan that fits this user.",
"request_token": saved_request_token,
"model": "gpt-4o-mini",
"max_tokens": 600,
},
timeout=60,
)
data = resp.json()
if not data["success"]:
raise RuntimeError(data.get("error") or "DBLU LLM call failed")
print(data["response"])
Tip
Use tokens_used from the response to track cost on a per-user
basis. DBLU also logs every call for the partner's monthly usage report.
Each call is rate-limited and recorded against your partner account. Re-using a
revoked or expired request_token always returns
success: false; ask the user to re-approve to get a fresh token.
Sample: Run it up (Android) — partner LLM POC
We ship a small open-source Android sample, Run it up, that walks through
the same partner pattern as this page: your app sends the user to the DBLU
login-page flow (with partner_id), the user approves access in
DoubleU, you obtain a request_token for that grant, then your integration calls
POST https://api.dblu.ai/partner/llm with
X-Client-Secret and that token so DBLU can return personalized
model output (the sample uses a running-shoe storefront as a concrete use case).
The app opens on a two-column product grid with category filters and a
typical ecommerce top bar; use the toolbar icons for Partner settings and
Recommendations (DBLU).
Note
The repository is a proof of concept for developers: it may paste
request_token manually after approval and is not a production
security pattern. For client_secret, the sample normally bakes
DBLU_POC_CLIENT_SECRET into the APK via Gradle; it may also
receive client_secret from partner POST /login JSON in the WebView.
Ship client_secret only on your server in production, and complete the login /
token capture flow for your platform.
Clone or fork the sample from https://github.com/dieffe/dblu-android-demo-partner-app (Kotlin, Material 3, Retrofit, Chrome Custom Tab stub for the login URL).
Using DBLU to build business apps
/governance-list - List User's Governance Packages
List the governance packages a DBLU user has created. Useful for MCP-style clients
and dashboards that want to display, audit, or attach the user's own governance
rule sets to a partner integration.
GET https://api.dblu.ai/governance-list
Important
This endpoint authenticates with the user's MCP token — not the
partner X-Client-Secret and not a request_token. The
MCP token is a personal credential issued to the user; treat it as a secret and
never expose it to a browser, mobile app, or any client-side code.
Prerequisites
- The user has generated an MCP token in their DBLU account.
- Your server holds that MCP token securely (typically per-user, scoped to that individual).
Request
Endpoint:
GET https://api.dblu.ai/governance-list
Required headers:
Authorization: Bearer <mcp_token>
This endpoint takes no query parameters and no body.
Response
On success, returns the user's custom governance packages, ordered alphabetically
by name. Built-in DBLU governance packages are intentionally excluded.
{
"packages": [
{
"id": "8c2e7a6f-7d4b-4a3a-9d52-6e2cc8f4b8a1",
"package_key": "marketing_safe_personal",
"persona_type": "generic",
"name": "Marketing — safe personal",
"description": "Share preferences and biographical context only.",
"data_permissions": [
{ "bundle": "preferences & tastes", "allowed": true },
{ "bundle": "biographical & demographic", "allowed": true },
{ "bundle": "health & wellness", "allowed": false }
],
"data_retention": "days_90",
"language_controls": "default_business",
"language_custom_blocklist": "",
"is_built_in": false,
"created_at": "2026-04-18T09:12:34.122000",
"updated_at": "2026-05-09T17:03:14.011000"
}
]
}
| Field | Type | Notes |
|---|---|---|
id |
UUID | Stable identifier of the governance package. |
package_key |
string | Short slug-style key the user can reference internally. |
persona_type |
string | Persona scope. New packages use "generic"; legacy values may appear. |
name |
string | Human-readable display name. |
description |
string | Free-text description. |
data_permissions |
array | Per-bundle allow/deny entries that drive what data partners may receive. |
data_retention |
string | One of session_only, days_90, until_manual_pii_90. |
language_controls |
string | One of default_business, default_custom, custom_only. |
language_custom_blocklist |
string | Newline-separated user blocklist. Only consulted for default_custom / custom_only. |
is_built_in |
boolean | Always false in this response — built-ins are not returned. |
created_at |
ISO 8601 | UTC timestamp the package was created. |
updated_at |
ISO 8601 | UTC timestamp of the last update. |
Errors
- 401
{"detail": "Invalid MCP authentication token"}— missing, malformed, or unknown bearer token. - 503 — the
governance_packagestable has not been migrated on this DBLU instance yet. The response body carries a human-readable hint with the migration to run. - 500 — unexpected server error.
Tip
An empty packages array is a valid success response — it means the
user simply hasn't created any custom governance package yet.
To load the full rules JSON for one package, use
GET /get-governance?governance_id=…
with the same MCP token (see the next section).
Examples
curl
curl -X GET https://api.dblu.ai/governance-list \
-H "Authorization: Bearer $DBLU_MCP_TOKEN"
Node.js (server-side)
const res = await fetch('https://api.dblu.ai/governance-list', {
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.DBLU_MCP_TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`DBLU governance-list failed: ${res.status}`);
}
const { packages } = await res.json();
console.log(`User has ${packages.length} custom governance package(s).`);
Python (server-side)
import os, requests
resp = requests.get(
"https://api.dblu.ai/governance-list",
headers={"Authorization": f"Bearer {os.environ['DBLU_MCP_TOKEN']}"},
timeout=30,
)
resp.raise_for_status()
packages = resp.json()["packages"]
print(f"User has {len(packages)} custom governance package(s).")
MCP tokens are scoped per user. Rotating or revoking the token in DBLU
immediately invalidates any client holding the old value; subsequent calls return
401.
/get-governance - Fetch rules for one governance package
After you list packages with /governance-list,
call this endpoint with a package id to retrieve the JSON rule payload:
data permissions (what may be shared and how), plus retention and
language-control settings for that package.
GET https://api.dblu.ai/get-governance
Important
Same authentication as /governance-list: the user's
MCP token in Authorization: Bearer …. Server-side only.
Prerequisites
- A valid MCP token for the user who owns the package.
- A
governance_idvalue taken from theidfield of an object in thepackagesarray returned byGET /governance-list.
Request
Endpoint (query string required):
GET https://api.dblu.ai/get-governance?governance_id=<UUID>
Required headers:
Authorization: Bearer <mcp_token>
Query parameter:
| Parameter | Type | Notes |
|---|---|---|
governance_id |
UUID | Required. Must match a custom package id from /governance-list for this user. |
Response
On success, 200 with a JSON object. The data_permissions array
is the primary rule set: each entry has category, policy
(allowed, ask_each_time, never, or trace),
and optional label.
{
"governance_id": "8c2e7a6f-7d4b-4a3a-9d52-6e2cc8f4b8a1",
"package_key": "marketing_safe_personal",
"name": "Marketing — safe personal",
"description": "Share preferences and biographical context only.",
"data_permissions": [
{ "category": "preferences & tastes", "policy": "allowed", "label": null },
{ "category": "biographical & demographic", "policy": "allowed", "label": null },
{ "category": "health & wellness", "policy": "never", "label": null }
],
"data_retention": "days_90",
"language_controls": "default_business",
"language_custom_blocklist": ""
}
| Field | Type | Notes |
|---|---|---|
governance_id |
UUID | Same as the package id you passed in. |
package_key |
string | Stable slug for the package. |
name / description |
string | Display metadata. |
data_permissions |
array | Governance rules for data categories. |
data_retention |
string | session_only, days_90, or until_manual_pii_90. |
language_controls |
string | default_business, default_custom, or custom_only. |
language_custom_blocklist |
string | Newline-separated terms when language controls use custom lists. |
Errors
- 401 — missing, malformed, or invalid MCP bearer token (same body as
/governance-list). - 404
{"detail":"Governance package not found"}— unknown UUID, another user's package, or a built-in package. - 422 —
governance_idmissing or not a valid UUID. - 503 — governance tables not migrated (same hint as
/governance-list). - 500 — unexpected server error.
Examples
curl
curl -sS -G "https://api.dblu.ai/get-governance" \
--data-urlencode "governance_id=${GOVERNANCE_ID}" \
-H "Authorization: Bearer ${DBLU_MCP_TOKEN}"
Node.js (server-side)
const gid = process.env.GOVERNANCE_ID;
const url = new URL('https://api.dblu.ai/get-governance');
url.searchParams.set('governance_id', gid);
const res = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.DBLU_MCP_TOKEN}`,
},
});
if (!res.ok) {
throw new Error(`DBLU get-governance failed: ${res.status}`);
}
const rules = await res.json();
console.log(rules.data_permissions);
Python (server-side)
import os, requests
gid = os.environ["GOVERNANCE_ID"]
resp = requests.get(
"https://api.dblu.ai/get-governance",
params={"governance_id": gid},
headers={"Authorization": f"Bearer {os.environ['DBLU_MCP_TOKEN']}"},
timeout=30,
)
resp.raise_for_status()
rules = resp.json()
print(rules["data_permissions"])
Typical flow: GET /governance-list → pick packages[i].id →
GET /get-governance?governance_id=… for the full rule JSON for integrations,
audits, or MCP tools.
/governance-challenge — Check text or PDF against DENY rules
Submit a governance_id (from /governance-list)
plus either plain text or a PDF file. DBLU runs the same deterministic
DENY / NEVER checks used in production governance flows. If any rule matches the content,
the response has "allowed": false and triggered_rules lists human-readable
rule names; otherwise "allowed": true and an empty list.
POST https://api.dblu.ai/governance-challenge
Important
MCP bearer authentication (same as /governance-list). Server-side only.
Send either form field text or form file file,
not both. Uploaded files must be PDF.
Request
multipart/form-data with:
| Field | Required | Notes |
|---|---|---|
governance_id |
Yes | UUID string for the package (id from /governance-list). |
text |
One of text / file | UTF-8 text to evaluate. Must be non-empty if no file is sent. |
file |
One of text / file | Single PDF; text is extracted server-side. Max 25 MB. |
Payloads longer than 500,000 characters are truncated before evaluation.
Response
200 JSON:
{ "allowed": true, "triggered_rules": [] }
{
"allowed": false,
"triggered_rules": [
"Employee / identity (restricted)",
"Public pricing & commercial terms"
]
}
triggered_rules entries are display labels for DENY/NEVER rows whose keyword
signatures matched the combined extracted text (same engine as DBLU chat governance).
Errors
- 400 — missing payload, both text and file sent, file not PDF, empty text extraction, etc.
- 401 — invalid MCP token.
- 404 — governance package not found for this user.
- 413 — PDF larger than 25 MB.
- 503 / 500 — same migration / server error patterns as other governance MCP routes.
Examples
curl (text)
curl -sS -X POST "https://api.dblu.ai/governance-challenge" \
-H "Authorization: Bearer ${DBLU_MCP_TOKEN}" \
-F "governance_id=${GOVERNANCE_ID}" \
-F "text=Who is your CEO?"
curl (PDF)
curl -sS -X POST "https://api.dblu.ai/governance-challenge" \
-H "Authorization: Bearer ${DBLU_MCP_TOKEN}" \
-F "governance_id=${GOVERNANCE_ID}" \
-F "file=@./briefing.pdf;type=application/pdf"
Python (server-side)
import os, requests
url = "https://api.dblu.ai/governance-challenge"
headers = {"Authorization": f"Bearer {os.environ['DBLU_MCP_TOKEN']}"}
data = {"governance_id": os.environ["GOVERNANCE_ID"], "text": "Who is your CTO?"}
r = requests.post(url, headers=headers, data=data, timeout=60)
r.raise_for_status()
print(r.json()) # {"allowed": bool, "triggered_rules": [...]}
Path spelling is /governance-challenge (with challenge).