Keypra API
Execute your workflows, compositions, skills, and personas programmatically. Build AI-powered pipelines with a single API call.
Machine-readable resources
Every discovery document lives at a predictable, stable URL so agents and tooling can find the API without scraping this page.
| Resource | URL |
|---|---|
| OpenAPI 3.1 (JSON) | /openapi.json |
| OpenAPI 3.1 (YAML) | /api/openapi.yaml |
| API catalog (RFC 9727) | /.well-known/api-catalog |
| Public API base | /api/public/v1 |
| API health | /api/public/v1/health |
| MCP server | /api/public/mcp |
| Agent skills index | /.well-known/agent-skills/index.json |
| Markdown gateway | /api/public/markdown-gateway |
| LLM site summary | /llms.txt |
Allowlisted pages also honour Accept: text/markdown directly on their canonical URL, and all negotiated responses send Vary: Accept, Accept-Encoding.
ChatGPT & MCP
Connect Keypra to ChatGPT, Claude, or any MCP client to get instant prompt health checks inside your conversations — then jump into Keypra to refine.
MCP server: https://keypra.com/api/public/mcp
No API key required. The analyze_prompt tool scores any prompt for clarity, role, task, context, format, and constraints, then returns a link to continue refining in Keypra.
# In ChatGPT, after adding the Keypra MCP server:
"Check this prompt with Keypra: Write a concise email to my team about the Q3 budget review."Quick Start
Get your API key
Go to Settings → API Access and create a key. The Keypra REST API is reserved for the Expert plan — Builder and Free plans cannot create or use API keys.
Make your first call
curl https://keypra.com/api/public/v1/workflows \
-H "X-API-Key: mdi_your_key_here"Authentication
All requests require an X-API-Key header with a valid Keypra API key (prefix mdi_).
Keys are SHA-256 hashed at rest. Each key has configurable scopes and rate limits.
Credits: API calls consume credits from the same pool as the UI (1 credit = 1,000 tokens). Check X-Credits-Remaining in response headers.
Available Scopes
| Scope | Grants Access To |
|---|---|
| execute:workflow | GET /workflows, POST /workflows/:id/execute |
| execute:composition | GET /compositions, POST /compositions/:id/execute |
| execute:skill | GET /skills, POST /skills/:id/execute |
| execute:persona | GET /personas, POST /personas/:id/execute |
Endpoint Reference
All responses include X-API-Version, X-RateLimit-*, and execution endpoints also include X-Credits-Remaining.
/workflowsList all your workflows
Response Example
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"name": "Content Pipeline",
"description": "Multi-step content generation",
"category": "content",
"usage_count": 42,
"created_at": "2026-02-13T10:00:00Z"
}
],
"count": 1
}
}/workflows/:id/executeExecute a workflow with runtime variables
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | uuid | Workflow ID |
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| variables | object | — | Key-value pairs injected into workflow nodes |
| include_trace | boolean | — | Include per-node execution trace (default: false) |
Request Body Example
{
"variables": { "topic": "AI trends" },
"include_trace": true
}Response Example
{
"success": true,
"data": {
"run_id": "uuid",
"status": "completed",
"outputs": { "summary": "..." },
"node_results": [ ... ],
"duration_ms": 3200,
"token_usage": { "input_tokens": 800, "output_tokens": 1500, "total_tokens": 2300 }
}
}/compositionsList all your prompt compositions
Response Example
{
"success": true,
"data": {
"items": [
{ "id": "uuid", "name": "Blog Writer", "description": "...", "tags": ["content"], "times_used": 15 }
],
"count": 1
}
}/compositions/:id/executeExecute a composition with variable substitution
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | uuid | Composition ID |
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| variables | object | — | Variable substitution values |
| model | string | — | Model override (tier-gated) |
Request Body Example
{
"variables": { "audience": "developers" },
"model": "openai/gpt-5-mini"
}Response Example
{
"success": true,
"data": {
"content": "Generated text...",
"model_used": "openai/gpt-5-mini",
"token_usage": { "input_tokens": 450, "output_tokens": 1200, "total_tokens": 1650 },
"duration_ms": 2100
}
}/skillsList all your custom skills
Response Example
{
"success": true,
"data": {
"items": [
{ "id": "uuid", "name": "Feedback Analyzer", "category": "analysis" }
],
"count": 1
}
}/skills/:id/executeExecute a skill with context input
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | uuid | Skill ID |
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| context | string | ✓ | Input text for the skill to process |
| model | string | — | Model override (tier-gated) |
Request Body Example
{
"context": "Analyze this customer feedback: ...",
"model": "google/gemini-2.5-flash"
}Response Example
{
"success": true,
"data": {
"content": "Analysis result...",
"skill_name": "Feedback Analyzer",
"model_used": "google/gemini-2.5-flash",
"token_usage": { "input_tokens": 300, "output_tokens": 900, "total_tokens": 1200 },
"duration_ms": 1800
}
}/personasList all your AI personas
Response Example
{
"success": true,
"data": {
"items": [
{
"id": "uuid",
"name": "Corporate Strategy Analyst",
"emoji": "🏢",
"description": "Expert in strategic planning",
"content_preview": "You are a corporate strategy analyst..."
}
],
"count": 1
}
}/personas/:id/executeExecute a persona with user input
Path Parameters
| Name | Type | Description |
|---|---|---|
| id | uuid | Persona ID |
Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
| input | string | ✓ | User input for the persona to process |
| context | string | — | Additional context appended to input |
| model | string | — | Model override (tier-gated) |
| temperature | number | — | Temperature for AI response (0-1) |
Request Body Example
{
"input": "Analyze this quarterly report...",
"context": "Additional context here...",
"model": "google/gemini-2.5-flash",
"temperature": 0.7
}Response Example
{
"success": true,
"data": {
"content": "Strategic analysis result...",
"persona_name": "Corporate Strategy Analyst",
"model_used": "google/gemini-2.5-flash",
"token_usage": { "input_tokens": 245, "output_tokens": 890, "total_tokens": 1135 },
"duration_ms": 2340
}
}OpenAPI Specification
Import the spec into Postman, Insomnia, or use it to generate typed SDKs in any language.
openapi: "3.1.0"
info:
title: Keypra API
version: "2026-05-05"
description: |
Execute your workflows, compositions, and skills programmatically.
Build AI-powered pipelines with a single API call.
contact:
name: Keypra Support
url: https://keypra.com/help
servers:
- url: https://keypra.com/api/public/v1
description: Production
security:
- apiKey: []
paths:
/workflows:
get:
operationId: listWorkflows
summary: List workflows
description: Returns all workflows owned by the authenticated user.
tags: [Workflows]
responses:
"200":
description: Workflow list
content:
application/json:
schema:
$ref: "#/components/schemas/WorkflowListResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
/workflows/{id}/execute:
post:
operationId: executeWorkflow
summary: Execute a workflow
description: Run a workflow with runtime variables. Returns outputs and optional trace.
tags: [Workflows]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteWorkflowRequest"
responses:
"200":
description: Execution result
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteWorkflowResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
X-Credits-Remaining:
$ref: "#/components/headers/X-Credits-Remaining"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"402":
$ref: "#/components/responses/InsufficientCredits"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/RateLimited"
/compositions:
get:
operationId: listCompositions
summary: List compositions
description: Returns all prompt compositions owned by the authenticated user.
tags: [Compositions]
responses:
"200":
description: Composition list
content:
application/json:
schema:
$ref: "#/components/schemas/CompositionListResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
/compositions/{id}/execute:
post:
operationId: executeComposition
summary: Execute a composition
description: Run a prompt composition with variable substitution and optional model override.
tags: [Compositions]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteCompositionRequest"
responses:
"200":
description: Execution result
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteCompositionResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
X-Credits-Remaining:
$ref: "#/components/headers/X-Credits-Remaining"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"402":
$ref: "#/components/responses/InsufficientCredits"
"403":
$ref: "#/components/responses/ModelAccessDenied"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/RateLimited"
/skills:
get:
operationId: listSkills
summary: List skills
description: Returns all custom skills owned by the authenticated user.
tags: [Skills]
responses:
"200":
description: Skill list
content:
application/json:
schema:
$ref: "#/components/schemas/SkillListResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
/skills/{id}/execute:
post:
operationId: executeSkill
summary: Execute a skill
description: Run a skill with context input and optional model override.
tags: [Skills]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
description: |
Provide either `input` (preferred, matches SDK) or `context` (legacy alias).
If both are sent, `input` wins.
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteSkillRequest"
responses:
"200":
description: Execution result
content:
application/json:
schema:
$ref: "#/components/schemas/ExecuteSkillResponse"
headers:
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Reset:
$ref: "#/components/headers/X-RateLimit-Reset"
X-Credits-Remaining:
$ref: "#/components/headers/X-Credits-Remaining"
"400":
$ref: "#/components/responses/ValidationError"
"401":
$ref: "#/components/responses/Unauthorized"
"402":
$ref: "#/components/responses/InsufficientCredits"
"403":
$ref: "#/components/responses/ModelAccessDenied"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/RateLimited"
/personas:
get:
operationId: listPersonas
summary: List personas
description: Returns all personas owned by the authenticated user.
tags: [Personas]
responses:
"200":
description: Persona list
content:
application/json:
schema:
$ref: "#/components/schemas/PersonaListResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
/personas/{id}/execute:
post:
operationId: executePersona
summary: Execute a persona conversation
description: Run a persona with a user message and optional model override.
tags: [Personas]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ExecutePersonaRequest"
responses:
"200":
description: Persona response
content:
application/json:
schema:
$ref: "#/components/schemas/ExecutePersonaResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"402":
$ref: "#/components/responses/InsufficientCredits"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/RateLimited"
/judge/run:
post:
operationId: runJudge
summary: Run the Judgement workflow against a prompt
description: |
Evaluates a prompt against optional criteria and returns a structured
verdict + score. Expert tier only.
tags: [Judge]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/JudgeRunRequest"
responses:
"200":
description: Judgement verdict
content:
application/json:
schema:
$ref: "#/components/schemas/JudgeRunResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"402":
$ref: "#/components/responses/InsufficientCredits"
"429":
$ref: "#/components/responses/RateLimited"
/sanitiser/scan:
post:
operationId: sanitiserScan
summary: Scan text against the org Prompt Sanitiser ruleset
description: |
Returns matched findings (PII, secrets, custom org rules) with severity.
Non-AI; deterministic. Expert tier only.
tags: [Sanitiser]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SanitiserScanRequest"
responses:
"200":
description: Sanitiser findings
content:
application/json:
schema:
$ref: "#/components/schemas/SanitiserScanResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/context-cards:
get:
operationId: listContextCards
summary: List the caller's Context Cards
description: RLS-scoped — only returns cards owned by the API key holder.
tags: [ContextCards]
responses:
"200":
description: Context card list
content:
application/json:
schema:
$ref: "#/components/schemas/ListResponse"
"401":
$ref: "#/components/responses/Unauthorized"
post:
operationId: createContextCard
summary: Create a Context Card
tags: [ContextCards]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ContextCardWriteRequest"
responses:
"201":
description: Created context card
"401":
$ref: "#/components/responses/Unauthorized"
/context-cards/{id}:
get:
operationId: getContextCard
summary: Fetch a single Context Card
tags: [ContextCards]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Context card
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: updateContextCard
summary: Update a Context Card
tags: [ContextCards]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ContextCardWriteRequest"
responses:
"200":
description: Updated card
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteContextCard
summary: Delete a Context Card
tags: [ContextCards]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
"204":
description: Deleted
"404":
$ref: "#/components/responses/NotFound"
components:
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-API-Key
description: Keypra API key (prefix mdi_)
headers:
X-RateLimit-Limit:
description: Maximum requests per minute for your tier
schema:
type: integer
X-RateLimit-Remaining:
description: Remaining requests in the current window
schema:
type: integer
X-RateLimit-Reset:
description: Unix timestamp when the rate limit resets
schema:
type: integer
X-Credits-Remaining:
description: Remaining token credits after this request
schema:
type: integer
schemas:
TokenUsage:
type: object
properties:
input_tokens:
type: integer
output_tokens:
type: integer
total_tokens:
type: integer
ErrorBody:
type: object
properties:
success:
type: boolean
example: false
error:
type: object
properties:
code:
type: string
example: INSUFFICIENT_CREDITS
message:
type: string
WorkflowListItem:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
category:
type: string
usage_count:
type: integer
created_at:
type: string
format: date-time
WorkflowListResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/WorkflowListItem"
count:
type: integer
ExecuteWorkflowRequest:
type: object
properties:
variables:
type: object
additionalProperties: true
description: Runtime variables to inject into the workflow
include_trace:
type: boolean
default: false
description: Include per-node execution trace
ExecuteWorkflowResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
run_id:
type: string
format: uuid
status:
type: string
enum: [completed, failed]
outputs:
type: object
node_results:
type: array
items:
type: object
duration_ms:
type: integer
token_usage:
$ref: "#/components/schemas/TokenUsage"
CompositionListItem:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
tags:
type: array
items:
type: string
times_used:
type: integer
created_at:
type: string
format: date-time
CompositionListResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/CompositionListItem"
count:
type: integer
ExecuteCompositionRequest:
type: object
properties:
variables:
type: object
additionalProperties: true
model:
type: string
description: "Model override (e.g. openai/gpt-5-mini)"
ExecuteCompositionResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
content:
type: string
model_used:
type: string
token_usage:
$ref: "#/components/schemas/TokenUsage"
duration_ms:
type: integer
SkillListItem:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
category:
type: string
created_at:
type: string
format: date-time
SkillListResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/SkillListItem"
count:
type: integer
ExecuteSkillRequest:
description: |
Provide either `input` (preferred, matches SDK) or `context` (legacy alias).
If both are sent, `input` wins.
oneOf:
- type: object
required: [input]
properties:
input:
type: string
description: Input text for the skill to process.
model:
type: string
description: "Model override (e.g. google/gemini-2.5-flash)"
variables:
type: object
additionalProperties: true
- type: object
required: [context]
properties:
context:
type: string
description: Legacy alias for `input`.
model:
type: string
variables:
type: object
additionalProperties: true
ExecuteSkillResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
content:
type: string
skill_name:
type: string
model_used:
type: string
token_usage:
$ref: "#/components/schemas/TokenUsage"
duration_ms:
type: integer
ListResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
type: object
additionalProperties: true
count:
type: integer
PersonaListItem:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
description:
type: string
emoji:
type: string
created_at:
type: string
format: date-time
PersonaListResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/PersonaListItem"
count:
type: integer
ExecutePersonaRequest:
type: object
required: [input]
properties:
input:
type: string
description: User message to send to the persona.
context:
type: string
description: Optional extra context appended to the user message.
model:
type: string
temperature:
type: number
ExecutePersonaResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
content:
type: string
persona_name:
type: string
model_used:
type: string
token_usage:
$ref: "#/components/schemas/TokenUsage"
JudgeRunRequest:
type: object
required: [prompt]
properties:
prompt:
type: string
criteria:
type: string
model:
type: string
JudgeRunResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
verdict:
type: string
enum: [pass, fail, needs_revision]
score:
type: number
minimum: 0
maximum: 100
reasoning:
type: string
suggestions:
type: array
items:
type: string
SanitiserScanRequest:
type: object
required: [text]
properties:
text:
type: string
SanitiserScanResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
findings:
type: array
items:
type: object
properties:
rule_id:
type: string
label:
type: string
severity:
type: string
enum: [info, warn, block]
match:
type: string
start:
type: integer
end:
type: integer
severity:
type: string
enum: [clean, info, warn, block]
ContextCardWriteRequest:
type: object
required: [title, content]
properties:
title:
type: string
content:
type: string
category:
type: string
tags:
type: array
items:
type: string
responses:
Unauthorized:
description: Missing or invalid API key
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
InsufficientCredits:
description: Not enough credits
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
ModelAccessDenied:
description: Model not available on your tier
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
ValidationError:
description: Invalid request body
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorBody"
SDK Wrappers
Copy these self-contained client classes into your project. They include automatic retry on 429, typed errors, and credit tracking via response headers.
import requests
import time
class KeypraError(Exception):
def __init__(self, code: str, message: str, status: int):
self.code = code
self.message = message
self.status = status
super().__init__(f"{code}: {message}")
class RateLimitError(KeypraError):
def __init__(self, message: str, retry_after: int = 60):
self.retry_after = retry_after
super().__init__("RATE_LIMITED", message, 429)
class InsufficientCreditsError(KeypraError):
def __init__(self, message: str):
super().__init__("INSUFFICIENT_CREDITS", message, 402)
class KeypraClient:
"""Keypra API client with automatic retry and credit monitoring."""
BASE_URL = "https://keypra.com/api/public/v1"
def __init__(self, api_key: str, max_retries: int = 3):
self.api_key = api_key
self.max_retries = max_retries
self.credits_remaining = None
self._session = requests.Session()
self._session.headers.update({
"X-API-Key": api_key,
"Content-Type": "application/json",
})
def _request(self, method: str, path: str, **kwargs) -> dict:
url = f"{self.BASE_URL}{path}"
for attempt in range(self.max_retries + 1):
resp = self._session.request(method, url, **kwargs)
# Update credit balance from headers
cr = resp.headers.get("X-Credits-Remaining")
if cr is not None:
self.credits_remaining = int(cr)
if resp.status_code == 429:
if attempt < self.max_retries:
wait = int(resp.headers.get("Retry-After", 60))
time.sleep(wait)
continue
raise RateLimitError(resp.json()["error"]["message"])
if resp.status_code == 402:
raise InsufficientCreditsError(resp.json()["error"]["message"])
if not resp.ok:
body = resp.json()
raise KeypraError(
body["error"]["code"],
body["error"]["message"],
resp.status_code,
)
return resp.json()
# ── Workflows ──────────────────────────────────────────
def list_workflows(self) -> dict:
return self._request("GET", "/workflows")
def execute_workflow(
self, workflow_id: str, variables: dict = None, include_trace: bool = False
) -> dict:
return self._request(
"POST",
f"/workflows/{workflow_id}/execute",
json={"variables": variables or {}, "include_trace": include_trace},
)
# ── Compositions ───────────────────────────────────────
def list_compositions(self) -> dict:
return self._request("GET", "/compositions")
def execute_composition(
self, composition_id: str, variables: dict = None, model: str = None
) -> dict:
body: dict = {"variables": variables or {}}
if model:
body["model"] = model
return self._request(
"POST", f"/compositions/{composition_id}/execute", json=body
)
# ── Skills ─────────────────────────────────────────────
def list_skills(self) -> dict:
return self._request("GET", "/skills")
def execute_skill(
self, skill_id: str, input: str, model: str = None
) -> dict:
# 'input' is the preferred field (matches OpenAPI). 'context' is
# accepted as a legacy alias server-side.
body: dict = {"input": input}
if model:
body["model"] = model
return self._request(
"POST", f"/skills/{skill_id}/execute", json=body
)
# ── Personas ───────────────────────────────────────────
def list_personas(self) -> dict:
return self._request("GET", "/personas")
def execute_persona(
self, persona_id: str, input: str, model: str = None
) -> dict:
body: dict = {"input": input}
if model:
body["model"] = model
return self._request(
"POST", f"/personas/{persona_id}/execute", json=body
)
# ── Usage ──────────────────────────────────────────────────
# client = KeypraClient("mdi_your_key_here")
# workflows = client.list_workflows()
# result = client.execute_workflow("uuid", {"topic": "AI trends"})
# print(f"Credits left: {client.credits_remaining}")Tutorials
Step-by-step guides for common API use cases.
Building a Content Pipeline
Chain workflow executions to build an automated content generation pipeline — from research to final article.
Create your workflow in the Keypra UI
Build a multi-node workflow in the visual editor. For this example, create a "Content Pipeline" workflow with variables: topic, audience, and tone.
List workflows to get the ID
from keypra_client import KeypraClient
client = KeypraClient("mdi_your_key_here")
workflows = client.list_workflows()
for wf in workflows["data"]["items"]:
print(f'{wf["name"]}: {wf["id"]}')Execute with variables
result = client.execute_workflow(
"YOUR_WORKFLOW_ID",
variables={
"topic": "AI agents in 2026",
"audience": "technical leaders",
"tone": "authoritative"
},
include_trace=True
)
print(f"Status: {result['data']['status']}")
print(f"Duration: {result['data']['duration_ms']}ms")
print(f"Credits remaining: {client.credits_remaining}")Chain outputs into a second workflow
Use the output of one workflow as input to the next — for example, feeding a research summary into an article writer.
# Step 1: Research
research = client.execute_workflow("RESEARCH_WF_ID", variables={"topic": "AI agents"})
summary = research["data"]["outputs"]["summary"]
# Step 2: Write article using research output
article = client.execute_workflow("WRITER_WF_ID", variables={
"research_summary": summary,
"word_count": 1500,
"style": "blog post"
})
print(article["data"]["outputs"]["article"])Running Persona Voting
Execute a multi-persona composition and aggregate voting results programmatically.
Set up your composition with multiple personas
In the Keypra Composition Builder, create a composition that includes multiple context cards (personas). Each persona will evaluate the same prompt from its perspective.
Execute the composition via API
result = client.execute_composition(
"YOUR_COMPOSITION_ID",
variables={
"proposal": "Launch a freemium tier with 50 free API calls/month",
"question": "Should we proceed? Vote YES or NO and explain."
},
model="openai/gpt-5-mini"
)
print(result["data"]["content"])Parse and aggregate voting results
import json
# Run multiple compositions for different persona groups
persona_groups = ["EXEC_COMP_ID", "TECH_COMP_ID", "MARKETING_COMP_ID"]
votes = {"YES": 0, "NO": 0, "reasons": []}
for comp_id in persona_groups:
result = client.execute_composition(comp_id, variables={
"proposal": "Launch a freemium API tier",
"question": "Vote YES or NO with reasoning. Respond as JSON."
})
# Parse structured response
content = result["data"]["content"]
try:
parsed = json.loads(content)
votes[parsed["vote"]] += 1
votes["reasons"].append({
"group": comp_id,
"vote": parsed["vote"],
"reasoning": parsed["reasoning"]
})
except json.JSONDecodeError:
print(f"Warning: Could not parse response from {comp_id}")
print(f"Results: YES={votes['YES']}, NO={votes['NO']}")
print(f"Credits remaining: {client.credits_remaining}")Direct Persona Execution
Execute AI personas directly via API — no composition wrapper needed. Use personas as standalone AI roles in external tools.
List your personas
Personas are context cards with the "persona" category. List them to find the ID you need.
personas = client.list_personas()
for p in personas["data"]["items"]:
print(f'{p["emoji"]} {p["name"]}: {p["id"]}')Execute a persona with user input
The persona's system prompt is used as the AI's role instruction. Your input becomes the user message.
result = client.execute_persona(
"YOUR_PERSONA_ID",
input="Review this email draft for tone and clarity...",
context="The email is going to a C-level executive.",
model="google/gemini-2.5-flash",
temperature=0.7
)
print(f"Persona: {result['data']['persona_name']}")
print(f"Response: {result['data']['content']}")
print(f"Tokens: {result['data']['token_usage']['total_tokens']}")Multi-persona analysis pattern
Run the same input through multiple personas to get diverse perspectives — without building a composition.
persona_ids = ["STRATEGIST_ID", "LEGAL_ID", "MARKETING_ID"]
proposal = "Launch a new AI-powered feature for enterprise clients."
perspectives = []
for pid in persona_ids:
result = client.execute_persona(pid, input=proposal)
perspectives.append({
"persona": result["data"]["persona_name"],
"analysis": result["data"]["content"]
})
for p in perspectives:
print(f"\n--- {p['persona']} ---")
print(p["analysis"])API Playground
Test API endpoints live. Paste your API key, select an endpoint, fill in parameters, and execute.
Paste the full API key you received when you created it. Keys are not stored in the browser.
No parameters required for this endpoint.
Rate Limits & Pricing
API access is an Expert-tier feature. Credits are shared with the UI — no separate API pool. Downgrading from Expert revokes all API keys after a 7-day grace period.
| Feature | Expert |
|---|---|
| Requests / minute | 120 |
| Requests / day | 20,000 |
| API keys | 5 |
| Credit pool | Shared with UI |
| Models | All models incl. GPT-5.2 |
| Webhook callbacks | ✓ |
| Endpoints | workflows, compositions, skills, personas, judge, sanitiser, context-cards |
Response Headers
X-API-Version: 2026-02-13
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1739462400
X-Credits-Remaining: 85200Retry Strategy
When you receive a 429 response, read the Retry-After header (seconds) and wait before retrying. The SDK wrappers above handle this automatically with configurable max retries.
# Retry-After example
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 60))
time.sleep(wait)
response = session.request(method, url, **kwargs) # retryError Handling
All errors return a structured JSON body with a machine-readable code field.
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits. Available: 200, minimum required: 500"
}
}| Code | Status | Description |
|---|---|---|
| MISSING_API_KEY | 401 | No X-API-Key header provided |
| INVALID_API_KEY | 401 | Key is invalid, expired, or revoked |
| RATE_LIMITED | 429 | Per-minute rate limit exceeded |
| DAILY_LIMIT_EXCEEDED | 429 | Daily request limit exceeded |
| INSUFFICIENT_CREDITS | 402 | Not enough credits to execute |
| BILLING_FAILED | 402 | Credit deduction failed post-execution; result withheld |
| INSUFFICIENT_PERMISSIONS | 403 | API key lacks the required scope |
| MODEL_ACCESS_DENIED | 403 | Requested model not available on your tier |
| RESOURCE_NOT_FOUND | 404 | Workflow/composition/skill/persona not found |
| ACCESS_DENIED | 403 | You don't own this resource |
| VALIDATION_ERROR | 400 | Malformed or missing request body fields |
| AI_SERVICE_ERROR | 500 | Upstream AI provider failed |
Backoff Guidance
| Error Type | Recommended Action |
|---|---|
| 429 Rate Limited | Wait Retry-After seconds, then retry (max 3 attempts) |
| 402 Insufficient Credits | Do not retry. Purchase more credits or wait for monthly reset. |
| 402 Billing Failed | Do not retry immediately. Check credit balance, then retry once. |
| 500 AI Service Error | Retry with exponential backoff (1s, 2s, 4s). Max 3 attempts. |
| 401 Auth Errors | Do not retry. Check API key validity. |