Getting Started
Your first integration
A full walkthrough wiring the platform into a real advice pipeline, from suitability report PDF to verifiable certificate.
This guide is the longer cousin of the Quickstart. It assumes you have already submitted a record by hand and now want to wire the platform into a real production pipeline. This guide builds a minimal Node service that:
- Receives a finished suitability report from your back-office system.
- Submits it to the platform 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 the service needs:
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, request a presigned S3 URL from the API 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. The document bytes never pass through the platform; you upload directly to S3, then hand back the key.
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',
};
async function uploadDocument(localPath: string, filename: string): Promise<string> {
// Step 1, get a presigned URL.
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;
};
// 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/review/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
The platform 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.bedrockgovernance.com/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.