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
PUTto the presigned URL fails (or itsContent-Typedoesn't match the value you passed when requesting the URL), the submit call returns404 DOCUMENT_NOT_FOUND. - Pin
aiContext.model.versionto 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.modelthat has no approved assessment returns409 IMPACT_ASSESSMENT_REQUIRED. See Impact assessments. - Set
clientReferenceanddocumentReferenceto stable values. They are how you join Bedrock data back to your customer and document records later.