Skip to main content

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​

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:

OptionDefaultPurpose
apiKeyresolved, see belowAPI key, sent as Authorization: Bearer …
profile / profilePathdefault / ~/.gisl/credentialswhich credentials-file profile to read, and where
environmentresolved, see below'prod' or 'staging'
baseUrlresolved, see belowAPI host; overrides environment. Does not move the progress stream: pair it with environment or streamBaseUrl
streamBaseUrlresolved, see belowhost for the progress stream only
headersnoneextra headers on every request
localenonesent as Accept-Language; wins over one in headers
timeout30000per-request timeout in ms, enforced by the SDK
multipartThreshold10000000files above this many bytes upload in parts
multipartConcurrency4part uploads in flight at once
multipartMaxAttempts3attempts per part, including the first
multipartRetryBaseMs500base of the jittered backoff between part attempts
presetDefaultsnoneclient-wide preset overrides for compress

How the key and hosts are resolved​

Both SDKs resolve in the same order; the first source that provides a value wins.

ValueResolution order
API keyapiKey → GISL_API_KEY → api_key in the profile of the credentials file
API hostbaseUrl → environment → GISL_BASE_URL → GISL_ENVIRONMENT → https://api.giveitsmaller.com
Stream hoststreamBaseUrl → 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.

import { GislClient } from '@giveitsmaller/sdk';

const client = new GislClient({ baseUrl: 'https://api.giveitsmaller.com', apiKey: 'REPLACE_ME_API_KEY' });

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​

MethodPurposeReturns
uploadFile(file, options?)Upload a file; multipart above the thresholdUploadResponse (its fileId)
getMetadata(fileId)Metadata the server extracted from an uploadMetadataResponse
getUploadStatus(uploadId)Parts recorded so far on a multipart uploadthe 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 expiryconfirmation

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​

MethodPurposeReturns
createWorkflow(payload)Create a workflow from a hand-built payloadWorkflowCreateResponse (workflowId, webhookSecret)
createWorkflowAwaitingProbe(payload, …)createWorkflow, retrying after a probe_pending refusalWorkflowCreateResponse
cancelWorkflow(workflowId)Cancel; idempotent, but 409 once it has completed, failed or expiredWorkflowCancelResponse
resumeWorkflow(workflowId)Resume a workflow paused for insufficient creditsWorkflowResumeResponse
retryOperation(operationId)Retry one failed operationRetryResponse
archiveWorkflow(workflowId)Hide a finished workflow from the list; not a deleteWorkflowArchiveResponse
restoreWorkflow(workflowId)Undo an archive; idempotentWorkflowRestoreResponse
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​

MethodPurposeReturns
getWorkflowStatus(workflowId)Current status, onceWorkflowStatusResponse
waitForWorkflow(workflowId, options?)Poll until the workflow stopsthe final WorkflowStatusResponse
getWorkflowDownloads(workflowId)Pre-signed output URLs, per jobWorkflowDownloadResponse

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.

const final = await client.waitForWorkflow(workflowId, {
intervalMs: 5_000,
timeoutMs: 30 * 60_000,
onPoll: (status) => console.log(status),
});

Streaming​

MethodPurposeReturns
streamEvents(workflowId, …)Live progress events over SSEasync 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​

MethodPurposeReturns
getSchema(options?)The operations, options and limits your tier can use, optionally filtered by MIME type and operationa hit with the schema, or not-modified
capabilities(opType?)The capability matrix from the schema, without the not-modified casesnapshot, or one operation's entry
operation(opType, input, options?)Run a single-input operation that has no dedicated verban 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​

MethodShortcut on the created clientReturns
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 directlyAuthenticatedIdentity: 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​

MethodPurposeReturns
createCheckoutSession(payload)Start a hosted checkout for a subscription or a credit pack; send the browser to its URLBillingCheckoutSession (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:

statusMeaning
paidApplied to your account: a pack's credits are granted, or the subscription is linked (linked, not necessarily paid up)
pendingRecorded as yours, not applied yet. Not a failure: ask again
unknownNot 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.

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');

Health​

MethodPurposeReturns
getHealth()Is the API up, and which build is it running? GET /healthzLivenessResponse: 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.

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');

Probing uploads​

MethodPurposeReturns
probeUpload(fileId)The server's readiness check for an upload: codec, corruption, routing. BetaUploadProbeResponse
waitForProbe(fileId, options?)Poll probeUpload until it lands, within a budgetProbeWaitResult
preflightClips(fileIds)Probe several uploads and sort themok, 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_typeThrown asMeaningwaitForProbe
probe_not_readyGislApiErrorNot landed yetPolls again, after Retry-After when the server sends one
probe_not_applicableGislApiErrorThis file type is never probed (an image, say); no result will ever existStops after one request: landed: false, reason: 'not_applicable'
feature_not_availableGislFeatureNotAvailableErrorEither of the above: what the server sends todayPolls 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):

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

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.

const upload = await client.uploadFile('./huge.mp4', {
onCheckpoint: (state) => saveCheckpoint(JSON.stringify(state)),
resumeUploadId: previous?.uploadId, // undefined on the first attempt
});

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 3xx from the API throws GislError naming the status and target host; point baseUrl at the final origin. A discovered Symfony client is switched to max_redirects: 0 for you. An injected client is left as you configured it, so it must not follow redirects, or must drop Authorization when the host, port or scheme changes. Symfony HttpClient follows redirects by default: pass ->withOptions(['max_redirects' => 0]).
  • Part uploads. With ext-curl loaded and multipartConcurrency above 1, parts go straight to storage through curl_multi, not through your client. Set multipartConcurrency: 1 if every request must go through it.

See also​