Partner APINew
Complete integration guide for the Brand Kit OS Partner API — authentication, endpoints, data model, webhooks, and Leafpad mapping.
Last updated: February 19, 2026
Partner API Integration Guide
The Brand Kit OS Partner API lets partner applications pull rich brand context to enhance AI-generated content. This guide covers everything you need to integrate.
Base URL:
https://fupwpcqmyykfiuakjxxc.supabase.co/functions/v1/partner-api
Getting Started
Integration takes three steps:
- Link — Call
POST /v1/integrations/partner/linkwith abrand_kit_idto receive your API key and webhook secret. - Pull — Call
GET /v1/brand-kits/{id}before generating content to fetch the latest brand context. - Listen — Receive webhook pushes when a brand kit is updated or deleted.
Authentication
Every request requires a Bearer token in the Authorization header:
Authorization: Bearer pk_a1b2c3d4e5f6...
Per-Partner Keys (recommended)
During the /link step, Brand Kit OS returns a unique API key prefixed with pk_. This key is:
- Shown only once — save it securely
- Hashed (SHA-256) before storage — we never store the plaintext
- Independently revocable per partner integration
- Used for all subsequent API calls
Global Key (deprecated)
The legacy PARTNER_API_KEY environment variable is still accepted as a fallback. It will be removed in a future version. Migrate to per-partner keys.
API Reference
POST /v1/integrations/partner/link
Links a partner app to a Brand Kit OS brand kit. Called once during onboarding.
Request:
curl -X POST \
https://fupwpcqmyykfiuakjxxc.supabase.co/functions/v1/partner-api/v1/integrations/partner/link \
-H "Authorization: Bearer <GLOBAL_PARTNER_KEY>" \
-H "Content-Type: application/json" \
-d '{"brand_kit_id": "uuid-here", "external_user_id": "leafpad-user-123", "external_workspace_id": "leafpad-ws-456", "webhook_url": "https://api.leafpad.app/webhooks/bkos"}'
| Field | Type | Required | Description |
|---|---|---|---|
brand_kit_id |
UUID | ✅ | The Brand Kit OS brand kit to link |
external_user_id |
string | No | Your user's ID in your system |
external_workspace_id |
string | No | Your workspace/org ID |
webhook_url |
string (HTTPS) | No | URL to receive webhook deliveries |
Response (201):
{
"integration": {
"id": "uuid",
"brand_kit_id": "uuid",
"partner_name": "leafpad",
"external_user_id": "leafpad-user-123",
"external_workspace_id": "leafpad-ws-456",
"webhook_url": "https://api.leafpad.app/webhooks/bkos",
"is_active": true
},
"partner_api_key": "pk_a1b2c3d4e5f67890...",
"webhook_secret": "uuid-uuid",
"message": "Save these credentials — they will not be shown again."
}
⚠️ Save
partner_api_keyandwebhook_secretimmediately. They are displayed only once.
GET /v1/brand-kits/{brand_kit_id}
Returns the full brand kit data for a single brand kit.
curl https://fupwpcqmyykfiuakjxxc.supabase.co/functions/v1/partner-api/v1/brand-kits/<BRAND_KIT_ID> \
-H "Authorization: Bearer pk_a1b2c3d4..."
Response (200):
{
"brand_kit_id": "uuid",
"workspace_id": "uuid",
"updated_at": "2026-02-19T12:00:00Z",
"version": 3,
"compat": { "..." },
"full": { "..." }
}
See Data Model Reference for the compat and full schemas.
GET /v1/brand-kits
Lists brand kits linked to a partner identity.
curl "https://fupwpcqmyykfiuakjxxc.supabase.co/functions/v1/partner-api/v1/brand-kits?external_user_id=leafpad-user-123&limit=20&offset=0" \
-H "Authorization: Bearer pk_a1b2c3d4..."
| Parameter | Type | Default | Description |
|---|---|---|---|
external_user_id |
string | — | Filter by your user ID |
external_workspace_id |
string | — | Filter by your workspace ID |
limit |
integer | 20 | Max results (1–100) |
offset |
integer | 0 | Pagination offset |
At least one of external_user_id or external_workspace_id is required.
Response (200):
{
"data": [ { "brand_kit_id": "...", "compat": {}, "full": {} } ],
"pagination": { "limit": 20, "offset": 0, "count": 1 }
}
Data Model Reference
Every brand kit response contains two data layers:
compat — Drop-in Fields
Simplified fields that map directly to common partner fields (Description, Keywords, Style).
| Field | Type | Description |
|---|---|---|
brand_description |
string | Combined business description and brand story |
writing_style |
string | Comma-separated tone descriptors, sentence style, vocabulary level |
seo_keywords |
string[] | SEO keywords derived from products and benefits |
brand_keywords |
string[] | Brand identity keywords from personality traits and values |
image_style |
string | Visual/image style description |
full — Rich Context
Detailed brand data organized into 8 categories.
full.voice
| Field | Type | Description |
|---|---|---|
tone |
string[] | Tone descriptors (e.g. "professional", "warm") |
personality_traits |
string[] | Brand personality trait names |
do_phrases |
string[] | Encouraged phrases and language patterns |
dont_phrases |
string[] | Phrases to avoid |
reading_level |
string | Target vocabulary level |
formality |
string | Formality level (e.g. "casual", "formal") |
humor_level |
string | How much humor to use |
verbal_style |
object | Full verbal style settings (sentence style, vocabulary, etc.) |
voice_archetypes |
array | Selected voice archetype definitions |
tone_dimensions |
object | Dimensional tone settings (e.g. enthusiasm, assertiveness) |
full.audience
| Field | Type | Description |
|---|---|---|
personas |
array | Target audience personas, each with: name, title, type, is_primary, demographics, goals, pain_points, values, channels |
target_roles |
string[] | Job titles / roles of target audience |
pain_points |
string[] | All audience pain points (flattened) |
objections |
string[] | Common sales objections / barriers |
full.messaging
| Field | Type | Description |
|---|---|---|
value_props |
string[] | Brand promises and value propositions |
positioning |
string | Brand positioning / mission statement |
differentiators |
string[] | Competitive differentiation points from products |
elevator_pitch |
string | Brand story as elevator pitch |
tagline_options |
string[] | Brand taglines |
full.visuals
| Field | Type | Description |
|---|---|---|
image_style |
string | Visual style description |
color_palette |
array | Colors: [{ "name": "primary", "value": "#hex" }] |
typography |
object | { "heading": "Font", "body": "Font", "paragraph": "Font" } |
visual_do |
string[] | Visual guidelines to follow |
visual_dont |
string[] | Visual elements to avoid |
full.seo
| Field | Type | Description |
|---|---|---|
primary_keywords |
string[] | Primary SEO keywords |
secondary_keywords |
string[] | Secondary keywords |
internal_linking_rules |
string | Linking strategy |
geo_focus |
string | Geographic focus |
competitor_keywords |
string[] | Competitor keyword targets |
full.constraints
| Field | Type | Description |
|---|---|---|
taboo_topics |
string[] | Topics to never mention |
compliance_notes |
string | Compliance/legal notes |
claims_to_avoid |
string[] | Claims that should not be made |
required_disclaimers |
string[] | Required disclosure text |
full.examples
| Field | Type | Description |
|---|---|---|
good_examples |
array | Positive examples: [{ "platform": "blog", "content": "...", "original": "..." }] |
bad_examples |
array | Negative examples with same structure |
reference_urls |
string[] | Brand website URL(s) |
full.ctas
| Field | Type | Description |
|---|---|---|
primary_ctas |
string[] | Primary calls-to-action (USPs from products) |
secondary_ctas |
string[] | Secondary CTAs (key benefits from products) |
offer_details |
string | Special pricing/offer information |
Webhooks
When a brand kit is updated or deleted, Brand Kit OS sends a POST request to your registered webhook_url.
Events
| Event | Trigger |
|---|---|
brand_kit.upserted |
Brand kit created or updated |
brand_kit.deleted |
Brand kit deleted |
Payload
{
"event_id": "uuid",
"event_type": "brand_kit.upserted",
"occurred_at": "2026-02-19T12:00:00Z",
"brand_kit_id": "uuid",
"workspace_id": "uuid",
"version": 3,
"compat": { "..." },
"full": { "..." }
}
For brand_kit.deleted, the compat and full fields are omitted.
Signature Verification
Every webhook includes two headers for verification:
| Header | Description |
|---|---|
X-Timestamp |
Unix epoch seconds when the payload was signed |
X-Signature |
hex(HMAC_SHA256(timestamp + "." + body, webhook_secret)) |
Reject the webhook if the timestamp is more than 5 minutes old (replay protection).
Node.js Example
const crypto = require("crypto");
function verifyWebhook(body, secret, timestamp, signature) {
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) return false; // 5 min window
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Python Example
import hmac, hashlib, time
def verify_webhook(body: str, secret: str, timestamp: int, signature: str) -> bool:
if abs(time.time() - timestamp) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Error Handling
All errors return a consistent JSON structure:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message",
"details": {}
}
}
| HTTP Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST |
Invalid or missing parameters |
| 400 | INVALID_ID |
UUID format is incorrect |
| 401 | UNAUTHORIZED |
Invalid or missing API key |
| 404 | NOT_FOUND |
Brand kit or endpoint not found |
| 429 | RATE_LIMITED |
Too many requests |
| 500 | SERVER_ERROR |
Internal error — retry with backoff |
Retry Guidance
- 429: Wait and retry after 60 seconds
- 500: Retry with exponential backoff (1s, 2s, 4s, max 3 retries)
- 4xx (other): Do not retry — fix the request
Rate Limits
| Limit | Value |
|---|---|
| Requests per minute | 60 |
| Applies to | All endpoints equally |
When rate limited, you receive a 429 response. Wait for the current window to expire (≤60 seconds) before retrying.
Leafpad Mapping Guide
This section maps Brand Kit OS data to Leafpad's current fields for easy integration.
Direct Field Mapping (compat layer)
| Leafpad Field | Brand Kit OS Field | Notes |
|---|---|---|
| Business Description | compat.brand_description |
Combined description + brand story |
| Keywords | compat.seo_keywords + compat.brand_keywords |
Merge both arrays for comprehensive keywords |
| Verbal Style | compat.writing_style |
Comma-separated style descriptors |
| Visual Style | compat.image_style |
Image/visual style description |
| Website URL | full.examples.reference_urls[0] |
First reference URL |
Enhanced AI Context (full layer)
For superior blog content, feed these additional fields into your AI prompt:
| Category | What to Include | Impact on Content |
|---|---|---|
| Voice | full.voice.tone, full.voice.do_phrases, full.voice.dont_phrases |
Matches brand tone exactly |
| Audience | full.audience.personas[0].pain_points, full.audience.personas[0].goals |
Targets the right reader |
| Messaging | full.messaging.value_props, full.messaging.differentiators |
Stays on brand message |
| Constraints | full.constraints.taboo_topics, full.constraints.claims_to_avoid |
Avoids brand violations |
| Examples | full.examples.good_examples |
Shows the AI what "good" looks like |
| CTAs | full.ctas.primary_ctas |
Drives conversions |
Example AI Prompt Enhancement
You are writing a blog post for [brand].
TONE: Use these descriptors: ${compat.writing_style}
DO SAY: ${full.voice.do_phrases.join(", ")}
DON'T SAY: ${full.voice.dont_phrases.join(", ")}
AUDIENCE: Writing for ${full.audience.personas[0].name} who struggles with ${full.audience.pain_points.join(", ")}
NEVER MENTION: ${full.constraints.taboo_topics.join(", ")}
INCLUDE CTA: ${full.ctas.primary_ctas[0]}
API Calls Summary
| Direction | Endpoint | When | Purpose |
|---|---|---|---|
| Partner → BKOS | POST /v1/integrations/partner/link |
Once during setup | Exchange credentials |
| Partner → BKOS | GET /v1/brand-kits/{id} |
Before generating content | Pull latest brand context |
| Partner → BKOS | GET /v1/brand-kits?external_user_id=X |
On dashboard load | List linked brand kits |
| BKOS → Partner | Webhook brand_kit.upserted |
When brand kit updates | Push fresh data |
| BKOS → Partner | Webhook brand_kit.deleted |
When brand kit deleted | Clean up cached data |