braskai / rask-sdk
PHP SDK for the Rask API v3 (video and audio localization).
Requires
- php: >=8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.9
- guzzlehttp/psr7: ^2.7
- psr/http-client: ^1.0
- psr/http-message: ^2.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
- ext-curl: Guzzle's curl handler: sub-second timeouts and connection reuse across retries.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-11-12 14:20:04 UTC
README
The Rask API v3 from PHP 8.2 or newer: all 82 operations, grouped by resource and named after their operationId, plus
retries, idempotency keys, auto-paging, jobs->wait() and uploads->uploadFile().
Version 3.0.0-alpha.0, on Packagist.
Install
composer require braskai/rask-sdk:3.0.0-alpha.0
Base URL
use Rask\Sdk\Rask; $rask = new Rask([ 'api_key' => $key, // or the RASK_API_KEY environment variable 'base_url' => 'https://gw.example.com/rask/v3', // or RASK_BASE_URL; default https://api.rask.ai/v3 ]);
Every operation and helper uses base_url. A path prefix is kept and a trailing slash is fine. The key goes only to URLs
under it: a presigned upload PUT carries no Authorization, and a redirect anywhere else (another scheme, host or port, or
out of the path prefix) drops it.
Worked example
Upload, create a project, wait, download. This is examples/quickstart.php, and a test runs it.
use Rask\Sdk\Enum\OutputKind; use Rask\Sdk\Exception\InsufficientBalanceError; use Rask\Sdk\Exception\JobFailedError; use Rask\Sdk\Model\ProjectProcessingJobResult; use Rask\Sdk\Rask; // The key comes from RASK_API_KEY, the base URL from RASK_BASE_URL. Or pass them: new Rask(['api_key' => $key, 'base_url' => $url]). $rask = new Rask(); try { // Upload. The bytes go straight to storage, without your key. The import job makes them a media item. $import = $rask->uploads->uploadFile($argv[1] ?? 'talk.mp4'); $media = $rask->jobs->wait((string) $import->id); // Create the project: transcription, translation, then an automatic dub within this many minutes. $created = $rask->projects->createProject([ 'media_id' => (string) $media->media_id, 'target_language' => 'es-es', 'expected_cost_minutes' => 10, ]); $processed = $rask->jobs->wait((string) $created->id); // The automatic dub is a job of its own. $result = $processed->result; if ($result instanceof ProjectProcessingJobResult && $result->auto_dub?->dub_job_id !== null) { $rask->jobs->wait($result->auto_dub->dub_job_id); } // Download. Output URLs are presigned: no key goes with them. foreach ($rask->exports->listProjectOutputs((string) $processed->project_id) as $output) { if ($output->kind === OutputKind::DUBBED_VIDEO && $output->url !== null) { // copy() answers false when storage refuses, as it does once the URL has expired. if (!copy($output->url, 'dubbed.mp4')) { echo "The download failed.\n"; exit(1); } echo "Saved dubbed.mp4\n"; } } } catch (InsufficientBalanceError $e) { echo "Top up first: {$e->balance_minutes} minutes left, {$e->required_minutes} needed.\n"; exit(1); } catch (JobFailedError $e) { echo "Job {$e->job->id} failed: {$e->code}: {$e->detail} (request {$e->request_id})\n"; exit(1); }
Calling the API
A method takes the path parameters, then $body and $query as arrays if the operation has them, then $options.
$rask->segments->updateSegment($projectId, $segmentId, ['translation_text' => 'Hola'], ['if_match' => $segment->etag]); $rask->projects->updateProject($projectId, ['folder_id' => '']); // "" clears a field; null is never sent foreach ($rask->projects->listProjects(['limit' => 50]) as $project) {} // every page $page = $rask->projects->listProjects(['limit' => 50]); // or by hand: $page->data, $page->nextPage() $folder = $rask->projects->createFolder(['name' => 'Q3']); $folder->response()->status; // 201, and header('Location'), request_id, ... $folder->idempotencyKey(); // the key it sent, or pass your own: ['idempotency_key' => ...]
| Option | |
|---|---|
api_key, base_url |
Above |
timeout_seconds |
Per API request, default 60 |
upload_timeout_seconds |
Per presigned PUT, default 3600 |
http_client |
A PSR-18 client, used for everything, and it then owns the timeouts. Default: Guzzle |
log |
'debug' logs traffic with secrets hidden. Default: the RASK_LOG environment variable |
logger |
PSR-3 logger for that log and for deprecation warnings. Default: stderr and E_USER_DEPRECATED |
on_deprecation |
callable(string $operationId, array $headers), once per operation |
Results are typed objects whose properties have the API's own names. Every property is nullable. A new field, enum value
or union variant does not break you: enum properties are plain strings (compare with Enum\JobStatus::SUCCEEDED), an unknown
variant is a Model\UnknownVariant, and toArray(), get('/a/b') and $model['a'] read everything the server sent.
Errors extend Exception\RaskError, with status, code (a string, also for codes this version does not know), type,
title, detail, request_id, retryable, retry, hint, errors, retry_after, idempotency_key, problem,
response() and any other Problem member by name. Subclasses go by status: InsufficientBalanceError, NotFoundError,
PreconditionFailedError, RateLimitError and so on, plus ConnectionError, TimeoutError, UploadError, JobFailedError,
JobCanceledError and WaitTimeoutError. A call that was wrong before it left raises UsageError.
Retries: 429, 502, 503, 504, connection errors and timeouts, up to 120 s in all, honouring Retry-After. A wait that
cannot fit in the 120 s ends the retries at once. 500 and state conflicts are not retried. Neither is a certificate that
fails verification: the call raises ConnectionError at once, and so does jobs->wait(). The SDK knows it by cURL's
error code, so that takes Guzzle on the cURL extension: without cURL Guzzle reports such a certificate as a refused
connection, and that is retried like any other, as is whatever another http_client reports.
Develop
Everything runs in Docker (make help reads the top of the Makefile):
make install make lint # composer validate, php -l, generated files match the contract, PHPStan at max make test # PHPUnit on PHP 8.2, against the shared harness in ../rask-sdk-acceptance make check # lint, then the tests on PHP 8.2 and 8.5
Against any harness or a test stand, without Docker:
RASK_ACCEPTANCE_CONTROL_URL=http://127.0.0.1:4010/__control vendor/bin/phpunit RASK_API_KEY=... RASK_BASE_URL=https://.../v3 php examples/smoke.php
How it is generated
spec/openapi.json is a copy of the contract (spec/SOURCE.json says of what). tools/generate.php, about 1,000 lines with no
dependencies, writes src/Model, src/Enum, src/Resource and the tables in src/Internal, which are committed. CI fails if
they differ from the contract, and the generator stops on a schema form it does not know. Everything else in src is written
by hand.
Measured on 2026-11-05 on this contract without overlays, decoding a Job from the contract's own example:
| Generator | Unknown enum value | Unknown union variant | Known variant |
|---|---|---|---|
OpenAPI Generator 7.27 php |
InvalidArgumentException |
InvalidArgumentException |
InvalidArgumentException on credit_type: minutes, the contract's own example |
Kiota 1.38 PHP |
InvalidArgumentException |
InvalidArgumentException |
Decodes into one class with every variant's fields |
tools/generate.php |
Kept | UnknownVariant, kept |
Its own class |
The fallback overlays could turn open enums into strings for those two, but not keep unknown variants (the same holds for Kiota in Java and Python), and S12 asks for that on every response. So a thin typed layer over the JSON costs less than patching their output.
License
MIT. See LICENSE.