Quickstart
Starting with data extracted by an assistant or another vendor? Use the vendor-neutral case integration kit for payload instructions, benefit IDs, compatibility checks, and source-file review workflows.
-
Get an API key
Sign in to Planlined and use the API keys section below to generate a sandbox key. Choose
cases:writeandwebhooks:writescopes to follow this guide.Production keys are prefixed
bart_sk_. Test keys are prefixedbart_sk_test_and operate against an isolated sandbox organization. -
Create your first case
Send a POST request with an idempotency key so retries are safe:
# Create up to 50 cases in a single call curl -X POST https://app.trybart.com/api/cases/bulk \ -H "Authorization: Bearer bart_sk_test_..." \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $(uuidgen)" \ -d '{ "cases": [{ "externalId": "partner-row-12345", "employerName": "Acme Manufacturing", "policyPeriod": "2026-01-01 to 2026-12-31", "planData": { "medical": [], "dental": [] }, "metadata": { "partnerId": "ext-12345" } }] }' # Status codes: # 200 every case succeeded # 207 partial — at least one succeeded AND at least one failed (Multi-Status) # 200 with ok:false every case failed (request itself was valid) # Each result row echoes externalId so you can reconcile against your # own primary keys. Subscribe to "case.bulk_imported" for a single # summary event per batch instead of N per-case deliveries.
-
Listen for case events
Subscribe a webhook endpoint so your system reacts when a case is created, updated, or its report is generated. The response includes a one-time HMAC signing secret — save it:
curl -X POST https://app.trybart.com/api/webhooks/subscriptions \ -H "Authorization: Bearer bart_sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme case sync", "url": "https://your-app.example.com/hooks/bart", "eventTypes": ["case.created", "case.updated", "report.generated"] }'
-
Verify the signature on each delivery
Planlined signs every delivery with HMAC-SHA256 over
{timestamp}.{rawBody}. Pick your language from the Webhooks section below and drop the snippet into your handler. The signature lives inX-Webhook-Signature; the timestamp inX-Webhook-Timestamp; reject anything older than 5 minutes to prevent replay attacks.Once your handler verifies, run a test from /developers/webhooks using the Test button on your subscription — you'll see the round-trip time and HTTP status your endpoint returned.
That's the whole loop. Your platform can now create cases via API, get notified the moment any change happens, and verify those notifications are genuinely from Planlined. Everything else — multiple cases per batch, idempotency for safe retries, plan data, reports, exports — layers on top of the same three primitives.
Your API keys
Authentication
All API requests are authenticated with a single header. Either form works:
Authorization: Bearer bart_sk_live_xxxxxxxxxxxxxxxx # or X-API-Key: bart_sk_live_xxxxxxxxxxxxxxxx
Keys carry scopes. A request that lacks a required scope returns
403 insufficient_scope.
Rate limits & quotas
Each key is provisioned with both a per-minute and per-day cap. Defaults for new keys:
| Quota | Default | Maximum |
|---|---|---|
| Requests per minute | 60 |
6,000 |
| Requests per day | 10,000 |
10,000,000 |
| Bulk-case batch size | n/a |
50 cases per
POST /api/cases/bulk
|
| Case payload size | n/a | 9 MB per case after JSON serialization |
| Idempotency key TTL | n/a | 24 hours |
Exceeding a key's rate limit returns 429 rate_limited with a
Retry-After header. Need higher limits? Contact Planlined support
after creating the key in the API keys section.
Idempotency
Any mutating endpoint (POST, PUT,
PATCH) accepts an
X-Idempotency-Key header. We cache the response for 24 hours,
keyed by user + key. Retrying with the same key returns the original response and sets
X-Idempotency-Replayed: true.
| Scenario | Response |
|---|---|
| New key | 200 — handler runs once, response cached |
| Same key, same body, completed |
200 — cached response,
X-Idempotency-Replayed: true
|
| Same key, different body | 422 idempotency_mismatch |
| Same key, still processing | 409 idempotency_conflict |
| Same key after prior failure | Allowed — new attempt runs |
Use any unique string up to 255 chars. UUID v4 is recommended. Keys expire after 24 hours.
Errors
Errors return JSON with two main fields: error (a
machine-readable slug for branching) and message (a
human-readable description). Some endpoints also include
code (a screaming-snake-case constant for auth/security
errors) or details (a Zod-shaped array of field-level
validation errors).
{
"success": false,
"error": "validation_error",
"message": "Invalid request parameters",
"details": [ /* per-field validation errors when applicable */ ]
}
Auth errors additionally include a screaming code:
{
"error": "Authentication required",
"message": "No valid authentication credentials provided",
"code": "AUTH_REQUIRED"
}
HTTP status codes
| Status | Meaning | Typical error slug |
|---|---|---|
200 / 201 |
Success | — |
400 |
Validation, missing input, or missing org context |
validation_error,
invalid_idempotency_key,
service_account_requires_org
|
401 |
Authentication required or token invalid (also see code:
AUTH_REQUIRED)
|
authentication_required,
authentication_failed,
invalid_api_key,
api_key_revoked,
api_key_expired
|
403 |
Authenticated but lacks the required scope or role |
insufficient_scope,
admin_only, forbidden
|
404 |
Resource not found, or not owned by your org |
not_found, case_not_found,
attachment_not_found, etc.
|
409 |
Conflict — same idempotency key still processing | idempotency_conflict |
422 |
Idempotency key reused with different body | idempotency_mismatch |
429 |
Rate limit exceeded — see Retry-After header
|
rate_limited,
rate_limit_exceeded
|
500 |
Internal server error — safe to retry with the same idempotency key | — |
503 |
Upstream dependency unavailable (rare, transient) | database_unavailable |
Resource-specific 404s use a typed suffix (e.g.,
case_not_found, job_not_found,
portal_not_found) when the API can identify which resource was
missing.
Any 5xx response with the same X-Idempotency-Key can be
safely retried — you'll either get a cached success response or trigger a fresh
attempt.
Pagination
Stable v1 collection endpoints use opaque cursor pagination. Send
limit (default 50, maximum 100), then pass the returned
pageInfo.nextCursor as
cursor until pageInfo.hasMore is
false. Do not inspect or modify the cursor, reuse it with different filters, or combine it
with offset.
GET /api/v1/cases?limit=25
{
"ok": true,
"items": [ /* up to 25 items */ ],
"total": 412,
"pageInfo": {
"limit": 25,
"hasMore": true,
"nextCursor": "eyJ2IjoxLCJleHAi..."
}
}
Cursors expire after 24 hours and are bound to the authenticated principal, collection, filters, and sort order. Historical unversioned collection routes continue to accept offset pagination during the compatibility window.
Try it
Send a real request to the sandbox. Use any
bart_sk_test_… key — data created with sandbox
keys lives in an isolated organization and never reaches production.
Response
Webhooks
Planlined sends signed POST requests when subscribed events fire. Each delivery includes:
X-Webhook-Signature: sha256=<hex_hmac> X-Webhook-Timestamp: 1735660800 X-Webhook-Event: case.created X-Webhook-Delivery-Id: evt_1735660800123_abc123 User-Agent: BART-Webhooks/1.0 Content-Type: application/json { "id": "evt_1735660800123_abc123", "type": "case.created", "created_at": "2026-01-01T12:00:00Z", "data": { "caseId": "case-acme-2026", "employerName": "Acme Corporation" }, "context": { "org_id": "org_xyz789", "user_id": "user_abc123" } }
The full envelope —
{id, type, created_at, data, context} — is consistent
across every event type. The data field varies by event;
context always carries org_id +
user_id.
Verifying signatures
// Node.js
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, timestamp, secret) {
const signed = timestamp + '.' + rawBody;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
# Python 3
import hmac, hashlib
def verify_signature(raw_body: bytes, signature_header: str, timestamp: str, secret: str) -> bool:
signed = f"{timestamp}.{raw_body.decode('utf-8')}".encode()
expected = "sha256=" + hmac.new(
secret.encode(), signed, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected)
# Ruby
require 'openssl'
def verify_signature(raw_body, signature_header, timestamp, secret)
signed = "#{timestamp}.#{raw_body}"
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, signed)
Rack::Utils.secure_compare(signature_header, expected)
end
// PHP 7.2+
function verify_signature(string $raw_body, string $signature_header, string $timestamp, string $secret): bool {
$signed = $timestamp . '.' . $raw_body;
$expected = 'sha256=' . hash_hmac('sha256', $signed, $secret);
return hash_equals($expected, $signature_header);
}
// Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func VerifySignature(rawBody []byte, signatureHeader, timestamp, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signatureHeader), []byte(expected))
}
Retry policy
A delivery is attempted up to 6 times total: the initial attempt plus 5 retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 30 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
After 6 attempts, the delivery is marked failed and stops
retrying automatically. You can manually replay it from
/developers/webhooks. Separately, if a subscription
accumulates 10 consecutive failed deliveries, the subscription
auto-pauses and stops receiving new events until you fix and resume it.
Success codes
Your endpoint should return one of 200 OK,
201 Created, 202 Accepted, or
204 No Content. Any other status triggers a retry. The
response body is captured (up to 1KB) for debugging but otherwise ignored.
Available events
| Event | Description | Status |
|---|---|---|
case.created |
New case created (via UI, file upload, or API) | Active |
case.bulk_imported |
Fired once per POST /api/cases/bulk with summary stats
|
Active |
case.updated |
Case data modified | Declared, not yet emitted |
case.deleted |
Case removed | Declared, not yet emitted |
plan.linked |
Two plans linked together (e.g., current ↔ renewal) | Active |
plan.created /
plan.updated / plan.deleted
|
Plan-level lifecycle | Declared, not yet emitted |
report.generated |
Report build completed | Active |
report.viewed |
Shared report opened — includes viewer engagement metrics | Active |
export.completed |
PDF/Excel export job finished | Active |
parse.completed |
Document parsing succeeded | Active |
parse.failed |
Document parsing failed | Active |
Events tagged Declared, not yet emitted are valid subscription targets (the API will accept them) but no call sites currently dispatch them. Subscribing produces no deliveries until the corresponding code paths land.
Event payload reference
Each data shape below is grepped from the actual
dispatchEvent call site. Wrap each in the standard envelope
{id, type, created_at, data, context: {org_id, user_id}} when
handling.
case.created
{
"case_id": "case-1782328962115-83d35d6d",
"employer_name": "Acme Corporation",
"policy_period": "2026-01-01 to 2026-12-31",
"plan_count": 12,
"report_types": ["benefits_comparison"],
"source": "create_from_files", // or "bulk_import", "create_with_report"
"files_parsed": ["medical-sbc.pdf"] // only on create_from_files
}
case.bulk_imported
{
"request_id": "req_abc123",
"total": 25,
"created": 23,
"failed": 2,
"case_ids": ["case-...", "case-..."],
"external_ids": ["ext-A1", "ext-B2"],
"latency_ms": 1842,
"source": "bulk_import"
}
plan.linked
{
"case_id": "case-1782328962115-83d35d6d",
"link_id": "link_xyz789",
"source_plan_id": "plan_current_001",
"target_plan_id": "plan_renewal_001",
"link_type": "renewal" // or "proposed", "alternate"
}
report.generated
{
"case_id": "case-1782328962115-83d35d6d",
"report_id": "report_abc123",
"share_url": "https://app.trybart.com/share/abc123",
"is_live": true,
"expires_at": "2026-09-24T12:00:00Z",
"source": "create_from_files"
}
report.viewed
Engagement-rich event for sales/CRM sync. Most fields can be absent on partial sessions.
{
"report_id": "report_abc123",
"report_title": "Acme 2026 Benefits Renewal",
"share_url": "https://app.trybart.com/share/abc123",
"session_id": "sess_xyz789",
"time_on_page": 184, // seconds
"scroll_depth": 87, // percent
"modeler_opened": true,
"pdf_downloaded": false,
"device": "desktop",
"browser": "Chrome",
"location": "San Francisco, CA, US",
"is_return_visit": false,
"previous_visits": 0,
"referrer_source": "direct"
}
export.completed
{
"export_type": "pdf", // "pdf" | "excel" | "csv"
"case_id": "case-1782328962115-83d35d6d",
"filename": "acme-2026-renewal.pdf",
"size_bytes": 482193,
"duration_ms": 2147
}
parse.completed
{
"request_id": "parse_req_abc123",
"filename": "uhc-quote-2026.pdf",
"document_type": "quote", // "quote" | "sbc" | "census" | etc.
"plan_count": 81,
"source": "ai", // "ai" | "uhc-parser" | "bcbs-tn-parser" | etc.
"confidence": 0.94,
"duration_ms": 4216
}
parse.failed
{
"request_id": "parse_req_abc123",
"filename": "corrupted-file.pdf",
"error_type": "ParseError",
"error_message": "Could not extract text from PDF",
"duration_ms": 1024
}
OAuth 2.0 partner apps
If your platform lets your users connect their Planlined data, register an OAuth app and use the authorization-code flow. Your app exchanges an authorization code for access + refresh tokens scoped to the granting user's organization.
Heads up — the consent screen is currently a placeholder. The
/oauth/authorize endpoint auto-approves for any signed-in
Planlined user and immediately issues a code. A proper consent UI is on the roadmap. Until
then, treat the authorize step as "Planlined asks the user to sign in, then returns to you."
Step 1. Register your app at
/developers/apps/new. Capture
client_id and client_secret.
Step 2. Redirect the user to the Planlined authorization URL. The user must have a Planlined account — if not signed in, they'll be sent through the Planlined login flow first.
https://app.trybart.com/api/external-apps/oauth/authorize ?client_id=bart_xxxxxxxxxxxx &redirect_uri=https://your-app.example.com/oauth/callback &response_type=code &scope=read:cases%20read:plans%20offline_access &state=<your-csrf-token> &code_challenge=<pkce-challenge> # optional, recommended &code_challenge_method=S256 # or "plain"
The endpoint supports PKCE. For public clients (mobile, SPA) PKCE is strongly recommended. For confidential clients (server-side), client_secret alone is sufficient.
Step 3. After approval, Planlined redirects to your
redirect_uri with
?code=…&state=…. Verify
state matches what you sent. Then exchange the code:
curl -X POST https://app.trybart.com/api/external-apps/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=<received-code>" \ -d "client_id=bart_xxxxxxxxxxxx" \ -d "client_secret=bartcs_xxxxxxxxxxxx" \ -d "redirect_uri=https://your-app.example.com/oauth/callback" \ -d "code_verifier=<pkce-verifier>" # only if you used PKCE
Successful response (JSON):
{
"access_token": "<opaque-token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:cases read:plans offline_access",
"refresh_token": "<opaque-token>" # only when offline_access requested
}
OAuth-standard error responses follow RFC 6749:
invalid_request, invalid_client,
invalid_grant,
unsupported_response_type,
invalid_redirect_uri,
server_error.
Step 4. Use the access token on subsequent API calls:
Authorization: Bearer <access_token>
Refresh. When the access token expires (1 hour), exchange the refresh token for a new pair:
curl -X POST https://app.trybart.com/api/external-apps/oauth/token \ -d "grant_type=refresh_token" \ -d "refresh_token=<your-refresh-token>" \ -d "client_id=bart_xxxxxxxxxxxx" \ -d "client_secret=bartcs_xxxxxxxxxxxx"
Revoke. To revoke a token (logout, user disconnects):
curl -X POST https://app.trybart.com/api/external-apps/oauth/revoke \ -d "token=<access_or_refresh_token>" \ -d "client_id=bart_xxxxxxxxxxxx" \ -d "client_secret=bartcs_xxxxxxxxxxxx"
Rotating your client_secret on
/developers/apps immediately revokes all outstanding
access and refresh tokens issued under the old secret. Existing users will need to
re-authorize.
Common scopes
cases:read |
Read case metadata and reports |
cases:write |
Create and modify cases |
plans:read / plans:write
|
Read or modify plans within cases |
exports:read |
Download generated reports and exports |
webhooks:write |
Configure and manage webhook subscriptions |
analytics:read |
Access benchmark and market data |
The full scope catalog is documented per-endpoint in the interactive reference.
Sandbox
Any key prefixed bart_sk_test_ operates against an isolated
sandbox organization. Data created with sandbox keys never appears in your production
account and never triggers production webhooks. Use sandbox keys for end-to-end
integration tests in your CI pipeline.
Plans & quotas
Every API key carries a per-minute and per-day request quota plus payload-size and batch-size limits. See the Rate limits & quotas table in Authentication for the defaults and ceilings.
Pricing tiers: not yet finalized for public release. If you're evaluating Planlined for integration use, talk to your account contact about quota uplifts, dedicated rate-limit pools, and SLA tiers. This section will be replaced with self-serve pricing once we publish it.
What's free today
-
Sandbox keys (
bart_sk_test_*) — unlimited issuance, isolated organization, default quotas. Use these in your CI pipeline. - Documentation, OpenAPI spec, and try-it sandbox — freely accessible, no signup required for read-only browsing.
- One signed-in Planlined user account can issue keys and OAuth apps for their organization at no charge during the current preview period.
What you should ask about
- Production-tier rate limits (above the default 60/min, 10,000/day per key)
- Service-account key quotas for multi-partner integrations
- Webhook delivery retention — how long delivery history stays queryable from /developers/webhooks
- SLA on idempotency-key TTL beyond 24 hours
- Dedicated subdomain or vanity URL for OAuth redirect URIs
- SCIM / SAML support for enterprise org provisioning (currently not in scope)
Versioning & breaking changes
/api/v1 is the stable namespace for partner integrations.
Existing unversioned operations remain available for compatibility unless their individual
lifecycle entry says otherwise.
Stable compatibility policy. Breaking request or response changes require a new major version. Additive optional fields and operations may be introduced within v1.
- Stable — supported for partner production use and protected by the v1 compatibility policy.
- Beta — available for evaluation; its contract may change before promotion to stable.
- Deprecated — still callable with a published replacement or migration path. Deprecated operations remain callable until their published Sunset date.
- Sunset — no longer callable on or after the announced Sunset date.
When removal is scheduled, deprecated operations identify the deprecation and removal
dates with Deprecation and
Sunset response headers and identify the replacement with a
successor Link header. No Sunset date means removal is not
scheduled.
Subscribe to the OpenAPI document in your CI pipeline and diff it against your last known-good build. The public changelog records additions, deprecations, replacements, and approved Sunset dates.
API reference
Swagger UI →
Interactive try-it-out browser. Best for exploring endpoints.
ReDoc →
Three-panel reference with detailed schemas. Best for reading.
OpenAPI JSON →
Raw spec. Feed into Postman, Insomnia, or code generators.
Connect your AI →
Use Planlined from Claude, ChatGPT or Microsoft Copilot.
OAuth apps →
Register a third-party app that connects users to Planlined via OAuth 2.0.
Webhook subscriptions →
Manage which events get delivered to your URLs and rotate signing secrets.
Changelog →
Track recent API additions, breaking changes, and deprecations.
Postman collection →
One-click import the Planlined API into Postman or Insomnia.