Skip to main content

Documentation

VegaCap Platform API v1 — sessions, webhooks, assessments, and reports.

Quickstart

Five steps to go from zero to a scored assessment. Takes about 10 minutes.

1. Get keys

Head to API Keys and create a test pair (vegacap_pk_test_* + vegacap_sk_test_*). Test keys return deterministic mock scoring for all 6 assessment types — use them until your integration is stable, then switch to a live pair.

2. Install the SDKs

Server side (Node / Bun / Deno)
npm install @vegacap/node@0.1.0
# or: bun add @vegacap/node@0.1.0
# or: pnpm add @vegacap/node@0.1.0
Client side (React)
npm install @vegacap/react@0.1.0
# or: bun add @vegacap/react@0.1.0
# or: pnpm add @vegacap/react@0.1.0

Latest release: @vegacap/node@0.1.0, @vegacap/react@0.1.0, @vegacap/sdk@0.1.0. All three are Apache-2.0 with zero runtime dependencies — the tarball contains everything needed to run.

Prefer no framework? Load the browser embed SDK from the CDN — no npm install: <script src="https://www.vegacapltd.com/sdk/v1/vegacap.js"></script>

3. Create a session (server)

Your server — never expose the secret key to the browser
import { VegaCap } from '@vegacap/node';

const vc = new VegaCap({ apiKey: process.env.VEGACAP_SECRET_KEY! });

const session = await vc.sessions.create({
  assessmentType: 'disc',
  candidate: {
    externalId: 'candidate-42',
    email: 'candidate@example.com',
    name: 'Sam Example',
  },
  redirectUrl: 'https://your-app.com/assessment/done',
});

// Send session.sessionToken to your frontend — it's the only piece the browser sees.

4. Embed in the browser

Pure HTML — no React required
<div id="vc-container"></div>
<script src="https://www.vegacapltd.com/sdk/v1/vegacap.js"></script>
<script>
  const vc = window.VegaCap.init({
    publishableKey: 'vegacap_pk_test_...',
  });
  vc.assessments.embed({
    sessionToken: 'vst_...',          // from step 3
    container: '#vc-container',
    onComplete: (result) => {
      console.log('done', result.sessionId, result.summary);
    },
  });
</script>
app/providers.tsx — client boundary for VegaCapProvider
'use client';

import { VegaCapProvider } from '@vegacap/react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <VegaCapProvider
      publishableKey={process.env.NEXT_PUBLIC_VEGACAP_PUBLISHABLE_KEY!}
    >
      {children}
    </VegaCapProvider>
  );
}
app/layout.tsx — server component, imports the client wrapper
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}
Any client component — useEmbed returns { container, state, error }
'use client';
import { useRouter } from 'next/navigation';
import { useEmbed } from '@vegacap/react';

export function AssessmentPage({ sessionToken }: { sessionToken: string }) {
  const router = useRouter();
  const { container, state } = useEmbed({
    sessionToken,
    onComplete: () => router.push('/thank-you'),
  });

  return (
    <>
      {state === 'loading' && <p>Loading…</p>}
      <div ref={container} />
    </>
  );
}

5. Subscribe to webhook events

Once the candidate submits, session.completed fires to any webhook endpoint you've registered. Verify the HMAC signature with the per-endpoint secret — drop any request whose signature doesn't match.

Your webhook receiver (Express — use express.raw() to get raw bytes)
import express from 'express';
import { VegaCap } from '@vegacap/node';

// express.raw() so req.body is the raw Buffer.
// Parsing + re-serializing JSON would break the HMAC.
app.post(
  '/vegacap/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.header('x-vegacap-signature') ?? '';
    const ok = VegaCap.webhooks.verify(
      req.body,                                   // raw Buffer
      header,                                     // "sha256=..."
      process.env.VEGACAP_WEBHOOK_SECRET!,         // whsec_...
    );
    if (!ok) return res.status(401).end();

    const event = JSON.parse(req.body.toString('utf8'));
    if (event.event === 'session.completed') {
      // event.data carries sessionId, results, candidate, metadata, etc.
      saveToDatabase(event.data);
    }
    res.status(200).end();
  },
);

Register the endpoint at Webhooks. Failed deliveries retry with exponential backoff up to 5 attempts; undeliverable after that lands in the dead-letter queue (visible in the admin view).

Test mode behaviour

  • All 6 assessments return mock resultSummary instantly.
  • No license credits are consumed.
  • Webhooks still fire end-to-end, so partner receivers can be validated.
  • Sessions are filtered out of production admin reports (appear only in the test-mode view).

Overview

The Platform API lets you create assessment sessions, stream submissions, and receive results via webhooks.

Base URL: https://api.vegacapltd.com/api/v1. All requests return JSON and use standard HTTP status codes. Errors follow a uniform { error: { code, message } } shape.

Orgs & access

How the portal decides which organizations you can see, and how to scope API requests when you belong to more than one.

Who can sign in?

Any user who is an admin or owner of at least one organization on VegaCap can sign in to the developer portal — the same credentials you use for the main dashboard. The portal reads from the same organizations and organization_members tables as the main app, so there is no separate onboarding or migration step.

Old organizations

Organizations created before the developer portal launched work exactly the same as newly created ones. If you see No organizations in the sidebar, make sure you are signed in with the email associated with your admin account (check the email on the avatar at the top of the main dashboard) and reload — the portal will pick up every org where you are an admin, owner, or portal-invited team member.

Multiple admins per org

Every admin of an org can access the portal independently — there is no single "owner seat." API keys, tokens, webhooks, and team-member invites are scoped to the org, not the individual admin, so you and your co-admins share the same view. The actor on audit entries (who created a key, who rotated a token) reflects the specific admin who performed the action.

Belonging to multiple orgs

If you are an admin of more than one organization, the sidebar shows an org switcher under the logo. The portal remembers your last selection in localStorage so you land on the same org next time. Session, key, webhook, and usage views are always scoped to the selected org.

When calling the portal API directly (e.g. from a custom script that hits /api/portal/* with your portal session cookie), multi-org users must pass the header below on every request. Single-org users can omit it.

Disambiguate the target org for multi-org portal users
GET /api/portal/sessions HTTP/1.1
Host: developer.vegacapltd.com
Cookie: better-auth.session=...
X-Organization-Id: <the-org-uuid>

Note: this only applies to the portal's own REST endpoints used by the UI. The public Platform API ( api.vegacapltd.com/api/v1/*) is scoped by the API key itself — each key belongs to exactly one org, so there is nothing to disambiguate.

Signed in but no orgs listed?

The portal recognises three states as "joined" (active, completed, joined) as well as the organization creator (organizations.admin_user_id). If you believe you should see an org but do not, confirm you are signed in with the account that was added as an admin — the match is on user ID, not email alias.

Authentication

Every request needs an API key. Publishable keys are safe for browsers; secret keys must stay server-side.

Authorization header
GET /api/v1/sessions HTTP/1.1
Host: api.vegacapltd.com
Authorization: Bearer vegacap_sk_live_7f8a...

Rotate keys from API Keys. Test-mode keys short-circuit scoring with deterministic mock data.

Rate Limits

Per-key, per-minute sliding window. Rate limit state surfaces in response headers.

Response headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1744876800

Exceeding the limit returns 429 Too Many Requests. Default is 120 requests/minute per key; contact us to raise it.

Endpoints

Core resources you will call day-to-day.

POST/api/v1/sessionsCreate an assessment session
GET/api/v1/sessionsList sessions with filters
GET/api/v1/sessions/{id}Fetch a single session
POST/api/v1/embed/submitSubmit assessment responses
GET/api/v1/reports/{id}/pdfStream the generated PDF report
GET/api/v1/org/meCurrent organization context
POST/api/v1/keys/{id}/rotateRotate an API key
GET/api/v1/tfd/configsList published TFD configs (occupations) for this org

Webhook Events

Verify the X-VegaCap-Signature header (HMAC-SHA256 of the raw body) before trusting any payload.

session.startedin_progress

Fires when the candidate first loads the assessment and the session transitions from created to in_progress.

POST <your-endpoint> · session.started
{
  "event": "session.started",
  "id": "evt_3f7a9c0d82b1e54fa6c2d908",
  "createdAt": "2026-04-17T08:42:11.284Z",
  "data": {
    "sessionId": "ses_0K7RfJ4m9xDqH3pN2vQwY8zB",
    "assessmentType": "disc",
    "status": "in_progress",
    "candidate": {
      "externalId": "cand_8a3c91",
      "email": "priya@example.com",
      "name": "Priya Sharma"
    },
    "results": null,
    "metadata": {
      "campaign": "summer-hiring-2026",
      "source": "careers-page"
    },
    "completedAt": null
  }
}
session.completedcompleted

Fires when the candidate submits the assessment and scoring succeeds. data.results contains the denormalized resultSummary.

POST <your-endpoint> · session.completed
{
  "event": "session.completed",
  "id": "evt_3f7a9c0d82b1e54fa6c2d908",
  "createdAt": "2026-04-17T08:42:11.284Z",
  "data": {
    "sessionId": "ses_0K7RfJ4m9xDqH3pN2vQwY8zB",
    "assessmentType": "disc",
    "status": "completed",
    "candidate": {
      "externalId": "cand_8a3c91",
      "email": "priya@example.com",
      "name": "Priya Sharma"
    },
    "results": {
      "primaryType": "D",
      "secondaryType": "I",
      "type12": "Di",
      "likertScores": {
        "D": 4.2,
        "I": 3.8,
        "S": 2.4,
        "C": 3.1
      },
      "bands": {
        "D": "high",
        "I": "moderate",
        "S": "low",
        "C": "moderate"
      },
      "reliabilityScore": 0.87,
      "reliabilityLevel": "high"
    },
    "metadata": {
      "campaign": "summer-hiring-2026",
      "source": "careers-page"
    },
    "completedAt": "2026-04-17T08:42:11.284Z"
  }
}
session.failedfailed

Fires if scoring fails (malformed responses, engine error). data.results may carry an error object.

POST <your-endpoint> · session.failed
{
  "event": "session.failed",
  "id": "evt_3f7a9c0d82b1e54fa6c2d908",
  "createdAt": "2026-04-17T08:42:11.284Z",
  "data": {
    "sessionId": "ses_0K7RfJ4m9xDqH3pN2vQwY8zB",
    "assessmentType": "disc",
    "status": "failed",
    "candidate": {
      "externalId": "cand_8a3c91",
      "email": "priya@example.com",
      "name": "Priya Sharma"
    },
    "results": null,
    "metadata": {
      "campaign": "summer-hiring-2026",
      "source": "careers-page"
    },
    "completedAt": "2026-04-17T08:42:11.284Z"
  }
}
session.expiredexpired

Fires when the session expiresAt passes without a successful submission.

POST <your-endpoint> · session.expired
{
  "event": "session.expired",
  "id": "evt_3f7a9c0d82b1e54fa6c2d908",
  "createdAt": "2026-04-17T08:42:11.284Z",
  "data": {
    "sessionId": "ses_0K7RfJ4m9xDqH3pN2vQwY8zB",
    "assessmentType": "disc",
    "status": "expired",
    "candidate": {
      "externalId": "cand_8a3c91",
      "email": "priya@example.com",
      "name": "Priya Sharma"
    },
    "results": null,
    "metadata": {
      "campaign": "summer-hiring-2026",
      "source": "careers-page"
    },
    "completedAt": null
  }
}

Submit Payloads

POST /api/v1/embed/submit accepts a different body per assessment type. The session token is read from the Authorization header.

assessmentType: disclive

Each of the 15 questions needs one response (1–5 Likert) tagged with its DiSC dimension (D, I, S, or C).

POST /api/v1/embed/submit
{
  "responses": [
    {
      "questionId": "q1",
      "dimension": "D",
      "response": 4
    },
    {
      "questionId": "q2",
      "dimension": "I",
      "response": 3
    },
    {
      "questionId": "q3",
      "dimension": "S",
      "response": 2
    },
    {
      "questionId": "q4",
      "dimension": "C",
      "response": 5
    }
  ]
}

TFD Sessions

TFD and TFD+ require a published occupation config. Use GET /api/v1/tfd/configs to list available configs and pass the chosen occupationId when creating a session.

1. Fetch published configs

Server side — list occupation configs for this org
const res = await fetch('https://api.vegacapltd.com/api/v1/tfd/configs', {
  headers: { Authorization: 'Bearer vegacap_sk_live_...' },
});
const { configs } = await res.json();
// configs: Array<{ id, occupationId, occupationName, mode, language, totalQuestions }>

// Example entry:
// { id: "cfg_...", occupationId: "occ_...", occupationName: "Welder",
//   mode: "tfd", language: "en", totalQuestions: 32 }

2. Create session with occupationId

Server side — pass occupationId in config
const session = await vc.sessions.create({
  assessmentType: 'tfd',          // or 'tfd_plus'
  candidate: { externalId: 'user-123', name: 'Sam' },
  config: {
    occupationId: configs[0].occupationId,   // required — from step 1
  },
});

Omitting config.occupationId returns 400 invalid_config. Every org config maps to exactly one occupation and question pool.

Error codes

Every non-2xx response has { error: { code, message, doc_url?, details? } }. Map on code, not message.

invalid_api_key401

Missing or malformed Authorization header.

origin_not_allowed403

Publishable key used from an origin not in the allowlist.

insufficient_permissions403

Key does not have the required scope for this action or assessment.

no_available_licenses402

Organization is at capacity for this assessment type. Top up or switch types.

assessment_not_enabled403

Assessment type is not enabled for this organization.

session_not_found404

Session ID does not belong to this organization or has been soft-deleted.

session_already_completed409

Submit was called on a session already in the completed state.

session_expired410

Session ran past its expiresAt before submit was called.

idempotency_conflict409

Idempotency-Key matched a soft-deleted session — pick a new key.

validation_error400

Submit payload failed Zod validation or the per-assessment validity gate (e.g. FRA requires ≥90% of items answered).

not_implemented501

Assessment type is not yet scored in live mode. Use a test key for end-to-end integration.

rate_limit_exceeded429

Per-key sliding window exceeded. Honor X-RateLimit-Reset before retrying.

internal_error500

Unexpected server error — retry with backoff, then contact support if persistent.

Stable contract: we won't rename an existing code. New codes may appear on the same HTTP status — handle the status as a fallback, then refine on the string code.

API Playground

Interactive, live reference rendered from our OpenAPI spec — try requests with your test key.

Interactive API referenceloading… · /api/openapi.json

Changelog

Each entry is an additive release of the Platform API.

2026-04-19SDKs 0.1.0
  • @vegacap/node@0.1.0 — typed server-side client for the Platform API. Zero runtime deps, Apache-2.0.
  • @vegacap/react@0.1.0 — VegaCapProvider context + useSession / useCreateSession / useEmbed hooks.
  • @vegacap/sdk@0.1.0 — browser embed SDK. Install via npm or load from CDN at https://www.vegacapltd.com/sdk/v1/vegacap.js.
  • FRA live scoring — the ≥90% items-answered validity gate surfaces as HTTP 400 (invalid_responses) instead of 500 when submissions are incomplete.
  • Platform API sessions now visible in the org admin under the new "Platform API" sidebar item (test-mode sessions excluded).
2026-04-17v1.1
  • Added GET /api/v1/sessions list endpoint with filtering (status, type, candidate, date range).
  • Added GET /api/v1/org/me for retrieving the authenticated org context.
  • Added GET /api/v1/reports/{id}/pdf for streaming generated PDF reports.
  • Added POST /api/v1/keys/{id}/rotate for zero-downtime API key rotation.
  • Docs portal: event payload schemas, per-assessment submit examples, embedded Scalar playground.
2026-03-28v1.0
  • Platform API v1 public beta: 13 routes across sessions, reports, webhooks, and keys.
  • Seven assessments wired (DiSC, TFD, TFD+ live; AAPA, FRA, LPM-VT, LPM-U behind 501 until scoring lands).
  • Webhooks with HMAC-SHA256 signing, exponential retry (1m, 5m, 30m, 2h, 12h), and SSRF protection.
  • Publishable + secret key pairs with test/live environments and per-key rate limits.