Getting Started
Your first integration
A full walkthrough wiring Bedrock into a real advice pipeline — from suitability report PDF to verifiable certificate.
This guide is the longer cousin of the Quickstart. It assumes you've already submitted a record by hand and now want to wire Bedrock into a real production pipeline. We'll build a minimal Node service that:
- Receives a finished suitability report from your back-office system.
- Submits it to Bedrock for review.
- Listens for the webhook that fires when the review completes.
- Stores the certificate URL on the customer record.
1. Project setup
mkdir bedrock-integration && cd bedrock-integration
npm init -y
npm install fastify undiciCreate a .env file with the two secrets we'll need:
BEDROCK_API_KEY=bk_live_...
BEDROCK_WEBHOOK_SECRET=...The webhook secret is the value returned in the secret field when you register a webhook via POST /v1/firm/me/webhooks.
2. Submit a record
Submission is a two-step flow. First, ask Bedrock for a presigned S3 URL and PUT the document bytes there yourself. Then submit the resulting documentKey for review along with any metadata you want to round-trip back through the certificate. Bedrock never proxies the document bytes itself — you upload directly to S3, then hand back the key.
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',
};
async function uploadDocument(localPath: string, filename: string): Promise<string> {
// Step 1 — get a presigned URL.
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;
};
// Step 2 — PUT the bytes to the presigned URL.
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;
}
export async function submitForReview(input: {
documentType: 'SUITABILITY_REPORT';
documentKey: string;
clientReference: string;
documentReference: string;
factFindSummary: Record<string, unknown>;
}) {
const res = await request(`${BASE}/v1/principal/jobs`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(input),
});
if (res.statusCode === 404) {
throw new Error('Document not found — make sure the upload PUT completed before submitting.');
}
if (res.statusCode !== 201) {
const body = await res.body.text();
throw new Error(`Bedrock submit failed: ${res.statusCode} ${body}`);
}
return (await res.body.json()) as { id: string; status: string };
}3. Receive the webhook
Bedrock signs every webhook payload with HMAC-SHA256 using your firm's webhook secret. Verify the signature on every request — never trust the payload alone.
import Fastify from 'fastify';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = Fastify();
// Capture the raw request body — the signature is computed over the exact bytes
// Bedrock sent. Re-serialising via JSON.stringify(req.body) will not match.
app.addContentTypeParser(
'application/json',
{ parseAs: 'buffer' },
(_req, body, done) => done(null, body),
);
app.post('/webhooks/bedrock', async (req, reply) => {
const header = req.headers['x-bedrock-signature'] as string | undefined;
const eventName = req.headers['x-bedrock-event'] as string | undefined;
if (!header || !eventName) return reply.code(400).send('missing signature');
// Header format is "sha256=<hex>"
const [scheme, provided] = header.split('=');
if (scheme !== 'sha256' || !provided) return reply.code(400).send('bad signature format');
const rawBody = req.body as Buffer;
const expected = createHmac('sha256', process.env.BEDROCK_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
const a = Buffer.from(provided, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return reply.code(401).send('bad signature');
}
// Every delivery carries the same flat envelope — fetch the full
// ledger record (and any related certificate or job) by id when you
// need more than the projection.
const payload = JSON.parse(rawBody.toString('utf8')) as {
id: string;
firmId: string;
sequenceNumber: number;
eventType: string;
timestamp: string;
documentHash: string;
chainHash: string;
};
if (eventName === 'DOCUMENT_APPROVED') {
await persistCertificate(payload.id);
}
return reply.code(204).send();
});
app.listen({ port: 8080 });4. Persist the certificate
The webhook gives you the certificate ID. Store it on your customer record alongside the canonical URL — clients can use that URL to verify the document themselves at any point in the future, without your involvement.
async function persistCertificate(jobId: string, certificateId: string) {
const url = `https://verify.bedrockcompliance.co.uk/c/${certificateId}`;
await db.advice.update({
where: { externalRef: jobId },
data: {
bedrockCertificateId: certificateId,
bedrockCertificateUrl: url,
bedrockReviewedAt: new Date(),
},
});
}5. Test end-to-end
Run your service and submit a record:
node server.js &
curl -X POST http://localhost:8080/internal/submit-testWithin seconds you should see the job land in Review, get reviewed (use a test reviewer in sandbox), and trigger your webhook. The certificate URL appears on the customer record.
What to build next
- Replay logic for missed webhooks (see Handle a webhook).
- A scheduled job to verify the chain integrity of last week's records.
- An incident response flow that creates a Review escalation when your back-office system flags a complaint.