# React components (API)
Source: https://docs.syncsnap.xyz/api-reference/react-components
Props for SyncsnapUploadButton and SyncsnapQrCode
# React components API
## SyncsnapUploadButton
**Import:** `import { SyncsnapUploadButton } from "@syncsnap/react";`
Props extend [UseSyncsnapJobOptions](/api-reference/react-use-syncsnap-job) (e.g. `createJobUrl`, `getJobUrl`, `getWaitForCompletionUrl`, `onCompleted`). In addition:
| Prop | Type | Default | Description |
| ---------------------- | -------------------------- | ---------------- | --------------------------------------------------------------------------- |
| `buttonText` | `string` | `"Start upload"` | Button label. |
| `className` | `string` | — | Wrapper className. |
| `qrSize` | `number` | `240` | QR size in pixels. |
| `qrBaseUrl` | `string` | — | Base URL for upload page (see [createUploadUrl](/react/create-upload-url)). |
| `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | QR error correction. |
See [SyncsnapUploadButton](/react/syncsnap-upload-button).
***
## SyncsnapQrCode
**Import:** `import { SyncsnapQrCode } from "@syncsnap/react";`
| Prop | Type | Default | Description |
| ---------------------- | -------------------------- | -------------------------------- | -------------------------- |
| `jobId` | `string` | *required* | Job ID. |
| `baseUrl` | `string` | `"https://upload.syncsnap.xyz/"` | Upload page base URL. |
| `size` | `number` | `240` | QR width/height in pixels. |
| `className` | `string` | — | Applied to the ``. |
| `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | QR error correction. |
See [SyncsnapQrCode](/react/syncsnap-qr-code).
# useSyncsnapJob (API)
Source: https://docs.syncsnap.xyz/api-reference/react-use-syncsnap-job
Full API for the useSyncsnapJob hook
# useSyncsnapJob
Hook to create a SyncSnap job and wait for completion via the server's wait endpoint (server-side polling). The result (from your server's `onCompleted` or the presigned URL) is passed to `onCompleted(job, result)`.
## Import
```ts theme={null}
import { useSyncsnapJob } from "@syncsnap/react";
```
## Usage
```ts theme={null}
const result = useSyncsnapJob(options?);
```
## Options (UseSyncsnapJobOptions)
| Option | Type | Default | Description |
| ------------------------- | -------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
| `createJobUrl` | `string` | `"/api/syncsnap/job"` | URL for POST to create a job. |
| `getJobUrl` | `(jobId: string) => string` | `id => "/api/syncsnap/job/" + id` | URL for GET job status. |
| `getWaitForCompletionUrl` | `(jobId: string) => string` | `id => getJobUrl(id) + "/wait"` | URL for the server's wait endpoint. |
| `waitTimeoutMs` | `number` | `120000` | Timeout for wait (ms), sent as query param. |
| `waitIntervalMs` | `number` | `2000` | Poll interval for wait (ms), sent as query param. |
| `autoStart` | `boolean` | `false` | If true, start() is called on mount. |
| `onJobCreated` | `(job: CreateJobResponse) => void` | — | Called when job is created. |
| `onCompleted` | `(job: Job, result?: unknown) => void` | — | Called when job completes or fails; result is server's onCompleted return (or presigned URL). |
| `onError` | `(error: Error) => void` | — | Called on create or wait errors. |
## Return value (UseSyncsnapJobResult)
| Property | Type | Description |
| ----------- | --------------------- | ----------------------------------------- |
| `job` | `Job \| null` | Current job or null. |
| `jobId` | `string \| null` | Job ID when available. |
| `status` | `JobStatus \| null` | Current job status. |
| `error` | `Error \| null` | Last error. |
| `isWaiting` | `boolean` | True while the wait request is in flight. |
| `start` | `() => Promise` | Create job and call wait endpoint. |
| `reset` | `() => void` | Abort wait and clear state. |
See [useSyncsnapJob](/react/use-syncsnap-job) for usage and examples.
# SyncsnapServer (API)
Source: https://docs.syncsnap.xyz/api-reference/server-syncsnap-server
Server SDK class and method reference
# SyncsnapServer API
**Import:** `import { SyncsnapServer } from "syncsnap";`
## Constructor
```ts theme={null}
const client = new SyncsnapServer();
```
Uses `process.env.SYNCSNAP_TOKEN` and the default SyncSnap API base URL.
## Methods
### createJob()
```ts theme={null}
createJob(): Promise
```
Creates a new job. Returns `{ id, projectId, status, createdAt, updatedAt }`.
***
### getJob(jobId)
```ts theme={null}
getJob(jobId: string): Promise
```
Returns the current job for the given ID.
***
### getUploadUrl(jobId, options)
```ts theme={null}
getUploadUrl(
jobId: string,
options: { fileName: string; expirationMinutes?: number }
): Promise
```
Returns a presigned upload URL for the job.
***
### getDownloadUrl(jobId, options?)
```ts theme={null}
getDownloadUrl(
jobId: string,
options?: { expirationMinutes?: number }
): Promise
```
Returns a presigned download URL for a completed job.
***
### waitForJobCompletion(jobId, options?)
```ts theme={null}
waitForJobCompletion(
jobId: string,
options?: WaitForJobOptions
): Promise
```
Polls until status is `completed` or `failed`, or until timeout/abort.
**WaitForJobOptions:**
| Option | Type | Default | Description |
| ------------ | -------------------- | -------- | ---------------------- |
| `intervalMs` | `number` | `2000` | Poll interval in ms. |
| `timeoutMs` | `number` | `120000` | Timeout in ms. |
| `onPoll` | `(job: Job) => void` | — | Called on each poll. |
| `signal` | `AbortSignal` | — | Abort to stop polling. |
See [SyncsnapServer](/server/syncsnap-server) for usage.
# Types
Source: https://docs.syncsnap.xyz/api-reference/types
Shared types for jobs, responses, and options
# Types
Types are shared between `@syncsnap/react` and `syncsnap` where applicable. Import from the package you use.
## JobStatus
```ts theme={null}
type JobStatus = "pending" | "in_progress" | "completed" | "failed";
```
## Job
```ts theme={null}
interface Job {
id: string;
projectId: string;
status: JobStatus;
fileName?: string | null;
createdAt: string;
updatedAt: string;
}
```
## CreateJobResponse
Returned when creating a job (POST job).
```ts theme={null}
interface CreateJobResponse {
id: string;
projectId: string;
status: JobStatus;
createdAt: string;
updatedAt: string;
}
```
## PresignedUrlResponse
Returned when requesting a presigned upload or download URL.
```ts theme={null}
interface PresignedUrlResponse {
url: string;
fileName: string;
expiration: string;
}
```
## CompletedJobResponse (React)
When the server is configured with `onCompleted`, the download response can include a custom payload.
```ts theme={null}
interface CompletedJobResponse extends PresignedUrlResponse {
completedPayload?: T;
}
```
## UseSyncsnapJobOptions (React)
Options for [useSyncsnapJob](/react/use-syncsnap-job). See [useSyncsnapJob API](/api-reference/react-use-syncsnap-job).
## UseSyncsnapJobResult (React)
Return value of [useSyncsnapJob](/react/use-syncsnap-job).
## WaitForJobOptions (Server)
Options for `SyncsnapServer.waitForJobCompletion`:
```ts theme={null}
interface WaitForJobOptions {
intervalMs?: number;
timeoutMs?: number;
onPoll?: (job: Job) => void;
signal?: AbortSignal;
}
```
# Next.js App Router
Source: https://docs.syncsnap.xyz/getting-started/nextjs-app-router
Integrate SyncSnap in a Next.js App Router application
# Next.js App Router
This guide walks through integrating SyncSnap in a Next.js app using the App Router: API route, React components, and styling.
## 1. Install dependencies
```bash theme={null}
npm install @syncsnap/react syncsnap
```
Ensure you have React and Tailwind (peer dependencies for `@syncsnap/react`):
```bash theme={null}
npm install react tailwindcss
```
## 2. Configure environment
Create or update `.env.local` with your SyncSnap API key (get it from the [dashboard](https://syncsnap.xyz/dashboard)):
```bash theme={null}
SYNCSNAP_TOKEN=your_syncsnap_api_key
```
The `syncsnap` server SDK reads this to authenticate with the SyncSnap API. Do not expose it to the client.
## 3. Create the SyncSnap API route
The React SDK expects a backend that can create jobs, return job status, and return presigned download URLs. The `syncsnap` package provides a Next.js handler that implements all of this with one catch-all route.
Create the file:
**`app/api/syncsnap/[...syncsnap]/route.ts`**
```ts theme={null}
import { SyncsnapServer } from "syncsnap";
import { createRouteHandler } from "syncsnap/next";
const client = new SyncsnapServer();
export const { GET, POST } = createRouteHandler({
client,
onCompleted: async (job, presigned) => {
// Optional. Return value is sent to the client as onCompleted(job, result).
return presigned ? { downloadUrl: presigned.url, fileName: presigned.fileName } : undefined;
},
});
```
This exports:
* **POST** for `POST /api/syncsnap/job` (create job)
* **GET** for:
* `GET /api/syncsnap/job/:id` (get job)
* `GET /api/syncsnap/job/:id/wait` (server waits until job completes; returns `{ job, result? }`; query: `timeoutMs`, `intervalMs`)
* `GET /api/syncsnap/job/:id/download` (get presigned download URL; optional `?expiration=15`)
Path segments are resolved from the catch-all `[...syncsnap]` (e.g. `job`, `jobId`, `download`).
## 4. Use the React components
In any **client** page or component, use the upload button and optional callbacks:
```tsx theme={null}
"use client";
import { SyncsnapUploadButton } from "@syncsnap/react";
export default function UploadPage() {
return (
console.log("Job created:", job.id)}
onCompleted={(job, result) => {
// result is whatever your server onCompleted returned
if (result && typeof result === "object" && "downloadUrl" in result) {
console.log("Download:", (result as { downloadUrl: string }).downloadUrl);
}
}}
/>
);
}
```
The button opens a dialog with a QR code. Users scan it with their phone to upload; your app polls until the job is completed or failed.
## 5. Add styles
Import the SDK theme so the button and dialog look correct. In your root layout or global CSS (e.g. `app/globals.css`):
**Tailwind v4:**
```css theme={null}
@import "tailwindcss";
@import "@syncsnap/react/theme";
```
**Tailwind v3:** Ensure Tailwind can see the SDK’s class names (e.g. via `content` including `node_modules/@syncsnap/react`), then:
```css theme={null}
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "@syncsnap/react/theme";
```
See [Styling](/guides/styling) for more options (e.g. shadcn, custom theme).
## 6. Custom API base path
If you mount the SyncSnap API under a different path, pass the URLs into the hook used by the button (or into `useSyncsnapJob` directly):
```tsx theme={null}
`/api/my-syncsnap/job/${id}`}
getWaitForCompletionUrl={(id) => `/api/my-syncsnap/job/${id}/wait`}
buttonText="Scan to upload"
/>
```
The default is `createJobUrl: "/api/syncsnap/job"`, `getJobUrl(id) => "/api/syncsnap/job/" + id`, and `getWaitForCompletionUrl(id) => getJobUrl(id) + "/wait"`.
## Summary
* Install `@syncsnap/react` and `syncsnap`, set `SYNCSNAP_TOKEN`.
* Add one catch-all route with `createRouteHandler({ client })`.
* Use `SyncsnapUploadButton` in a client component and import the SDK theme.
* Optionally customize `createJobUrl`, `getJobUrl`, `getWaitForCompletionUrl`, and `qrBaseUrl`.
Next: [SyncsnapUploadButton](/react/syncsnap-upload-button), [useSyncsnapJob](/react/use-syncsnap-job), or [SyncsnapServer](/server/syncsnap-server).
# Error handling
Source: https://docs.syncsnap.xyz/guides/error-handling
Handle errors from job creation and wait endpoint
# Error handling
SyncSnap flows can fail at several points: creating the job or while the server is waiting for completion (wait endpoint). The React and server SDKs surface these as thrown errors or via the `onError` callback.
## React SDK: onError and error state
`useSyncsnapJob` (and therefore `SyncsnapUploadButton`) exposes:
* **error** — `Error | null`; set when create or wait fails.
* **onError** — optional callback called with the same `Error`.
Use both to show a message and/or log:
```tsx theme={null}
{
console.error("SyncSnap error:", err);
toast.error(err.message);
}}
/>
```
Or with the hook:
```tsx theme={null}
const { error, start } = useSyncsnapJob({
onError: (err) => reportToAnalytics(err),
});
return (
<>
{error &&
{error.message}
}
>
);
```
## Common error cases
* **Job creation fails** — Network issue, invalid API key, or SyncSnap API error. User sees the error; they can retry (e.g. call `start()` again).
* **Wait fails** — Network or backend error while the wait request is in flight. Hook sets `error` and stops; call `reset()` then `start()` to retry.
* **Timeout** — Job not completed within `waitTimeoutMs`. The server may return 408; the hook sets `error`. User can start a new transfer.
* **Abort** — If the user closes the dialog or you call `reset()`, the wait request is aborted and `onError` is not called.
## Server SDK
The `syncsnap` server SDK throws when the SyncSnap API returns a non-2xx or an error body. Catch in your route and return an appropriate HTTP response:
```ts theme={null}
try {
const job = await client.createJob();
return Response.json(job);
} catch (err) {
const message = err instanceof Error ? err.message : "Create job failed";
return Response.json({ error: message }, { status: 502 });
}
```
Do the same for `getJob`, the wait handler, and `getDownloadUrl` so the frontend receives a consistent error shape (e.g. `{ error: string }`).
## User-friendly messages
Map known error messages to user-facing copy if you want:
```tsx theme={null}
function getErrorMessage(err: Error): string {
if (err.message.includes("timed out")) return "Upload is taking too long. Please try again.";
if (err.message.includes("Failed to create job")) return "Could not start transfer. Check your connection.";
return err.message;
}
```
Then display `getErrorMessage(error)` in the UI.
# Rate limits
Source: https://docs.syncsnap.xyz/guides/rate-limits
SyncSnap API rate limits per project and how to handle 429 responses
# Rate limits
The SyncSnap API applies rate limiting per project so that no single project can overwhelm the service. When the limit is exceeded, the API returns **429 Too Many Requests** with a JSON body and rate-limit headers (including when to retry).
## Limits
Limits are enforced **per project** (per API key), **per minute**, using a token-bucket algorithm (smooth rate, with burst up to the configured maximum).
| Scope | Default | Description |
| --------------- | ---------------- | ----------------------------------------------------------- |
| **Per project** | 100 requests/min | All requests authenticated with the same project (API key). |
Only requests that are scoped to a project (e.g. job routes with a valid API key) are rate limited. Requests without a project are not limited by this middleware.
## Which endpoints are limited?
Rate limiting is applied to routes where a project is identified (e.g. after API key validation):
* **Auth** — API key validation and related auth routes.
* **Job** — Create job, get job, wait for completion, and get download URL.
Job creation, status checks, wait, and download requests all count toward the per-project limit.
## 429 response body
When rate limited, the API responds with:
```json theme={null}
{
"error": "rate limit exceeded: too many requests for this project"
}
```
HTTP status is always **429**. The response also includes [rate-limit headers](#response-headers) and `Retry-After` so clients know when to retry.
## Response headers
For requests that are scoped to a project, the API sets these rate-limit headers on **both successful and 429** responses:
| Header | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------- |
| `x-rate-limit-limit` | Maximum requests per minute allowed for this project. |
| `x-rate-limit-remaining` | Number of requests remaining in the current window (0 when rate limited). |
| `x-rate-limit-reset` | Unix timestamp (seconds) when the current rate-limit window resets (e.g. start of the next minute). |
On **429** responses only, the API also sets:
| Header | Description |
| ------------- | ---------------------------------------------------------------- |
| `Retry-After` | Seconds until the window resets; wait this long before retrying. |
These headers are only present when the request is authenticated with a project.
## Handling rate limits in your app
* **Backend** — If your server proxies to SyncSnap and you get a 429, return a sensible status and message to the client (e.g. 429 with the same `error` body) so the frontend can show a “Too many requests, try again shortly” message.
* **Frontend** — Use [Error handling](/guides/error-handling): the React SDK’s `onError` and `error` state will receive failures from the wait endpoint. If the backend returns 429, the error message will reflect that; you can detect it (e.g. `err.message.includes('rate limit')`) and show a user-friendly message or retry after a short delay.
Example: surface rate-limit errors in the UI and suggest retrying:
```tsx theme={null}
{
if (err.message.includes("rate limit")) {
toast.error(
"Too many requests. Please wait a moment and try again.",
);
} else {
toast.error(err.message);
}
}}
/>
```
Next: [Error handling](/guides/error-handling) for handling 429 and other errors in the React and server SDKs.
# Styling
Source: https://docs.syncsnap.xyz/guides/styling
Tailwind and theme setup for @syncsnap/react components
# Styling the React SDK
Components in `@syncsnap/react` use **Tailwind CSS** and **CSS variables** for the theme. You can use the built-in theme or adapt it to your app.
## Option 1: Minimal setup (recommended)
If your app uses **Tailwind v4**, add these imports in your main CSS (e.g. `app/globals.css`):
```css theme={null}
@import "tailwindcss";
@import "@syncsnap/react/theme";
```
The theme file includes an `@source` so Tailwind scans the SDK for class names. No extra config or copy-paste needed.
If you use **shadcn** or **tw-animate**, you may need:
```css theme={null}
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@syncsnap/react/theme";
```
Install peer deps if needed:
```bash theme={null}
npm install tailwindcss tw-animate-css shadcn
```
## Option 2: Tailwind v3
Ensure Tailwind can see the SDK’s class names. In `tailwind.config.js`:
```js theme={null}
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./node_modules/@syncsnap/react/**/*.{js,ts,jsx,tsx}",
],
```
Then in your global CSS:
```css theme={null}
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "@syncsnap/react/theme";
```
## Option 3: Copy the full stylesheet
You can copy the SDK’s full styles (e.g. from the example app in this repo) into your project and import that instead. Use this if you want a single self-contained file or need to tweak the theme heavily.
## Requirements
* **Tailwind** (v3 or v4) so utility classes used by the components are generated.
* **Content scanning** of the SDK so classes like `bg-background`, `rounded-xl`, etc. are included (via `@source` in v4 or `content` in v3 config).
## Customizing the button
You can pass a `className` to `SyncsnapUploadButton` to style the wrapper or rely on your own button:
```tsx theme={null}
```
For full control, use [useSyncsnapJob](/react/use-syncsnap-job) and [SyncsnapQrCode](/react/syncsnap-qr-code) and render your own button and layout.
# Introduction
Source: https://docs.syncsnap.xyz/index
Cross-device file uploads with a developer-first experience. Integrate in minutes.
# Cross-device uploads with a developer-first experience
SyncSnap helps users move from desktop to mobile capture in seconds. You ship a polished upload flow with fewer edge cases and no separate mobile app—one SDK, one API, and no account or app install required on the user's phone.
Integrate in minutes — two files, a few lines each
Step-by-step integration for Next.js
## How it works
1. **Embed once** — Add the SyncSnap SDK and render one component where uploads begin.
2. **Capture anywhere** — The user scans a QR code and switches to their phone for camera-friendly uploads.
3. **Continue your flow** — Files stream back to your app and the wait endpoint returns the result (e.g. presigned download URL or custom payload).
No mobile app, no sign-in on the phone—just scan and upload.
## What you need
* A **SyncSnap** project and API key from the [dashboard](https://syncsnap.xyz/dashboard).
* **Backend** — the `syncsnap` server SDK to create jobs and proxy job/download requests.
* **Frontend** — the `@syncsnap/react` package for the upload button, QR code, and job polling.
## Frameworks
SyncSnap provides a first-party integration for **Next.js (App Router)**. The server SDK is Node-friendly and can be used with other frameworks by implementing the same API routes.
Recommended — catch-all route and React components
Jobs, QR flow, and API routes
## Next steps
* [Quickstart](/quickstart) — Install packages and run a minimal example.
* [Next.js App Router](/getting-started/nextjs-app-router) — Full setup with API route and React components.
* [Concepts](/introduction/concepts) — Jobs, statuses, and the upload URL.
# Concepts
Source: https://docs.syncsnap.xyz/introduction/concepts
Jobs, upload URLs, wait endpoint, and download URLs
# Core concepts
## Jobs
A **job** represents a single transfer session: one QR code, one upload from the user’s device. Your backend creates a job via the SyncSnap API (using the `syncsnap` server SDK). The API returns a job object with:
* `id` — Use this to build the QR URL and to call the wait endpoint.
* `projectId` — Your SyncSnap project.
* `status` — One of `pending`, `in_progress`, `completed`, `failed`.
* `fileName` — Set when the upload completes (if provided by the service).
* `createdAt`, `updatedAt` — Timestamps.
Jobs are **one-time**: one job corresponds to one upload. For another file, create a new job and show a new QR code.
## Upload URL (QR code target)
The URL encoded in the QR code tells the user’s phone **where** to upload. SyncSnap’s upload page lives at a host like `https://upload.syncsnap.xyz/`. The only required query parameter is the job ID:
* **Default**: `https://upload.syncsnap.xyz/?job_id=`
* **Custom base**: You can pass a different `baseUrl` (e.g. for staging or a custom domain) into `createUploadUrl(jobId, { baseUrl })` in `@syncsnap/react`.
The React component `SyncsnapQrCode` uses `createUploadUrl` internally. If you build your own QR UI, use `createUploadUrl` so the URL stays correct.
## Wait for completion
The web app does not get a push when the upload finishes. It calls a **wait** endpoint (e.g. `GET /api/syncsnap/job/:id/wait`) once. Your backend then polls the SyncSnap API until the job is `completed` or `failed`, and returns the final job plus a **result** (e.g. presigned download URL or a custom payload from your server's `onCompleted` callback). The `useSyncsnapJob` hook calls this wait endpoint and passes the result to your `onCompleted(job, result)`.
## Download URL
After a job is `completed`, your backend can get a **presigned download URL** from the SyncSnap API. In the default flow, the wait handler does this and returns it (or your custom payload) as `result`. You can also expose `GET /api/syncsnap/job/:id/download` so the client can request a fresh URL later. The URL is time-limited; you can pass an expiration in minutes. The `syncsnap` server SDK’s `getDownloadUrl` and the Next.js download handler support this.
## Summary
* **Job** = one transfer; create one per QR code.
* **Upload URL** = SyncSnap host + `?job_id=` (use `createUploadUrl`).
* **Wait** = client calls one wait endpoint; server polls until done and returns job + result.
* **Download URL** = backend gets a presigned URL from SyncSnap; returned to the client via the wait result or the download endpoint.
Next: [Quickstart](/quickstart) or [Next.js App Router](/getting-started/nextjs-app-router).
# How it works
Source: https://docs.syncsnap.xyz/introduction/how-it-works
End-to-end flow from job creation to file download
# How SyncSnap works
This page walks through the flow from your web app to the uploaded file.
## Overview
Your backend calls the [SyncSnap API](https://syncsnap.xyz) (via the `syncsnap` server SDK) to create a **job**. Each job has a unique ID and starts in `pending` status.
Your frontend receives the job ID and displays a QR code. The QR code encodes a URL like `https://upload.syncsnap.xyz/?job_id=`. Use `createUploadUrl(jobId, { baseUrl })` from `@syncsnap/react` to build this URL.
The user scans the QR code with their phone. Their browser opens the SyncSnap upload page. They select a file and upload it. The SyncSnap service associates the file with the job and sets the job status to `completed` (or `failed` on error).
Your frontend calls the **wait** endpoint (e.g. `GET /api/syncsnap/job/:id/wait`). Your backend runs server-side polling until the job is `completed` or `failed`, then returns the final job and a **result** (e.g. presigned download URL or custom payload from your `onCompleted` callback). The client receives this in `useSyncsnapJob`'s `onCompleted(job, result)`.
## API routes your backend must expose
The React SDK expects your app to expose these endpoints (the `syncsnap` Next.js helper implements them via a single catch-all route):
| Method | Path | Purpose |
| ------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/api/syncsnap/job` | Create a new job. Returns `{ id, projectId, status, createdAt, updatedAt }`. |
| `GET` | `/api/syncsnap/job/:id` | Get job status. Returns the full job object. |
| `GET` | `/api/syncsnap/job/:id/wait` | Server-side wait: backend polls until job completes or fails, then returns `{ job, result? }`. Query: `timeoutMs`, `intervalMs`. The `result` is whatever your server's `onCompleted` callback returns (or the presigned download URL when no callback). |
| `GET` | `/api/syncsnap/job/:id/download` | Get a presigned download URL for a completed job. Optional query: `?expiration=15` (minutes). Returns `{ url, fileName, expiration }` and optionally `completedPayload` when `onCompleted` is configured. |
The **base path** (`/api/syncsnap`) is configurable on the client via `createJobUrl`, `getJobUrl`, and `getWaitForCompletionUrl` in `useSyncsnapJob`.
## Job statuses
| Status | Meaning |
| ------------- | --------------------------------------------------------------------- |
| `pending` | Job created; waiting for the user to open the upload page. |
| `in_progress` | User has started or is in the middle of uploading. |
| `completed` | File uploaded successfully. You can fetch the presigned download URL. |
| `failed` | Upload failed or was abandoned. |
Your frontend typically calls the wait endpoint once; the server polls the job and returns when it's done, so the client doesn't poll itself.
## Security
* **API key**: Your server uses a SyncSnap API key (e.g. `SYNCSNAP_TOKEN`) to create jobs and get download URLs. Never expose this key to the browser.
* **Presigned URLs**: Download URLs are short-lived. Your backend requests them from SyncSnap and forwards them to your frontend so the client never talks to SyncSnap directly with the API key.
Next: [Concepts](/introduction/concepts) for more detail on jobs and the upload URL, or [Next.js App Router](/getting-started/nextjs-app-router) to implement this in your app.
# Quickstart
Source: https://docs.syncsnap.xyz/quickstart
Integrate in minutes — two files, a few lines each. Get a minimal SyncSnap flow running with Next.js and the React SDK.
## Prerequisites
* Node.js 18+
* A SyncSnap project and API key from the [dashboard](https://syncsnap.xyz/dashboard) (or your backend that creates jobs and returns download URLs)
* Next.js app using the App Router
## Step 1: Install packages
```bash theme={null}
npm install @syncsnap/react syncsnap
```
Peer dependencies for the React components (Tailwind and React):
```bash theme={null}
npm install react tailwindcss
```
## Step 2: Environment variable
Your server needs the SyncSnap API key. In `.env.local`:
```bash theme={null}
SYNCSNAP_TOKEN=your_api_key_here
```
Never expose this key to the browser; only the server SDK should use it.
## Step 3: API route
Create the SyncSnap API route so the frontend can create jobs and wait for completion (and optionally get download URLs). Create this file:
**`app/api/syncsnap/[...syncsnap]/route.ts`**
```ts theme={null}
import { SyncsnapServer } from "syncsnap";
import { createRouteHandler } from "syncsnap/next";
const client = new SyncsnapServer();
export const { GET, POST } = createRouteHandler({
client,
onCompleted: async (job, presigned) => {
// Optional: run when job completes. Return value is sent to the client as onCompleted(job, result).
return presigned
? { downloadUrl: presigned.url, fileName: presigned.fileName }
: undefined;
},
});
```
This single catch-all route handles:
* `POST /api/syncsnap/job` — create job
* `GET /api/syncsnap/job/:id` — get job
* `GET /api/syncsnap/job/:id/wait` — server waits until job completes, then returns `{ job, result? }` (result is from your `onCompleted` callback or the presigned URL)
* `GET /api/syncsnap/job/:id/download` — get presigned download URL
## Step 4: Add the upload button
In any client component:
```tsx theme={null}
"use client";
import { SyncsnapUploadButton } from "@syncsnap/react";
export default function Page() {
return (
{
// result is whatever your server onCompleted returned (e.g. { downloadUrl, fileName })
if (
result &&
typeof result === "object" &&
"downloadUrl" in result
)
console.log(
"Download URL:",
(result as { downloadUrl: string }).downloadUrl,
);
}}
/>
);
}
```
## Step 5: Styles (Tailwind)
The button and dialog use Tailwind and the SDK theme. In your main CSS (e.g. `app/globals.css`):
```css theme={null}
@import "tailwindcss";
@import "@syncsnap/react/theme";
```
If you use Tailwind v3 or a different setup, see [Styling](/guides/styling).
## Step 6: Run and test
1. Start the dev server: `npm run dev`
2. Open the page and click **Scan to upload**.
3. A dialog opens with a QR code. Scan it with your phone and upload a file.
4. When the job completes, `onCompleted` runs and you can use the presigned download URL.
## Optional: QR code base URL
If the default upload host is wrong (e.g. local or staging), set where the QR code should point:
```tsx theme={null}
```
Next: [Next.js App Router](/getting-started/nextjs-app-router) for a fuller walkthrough, or [React SDK](/react/syncsnap-upload-button) and [Server SDK](/server/syncsnap-server) for all options.
# createUploadUrl
Source: https://docs.syncsnap.xyz/react/create-upload-url
Build the URL to show in the QR code for the upload page
# createUploadUrl
`createUploadUrl` returns the full URL that the user’s phone should open to upload a file for a given job. This is the URL you encode in the QR code (and the one [SyncsnapQrCode](/react/syncsnap-qr-code) uses internally).
## Usage
```ts theme={null}
import { createUploadUrl } from "@syncsnap/react";
const url = createUploadUrl(jobId);
// "https://upload.syncsnap.xyz/?job_id=abc123"
```
With a custom base URL (e.g. staging or custom domain):
```ts theme={null}
const url = createUploadUrl(jobId, {
baseUrl: "https://upload.staging.syncsnap.xyz/",
});
```
## Parameters
| Parameter | Type | Default | Description |
| ----------------- | -------- | -------------------------------- | ------------------------------------------------ |
| `jobId` | `string` | *required* | The SyncSnap job ID. |
| `options.baseUrl` | `string` | `"https://upload.syncsnap.xyz/"` | Base URL of the SyncSnap upload page (no query). |
## Return value
A string URL: `baseUrl` with a `job_id` query parameter set to `jobId`. The upload page reads this to know which job to attach the file to.
## When to use
* **SyncsnapQrCode** and **SyncsnapUploadButton** use this internally; you don’t need to call it if you use those components.
* Use **createUploadUrl** when you generate the QR code yourself (e.g. another QR library or native app) or need the URL for sharing or deep links.
## Example: custom QR library
```tsx theme={null}
import { createUploadUrl } from "@syncsnap/react";
import MyQRComponent from "my-qr-library";
function CustomQR({ jobId }: { jobId: string }) {
const url = createUploadUrl(jobId, {
baseUrl: "https://upload.syncsnap.xyz/",
});
return ;
}
```
# Installation
Source: https://docs.syncsnap.xyz/react/installation
Install and configure the React SDK (@syncsnap/react)
# React SDK installation
The `@syncsnap/react` package provides React components and hooks for SyncSnap: upload button, QR code, and job polling.
## Install
```bash theme={null}
npm install @syncsnap/react
```
## Peer dependencies
* **React** `^18.0.0`
* **Tailwind CSS** `^3.0.0` or `^4.0.0` (for component styles)
Install them if your project doesn’t already have them:
```bash theme={null}
npm install react tailwindcss
```
## Backend requirement
The React SDK talks to **your** backend, not directly to SyncSnap. You must expose:
* `POST /api/syncsnap/job` — create job
* `GET /api/syncsnap/job/:id` — get job
* `GET /api/syncsnap/job/:id/wait` — server waits until job completes; returns `{ job, result? }`
* `GET /api/syncsnap/job/:id/download` — get presigned download URL (optional)
Use the [Server SDK](/server/installation) (e.g. with [Next.js route handler](/server/nextjs-route-handler)) to implement these.
## Styling
Components use Tailwind and the SDK theme. Add to your global CSS:
```css theme={null}
@import "tailwindcss";
@import "@syncsnap/react/theme";
```
See [Styling](/guides/styling) for Tailwind v3, shadcn, or custom setup.
## What’s included
| Export | Purpose |
| ---------------------- | -------------------------------------------------------------- |
| `SyncsnapUploadButton` | Button + dialog with QR code; uses `useSyncsnapJob` internally |
| `SyncsnapQrCode` | QR code only (for custom UI) |
| `useSyncsnapJob` | Hook to create job, poll status, and get download URL |
| `createUploadUrl` | Build the URL to encode in the QR code |
Next: [SyncsnapUploadButton](/react/syncsnap-upload-button) or [useSyncsnapJob](/react/use-syncsnap-job).
# SyncsnapQrCode
Source: https://docs.syncsnap.xyz/react/syncsnap-qr-code
Display a QR code that points to the SyncSnap upload page
# SyncsnapQrCode
`SyncsnapQrCode` renders a QR code image that encodes the upload URL for a given job. The URL is built with [createUploadUrl](/react/create-upload-url) so the user’s phone opens the correct SyncSnap upload page with the job ID.
## When to use
* **SyncsnapUploadButton** — Use when you want the default button + dialog; it uses the QR internally.
* **SyncsnapQrCode** — Use when you build your own UI (e.g. custom modal, different layout) and only need the QR image.
You must have a `jobId` (from creating a job via your backend or [useSyncsnapJob](/react/use-syncsnap-job)) before rendering this component.
## Basic usage
```tsx theme={null}
import { SyncsnapQrCode } from "@syncsnap/react";
```
Renders a 240×240 QR code by default. The encoded URL is `https://upload.syncsnap.xyz/?job_id=` unless you pass `baseUrl`.
## Props
| Prop | Type | Default | Description |
| ---------------------- | -------------------------- | -------------------------------- | ------------------------------------------------------------------------------ |
| `jobId` | `string` | *required* | The SyncSnap job ID (from job creation). |
| `baseUrl` | `string` | `"https://upload.syncsnap.xyz/"` | Base URL for the upload page. The final URL is `baseUrl + "?job_id=" + jobId`. |
| `size` | `number` | `240` | Width and height of the QR image in pixels. |
| `className` | `string` | — | Applied to the `` element. |
| `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | QR error correction level. |
## Custom base URL
For staging or a custom upload host:
```tsx theme={null}
```
## With useSyncsnapJob
Typical pattern: start a job with the hook, then show the QR when you have a `jobId`:
```tsx theme={null}
const { jobId, start, isPolling, error } = useSyncsnapJob({
onCompleted: (job, presignedUrl) => { /* ... */ },
});
return (
<>
{error &&
{error.message}
}
{jobId && }
>
);
```
The component renders nothing until the QR data URL is generated (brief delay); it does not show a loading state by default. Add your own loading UI if needed.
# SyncsnapUploadButton
Source: https://docs.syncsnap.xyz/react/syncsnap-upload-button
Button and dialog that show a QR code for phone uploads
# SyncsnapUploadButton
`SyncsnapUploadButton` is an all-in-one component: a button that opens a dialog with a QR code. When the user clicks the button, a job is created and the QR code is shown; the hook then calls your backend's **wait** endpoint (the server polls until the job completes or fails). When done, your `onCompleted(job, result)` runs with the server's result.
## Basic usage
```tsx theme={null}
import { SyncsnapUploadButton } from "@syncsnap/react";
```
Clicking the button creates a job, opens the dialog, and shows the QR code. When the user uploads from their phone, the server waits for completion and returns a result; your `onCompleted(job, result)` receives it (e.g. a custom payload from your server's `onCompleted` callback or the presigned download URL).
## Props
All [useSyncsnapJob](/react/use-syncsnap-job) options are supported (e.g. `createJobUrl`, `getJobUrl`, `getWaitForCompletionUrl`, `waitTimeoutMs`, `waitIntervalMs`, `onCompleted`). In addition:
| Prop | Type | Default | Description |
| ---------------------- | -------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `buttonText` | `string` | `"Start upload"` | Label of the trigger button. |
| `className` | `string` | — | Applied to the wrapper div. |
| `qrSize` | `number` | `240` | QR code size in pixels. |
| `qrBaseUrl` | `string` | — | Base URL for the upload page (see [createUploadUrl](/react/create-upload-url)). Overrides default `https://upload.syncsnap.xyz/`. |
| `errorCorrectionLevel` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | QR code error correction level. |
## Callbacks
Pass the same callbacks as [useSyncsnapJob](/react/use-syncsnap-job):
```tsx theme={null}
console.log("Job created:", job.id)}
onCompleted={(job, result) => {
// result is whatever your server onCompleted returned
if (result && typeof result === "object" && "downloadUrl" in result) {
window.open((result as { downloadUrl: string }).downloadUrl);
}
}}
onError={(err) => console.error(err)}
/>
```
## Custom API paths
If your API is not under `/api/syncsnap/job`:
```tsx theme={null}
`/api/transfer/job/${id}`}
getWaitForCompletionUrl={(id) => `/api/transfer/job/${id}/wait`}
buttonText="Scan to upload"
/>
```
## Wait timeout and interval
Defaults: server waits up to 2 minutes, polling every 2 seconds. Override as needed:
```tsx theme={null}
```
## Building your own UI
If you prefer not to use the button + dialog, use [SyncsnapQrCode](/react/syncsnap-qr-code) and [useSyncsnapJob](/react/use-syncsnap-job) to create jobs and show the QR code in your own layout.
# useSyncsnapJob
Source: https://docs.syncsnap.xyz/react/use-syncsnap-job
Hook to create a job and wait for completion (server-side wait)
# useSyncsnapJob
`useSyncsnapJob` creates a transfer job, then calls your backend's **wait** endpoint. The server polls until the job completes or fails and returns the final job plus a **result** (e.g. presigned download URL or a custom payload from your server's `onCompleted` callback). The hook passes that result to your `onCompleted(job, result)`. It powers `SyncsnapUploadButton` and can be used on its own for custom UIs.
## Basic usage
```tsx theme={null}
const { jobId, status, start, isWaiting, error, onCompleted } = useSyncsnapJob({
onCompleted: (job, result) => {
// result is whatever your server onCompleted returned (or the presigned URL when no callback)
if (result && typeof result === "object" && "downloadUrl" in result)
console.log("Download:", (result as { downloadUrl: string }).downloadUrl);
},
});
return (
);
```
* Call **start()** to create a job and call the wait endpoint (server does the polling).
* **jobId** is set once the job is created; use it with [SyncsnapQrCode](/react/syncsnap-qr-code) or [createUploadUrl](/react/create-upload-url).
* **status** is the current job status; **isWaiting** is true while the wait request is in flight.
* **onCompleted(job, result)** runs when the job finishes; **result** is what your server's `onCompleted` returned, or the presigned download URL when no callback is set.
## Options
| Option | Type | Default | Description |
| ------------------------- | -------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------- |
| `createJobUrl` | `string` | `"/api/syncsnap/job"` | URL for POST to create a job. |
| `getJobUrl` | `(jobId: string) => string` | `id => "/api/syncsnap/job/" + id` | URL for GET job status. |
| `getWaitForCompletionUrl` | `(jobId: string) => string` | `id => getJobUrl(id) + "/wait"` | URL for the server's wait endpoint (server polls until job completes). |
| `waitTimeoutMs` | `number` | `120000` | Timeout passed to the wait endpoint (ms). |
| `waitIntervalMs` | `number` | `2000` | Poll interval passed to the wait endpoint (ms). |
| `autoStart` | `boolean` | `false` | If true, call start() once on mount. |
| `onJobCreated` | `(job: CreateJobResponse) => void` | — | Called when the job is created. |
| `onCompleted` | `(job: Job, result?: unknown) => void` | — | Called when job completes or fails; result is server's onCompleted return (or presigned URL). |
| `onError` | `(error: Error) => void` | — | Called on create or wait errors. |
## Return value
| Property | Type | Description |
| ----------- | --------------------- | ------------------------------------------------------------- |
| `job` | `Job \| null` | Current job object, or null before create / after completion. |
| `jobId` | `string \| null` | Job ID when available. |
| `status` | `JobStatus \| null` | Current status. |
| `error` | `Error \| null` | Last error from create or wait. |
| `isWaiting` | `boolean` | True while the wait request is in flight. |
| `start` | `() => Promise` | Create job and call wait endpoint. |
| `reset` | `() => void` | Abort wait, clear state, reset error. |
## Custom URLs
Use a different API base path:
```tsx theme={null}
useSyncsnapJob({
createJobUrl: "/api/transfer/job",
getJobUrl: (id) => `/api/transfer/job/${id}`,
getWaitForCompletionUrl: (id) => `/api/transfer/job/${id}/wait`,
onCompleted: (job, result) => { /* ... */ },
});
```
## Wait timeout and interval
The client sends `timeoutMs` and `intervalMs` as query params to the wait endpoint. Override the defaults:
```tsx theme={null}
useSyncsnapJob({
waitTimeoutMs: 60000,
waitIntervalMs: 3000,
onCompleted: (job, result) => { /* ... */ },
});
```
## Auto-start
Create and call the wait endpoint as soon as the component mounts:
```tsx theme={null}
const { jobId, status } = useSyncsnapJob({ autoStart: true });
```
Use with care (e.g. dedicated page) so you don't create jobs unnecessarily.
## Reset
Call **reset()** to abort the wait and clear state (e.g. when the user closes the dialog):
```tsx theme={null}
const { start, reset } = useSyncsnapJob();
const handleClose = () => {
reset();
};
```
## API reference
Full types: [useSyncsnapJob (API)](/api-reference/react-use-syncsnap-job).
# Installation
Source: https://docs.syncsnap.xyz/server/installation
Install and configure the server SDK (syncsnap)
# Server SDK installation
The `syncsnap` package is the server-side SDK for SyncSnap. It talks to the SyncSnap API to create jobs, fetch job status, and get presigned upload/download URLs. Your backend uses it so the React SDK can work without ever touching the API key.
## Install
```bash theme={null}
npm install syncsnap
```
## Environment
Set your SyncSnap API key so the SDK can authenticate. Get your key from the [SyncSnap dashboard](https://syncsnap.xyz/dashboard):
```bash theme={null}
SYNCSNAP_TOKEN=your_api_key_here
```
Use `.env` or `.env.local` (and never expose this in the client). The `SyncsnapServer` constructor reads `process.env.SYNCSNAP_TOKEN`.
## What the server does
1. **Create job** — Your route calls `client.createJob()` and returns the job to the frontend.
2. **Get job** — Your route receives a job ID, calls `client.getJob(id)`, and returns the job.
3. **Wait for completion** — Your route (e.g. `/job/:id/wait`) runs `client.waitForJobCompletion(id)` until the job completes or fails, then returns the job and an optional result (e.g. from your `onCompleted` callback).
4. **Get download URL** — For a completed job, your route calls `client.getDownloadUrl(id)` and returns the presigned URL to the frontend.
The React SDK expects these as HTTP endpoints; the syncsnap package provides a [Next.js route handler](/server/nextjs-route-handler) that implements all of them.
## Next steps
* [SyncsnapServer](/server/syncsnap-server) — API and configuration.
* [Next.js route handler](/server/nextjs-route-handler) — Single catch-all route for Next.js App Router.
# Next.js route handler
Source: https://docs.syncsnap.xyz/server/nextjs-route-handler
Single catch-all route for SyncSnap with the App Router
# Next.js route handler
The `syncsnap` package exports `createRouteHandler` from `syncsnap/next`. It returns `GET` and `POST` handlers that implement the endpoints the React SDK expects (create job, get job, **wait for completion**, get download URL), so you can wire SyncSnap with one file.
## Setup
Create this file in your Next.js App Router app:
**`app/api/syncsnap/[...syncsnap]/route.ts`**
```ts theme={null}
import { SyncsnapServer } from "syncsnap";
import { createRouteHandler } from "syncsnap/next";
const client = new SyncsnapServer();
export const { GET, POST } = createRouteHandler({
client,
onCompleted: async (job, presigned) => {
// Optional. Runs when the job completes (after server-side wait). Return value is sent to the client as onCompleted(job, result).
return presigned ? { downloadUrl: presigned.url, fileName: presigned.fileName } : undefined;
},
});
```
Ensure `SYNCSNAP_TOKEN` is set in your environment.
## Routes implemented
The catch-all segment `[...syncsnap]` is resolved to path segments. The handler maps them as follows:
| Request | Path segments | Action |
| ------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/syncsnap/job` | `["job"]` | Create job → `client.createJob()`, return JSON. |
| `GET /api/syncsnap/job/:id` | `["job", id]` | Get job → `client.getJob(id)`, return JSON. |
| `GET /api/syncsnap/job/:id/wait` | `["job", id, "wait"]` | Wait for completion → `client.waitForJobCompletion(id)` (with optional `timeoutMs`, `intervalMs` query params), then return `{ job, result? }`. The `result` is what your `onCompleted` callback returned, or the presigned URL when no callback. |
| `GET /api/syncsnap/job/:id/download` | `["job", id, "download"]` | Get download URL → `client.getDownloadUrl(id, { expirationMinutes })`, return JSON (and optionally `completedPayload` when `onCompleted` is set). |
Any other path returns `404` with `{ error: "Not found" }`.
## onCompleted callback
When you pass `onCompleted` to `createRouteHandler`, it is called when a job completes (in the wait handler) or when the download endpoint is used. Its return value is sent to the client: in the wait flow it becomes the `result` in `onCompleted(job, result)`; in the download response it is included as `completedPayload`. Use it to return a custom payload (e.g. `{ downloadUrl, fileName }`) instead of exposing the raw presigned URL.
## Wait endpoint query params
The wait route accepts:
* `timeoutMs` — Max time to wait (ms). Default from client is 120000.
* `intervalMs` — Poll interval (ms). Default from client is 2000.
The React SDK sends these when it calls the wait URL.
## Download URL expiration
The download endpoint reads the `expiration` query parameter (in minutes) and passes it to `getDownloadUrl`:
```
GET /api/syncsnap/job/:id/download?expiration=15
```
If omitted, the SDK default is used.
## Using a different base path
If you want the API under a different prefix (e.g. `/api/transfer`), move the route folder:
* **`app/api/transfer/[...syncsnap]/route.ts`** — same code; routes become `POST /api/transfer/job`, `GET /api/transfer/job/:id`, `GET /api/transfer/job/:id/wait`, `GET /api/transfer/job/:id/download`.
Then point the React SDK at that base:
```tsx theme={null}
`/api/transfer/job/${id}`}
getWaitForCompletionUrl={(id) => `/api/transfer/job/${id}/wait`}
/>
```
## Without the catch-all
If you prefer separate route files, implement the same logic: create job (POST), get job (GET), wait for completion (GET with optional timeout/interval), get download URL (GET with optional expiration). The React SDK only cares about the URL contract, not how you implement it.
# SyncsnapServer
Source: https://docs.syncsnap.xyz/server/syncsnap-server
Server-side client for the SyncSnap API
# SyncsnapServer
`SyncsnapServer` is the server-side client used to create jobs, get job status, and get presigned upload/download URLs. Use it only in Node (e.g. Next.js route handlers, API routes, or other backend code). It reads `process.env.SYNCSNAP_TOKEN` for authentication.
## Construction
```ts theme={null}
import { SyncsnapServer } from "syncsnap";
const client = new SyncsnapServer();
```
The default base URL for the SyncSnap API is configured inside the SDK. Ensure `SYNCSNAP_TOKEN` is set (get it from the [dashboard](https://syncsnap.xyz/dashboard)).
## Methods
### createJob()
Creates a new transfer job. Returns the job object (id, projectId, status, createdAt, updatedAt). The frontend uses the `id` to build the QR URL and poll status.
```ts theme={null}
const job = await client.createJob();
// { id, projectId, status, createdAt, updatedAt }
```
### getJob(jobId: string)
Fetches the current job (status, fileName, etc.). Used by your polling endpoint.
```ts theme={null}
const job = await client.getJob("job_abc123");
```
### getUploadUrl(jobId, options)
Gets a presigned **upload** URL for the given job. Used when the phone needs to upload a file; the SyncSnap upload page typically calls your backend or the API. Options: `fileName` (required), optional `expirationMinutes`.
```ts theme={null}
const { url, fileName, expiration } = await client.getUploadUrl("job_abc123", {
fileName: "photo.jpg",
expirationMinutes: 15,
});
```
### getDownloadUrl(jobId, options?)
Gets a presigned **download** URL for a completed job. Your backend calls this and returns the URL to the frontend. Options: optional `expirationMinutes`.
```ts theme={null}
const { url, fileName, expiration } = await client.getDownloadUrl("job_abc123", {
expirationMinutes: 15,
});
```
### waitForJobCompletion(jobId, options?)
Polls the job until status is `completed` or `failed`, or until timeout/abort. Optional: `intervalMs`, `timeoutMs`, `onPoll(job)`, `signal` (AbortSignal).
```ts theme={null}
const job = await client.waitForJobCompletion("job_abc123", {
intervalMs: 2000,
timeoutMs: 120000,
});
if (job.status === "completed") {
const { url } = await client.getDownloadUrl(job.id);
// ...
}
```
## Errors
If the SyncSnap API returns an error or a non-2xx status, the SDK throws an `Error` with a message (e.g. from the API body or a generic "Syncsnap request failed (status)"). Handle these in your route and return an appropriate HTTP response.
## API reference
Full types: [SyncsnapServer (API)](/api-reference/server-syncsnap-server).