Client reference
gisl.create() / Gisl::create() returns one object with two layers:
- the file-first builders (
file(),files(),batch(),merge(), and the single-operation verbs), covered in the Quickstart and the Operations pages; - the low-level client underneath them: one method per API call, for when you build the workflow payload yourself. This page is the reference for that layer and for creating the client.
Endpoints that only GISL's own web app uses (sign-in and session cookies, checkout, the contact
form) are omitted, as are createExternalImport and decodeAudioWatermark, whose operations are
planned: they return feature_not_available (422) today.
Creating a client
- TypeScript
- PHP
- Python
- Rust
import { gisl } from '@giveitsmaller/sdk';
const client = await gisl.create({ apiKey: 'REPLACE_ME_API_KEY', environment: 'staging' });
gisl.create() is async because it may read the credentials file. Every option is optional:
| Option | Default | Purpose |
|---|---|---|
apiKey | resolved, see below | API key, sent as Authorization: Bearer … |
profile / profilePath | default / ~/.gisl/credentials | which credentials-file profile to read, and where |
environment | resolved, see below | 'prod' or 'staging' |
baseUrl | resolved, see below | API host; overrides environment. Does not move the progress stream: pair it with environment or streamBaseUrl |
streamBaseUrl | resolved, see below | host for the progress stream only |
headers | none | extra headers on every request |
locale | none | sent as Accept-Language; wins over one in headers |
timeout | 30000 | per-request timeout in ms, enforced by the SDK |
multipartThreshold | 10000000 | files above this many bytes upload in parts |
multipartConcurrency | 4 | part uploads in flight at once |
multipartMaxAttempts | 3 | attempts per part, including the first |
multipartRetryBaseMs | 500 | base of the jittered backoff between part attempts |
presetDefaults | none | client-wide preset overrides for compress |
use Gisl\Sdk\Environment;
use Gisl\Sdk\Gisl;
$client = Gisl::create(apiKey: 'sk_...', environment: Environment::Staging);
Every argument is optional and named:
| Argument | Default | Purpose |
|---|---|---|
apiKey | resolved, see below | API key, sent as Authorization: Bearer … |
profile / profilePath | default / ~/.gisl/credentials | which credentials-file profile to read, and where |
environment | resolved, see below | Environment::Prod or Environment::Staging |
baseUrl | resolved, see below | API host; overrides environment. Does not move the progress stream: pair it with environment or streamBaseUrl |
streamBaseUrl | resolved, see below | host for the progress stream only |
headers | [] | extra headers on every request |
locale | none | sent as Accept-Language; wins over one in headers |
timeoutMs | 30000 | recorded but not enforced: set timeouts on your HTTP client |
multipartThresholdBytes | 10000000 | files above this many bytes upload in parts |
multipartConcurrency | 4 | part uploads in flight at once (1 uploads them one at a time) |
multipartMaxAttempts | 3 | attempts per part, including the first |
multipartRetryBaseMs | 500 | base of the jittered backoff between part attempts |
httpClient / requestFactory / streamFactory | discovered | your own PSR-18 / PSR-17 implementations; see below |
presetDefaults | none | client-wide preset overrides for compress |
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
How the key and hosts are resolved
Both SDKs resolve in the same order; the first source that provides a value wins.
| Value | Resolution order |
|---|---|
| API key | apiKey → GISL_API_KEY → api_key in the profile of the credentials file |
| API host | baseUrl → environment → GISL_BASE_URL → GISL_ENVIRONMENT → https://api.giveitsmaller.com |
| Stream host | streamBaseUrl → environment → GISL_STREAM_BASE_URL → GISL_ENVIRONMENT → production, unless you set an API host |
The credentials file is INI, one section per profile:
[default]
api_key = sk_live_...
[staging]
api_key = sk_test_...
A missing file is skipped. A file that exists but lacks the requested profile, or cannot be
parsed, throws GislConfigError. With no key from any source, creation throws
GislMissingCredentialsError before any request. In a browser the TypeScript SDK reads neither
environment variables nor the file.
A baseUrl or GISL_BASE_URL that is set but blank throws GislConfigError. It does not fall
back to production, because you would be sending your key somewhere you did not choose. A blank
streamBaseUrl throws only when nothing else declares a stream host; a lower-priority source
(GISL_STREAM_BASE_URL, GISL_ENVIRONMENT) that does declare one is used instead.
The progress stream is served from a second host (https://stream.giveitsmaller.com in
production). If you point baseUrl at your own host and declare no stream host, the SDK does not
guess one: streamEvents() throws GislStreamHostNotDeclaredError and run() polls instead.
That poll is visible: the result's transport is 'polling', and the client emits one warning
(Node process.emitWarning, code GISL_STREAM_HOST_NOT_DECLARED; PHP E_USER_WARNING) naming the
fix. To keep live progress through your own API host, pass environment or streamBaseUrl as
well, or set GISL_STREAM_BASE_URL. See
Progress streaming.
Constructing the low-level client directly
GislClient can be built without the factory. It skips all of the resolution above, so pass the
host and key yourself, and it has none of the file-first builders.
- TypeScript
- PHP
- Python
- Rust
import { GislClient } from '@giveitsmaller/sdk';
const client = new GislClient({ baseUrl: 'https://api.giveitsmaller.com', apiKey: 'REPLACE_ME_API_KEY' });
use Gisl\Sdk\GislClient;
use Gisl\Sdk\GislClientConfig;
$client = new GislClient(new GislClientConfig(
baseUrl: 'https://api.giveitsmaller.com',
apiKey: 'sk_...',
));
GislClientConfig names the timeout timeout: (not timeoutMs:). A client built this way uploads
parts one at a time through the PSR-18 client.
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
Low-level methods
The method names are the same in both SDKs. TypeScript takes an options object where PHP takes an
options class or named arguments. Response types share their names across the two; in PHP they are
generated models read through getters ($created->getWorkflowId()).
Uploads
| Method | Purpose | Returns |
|---|---|---|
uploadFile(file, options?) | Upload a file; multipart above the threshold | UploadResponse (its fileId) |
getMetadata(fileId) | Metadata the server extracted from an upload | MetadataResponse |
getUploadStatus(uploadId) | Parts recorded so far on a multipart upload | the upload's state |
presignParts(uploadId, partNumbers, totalParts) | Fresh URLs for missing parts (at most 100, part 1 excluded) | presigned URLs |
keepaliveUpload(uploadId) | Extend an unfinished multipart upload's 48-hour expiry | confirmation |
file is a path or a Blob/File in TypeScript, and a path or a seekable stream resource in
PHP. PHP rejects a non-seekable stream (php://stdin, a pipe) unless you pass
new UploadOptions(bufferNonSeekable: true), which copies it to a temporary buffer first. The
three multipart methods return plain objects in TypeScript and arrays in PHP; you need them only
for a custom resume flow (see Multipart uploads).
Workflows
| Method | Purpose | Returns |
|---|---|---|
createWorkflow(payload) | Create a workflow from a hand-built payload | WorkflowCreateResponse (workflowId, webhookSecret) |
createWorkflowAwaitingProbe(payload, …) | createWorkflow, retrying after a probe_pending refusal | WorkflowCreateResponse |
cancelWorkflow(workflowId) | Cancel; idempotent, but 409 once it has completed, failed or expired | WorkflowCancelResponse |
resumeWorkflow(workflowId) | Resume a workflow paused for insufficient credits | WorkflowResumeResponse |
retryOperation(operationId) | Retry one failed operation | RetryResponse |
archiveWorkflow(workflowId) | Hide a finished workflow from the list; not a delete | WorkflowArchiveResponse |
restoreWorkflow(workflowId) | Undo an archive; idempotent | WorkflowRestoreResponse |
listWorkflows(…) | One page of your workflows, newest first. TS takes one options object { cursor?, limit?, archived? }; PHP takes them positionally ($cursor, $limit, $archived) | WorkflowListResponse |
workflows(…) | Every workflow, following the cursor for you. TS { limit?, archived? }; PHP ($limit, $archived) | async iterator (TS), Generator (PHP) of WorkflowSummary |
The source factories for a hand-built payload are described in
Workflows. resumeWorkflow throws
GislBalanceExhaustedError if the balance still does not cover the next step, and
GislWorkflowExpiredError once the paused workflow has expired. Archiving a workflow that has not
finished is a 409: cancel it first.
Status, waiting and downloads
| Method | Purpose | Returns |
|---|---|---|
getWorkflowStatus(workflowId) | Current status, once | WorkflowStatusResponse |
waitForWorkflow(workflowId, options?) | Poll until the workflow stops | the final WorkflowStatusResponse |
getWorkflowDownloads(workflowId) | Pre-signed output URLs, per job | WorkflowDownloadResponse |
waitForWorkflow returns on completed, failed, partially_failed, cancelled, expired and
also paused_insufficient_credits, which waits on you (top up, then resumeWorkflow) rather than
on time. It polls every 2 seconds and gives up after 10 minutes with GislTimeoutError.
- TypeScript
- PHP
- Python
- Rust
const final = await client.waitForWorkflow(workflowId, {
intervalMs: 5_000,
timeoutMs: 30 * 60_000,
onPoll: (status) => console.log(status),
});
use Gisl\Sdk\WaitOptions;
$final = $client->waitForWorkflow($workflowId, new WaitOptions(
intervalMs: 5_000,
timeoutMs: 30 * 60_000,
onPoll: fn (string $status) => print($status . "\n"),
));
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
Streaming
| Method | Purpose | Returns |
|---|---|---|
streamEvents(workflowId, …) | Live progress events over SSE | async generator (TS), Generator (PHP) of GislSseEvent |
Both accept an onParseError callback for frames that fail to parse; TypeScript also takes a
signal. Event shapes, reconnects and the second host are on
Progress streaming.
Schema and capabilities
| Method | Purpose | Returns |
|---|---|---|
getSchema(options?) | The operations, options and limits your tier can use, optionally filtered by MIME type and operation | a hit with the schema, or not-modified |
capabilities(opType?) | The capability matrix from the schema, without the not-modified case | snapshot, or one operation's entry |
operation(opType, input, options?) | Run a single-input operation that has no dedicated verb | an operation builder |
getSchema supports conditional requests: pass a previous result's etag as ifNoneMatch. The
ifModifiedSince option exists but has no effect today, because the server ignores
If-Modified-Since; lastModified is informational only. In TypeScript the
result has notModified: true | false; in PHP it is a GetSchemaHitResult or a
GetSchemaNotModifiedResult. capabilities() and operation() exist only on the client from
gisl.create() / Gisl::create(). operation() passes options through unchanged, so prefer a
dedicated verb when there is one; multi-input operations (merge, archive, overlay watermarks) have
their own builders.
Credits and limits
| Method | Shortcut on the created client | Returns |
|---|---|---|
getCreditsBalance() | credits() | CreditsBalanceResponse |
getCreditsUsage(options?) | creditsUsage(options?) | CreditsUsageResponse, paged by limit and offset |
getAccountLimits() | limits() | AccountLimits, your tier's caps |
getProfile() | none; call getProfile() on it directly | AuthenticatedIdentity: the id, email and tier your key resolves to |
Call getProfile() before a destructive call to check that your key resolves to the account you mean
to act on: an API key and a user id you supply separately are otherwise never checked against each
other. It is read-only, and a 401 raises the same error class as every other call.
Checkout
| Method | Purpose | Returns |
|---|---|---|
createCheckoutSession(payload) | Start a hosted checkout for a subscription or a credit pack; send the browser to its URL | BillingCheckoutSession (checkoutUrl, sessionId) |
getCheckoutSessionStatus(sessionId) | Has the purchase behind a checkout you started been applied? Stable (since contracts v2.236.0) | CheckoutSessionStatusResponseData: sessionId and status |
Both need an account: on an anonymous client they throw GislFeatureRequiresAuthError before
anything is sent. status is one of three values:
status | Meaning |
|---|---|
paid | Applied to your account: a pack's credits are granted, or the subscription is linked (linked, not necessarily paid up) |
pending | Recorded as yours, not applied yet. Not a failure: ask again |
unknown | Not an error. The session is not provably yours: someone else's, one that never existed, or one created before this endpoint shipped. The API deliberately does not say which |
A session id that is not yours reads unknown, never a 403 or 404, so the endpoint cannot be
used to learn whether someone else's session exists. No timing is promised between pending and
paid. The three values are the whole set: any other status, a body without a string
session_id (an empty data included), any success status other than 200 (a 204 included), or
a 200 that is not a JSON body at all throws GislResponseContractError. An empty sessionId throws GislError before any
request; any other id is sent as one encoded path segment. A 401 and the router's 404 for an id
it cannot route take the shared error mapping.
- TypeScript
- PHP
- Python
- Rust
const { checkoutUrl, sessionId } = await client.createCheckoutSession({ type: 'pack', key: 'pack_25' });
// ... the browser completes checkout and returns ...
const { status } = await client.getCheckoutSessionStatus(sessionId);
if (status === 'pending') console.log('not applied yet; ask again shortly');
use Gisl\Generated\OpenApi\Model\BillingCheckoutRequest;
use Gisl\Generated\OpenApi\Model\CheckoutSessionStatusResponseData;
$session = $client->createCheckoutSession(new BillingCheckoutRequest(['type' => 'pack', 'key' => 'pack_25']));
// ... the browser completes checkout and returns ...
$result = $client->getCheckoutSessionStatus($session->getSessionId());
if ($result->getStatus() === CheckoutSessionStatusResponseData::STATUS_PENDING) {
echo "not applied yet; ask again shortly\n";
}
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
Health
| Method | Purpose | Returns |
|---|---|---|
getHealth() | Is the API up, and which build is it running? GET /healthz | LivenessResponse: app, and build when the API sends it |
getHealth() never sends your key, a session cookie or a workflow capability, even from a client
built with one, so it also works on an anonymous client. It uses the client's normal timeout and
does not follow a redirect: a 3xx throws GislError. build is the release the API was built as
(for example 1.17.0-rc.1, dev or unknown) and is undefined in TypeScript and null in PHP
when absent; compare it for equality rather than parsing it. A body whose app is not a boolean,
or whose build is not a string, throws GislResponseContractError.
- TypeScript
- PHP
- Python
- Rust
const before = await client.getHealth();
// ... timed run ...
const after = await client.getHealth();
if (before.build !== after.build) console.warn('the API was redeployed during the run');
$before = $client->getHealth();
// ... timed run ...
$after = $client->getHealth();
if ($before->getBuild() !== $after->getBuild()) {
fwrite(STDERR, "the API was redeployed during the run\n");
}
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
Probing uploads
| Method | Purpose | Returns |
|---|---|---|
probeUpload(fileId) | The server's readiness check for an upload: codec, corruption, routing. Beta | UploadProbeResponse |
waitForProbe(fileId, options?) | Poll probeUpload until it lands, within a budget | ProbeWaitResult |
preflightClips(fileIds) | Probe several uploads and sort them | ok, rejected and errors lists |
The probe runs after the upload finishes. While there is no result yet, probeUpload
throws a 422, and its error_type says why:
error_type | Thrown as | Meaning | waitForProbe |
|---|---|---|---|
probe_not_ready | GislApiError | Not landed yet | Polls again, after Retry-After when the server sends one |
probe_not_applicable | GislApiError | This file type is never probed (an image, say); no result will ever exist | Stops after one request: landed: false, reason: 'not_applicable' |
feature_not_available | GislFeatureNotAvailableError | Either of the above: what the server sends today | Polls again until timeoutMs runs out, then reason: 'timeout' |
The server sends the first two only once an SDK release that reads them is published; until then
every not-ready answer is feature_not_available, and timeoutMs is the ceiling on retrying it. For
a large video, a landed probe lets the server split the work, so probing between upload and workflow
creation makes it faster.
waitForProbe waits up to 30 seconds by default (timeoutMs) and does not throw when it gives
up: it resolves with landed: false and a reason (timeout, prober_error or
not_applicable), so you create the workflow anyway. A 404, an authentication error, any other
422 or a cancellation still throws. If you switch over reason exhaustively, add a
not_applicable case. onPoll receives the attempt number and elapsed time, for a progress
indicator. preflightClips never throws either: a failed probe, any of the three 422s included,
lands in errors.
For a long wait pass patient: true. Polls then start at least the Retry-After, or 3 s doubling
per poll up to 30 s, apart, instead of every few hundred milliseconds, and a 429 from the probe
endpoint is waited out within timeoutMs instead of thrown. Without it a 429 still throws.
You rarely need these by hand: every run() and submit() already recovers from a probe_pending
refusal through createWorkflowAwaitingProbe. That recovery skips an upload whose probe is
not_applicable (an image input beside the video, say) rather than waiting on it.
The recovery's budget (timeoutMs in TypeScript, $probeTimeoutMs in PHP) defaults to the
contract's 900 s probe-wait bound for every caller, after which the server stops answering
probe_pending. There is no limit on how many creates it makes: the budget alone ends it. Signed in,
it polls patiently, starts each re-create at least the Retry-After, or 1 s doubling per create up
to 30 s, after the previous one started, and waits out a re-create refused with a plain 429 TOO_MANY_REQUESTS. DUPLICATE_IN_PROGRESS and the long-form concurrency 429 are thrown at once,
and a probe that has not landed by the run's deadline is a GislTimeoutError.
When the probe still has not landed at the end of the DEFAULT budget, the recovery makes one last create about 2 s past it, which the server then accepts, so the default budget can end up to about 3 s late. An explicit budget is a strict cap: no create is scheduled past it, and a late timer can start one at most 1 s past. An explicit budget of at least about 902 s makes that one create once the server's 900 s bound has passed, and stops polling there. A probe request already in flight can still finish after the budget.
So submit(), and createWorkflowAwaitingProbe called directly, can block for up to about 15
minutes on a video whose probe is slow; pass a budget to bound that. run() stays bounded by its
maxWait.
Reattaching to a workflow
submit() returns a Handle without waiting. To come back to the workflow later, possibly from
another process, keep its workflowId and call client.workflow(id):
- TypeScript
- PHP
- Python
- Rust
const handle = client.workflow(savedWorkflowId);
const snapshot = await handle.status(); // one request: snapshot.state, snapshot.isTerminal()
const result = await handle.wait('15m'); // blocks until finished, then fetches outputs
const ready = await handle.result(); // no wait: GislResultNotReadyError if not finished
$handle = $client->workflow($savedWorkflowId);
$snapshot = $handle->status(); // one request: $snapshot->state, $snapshot->isTerminal()
$result = $handle->wait(maxWait: '15m'); // blocks until finished, then fetches outputs
$ready = $handle->result(); // no wait: GislResultNotReadyError if not finished
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
wait() streams progress and falls back to polling, with the same 10-minute default as run().
It returns a RunResult whose transport says which of the two finished the wait; result()
does not wait, so its transport is absent. A reattached handle has no recipe keys, so address its outputs by
position rather than byKey(). The webhookSecret is only on the handle submit() returned.
Multipart uploads and resume
Files larger than the multipart threshold (10,000,000 bytes by default) upload in parts. The first
part is always 8 MiB, so a threshold set below that is raised to 8 MiB. Part uploads retry
server errors, 429 and network failures with jittered backoff; see
Troubleshooting.
To survive a crash or a lost connection, save the checkpoint the SDK reports after each part, and
pass its uploadId as resumeUploadId when you retry with the same bytes. Only the missing
parts are sent.
- TypeScript
- PHP
- Python
- Rust
const upload = await client.uploadFile('./huge.mp4', {
onCheckpoint: (state) => saveCheckpoint(JSON.stringify(state)),
resumeUploadId: previous?.uploadId, // undefined on the first attempt
});
use Gisl\Sdk\MultipartCheckpointState;
use Gisl\Sdk\UploadOptions;
$upload = $client->uploadFile('./huge.mp4', new UploadOptions(
onCheckpoint: fn (MultipartCheckpointState $state) => saveCheckpoint(json_encode($state)),
resumeUploadId: $previous['uploadId'] ?? null,
));
Resume needs a filesystem path; a stream resource cannot be resumed.
Not available: the Python SDK is not published and no release is scheduled — see Python.
Not an application SDK: Rust is internal contract types — see Rust.
The checkpoint carries uploadId, totalParts, uploadedPartNumbers and manifestExpiresAt.
The server keeps an unfinished upload for 48 hours; for a resume that spans days, call
keepaliveUpload(uploadId) every 12 to 24 hours. A resumeUploadId on a file at or below the
threshold is an error, and an expired or unknown upload fails with
GislMultipartSessionNotFoundError.
PHP: bring your own HTTP client
By default the SDK finds a PSR-18 client and PSR-17 factories through php-http/discovery. Inject
your own to control timeouts, proxies or retries:
$client = Gisl::create(
apiKey: 'sk_...',
httpClient: $psr18Client,
requestFactory: $psr17RequestFactory,
streamFactory: $psr17StreamFactory,
);
- Redirects. The SDK never follows a redirect with your key attached. A
3xxfrom the API throwsGislErrornaming the status and target host; pointbaseUrlat the final origin. A discovered Symfony client is switched tomax_redirects: 0for you. An injected client is left as you configured it, so it must not follow redirects, or must dropAuthorizationwhen the host, port or scheme changes. Symfony HttpClient follows redirects by default: pass->withOptions(['max_redirects' => 0]). - Part uploads. With
ext-curlloaded andmultipartConcurrencyabove 1, parts go straight to storage throughcurl_multi, not through your client. SetmultipartConcurrency: 1if every request must go through it.
See also
- TypeScript and PHP: install, authenticate, browser use.
- Workflows: jobs, sources and outputs.
- Errors: what each error class carries.