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: ask Bedrock for a presigned upload URL, PUT the document bytes there yourself, then submit the resulting documentKey as part of the job. Bedrock never proxies the document bytes — your service uploads directly to S3.

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

type SubmitInput = {
  // Required
  documentKey: string;          // returned from POST /v1/principal/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.bedrockcompliance.co.uk';
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/principal/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/principal/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 doesn't 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 Bedrock data back to your customer and document records later.

See also

Bedrock AIAsk me anything about Bedrock

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?