Skip to content

Cookbook

Submit an advice record

POST a piece of advice into the review queue, with model provenance, vulnerability flags, and metadata.

The shape of the request

Submission is a two-step flow: request a presigned upload URL from the API, PUT the document bytes there yourself, then submit the resulting documentKey as part of the job. The document bytes never pass through the platform; your service uploads directly to S3.

ts
type VulnerabilityFlag = 'health' | 'life_event' | 'capability' | 'resilience';

type SubmitInput = {
  // Required
  documentKey: string;          // returned from POST /v1/review/uploads
  documentType: string;         // slug from your firm's document types (e.g. "SUITABILITY_REPORT")
  clientReference: string;      // your stable client identifier
  documentReference: string;    // your stable document identifier
  factFindSummary: Record<string, unknown>; // structured summary of the client fact find

  // Optional
  priority?: 'STANDARD' | 'URGENT';
  aiContext?: {
    model: { provider: string; version: string }; // required inside aiContext
    inputs?: Record<string, unknown>;
    outputs?: Record<string, unknown>;
    factors?: Array<{ input: string; influence: number }>;
    confidence?: number;
    guardrails?: Array<{ rule: string; triggered: boolean }>;
  };

  // FG21/1 vulnerability routing, any non-empty array forces
  // requiresSeniorSignOff: true and restricts the job to specialist
  // reviewers. See /docs/features/vulnerability-routing.
  vulnerabilityFlags?: VulnerabilityFlag[];
  requiresSeniorSignOff?: boolean;

  // Anonymised categorical segments for bias / fairness monitoring.
  // Values are plain strings and aggregated across jobs on the
  // /v1/bias report, pick categorical labels rather than
  // identifiers. See /docs/features/bias-monitoring.
  clientSegments?: Record<string, string>;
};

A complete TypeScript example

ts
import { request } from 'undici';
import { readFile } from 'node:fs/promises';

const BASE = 'https://api.bedrockgovernance.com';
const HEADERS = {
  'X-Bedrock-Key': process.env.BEDROCK_API_KEY!,
  'Content-Type': 'application/json',
};

// Step 1, upload the document bytes directly to S3 via a presigned URL.
export async function uploadDocument(localPath: string, filename: string): Promise<string> {
  const presignRes = await request(`${BASE}/v1/review/uploads`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify({ filename, contentType: 'application/pdf' }),
  });
  if (presignRes.statusCode !== 200) {
    throw new Error(`Bedrock presign failed: ${presignRes.statusCode}`);
  }
  const { uploadUrl, documentKey } = (await presignRes.body.json()) as {
    uploadUrl: string;
    documentKey: string;
  };

  const bytes = await readFile(localPath);
  const putRes = await request(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/pdf' },
    body: bytes,
  });
  if (putRes.statusCode !== 200) {
    throw new Error(`S3 upload failed: ${putRes.statusCode}`);
  }

  return documentKey;
}

// Step 2, submit the uploaded document for review.
export async function submitAdvice(input: SubmitInput) {
  const res = await request(`${BASE}/v1/review/jobs`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify(input),
  });

  if (res.statusCode === 403) {
    throw new Error('Plan insufficient for review services.');
  }
  if (res.statusCode === 404) {
    throw new Error('Document not found, make sure the upload PUT completed before submitting.');
  }
  if (res.statusCode === 409) {
    throw new Error('No approved impact assessment for this model. File one and have a senior sign off.');
  }
  if (res.statusCode !== 201) {
    throw new Error(`Bedrock submit failed: ${res.statusCode}`);
  }
  return (await res.body.json()) as {
    id: string;
    status: 'QUEUED' | 'ASSIGNED' | 'IN_REVIEW' | 'ESCALATED' | 'APPROVED' | 'MODIFIED' | 'REJECTED' | 'CANCELLED';
  };
}

Calling it

ts
const documentKey = await uploadDocument(
  './suitability-12345.pdf',
  'suitability-12345.pdf',
);

const job = await submitAdvice({
  documentKey,
  documentType: 'SUITABILITY_REPORT',
  clientReference: 'client-12345',
  documentReference: 'suitability-12345',
  factFindSummary: {
    riskProfile: 'Moderate',
    investmentObjective: 'Capital growth',
    investmentHorizon: '5-10 years',
    capacityForLoss: 'Can absorb 20% loss',
    existingHoldings: ['Cash ISA'],
    vulnerabilityFlags: [],
    annualIncome: '£55,000',
    netWorth: '£180,000',
  },
  priority: 'STANDARD',
  aiContext: {
    model: { provider: 'openai', version: 'gpt-4o-2024-08-06' },
    inputs: { riskProfile: 'Moderate', investmentHorizon: '5-10 years' },
    outputs: { recommendation: 'Rebalance to 60/40' },
    factors: [{ input: 'riskProfile', influence: 0.42 }],
    confidence: 0.87,
    guardrails: [{ rule: 'MAX_EQUITY_ALLOCATION', triggered: false }],
  },
  vulnerabilityFlags: ['health', 'life_event'], // routed to a specialist
  clientSegments: {
    ageBand: '65+',
    riskProfile: 'Cautious',
    productType: 'SIPP',
  },
});

console.log('Submitted as', job.id);

Common gotchas

  • Upload before you submit. If the PUT to the presigned URL fails (or its Content-Type does not match the value you passed when requesting the URL), the submit call returns 404 DOCUMENT_NOT_FOUND.
  • Pin aiContext.model.version to an exact version, not a moving alias. Drift detection only works against pinned versions.
  • File an impact assessment first. With the firm's impact-assessment gate on (the default), submitting a job with an aiContext.model that has no approved assessment returns 409 IMPACT_ASSESSMENT_REQUIRED. See Impact assessments.
  • Set clientReference and documentReference to stable values. They are how you join ledger records back to your customer and document records later.

See also

Hi! I'm Bedrock's AI assistant. I can answer questions about the product, pricing, compliance coverage, and integrations. What would you like to know?