Search by

cutoptim / engine

cutoptim

Official client for the CutOptim Engine API — cutting optimization (2D panels, 1D linear, timber, true-shape nesting) over HTTPS.

v1.0.0 2026-10-20 09:48 UTC

This package is not auto-updated.

Last update: 2026-10-21 08:54:39 UTC


README

CutOptim

CutOptim Engine API — PHP SDK

Cutting optimization over HTTPS: 2D panels, 1D linear, timber and true-shape nesting.
Deterministic plans, saw-ready cut sequences — PHP 8.1+, cURL, no dependencies.

Packagist CI PHP 8.1+ Zero dependencies MIT

Quickstart · API reference · Webhooks · Changelog · Get an API key

Why this SDK

  • One method per endpoint, from the contract. The client's operation table is generated from the OpenAPI 3.1 document; requests and answers are plain arrays shaped exactly as in the reference.
  • No dependencies. cURL and JSON — drops into WordPress / WooCommerce, Laravel, Symfony or a plain script.
  • Retries that never cost twice. Every optimize call carries an Idempotency-Key; a retried call returns the stored answer instead of a second charge.
  • Async jobs without the boilerplate. waitForJob() polls the max engine for you — or let a webhook tell you.
  • Webhook verification built in. Webhook::verify() checks the signature on the raw body, with the replay window and secret rotation handled.

Install

composer require cutoptim/engine

A free test key (co_test_…, never billed) is enough to build the whole integration — create one on your dashboard.

Quickstart

use CutOptim\Engine\Client;

$client = new Client(getenv('CUTOPTIM_KEY'));

$plan = $client->optimize2d([
    'parts' => [
        ['name' => 'Side', 'w' => 720, 'h' => 560, 'qty' => 2],
        ['name' => 'Shelf', 'w' => 764, 'h' => 540, 'qty' => 3, 'rotatable' => false],
    ],
    'stock' => [['w' => 2800, 'h' => 2070, 'price' => 48]],
    'options' => ['kerf' => 4],
]);

if (!Client::isJobAccepted($plan)) {
    printf("%d sheet(s), %s%% yield\n", $plan['metrics']['sheetCount'], $plan['metrics']['yieldPct']);
    foreach ($plan['sheets'] as $sheet) {
        foreach ($sheet['parts'] as $p) {
            echo $p['name'], ' ', $p['x'], ' ', $p['y'], ' ', $p['w'], ' ', $p['h'], $p['rotated'] ? ' rotated' : '', "\n";
        }
    }
}

The same input always returns the same plan — cache it, test against it, quote from it.

Every mode

// 1D linear — bars, profiles, tubes
$bars = $client->optimize1d(['parts' => [['name' => 'Rail', 'length' => 1200, 'qty' => 6]], 'stock' => [['length' => 6000]]]);

// Timber — lengths cut from beams of a given cross-section (sw × sh)
$timber = $client->optimizeWood([
    'parts' => [['name' => 'Joist', 'sw' => 45, 'sh' => 145, 'length' => 3600, 'qty' => 4]],
    'stock' => [['sw' => 45, 'sh' => 145, 'length' => 4800]],
]);

// True-shape nesting — polygons (or SVG/DXF sources) on sheets
$nest = $client->optimizeNest([
    'parts' => [['name' => 'Bracket', 'polygon' => [[0, 0], [120, 0], [120, 40], [40, 120], [0, 120]], 'qty' => 10]],
    'stock' => [['w' => 1000, 'h' => 500]],
]);

Check a request without spending a call — the validate endpoints need no key and give the same verdict:

(new Client())->validate2D($request); // ['valid' => true, 'warnings' => [...]] or a CutOptimException (400)

Async max jobs

'engine' => 'max' searches harder (seconds to a minute) and answers 202 with a job id instead of a plan:

$accepted = $client->optimize2d($request + ['engine' => 'max']);
if (Client::isJobAccepted($accepted)) {
    $job = $client->waitForJob($accepted['jobId'], 300); // polls GET /v1/jobs/{id}, no quota
    if ($job['status'] === 'succeeded') {
        $result = $job['result'];
    }
}

Webhooks

Instead of polling, register an endpoint on your dashboard and receive a signed POST (job.succeeded / job.failed) when a max job finishes. Verify it on the raw body:

// Plain PHP
use CutOptim\Engine\Webhook;

$raw = file_get_contents('php://input');
if (!Webhook::verify($raw, $_SERVER['HTTP_CUTOPTIM_SIGNATURE'] ?? null, getenv('CUTOPTIM_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);   // deduplicate on $event['id'] — delivery is at least once
http_response_code(204);            // answer within 10 s, then do the work
// Laravel
Route::post('/cutoptim/webhook', function (Illuminate\Http\Request $request) {
    abort_unless(Webhook::verify($request->getContent(), $request->header('CutOptim-Signature'), config('services.cutoptim.webhook_secret')), 400);
    return response()->noContent();
});

During a secret rotation both secrets sign for 24 hours — pass both: Webhook::verify($raw, $header, [$newSecret, $oldSecret]).

Retries and idempotency

Situation What the client does
429 busy, 503, network error retries (2× by default), waiting Retry-After when the API sends it
409 idempotency_in_progress retries — the first request is still running; the retry receives its answer
any optimize call sends an Idempotency-Key (generated, or yours) — the same key on every retry

Pass your own key as the second argument to make retries safe across requests too:

$client->optimizeNest($request, 'order-' . $orderId);

Within 24 hours the same key and request return the stored answer — not charged again, and a max job is not queued twice.

Errors

Any non-2xx answer throws CutOptimException:

use CutOptim\Engine\CutOptimException;

try {
    $client->optimize2d($request);
} catch (CutOptimException $e) {
    echo $e->getStatus(), ' ', $e->getErrorCode();   // branch on getErrorCode(), never on the message
    var_dump($e->getDetails(), $e->getRetryAfter());
}
getErrorCode() Status Meaning
invalid_request 400 the request is wrong — getDetails()['path'] names the field; never charged
unauthorized 401 missing or unknown API key
quota_exceeded 402 the account's monthly quota is used up — getRetryAfter() counts to the reset
key_revoked 403 the key is revoked, or the API subscription is not active
idempotency_conflict 409 the key was used for a different request — use a new key
too_large 413 an input cap was exceeded (parts, rows, body size)
busy 429 at capacity right now — retried automatically
service_unavailable 503 an engine is not available on this deployment
solve_timeout 504 the nesting solve ran out of time
network_error 0 the connection failed after the retries
timeout 0 waitForJob() gave up waiting (the job keeps running)

The full list is on the errors page.

Configuration

new Client(
    apiKey: 'co_live_…',                    // not needed for getHealth() and validate*()
    baseUrl: 'https://api.cutoptim.com',    // default
    maxRetries: 2,                          // 429 / 503 / 409 in-progress / network errors
    timeoutS: 30,                           // per request
);

API coverage

Method Endpoint Quota
optimize2d($req, $idempotencyKey = null) POST /v1/optimize/2d 1 call
optimize1d($req, $idempotencyKey = null) POST /v1/optimize/1d 1 call
optimizeWood($req, $idempotencyKey = null) POST /v1/optimize/wood 1 call
optimizeNest($req, $idempotencyKey = null) POST /v1/optimize/nest 1 call
validate2D · validate1D · validateWood · validateNest POST /v1/validate/* free, no key
importNest($req) POST /v1/import/nest free (key required)
getJob($id) · waitForJob($id) GET /v1/jobs/{id} free
getUsage() GET /v1/usage free
getHealth() GET /v1/health free, no key

Versioning

The SDK follows semver. The API is v1 and only grows within it: a new endpoint becomes a new method with a minor SDK release; nothing you use today is removed or narrowed. See versioning and stability and the API changelog (also as RSS).

Support

This repository is published from the CutOptim monorepo; pull requests are welcome and applied upstream.

License

MIT © CutOptim