Docs · API

REST API reference

Stable, tenant-scoped JSON over HTTPS. Every endpoint is authenticated by an API key issued from the dashboard (Settings → API keys), gated by the scopes you select at issue time, and rate-limited at 600 requests per minute per key. Stripe-style envelope conventions where applicable.

OpenAPI 3.1Download specPipe into openapi-generator-cli or fern to scaffold a typed SDK for your language.
Quickstart

Issue a key, then call /meta

  1. In the dashboard, go to Settings → API keys. Issue a key with the smallest scope set that fits your integration.
  2. Copy the ph_live_ or ph_test_ secret immediately — it's shown once. Store it in your secret manager.
  3. Call /api/v1/meta to confirm the tenant + scopes the key carries.
curl https://api.joinplatformhealth.com/api/v1/meta \
  -H "Authorization: Bearer ph_live_..."
Conventions

Auth, envelopes, errors

Auth. Every request must include Authorization: Bearer ph_{env}_…. Keys not in live or test are rejected.

Lists. Wrapped in { data: [...] } with hasMore + nextCursor on paginated endpoints.

Single resources. Returned bare, without an envelope.

Errors. { error: { code, message } }. Codes:

CodeStatusWhen
unauthorized401Bearer header missing, malformed, or key not recognized / revoked.
forbidden403Key is valid, but its scope set does not include the operation.
not_found404Resource does not exist for the calling tenant. Cross-tenant requests resolve here, never to 403.
rate_limited429600 req/min/key exceeded. Response includes a Retry-After header (seconds).
validation_error422Body or query failed schema validation. The message names the offending field.
internal_error500Something we did not anticipate. Reach out at support@joinplatformhealth.com with the request id from the response headers.

Rate limits. 600 req/min per key. On 429 the response includes Retry-After in seconds; back off for that long before retrying.

Tenant isolation. Every endpoint is scoped to the calling key's tenant. Cross-tenant resource access returns 404 not_found (never 403) — existence isn't leakable across tenants.

Idempotency. Every write endpoint (POST and PATCH) accepts an Idempotency-Key header. Send any unique value (a UUID is fine) per logical operation; if the network drops mid-flight, retry with the same key and you get the original response back instead of a duplicate write. Cached responses include Idempotent-Replay: true so you can tell a replay from a fresh execution. Entries live for 24h and are scoped per tenant — a retry from a different key on the same tenant still hits cache. Reusing a key with a different body is rejected with 409 idempotency_conflict — that's a sign the caller is recycling keys across distinct operations, which would otherwise mask the second write. Failed responses (4xx/5xx) are not cached, so a retry after a transient error can still succeed. DELETE doesn't accept the header: deletes are naturally idempotent — replaying a delete is the same as the first delete.

Endpoints · meta

Meta

Introspect the calling key — useful for clients to confirm tenant + scopes before issuing the first real call.

GET/api/v1/metaany valid key

Return the tenant id, environment, scopes, and rate-limit settings of the calling key.

Response
{
  "tenantId": "halcyon-wellness",
  "env": "live",
  "scopes": ["orders.read", "products.read"],
  "rateLimitWindow": "1m",
  "rateLimitMax": 600
}
Example
curl https://api.joinplatformhealth.com/api/v1/meta \
  -H "Authorization: Bearer ph_live_..."
Endpoints · orders

Orders

Read-only access to order history. Every order is tenant-scoped — you only see orders that belong to the key’s tenant.

GET/api/v1/ordersorders.read

List orders. Newest first. Keyset pagination via cursor.

Query
  • limit(integer)Max items per page. Default 50, max 200.
  • cursor(string)Pass the previous response’s nextCursor.
Response
{
  "data": [
    {
      "orderId": "ord-AB12CD",
      "tenantId": "halcyon-wellness",
      "customerEmail": "client@example.com",
      "status": "shipped",
      "totalMinorUnits": 24850,
      "currency": "USD",
      "createdAt": 1733000000000,
      "lines": [ … ]
    }
  ],
  "hasMore": true,
  "nextCursor": "ord-AB12CD"
}
Example
curl "https://api.joinplatformhealth.com/api/v1/orders?limit=50" \
  -H "Authorization: Bearer ph_live_..."
GET/api/v1/orders/{id}orders.read

Fetch a single order. 404 if not in your tenant.

Response
{
  "orderId": "ord-AB12CD",
  "tenantId": "halcyon-wellness",
  …
}
Endpoint-specific errors
  • 404 not_found Order does not exist OR belongs to a different tenant.
Example
curl https://api.joinplatformhealth.com/api/v1/orders/ord-AB12CD \
  -H "Authorization: Bearer ph_live_..."
Endpoints · products

Products

Read + write product catalog. Tenant-scoped; the tenant is taken from the calling key, never from the request body.

GET/api/v1/productsproducts.read

List products. Newest first.

Response
{
  "data": [
    { "productId": "prd-…", "title": "…", "priceMinorUnits": 12500, … }
  ]
}
Example
curl https://api.joinplatformhealth.com/api/v1/products \
  -H "Authorization: Bearer ph_live_..."
POST/api/v1/productsproducts.write

Create a product. Returns 201 with the new resource.

Request body
{
  "title": "Tirzepatide 10mg/ml · 4-week",
  "summary": "GLP-1 receptor agonist · compounded.",
  "description": "Long-form HTML / markdown body…",
  "priceMinorUnits": 80000,
  "category": "peptides"
}
Response
{
  "productId": "prd-…",
  "tenantId": "halcyon-wellness",
  "title": "Tirzepatide 10mg/ml · 4-week",
  …
}
Endpoint-specific errors
  • 422 validation_error Body is missing required fields or contains an invalid category.
Example
curl -X POST https://api.joinplatformhealth.com/api/v1/products \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Tirzepatide 10mg/ml · 4-week",
    "summary": "GLP-1 receptor agonist · compounded.",
    "description": "Long-form body",
    "priceMinorUnits": 80000,
    "category": "peptides"
  }'
GET/api/v1/products/{id}products.read

Fetch a single product. 404 if not in your tenant.

Response
{ "productId": "prd-…", … }
Example
curl https://api.joinplatformhealth.com/api/v1/products/prd-XYZ \
  -H "Authorization: Bearer ph_live_..."
PATCH/api/v1/products/{id}products.write

Update a product. Partial — only the fields you send change.

Request body
{
  "title": "Tirzepatide 10mg/ml · 8-week",
  "priceMinorUnits": 150000
}
Response
{ "productId": "prd-…", "title": "Tirzepatide 10mg/ml · 8-week", … }
Example
curl -X PATCH https://api.joinplatformhealth.com/api/v1/products/prd-XYZ \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "priceMinorUnits": 150000 }'
Endpoints · customers

Customers

Client / customer roster. PII (name, email, phone, address) is included by design — the integration is owned by the tenant who owns the data.

GET/api/v1/customerscustomers.read

List customers. Returns every record for your tenant.

Response
{
  "data": [
    {
      "email": "client@example.com",
      "displayName": "…",
      "tenantId": "halcyon-wellness",
      "firstSeenAt": 1730000000000
    }
  ]
}
Example
curl https://api.joinplatformhealth.com/api/v1/customers \
  -H "Authorization: Bearer ph_live_..."
Endpoints · subscriptions

Subscriptions

Recurring orders. Use these endpoints to integrate refill cadence into a CRM, or to surface pause/resume flows in a custom client portal.

GET/api/v1/subscriptionssubscriptions.read

List subscriptions. Newest first.

Response
{ "data": [ { "id": "sub-…", "status": "active", "intervalDays": 30, … } ] }
Example
curl https://api.joinplatformhealth.com/api/v1/subscriptions \
  -H "Authorization: Bearer ph_live_..."
GET/api/v1/subscriptions/{id}subscriptions.read

Fetch a single subscription. 404 if not in your tenant.

Response
{ "id": "sub-…", "status": "paused", "pausedUntilMs": 1737000000000, … }
Example
curl https://api.joinplatformhealth.com/api/v1/subscriptions/sub-XYZ \
  -H "Authorization: Bearer ph_live_..."
POST/api/v1/subscriptions/{id}subscriptions.write

Pause, resume, or cancel a subscription. Returns the updated resource.

Request body
{ "action": "pause" | "resume" | "cancel" }
Response
{ "id": "sub-…", "status": "canceled", … }
Example
curl -X POST https://api.joinplatformhealth.com/api/v1/subscriptions/sub-XYZ \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "action": "pause" }'
Endpoints · support

Support

Read tickets and reply on the operator’s behalf. Useful for routing customer-service queries through your existing helpdesk product.

GET/api/v1/support/ticketssupport.read

List support tickets. Newest first.

Response
{ "data": [ { "id": "tkt-…", "status": "open", "subject": "…", … } ] }
Example
curl https://api.joinplatformhealth.com/api/v1/support/tickets \
  -H "Authorization: Bearer ph_live_..."
GET/api/v1/support/tickets/{id}support.read

Fetch a single ticket plus its messages. Cross-tenant 404.

Response
{
  "ticket": { "id": "tkt-…", "status": "open", … },
  "messages": [ { "id": "msg-…", "author": "customer", "body": "…", "createdAt": … } ]
}
Example
curl https://api.joinplatformhealth.com/api/v1/support/tickets/tkt-XYZ \
  -H "Authorization: Bearer ph_live_..."
POST/api/v1/support/tickets/{id}support.write

Append a reply. Set internal:true to leave a non-customer-visible note.

Request body
{ "body": "Refill is shipped — tracking…", "internal": false }
Response
{ "id": "msg-…", "author": "apikey:key_xyz", "body": "…", "createdAt": … }
Example
curl -X POST https://api.joinplatformhealth.com/api/v1/support/tickets/tkt-XYZ \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "body": "Hi, your refill shipped today." }'
Endpoints · webhooks

Webhooks

Subscribe to event streams. We sign every outgoing payload with HMAC; the secret is shown once at registration time.

GET/api/v1/webhookswebhooks.manage

List registered webhook subscriptions.

Response
{ "data": [ { "id": "wh-…", "url": "https://…", "events": ["order.placed"], … } ] }
Example
curl https://api.joinplatformhealth.com/api/v1/webhooks \
  -H "Authorization: Bearer ph_live_..."
POST/api/v1/webhookswebhooks.manage

Register a webhook. The HMAC secret is included in the response — copy it once.

Request body
{
  "url": "https://example.com/ph",
  "events": ["order.placed", "subscription.canceled"],
  "label": "internal CRM sync"
}
Response
{ "id": "wh-…", "url": "…", "events": [...], "secret": "whsec_..." }
Endpoint-specific errors
  • 400 validation_error URL is private/blocked or events list is empty.
Example
curl -X POST https://api.joinplatformhealth.com/api/v1/webhooks \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/ph", "events": ["order.placed"] }'
DELETE/api/v1/webhooks/{id}webhooks.manage

Remove a webhook. Cross-tenant 404.

Response
{ "ok": true }
Example
curl -X DELETE https://api.joinplatformhealth.com/api/v1/webhooks/wh-XYZ \
  -H "Authorization: Bearer ph_live_..."
POST/api/v1/webhooks/{id}/rotatewebhooks.manage

Mint a new HMAC signing secret. Previous secret stops working immediately. Send Idempotency-Key on retries.

Response
{ "id": "wh-…", "url": "…", "events": [...], "secret": "whsec_..." }
Endpoint-specific errors
  • 409 idempotency_conflict Idempotency-Key reused with a different body.
Example
curl -X POST https://api.joinplatformhealth.com/api/v1/webhooks/wh-XYZ/rotate \
  -H "Authorization: Bearer ph_live_..." \
  -H "Idempotency-Key: rotate-2026-05-06-deploy-42"
MCP server

MCP server bridge for AI agents

The MCP (Model Context Protocol) server is the AI-facing parallel to the REST API. Where /api/v1/* is shaped for human-written code (one URL per resource, query strings, REST verbs), /api/mcp is shaped for LLMs: a single JSON-RPC 2.0 endpoint that exposes a catalog of tools the model can call and resources it can read. Use it to wire Platform Health into Claude Desktop, Claude Code, custom Anthropic SDK apps, ChatGPT custom GPTs, or any other MCP-aware client.

Endpoint
POST https://api.joinplatformhealth.com/api/mcp
  • Auth. Same Authorization: Bearer ph_{env}_… key as the REST API. Keys carry the same scopes; each tool requires the matching scope. Forbidden calls return JSON-RPC code -32002.
  • Transport. JSON-RPC 2.0 over HTTPS POST. One request per HTTP call; response is a single JSON-RPC envelope. SSE/WebSocket streaming isn’t implemented in v1 and isn’t required for any current client.
  • Rate limit. 600 req/min/key, separate bucket from REST. 429 returns JSON-RPC code -32003 and a Retry-After header.
  • Audit + telemetry. Every tool call lands in the same audit log as REST, and the per-key usage counters on Settings → API keys include MCP traffic.

Methods

  • initialize — handshake. Returns protocolVersion, capabilities, and serverInfo.
  • tools/list — return the catalog (name, description, JSON-Schema input). Call this from your client at session start.
  • tools/call — invoke a tool. Params: { name, arguments }. Result: { content: [{ type: "text", text }], isError? }.
  • resources/list — list read-only documents the agent can pull into context.
  • resources/read — fetch one resource by URI.

Tool catalog

Authoritative list comes from tools/list at runtime; this table is for browsing.

ToolScopeSummary
list_ordersorders.readList recent orders. Supports `limit` + `cursor` keyset pagination.
get_orderorders.readFetch one order by id.
list_productsproducts.readList the calling tenant’s product catalog.
get_productproducts.readFetch one product by id.
create_productproducts.writeCreate a new product. Args: title, summary, description, priceMinorUnits, category.
update_productproducts.writePartial-update a product. Pass productId + any of the editable fields.
list_customerscustomers.readList storefront customers (PII included; key-gated).
list_subscriptionssubscriptions.readList the calling tenant’s recurring subscriptions.
pause_subscriptionsubscriptions.writePause a subscription indefinitely.
cancel_subscriptionsubscriptions.writeCancel a subscription. Terminal.
list_ticketssupport.readList support tickets. Optional `status` filter.
reply_to_ticketsupport.writeAppend a reply (or `internal: true` note) to a support ticket.

Resources

URIMIMESummary
ph://ordersapplication/jsonUp to 100 most recent orders, newest first.
ph://productsapplication/jsonCurrent product catalog.
ph://customersapplication/jsonStorefront customer roster.
ph://docs/apitext/markdownA condensed Markdown view of this REST reference so the agent can ground itself before calling tools.

Examples

Discover the catalog:

curl -X POST https://api.joinplatformhealth.com/api/mcp \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

Call a tool:

curl -X POST https://api.joinplatformhealth.com/api/mcp \
  -H "Authorization: Bearer ph_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "list_orders",
      "arguments": { "limit": 10 }
    }
  }'

Claude Desktop

Add an entry to claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/claude_desktop_config.json) and restart the app. The desktop client will pick up the tool catalog automatically.

{
  "mcpServers": {
    "ph": {
      "url": "https://api.joinplatformhealth.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ph_live_..."
      }
    }
  }
}

Errors

  • -32700 parse error — body wasn’t valid JSON.
  • -32600 invalid request — envelope wasn’t a JSON-RPC 2.0 request.
  • -32601 method not found — unknown method, tool, or resource URI.
  • -32602 invalid params — arguments failed schema validation.
  • -32603 internal error — unexpected server-side failure.
  • -32001 auth_required — missing/invalid bearer.
  • -32002 forbidden — key is missing the tool’s required scope.
  • -32003 rate_limited — 600 req/min/key budget exhausted; back off per Retry-After.

Note: tool-level errors (a not-found id, a validation message from the handler) come back inside a successful JSON-RPC response with isError: true on the content payload — that lets the model see the message and recover, rather than treating it as a protocol failure.

Roadmap

What's next

  • Bulk endpoints for orders + products so AI agents and integrators can move whole batches in one round-trip.
  • GraphQL gateway over the same resources for clients that prefer to fetch only the fields they render.
  • SDK packages on npm + PyPI generated from the OpenAPI spec, with first-class types.