Controlled access · API v1

PsyMind API v1

Developer Documentation

Start with one authenticated read, then move through clients, practices, assignments, outcomes, and webhooks. The contract below is based on OpenAPI 1.7.0.

Start here

Quickstart

The fastest contract check is an authenticated GET /voices. It does not create data and is available to service credentials with practices:read.

Access status

Approved organization owners and admins can create, rotate, and revoke credentials in the Developer Portal. Organization access remains controlled.

1. Store the credential

Keep the service credential on your server. Never expose it in browser or mobile client code.

export PSYMIND_API_TOKEN="your_service_credential"

2. Make your first call

curl "https://psymind.app/api/v1/voices?language=en" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN"
const response = await fetch(
  'https://psymind.app/api/v1/voices?language=en',
  { headers: { Authorization: `Bearer ${process.env.PSYMIND_API_TOKEN}` } }
);
const body = await response.json();
if (!response.ok) throw new Error(`${body.error?.code}: ${body.error?.message}`);
import os
import requests

response = requests.get(
    "https://psymind.app/api/v1/voices",
    params={"language": "en"},
    headers={"Authorization": f"Bearer {os.environ['PSYMIND_API_TOKEN']}"},
    timeout=10,
)
response.raise_for_status()
body = response.json()

3. Keep the request ID

Error responses include error.request_id. Preserve it in your server logs without recording sensitive client content.

Authentication

Send a bearer token with every protected request:

Authorization: Bearer <token>

The contract supports two actor types:

  • Human actor: a Supabase user access token. Access is filtered by membership and role.
  • Service actor: an opaque PsyMind service credential bound to one organization and an explicit scope set.

When a human actor belongs to multiple organizations, send X-PsyMind-Organization-Id. Service credentials already carry their organization boundary and do not use this header to switch tenants.

Service scopes

Scopes are operation-specific and declared in OpenAPI. Current B2B scope families are:

ResourceReadWrite
Organizationsorganizations:readorganizations:write
Practitionerspractitioners:readpractitioners:write
Clientsclients:readclients:write
Client contextcontext:readcontext:write
Practicespractices:readpractices:write
Assignmentsassignments:readassignments:write
Outcomesoutcomes:readoutcomes:write

Model Context Protocol

PsyMind MCP server

The controlled-access MCP endpoint is https://psymind.app/api/mcp. It is a thin transport over API v1: every business operation uses the same scopes, tenant boundary, idempotency, rate limits, and error contracts as the HTTP API.

Authentication compatibility

Use an organization-bound PsyMind service credential. Clients that can send a static bearer header can connect. OAuth discovery, Dynamic Client Registration, and interactive consent are not available in the current API credential flow.

{
  "url": "https://psymind.app/api/mcp",
  "headers": {
    "Authorization": "Bearer <resolved-by-your-secret-manager>"
  }
}

Tool catalog

ToolAPI v1 operationScope
create_practicePOST /practicespractices:write
get_practiceGET /practices/{practiceId}practices:read
list_practicesGET /practicespractices:read
get_clientGET /clients/{clientId}clients:read
assign_practicePOST /practices/{practiceId}/assignassignments:write
get_practice_audioGET /practices/{practiceId}/audiopractices:read

Mutating tools require a stable idempotency key. Practice listing uses the API's opaque cursor and a limit from 1 to 50. Tool results include the API request ID for support correlation.

Deliberate gaps

get_client_history and get_usage have no API v1 operation. get_outcomes is withheld because its current API operation is not paginated. Audio generation is requested through create_practice(generate_audio=true), then retrieved with get_practice_audio.

Readiness: https://psymind.app/api/mcp-health. Full schemas, errors, limits, and security boundaries are maintained with the server source.

Clients

Client records are tenant-scoped integration objects. Use your stable identifier as external_id; PsyMind returns its own resource ID for subsequent requests.

MethodPathPurpose
POST/clientsCreate a client
GET/clientsList clients
GET/clients/{clientId}Retrieve a client
PATCH/clients/{clientId}Update a client
GET/clients/{clientId}/contextRead personalization context
POST/clients/{clientId}/contextUpdate personalization context
curl -X POST "https://psymind.app/api/v1/clients" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "customer_1042",
    "preferred_language": "en",
    "timezone": "Europe/London",
    "consent_status": "granted"
  }'

Practices

Create a personalized practice for an existing client. The create operation requires client_id and goal and returns an asynchronous resource; retrieve it until it reaches a terminal state.

MethodPathPurpose
POST/practicesCreate a practice
GET/practicesList practices
GET/practices/{practiceId}Retrieve status and metadata
POST/practices/{practiceId}/regenerateRequest regeneration
GET/practices/{practiceId}/audioRetrieve audio metadata
curl -X POST "https://psymind.app/api/v1/practices" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN" \
  -H "Idempotency-Key: practice-customer-1042-001" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "00000000-0000-4000-8000-000000000000",
    "goal": "Prepare for a focused work session",
    "language": "en",
    "duration_seconds": 600,
    "generate_audio": true
  }'
Product boundary

The API creates personalized practices and self-suggestion content. It is not a diagnostic, treatment, cure, or replacement for professional care.

Assignments

An assignment connects a ready practice to a client. Lifecycle writes support idempotency, so retry the same logical transition with the same key.

MethodPathPurpose
POST/practices/{practiceId}/assignCreate an assignment
GET/assignmentsList assignments
GET/assignments/{assignmentId}Retrieve an assignment
POST/assignments/{assignmentId}/startedRecord started
POST/assignments/{assignmentId}/completedRecord completed
curl -X POST "https://psymind.app/api/v1/practices/$PRACTICE_ID/assign" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN" \
  -H "Idempotency-Key: assignment-customer-1042-001" \
  -H "Content-Type: application/json" \
  -d '{ "client_id": "00000000-0000-4000-8000-000000000000" }'

Outcomes

Outcomes are structured measurements recorded against an assignment. Every write requires a metric name, type, value, and phase.

MethodPathPurpose
POST/assignments/{assignmentId}/outcomesRecord a measurement
GET/assignments/{assignmentId}/outcomesList measurements
curl -X POST "https://psymind.app/api/v1/assignments/$ASSIGNMENT_ID/outcomes" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN" \
  -H "Idempotency-Key: outcome-assignment-001-post" \
  -H "Content-Type: application/json" \
  -d '{
    "metric_name": "focus_readiness",
    "metric_type": "rating",
    "metric_value": 8,
    "phase": "post",
    "source": "partner_app"
  }'

Webhooks

API v1 delivers one minimal notification per canonical B2B transition through a transactional outbox. Manage endpoints with /webhook-endpoints, inspect history with /webhook-deliveries, and use webhooks:read/webhooks:write service scopes. The signing secret is revealed only on create or rotation; deleting an endpoint revokes its encrypted secret.

For the complete event catalog, signed payload contract, retry behavior, security requirements, and curl/TypeScript/Python examples, see the dedicated Webhooks guide.

Secret rotation

Send PATCH /v1/webhook-endpoints/{webhookEndpointId} with {"regenerate_secret":true}. Rotation is an atomic tenant-scoped compare-and-swap: one concurrent request receives the new one-time signing_secret. A stale concurrent request receives HTTP 409 with error.code=webhook_secret_rotation_conflict and no plaintext; retry the complete PATCH.

Canonical events

  • practice.created, practice.ready, practice.failed
  • audio.ready, audio.failed
  • practice.assigned, practice.started, practice.completed
  • outcome.submitted

Each payload contains only event id/type/time/version and data.object with resource id/type/status. It excludes names, prompts, notes, free text, client identifiers, outcome values, and sensitive context.

Signature headers

PsyMind-Event-Id: <event-id>
PsyMind-Timestamp: <unix-seconds>
PsyMind-Signature: v1=<64-lowercase-hex-HMAC>
Idempotency-Key: <same-event-id>

Verify HMAC-SHA256 over {PsyMind-Timestamp}.{exact-raw-body} with the full whsec_ secret before parsing JSON. Use constant-time comparison, reject timestamps outside ±300 seconds, and require the parsed payload id to equal PsyMind-Event-Id. Persist that event id atomically before side effects; if it already exists, return 2xx without repeating work.

import { verifyWebhook } from "@psymind/sdk";

const event = verifyWebhook(rawBody, request.headers, process.env.PSYMIND_WEBHOOK_SECRET);
// INSERT event.id with a UNIQUE constraint before processing.

Delivery behavior

  • Every 2xx response is success; redirects, all non-2xx responses, network errors, and the 10-second timeout are failures.
  • Up to seven attempts: immediate, then 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, and 24 hours.
  • Retries and manual replay reuse the same event id and payload. Manual replay is available only for a terminally failed delivery.
  • webhook.test uses the same signed outbox/delivery/history pipeline and targets only the selected endpoint.
  • Endpoints must use public HTTPS port 443; redirects and private, loopback, link-local, reserved, mixed-DNS, IPv4/IPv6, and DNS-rebinding destinations are rejected.

Errors

Non-success responses use one envelope. Branch on error.code; use the human-readable message for diagnostics, not control flow.

{
  "error": {
    "code": "invalid_request",
    "message": "The request could not be processed.",
    "request_id": "req-…",
    "details": {}
  }
}

Expect contract-declared 4xx and 5xx responses per operation. A 409 can indicate an idempotency-key conflict or domain conflict; 429 indicates throttling.

Rate limits

Operations declare a rate-limit class such as read, search, or write. Numeric limits are deployment configuration and are not documented as fixed API promises.

On 429, read X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After, then retry after the indicated delay with bounded jitter.

Idempotency

Supported write operations accept Idempotency-Key. The key is organization-scoped and may contain 1–200 characters matching letters, digits, dot, underscore, colon, or hyphen.

  • Same key + same payload: returns the same logical resource for 24 hours.
  • Same key + different payload: returns HTTP 409.
  • Use a stable key per business action, not a random key per retry.

The generated API Reference identifies operations where the header is declared.

Pagination

List endpoints use limit and an opaque signed cursor. The default limit is 20 and the allowed range is 1–50.

  • Follow the returned cursor; do not decode or construct it.
  • A cursor is bound to route, sort, order, normalized filters, and relevant authorization context.
  • It is not a database snapshot. On mutable listings, deduplicate resource IDs across pages.
curl "https://psymind.app/api/v1/clients?limit=20&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $PSYMIND_API_TOKEN"

Full contract

Open the generated API Reference for operation-level parameters, required fields, scopes, rate classes, and response codes. Its source version and SHA-256 prefix are displayed in the reference header.