rekhu

Packages and API

The core package

rekhu/core and @rekhu/core put Rekhu's knowers in your Laravel or Node.js app, with your own tenants, languages, limits, prices and usage ledger.

What the core package is for

The core package is how your own code calls Rekhu's knowers. A knower is a specialist that knows one part of a trade the way its best expert does: it understands what is asked, decides, explains in your user's language, and learns from what your users accept or correct.

Knower Knows Docs
Seshat (seshat) print files: check, fix, proof, templates, pictures Seshat overview
Rekhu Machines (machines) printing and finishing machines: identify, profile, check, choose below

You install the package once and add the knowers your product needs. Your tenants, users, limits and prices stay yours: every call is kept in your own ledger, next to what Rekhu charged, so you can charge your customers per call or per plan.

Install

  1. Make a project for your app in your Rekhu account (Projects → New project, Laravel app or Node.js app), and add a key on its Connection tab. See Sites and keys.
  2. Install the package and connect the app with the key.
  3. Run the migration (Laravel) for the usage table.
  4. Add the knowers you use.

For Laravel, the install command asks for the key, checks it with Rekhu, and writes the settings to .env:

composer require rekhu/core:^0.10
php artisan rekhu:core --install --tenancy=multi --locales=en,nl,ar
php artisan migrate
php artisan rekhu:core --install=seshat
php artisan rekhu:core --install=machines

For Node.js, install the package and create the client once:

npm install @rekhu/core@^0.10

Configuration

// .env, written by php artisan rekhu:core --install
// REKHU_APP_KEY=agk_…           the app key; an app with Rekhu Assistant can use REKHU_ASSISTANT_KEY
// REKHU_TENANCY=multi           single (the app is one customer) or multi (users belong to your customers)
// REKHU_LOCALES=en,nl,ar        the languages your users read
// REKHU_KNOWERS=seshat,machines optional: empty means every knower the key allows
// REKHU_TIMEOUT=180             seconds
// REKHU_LEDGER=true             keep every call in rekhu_usage
import { createRekhu } from '@rekhu/core';

export const rekhu = createRekhu({
    key: process.env.REKHU_APP_KEY,            // or REKHU_SECRET_KEY
    knowers: ['seshat', 'machines'],           // optional: empty means every knower the key allows
    locales: ['en', 'nl', 'ar'],
    timeout: 180000,                            // milliseconds
});

php artisan rekhu:core --check calls Rekhu with the key and shows which platform answered, its version, the account and the app.

Tenants and languages

A multi-tenant app tells the package which of your customers a user belongs to, and which language the user reads. Knowers answer in that language, and your usage is kept per tenant.

use Rekhu\Core\Rekhu;

// in a service provider
Rekhu::tenantUsing(fn ($user) => $user->team);        // a model, ['id' => …, 'name' => …] or an id
Rekhu::localeUsing(fn ($user) => $user->language);    // else the app's locale
export const rekhu = createRekhu({
    // …
    tenant: (user) => ({ id: user.teamId, name: user.teamName }),   // setting it makes the app multi-tenant
    locale: (user) => user.language,
});

Each call is made for a user. In Laravel it is the signed-in user, or the one you name with for($user). In Node.js you pass it: rekhu.seshat({ user }). Work without a user, such as a queued job, names the tenant:

Rekhu::seshat()->forTenant('shop-42', 'Print shop 42')->check($path, $order);
await rekhu.seshat({ tenant: { id: 'shop-42', name: 'Print shop 42' } }).check(path, { order });

A language outside your locales falls back to the first one.

Limits and your own prices

Set limits a day per tenant and per user, and the price you charge your customer for a call. A call over a limit is refused with LimitReached before anything is sent to Rekhu.

// config/rekhu.php: 'limits' => ['seshat' => ['per_day_per_tenant' => 500, 'per_day_per_user' => null]]
Rekhu::limitsUsing(fn ($tenant, $knower) => ($tenant['plan'] ?? null) === 'basic' ? ['per_day_per_tenant' => 50] : []);
Rekhu::priceUsing(fn ($tenant, $knower, $action, $meta) => 0.02);
createRekhu({
    // …
    limits: { seshat: { perDayPerTenant: 500 } },
    limitsFor: (tenant, knower) => (tenant?.plan === 'basic' ? { perDayPerTenant: 50 } : {}),
    price: (tenant, knower, action, meta) => 0.02,
});

Limits are counted in the ledger, so they need it on.

The usage ledger

Every charged call is kept with the tenant, the user, the knower and action, the model calls, what Rekhu charged (cost_usd) and your own price. Laravel keeps it in the rekhu_usage table and fires Rekhu\Core\Events\KnowerUsed after every call. Node.js keeps it in memory by default; give fileLedger(path) for a single server, or ledger: { record, count } backed by your database.

use Rekhu\Core\Events\KnowerUsed;

Event::listen(function (KnowerUsed $e) {
    $e->tenant; $e->knower; $e->action; $e->costUsd; $e->price;   // charge a plan's credit here
});
import { createRekhu, fileLedger } from '@rekhu/core';

const rekhu = createRekhu({
    // …
    ledger: fileLedger('storage/rekhu-usage.jsonl'),
    onUsage: (row) => billing.record(row),     // row: tenant_id, knower, action, cost_usd, price, …
});

await rekhu.usage({ month: '2026-11', tenant: 'shop-42' });

The usage per tenant and month, with a CSV, from the command line:

php artisan rekhu:core --usage --month=2026-11 --tenant=shop-42 --csv=storage/rekhu-2026-11.csv
npx rekhu-core usage --ledger storage/rekhu-usage.jsonl --month 2026-11 --csv usage-2026-11.csv

A file from the command line

Checking a print file needs no code. With the app key in REKHU_APP_KEY:

npx @rekhu/core check flyer.pdf --size 148x210 --bleed 3
php artisan rekhu:check flyer.pdf --size=148x210 --bleed=3

Each finding prints on its own line with Seshat's decision and its pages. Add --fix --out flyer-ready.pdf (Laravel: --fix --out=flyer-ready.pdf) to save the print-ready version. The order also takes --pages, --colour, --max-ink, --min-ppi and --profile. --explain adds the explanation for your customer, --json prints the whole answer.

Rekhu Machines

Machines are sent as a Rekhu machine profile: type, method, maker, model, sheet_max_mm, gsm, speed and more. Map your own machine records to it.

$machines = Rekhu::machines()->for($user);

$found = $machines->identify('A four-colour B2 offset press with a coater, built in 2009');
$choice = $machines->choose(['width_mm' => 85, 'height_mm' => 55, 'quantity' => 5000, 'colours_front' => 4, 'colours_back' => 4, 'gsm' => 350], $shopMachines);
$machines->feedback($choice['ref'], 'accepted');
const machines = rekhu.machines({ user });

const found = await machines.identify('A four-colour B2 offset press with a coater, built in 2009');
const choice = await machines.choose({ width_mm: 85, height_mm: 55, quantity: 5000, colours_front: 4, colours_back: 4, gsm: 350 }, shopMachines);
await machines.feedback(choice.ref, 'accepted');

profile, check and explain fill in a profile, find values that cannot be right, and answer questions about a machine. Every answer has a ref: tell the knower what your user did with it (accepted, corrected with the correction, or rejected), so it learns.

When your own tool reads the same file differently, send second_reading instead of corrected. It does not say who is right. Rekhu keeps each difference for review, and nothing is learned from it until it has been looked into:

Rekhu::seshat()->feedback($result['ref'], 'second_reading', [
    'tool' => 'pdftool', 'tool_version' => '2.3.1', 'file_id' => $fileId,
    'differences' => [['code' => 'ink_too_heavy', 'page' => 4, 'pdftool' => ['highest' => 181], 'seshat' => ['highest' => 189]]],
]);
await seshat.feedback(result.ref, 'second_reading', {
    tool: 'pdftool', tool_version: '2.3.1', file_id: fileId,
    differences: [{ code: 'ink_too_heavy', page: 4, pdftool: { highest: 181 }, seshat: { highest: 189 } }],
});

Each difference names the finding (code), the page when there is one, and what each side read, under the tool's name and the knower's name. Use null when one side does not report the finding. With file_id, Rekhu can reproduce the case.

When Rekhu cannot be reached

A call that cannot reach Rekhu is tried again after 2, 4 and 8 seconds, about 14 seconds in all, before it fails with Unavailable. On a screen a user waits on, such as an upload, set fewer or shorter waits: REKHU_RETRY_WAITS=1 (one more try after a second; in seconds, separated by commas) or an empty value (no further try). In Node.js you can also pass createRekhu({ retryWaits: [1000] }), in milliseconds.

Where Rekhu runs

REKHU_DEPLOYMENT=cloud, the default, uses Rekhu's platform. private uses Rekhu inside your own network: set REKHU_PLATFORM to its address. The knowers, their answers and the package are the same in both.

Errors and retries

Every refusal is a RekhuException (Laravel) or RekhuError (Node.js) with status and type, and a class of its own. A call that cannot reach Rekhu, or finds it busy, is tried again by itself, and every charged call carries an idempotency key, so a retry is never charged twice. See Errors.

Next