Skip to content

Quickstart

The fastest path from zero to a published Knowledge Node: create an account, verify ownership, issue a connector token, push content, publish, and read the node files.

The production API base URL is https://api.bainquet.online. Every route below is shown with its full /v1/... path. Responses use the standard envelope; branch on the boolean ok, never on HTTP status alone. See Envelope and errors.

1. Create an account, organization, and website

Registering creates the user and an organization in one call. The caller becomes the organization owner.

http
POST /v1/auth/register
Content-Type: application/json

{ "email": "owner@acme.com", "password": "a-strong-password", "orgName": "Acme" }

Verify your email, then log in. Login sets a rotating refresh token in an HttpOnly bnq_refresh cookie and returns a short-lived access JWT in the body.

bash
curl -sS -X POST https://api.bainquet.online/v1/auth/login \
  -H 'Content-Type: application/json' \
  -c cookies.txt \
  -d '{ "email": "owner@acme.com", "password": "a-strong-password" }'

Use the access token as Authorization: Bearer <access JWT> on the calls below. Create a website under your organization:

bash
curl -sS -X POST https://api.bainquet.online/v1/organizations/$ORG_ID/websites \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H 'Content-Type: application/json' \
  -d '{ "domain": "example.com", "name": "Example" }'

The response data.id is your WEBSITE_ID. See Authentication and Organizations and websites.

2. Verify ownership

A website must prove domain control before it can publish a trusted node. Request a challenge, place the token, then ask the server to check it.

bash
# Request a challenge token (choose a method: dns_txt, well_known_file, meta_tag, plugin)
curl -sS -X POST https://api.bainquet.online/v1/websites/$WEBSITE_ID/verification/challenge \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H 'Content-Type: application/json' \
  -d '{ "method": "dns_txt" }'

# Place the returned token (for dns_txt, a TXT record), then check:
curl -sS -X POST https://api.bainquet.online/v1/websites/$WEBSITE_ID/verification/check \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H 'Content-Type: application/json' \
  -d '{}'

The verification state advances to verified on success. See Verifying ownership.

3. Issue a connector token

A connector token authenticates content pushes for one website. The token is shown once, at creation; only a hash persists. Store it immediately.

bash
curl -sS -X POST https://api.bainquet.online/v1/websites/$WEBSITE_ID/connectors \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H 'Content-Type: application/json' \
  -d '{ "name": "wp-prod", "connectorType": "api", "sourceTypes": ["page","post","product"] }'

The data.token field is your connector token, in the form connectorId.secret. This is the exact value the SDK and plugins sign with. See Connector tokens.

4. Push content

You can push with the official SDK (it handles signing for you) or with a raw signed HTTP request.

Option A: the SDK

The SDK derives checksums, signs each request, and retries transient failures. Note that baseUrl is the host root without /v1; the client appends /v1/ingest/... itself.

ts
import { createBainquetConnector, makeItem } from "@bainquet/connector-sdk";

const bq = createBainquetConnector({
  baseUrl: "https://api.bainquet.online", // no /v1; the client adds it
  token: process.env.BAINQUET_CONNECTOR_TOKEN!, // "connectorId.secret"
  websiteId: process.env.BAINQUET_WEBSITE_ID!,
  siteDomain: "example.com",
  connectorType: "api",
});

const result = await bq.ingestBatch([
  makeItem({
    type: "product",
    id: "p_1",
    url: "https://example.com/product/a",
    title: "Product A",
    json: { price: 16.99, currency: "EUR", sku: "A-001" },
    language: "en",
  }),
]);

console.log(result.accepted, "accepted,", result.skipped, "skipped");

makeItem computes the required sha256: checksum for you.

Option B: raw HTTP with HMAC

Every ingest request is signed with HMAC-SHA256 over a six-line canonical string, using a key derived from your connector secret. You cannot produce a valid signature in a single curl line, so compute the headers first, then call curl. This Node script signs one batch and posts it:

js
// sign-and-post.mjs   run: node sign-and-post.mjs
import { createHash, createHmac, hkdfSync, randomUUID } from "node:crypto";

const TOKEN = process.env.BAINQUET_CONNECTOR_TOKEN; // "connectorId.secret"
const WEBSITE_ID = process.env.BAINQUET_WEBSITE_ID;
const BASE = "https://api.bainquet.online";
const PATH = "/v1/ingest/batch";
const HKDF_INFO = "bq.connector.hmac.v1";

const secret = TOKEN.slice(TOKEN.indexOf(".") + 1);
const checksumBody = (s) => "sha256:" + createHash("sha256").update(s, "utf8").digest("hex");

const body = JSON.stringify({
  website_id: WEBSITE_ID,
  partial: true,
  items: [
    {
      type: "product",
      id: "p_1",
      url: "https://example.com/product/a",
      title: "Product A",
      json: { price: 16.99, currency: "EUR", sku: "A-001" },
      language: "en",
      checksum: checksumBody(JSON.stringify({ price: 16.99, currency: "EUR", sku: "A-001" })),
    },
  ],
});

const signingKey = Buffer.from(
  hkdfSync("sha256", Buffer.from(secret), Buffer.from(WEBSITE_ID), Buffer.from(HKDF_INFO), 32),
);
const issuedAt = String(Math.floor(Date.now() / 1000));
const nonce = randomUUID();
const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex");
const canonical = ["POST", PATH, bodySha256, issuedAt, nonce, WEBSITE_ID].join("\n");
const signature = createHmac("sha256", signingKey).update(canonical).digest("hex");

const res = await fetch(`${BASE}${PATH}`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
    "X-Signature": signature,
    "X-Timestamp": issuedAt,
    "X-Nonce": nonce,
    "X-Body-Sha256": bodySha256,
    "Idempotency-Key": randomUUID(),
    "X-Site-Domain": "example.com",
    "X-Connector-Type": "api",
    "X-Connector-Version": "1.0.0",
  },
  body,
});
console.log(res.status, await res.json());

A successful batch returns ok:true with a per-item results[] array, even when some items error. The invariant is received == accepted + skipped + errored == results.length. See Ingestion and signing for the full canonical-string definition and the verification pipeline.

WARNING

A batch route is always ok:true at the envelope level when some items fail. Inspect each entry in results[] for its per-item status (accepted, skipped, or error). Do not assume a 200 means every item was accepted.

5. Publish the node

Publishing runs the exporter pipeline and writes a new immutable version, then flips the latest/ pointer.

bash
curl -sS -X POST https://api.bainquet.online/v1/websites/$WEBSITE_ID/publish \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H 'Content-Type: application/json' \
  -d '{}'

The response carries { versionUlid, versionLabel, files, cdnPath }. (The current build runs publish synchronously and returns 200.)

6. See the node files

The published node is a set of static files. The entry point is always manifest.json, which lists every file with its checksum, byte size, and record count. A consumer reads manifest.json, then ai.json, then the record files it needs.

bash
curl -sS https://cdn.bainquet.online/<cdnPath>/latest/manifest.json

The free tier produces manifest.json, ai.json, llms.txt, entities.jsonl, facts.jsonl, relationships.jsonl, chunks.jsonl, qa.jsonl, sources.jsonl, schema.jsonld, and sitemap-ai.xml. See Node files for the full file set and the versions/{ulid}/ plus latest/ layout.

Next steps

Owner-controlled structured data for AI.