PDaaS API Reference
Personal Data as a Service — multi-tenant vault access for organizations and developers.
Base URL
https://api.boxowl.me/api/v1
Authentication — Org API Key
PDaaS uses bearer-token API key authentication. Keys are created per organization and presented in the standard Authorization header.
Authorization: Bearer bxorg_01h455vb4pex5vsknk084sn02q_abc123def456
API keys are prefixed with bxorg_. Keys can be created, rotated, and revoked from your organization dashboard or via the API.
Key scopes
Each key is created with a set of scopes describing intended use. Endpoints today check for an owning org or a valid API-key principal rather than fine-grained scope enforcement; track the scopes you grant so you can tighten them as scope-level checks roll out.
| Scope | Description |
|---|---|
vault:read | Read connected customer vault data |
connections:read | List connected customer accounts |
connections:write | Exchange grant codes and revoke connections |
webhooks:write | Subscribe and unsubscribe webhooks |
Endpoints
Connect flow (user authorizes your app)
Read connected customer data
Org & admin
Webhooks
Ownership boundary
The user owns their personal data. An organization cannot delete a user's vault entries or BoxOwl account. An organization can only:
- Revoke its own connection to a user (and stop reading their data immediately), and
- Hard-delete its own local copies of any data it cached.
When a user deletes their BoxOwl account from their app, every connected org receives the 30-day deletion lifecycle webhooks (customer.vault.deletion-pending, then ...deletion-confirmed or ...deletion-cancelled) so they can keep their own records in sync. There is no org-callable "erase this user" endpoint, and there will not be one.
Webhook Events
When you subscribe, BoxOwl pushes HTTP POST payloads to your endpoint. Each event includes a type, timestamp, organizationId, and data object.
| Event type | Description |
|---|---|
customer.connection-established | A user granted your org access to their vault |
customer.connection-revoked | The connection was revoked (by user or org). Stop reading their vault immediately |
customer.vault.updated | A connected user's vault was modified |
customer.vault.deletion-pending | User has initiated account deletion. 30-day grace period; hide local copies |
customer.vault.deletion-cancelled | User cancelled the deletion during the grace period |
customer.vault.deletion-confirmed | 30-day grace expired; the user's BoxOwl account is gone. Hard-delete your copies |
customer.vault.exported | User issued a DSAR export against the connection |
customer.re-consent.required | Re-consent is needed (e.g. new categories requested) |
customer.re-consent.completed | Re-consent was granted |
customer.vault.written-by-app | An org wrote to the user's vault via the PDaaS framework (write-back path). Distinguishes "user-self" from "another app on the user's behalf." |
customer.vault.flags-changed | Collection-row flags (e.g. is_primary) changed via the PDaaS framework. |
customer.vault.user-updated | PDaaS True only. A user-self vault change. Fires to every connected PDaaS True org with scope for the changed field (excluding the org that drove the write, if any). See the PDaaS True section below. |
Payload signature
Each webhook request includes a X-BoxOwl-Signature header of the form sha256=<hex>, computed as HMAC-SHA256 of the raw JSON body using the secret returned when you registered the webhook (not your API key's secret). Two additional headers identify the delivery: X-BoxOwl-Event (event type) and X-BoxOwl-Delivery (unique ID, useful for idempotency).
The body BoxOwl signs and POSTs is:
{
"event": "customer.connection-established",
"timestamp": "2026-05-23T14:22:08Z",
"deliveryId": "owd_01h...",
"data": { ... event-specific payload ... }
}
Verify the signature against the raw bytes BoxOwl posted (do not re-serialize) and compare in constant time:
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.BOXOWL_WEBHOOK_SECRET;
// Capture the raw body so we sign the exact bytes BoxOwl sent.
app.post('/webhooks/boxowl',
express.raw({ type: 'application/json' }),
(req, res) => {
const sigHeader = req.header('X-BoxOwl-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(req.body)
.digest('hex');
const a = Buffer.from(sigHeader);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
// ... handle payload.event ...
res.status(204).end();
});
Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['BOXOWL_WEBHOOK_SECRET'].encode()
@app.post('/webhooks/boxowl')
def handle():
sig = request.headers.get('X-BoxOwl-Signature', '')
expected = 'sha256=' + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
payload = request.get_json()
# ... handle payload['event'] ...
return '', 204
Idempotency: BoxOwl retries delivery up to 5 times with exponential backoff. Persist the deliveryId on first successful handling and short-circuit replays so retries are safe.
PDaaS True endpoints
Three additional endpoints unlock for orgs on the PDaaS True subscription tier. Standard PDaaS orgs that hit them get 403 upgrade_required. Full integrator narrative on the developer docs page.
Identity JWT — tags claim
PDaaS True orgs get a tags claim on every X-BoxOwl-Identity JWT delivered to their domain, alongside the standard tier: "pdaas_true" marker. The claim is a flat { key: stringValue } map sourced from OrgUserTag rows on the connection.
{
"sub": "ouid-alice-yourshop-1",
"name": "Alice Smith",
"verified": true,
"tier": "pdaas_true",
"org": "your-shop",
"tags": { "customerId": "C123", "memberTier": "gold" },
"smrt": { "ageBand": "25-34", "region": "CO" },
"iss": "boxowl.me",
"exp": 1748392200
}
Tag write — POST /api/v1/orgs/{slug}/connections/{orgUid}/tags
Org API key auth. Writes (or upserts) tags on the connection. Path uses the pairwise orgUid as the per-user identifier.
Request body:
{ "customerId": "C123", "memberTier": "gold", "loyaltyPoints": "4200" }
Values are strings only. Don't put PII in tags — they're meant for your internal references.
Batch sync — POST /api/v1/pdaas/true/sync
Org API key auth. Returns the latest snapshot of consented fields for a batch of orgUids.
Request body:
{
"orgUids": ["ouid-alice-...", "ouid-bob-..."],
"scopes": ["address.primary", "identity.name"]
}
Response:
{
"results": [
{
"orgUid": "ouid-alice-...",
"data": { "address.primary": { "line1": "..." }, "identity.name": { ... } },
"fetchedAt": "2026-05-28T14:30:00Z",
"missingScopes": []
},
{ "orgUid": "ouid-bob-...", "data": { ... }, "fetchedAt": "...", "missingScopes": ["address.primary"] }
]
}
- Batch size cap: 100 orgUids per request.
- Scopes in the request narrow what the response returns; missing-from-grant scopes appear in
missingScopesand are silently omitted fromdata. - OrgUids that don't belong to the calling org are silently 404'd in the result. No information leak.
- Connections with re-consent pending are silently omitted from the result set.
- Rate-limited per API key.
Missed-events recovery — GET /api/v1/pdaas/true/events?since=<ISO>
Org API key auth. Returns persisted customer.vault.user-updated events created strictly after the since cursor, oldest first.
Response:
{
"events": [
{
"event": "customer.vault.user-updated",
"orgUid": "ouid-alice-...",
"payload": { "orgUid": "ouid-alice-...", "changes": { "address.primary": { ... } }, "updatedAt": "..." },
"createdAt": "2026-05-28T10:05:00Z"
},
...
]
}
- Cap: 500 events per page. Advance the cursor by the last entry's
createdAtand re-call. - Retention: 30 days.
- Errors:
400ifsinceisn't ISO-8601,403 upgrade_requiredfor non-PDaaS-True callers.
True-up webhook payload — customer.vault.user-updated
The webhook side of the recovery endpoint. Fires the moment a user-self vault change lands at BoxOwl, scoped to every connected PDaaS True org that has scope for the changed field.
POST <your webhook URL>
X-BoxOwl-Signature: sha256=<hex>
Content-Type: application/json
{
"event": "customer.vault.user-updated",
"orgUid": "ouid-alice-yourshop-1",
"changes": { "address.primary": { "line1": "123 New St", "city": "Denver", ... } },
"updatedAt": "2026-05-28T14:30:00Z"
}
Same HMAC signature scheme as the standard webhooks above. The orgUid in the payload matches the sub claim from the identity JWT for the same user.
Errors
403 upgrade_required— calling org is not on the PDaaS True subscription tier.400— malformed request body,sincenot ISO-8601, batch size over cap.401— missing or invalid org API key.
Error Codes
| HTTP | Typical meaning |
|---|---|
| 401 | Missing or invalid API key |
| 403 | API key valid but not authorised for this org / connection; or category not in the user's sharedCategories |
| 404 | Organisation, connection, or resource not found |
| 410 | Grant code already exchanged or expired |
| 429 | Rate limit exceeded |