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:

  1. Receives a finished suitability report from your back-office system.
  2. Submits it to Bedrock for review.
  3. Listens for the webhook that fires when the review completes.
  4. Stores the certificate URL on the customer record.

1. Project setup

bash
mkdir bedrock-integration && cd bedrock-integration
npm init -y
npm install fastify undici

Create a .env file with the two secrets we'll need:

bash
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.

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',
};

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.

ts
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.

ts
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:

bash
node server.js &
curl -X POST http://localhost:8080/internal/submit-test

Within 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.
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?