> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yungle.co/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> Install and use the JavaScript (yungle-client) and Python (yungle) SDKs: clients, operations, uploads, errors, retries and webhook verification.

Two official SDKs wrap the [REST API](https://docs.yungle.co/api-reference.md): `yungle-client` for JavaScript and TypeScript, and `yungle` for Python. Use one when you would rather call methods than build requests, and want resumable uploads and safe retries handled for you.

Both are open source in [heindewilde/yungle-clients](https://github.com/heindewilde/yungle-clients). Their READMEs: [JavaScript](https://github.com/heindewilde/yungle-clients/tree/main/packages/api-client#readme) and [Python](https://github.com/heindewilde/yungle-clients/tree/main/python#readme).

## Install

```bash JavaScript theme={null}
npm install yungle-client
```

```bash Python theme={null}
pip install yungle
```

| | JavaScript (`yungle-client`) | Python (`yungle`) |
| - | - | - |
| Runtime | Node 22+, Bun, Deno, edge runtimes | Python 3.9+ |
| Dependencies | None; built on `fetch` | `httpx` |
| Types | TypeScript types for every response | Type hints; responses are the API's JSON as `dict`s |

## Create a client

Create an [API key](https://docs.yungle.co/authentication.md) with the scopes you need and keep it in `YUNGLE_API_KEY`.

```javascript JavaScript theme={null}
import { YungleClient } from 'yungle-client';

const yungle = new YungleClient({ apiKey: process.env.YUNGLE_API_KEY });

const me = await yungle.me();
```

```python Python theme={null}
from yungle import Yungle

yungle = Yungle()  # reads YUNGLE_API_KEY; or Yungle(api_key="yk_live_…")

me = yungle.me()
```

| Option | JavaScript | Python | Default |
| - | - | - | - |
| API key | `apiKey` (required) | `api_key`, else `YUNGLE_API_KEY` | none |
| API base URL | `baseUrl` | `base_url`, else `YUNGLE_API_URL` | `https://yungle.co/api/v1` |
| Retries | `maxRetries` | `max_retries` | `3` |
| HTTP client | `fetch` | `http` (an `httpx.Client`) | the platform's own |
| Timeout | via your `fetch` | `timeout` (seconds) | none in JS, `60` in Python |
| User agent | `userAgent` | not configurable | `yungle-client/<version>`, `yungle-python/<version>` |

The Python client holds an HTTP connection pool. Use it as a context manager (`with Yungle() as yungle:`) or call `close()` when you are done.

## Operations

Every endpoint is a method. The table lists the ones you reach for most; the [API reference](https://docs.yungle.co/api-reference.md) documents each request and response.

| Operation | JavaScript | Python |
| - | - | - |
| Send files in one call | not available; see [uploads](#uploads) | `send(paths, to=…, message=…)` |
| Create a draft transfer | `createTransfer({ files, title })` | `create_transfer(files, title=…)` |
| Add files to a draft | `addTransferFiles(id, files)` | `add_transfer_files(id, files)` |
| Send a transfer | `finalizeTransfer(id, { recipients, message })` | `finalize_transfer(id, recipients=…, message=…)` |
| List transfers (one page) | `listTransfers({ limit, cursor })` | `list_transfers(limit=…, cursor=…)` |
| Iterate all transfers | `allTransfers()` | `all_transfers()` |
| Transfer detail | `getTransfer(id)` | `get_transfer(id)` |
| Change expiry or download limit | `updateTransfer(id, { expiresInDays, maxDownloads })` | `update_transfer(id, expires_in_days=…, max_downloads=…)` |
| Revoke a transfer | `revokeTransfer(id)` | `revoke_transfer(id)` |
| Download receipts | `transferDownloads(id)` | `transfer_downloads(id)` |
| Download links for your transfer | `transferDownloadLinks(id)` | `transfer_download_links(id)` |
| Open a link someone shared | `resolveLink(url, password?)` | `resolve_link(url, password=…)` |
| Save a link's files to disk | not available | `download(url, out_dir)` |
| Create a collection | `createCollection({ title, description })` | `create_collection(title, description=…)` |
| Register collection files | `addCollectionFiles(id, files, folderId?)` | `add_collection_files(id, files, folder_id=…)` |
| Iterate collection files | `allCollectionFiles(id, folderId?)` | `all_collection_files(id, folder_id=…)` |
| Create a folder | `createFolder(id, { name, parentId })` | `create_folder(id, name, parent_id=…)` |
| Invite guests | `inviteGuests(id, emails)` | `invite_guests(id, emails)` |
| Remove a guest | `removeGuest(id, guestId)` | `remove_guest(id, guest_id)` |
| Create an upload request | `createRequest({ collectionId, title })` | `create_request(collection_id, title)` |
| Pause, resume or close a request | `setRequestStatus(id, status)` | `set_request_status(id, status)` |
| Create a contact | `createContact({ email, firstName })` | `create_contact(email, first_name=…)` |
| Create a webhook endpoint | `createWebhook({ url, events })` | `create_webhook(url, events)` |
| Read pull-mode events | `listWebhookEvents(id, { cursor })` | `list_webhook_events(id, cursor=…)` |
| Send a test event | `testWebhook(id)` | `test_webhook(id)` |

`files` entries are `{ name, size, path?, type? }`, with `size` the exact byte count and `path` the folder the file belongs in, such as `Ceremony/Raw`. Both SDKs also accept `imports`, files Yungle fetches from a URL itself; wait for them with `waitForImports` or `wait_for_imports`.

## Uploads

Creating a transfer, or registering collection files, returns a `tusEndpoint` and one target per file. Each SDK uploads one file to one target over tus, resumably:

```javascript JavaScript theme={null}
import { openAsBlob, statSync } from 'node:fs';

const draft = await yungle.createTransfer({
  files: [{ name: 'final-cut.mov', size: statSync('final-cut.mov').size }],
});
await yungle.uploadFile(draft.tusEndpoint, draft.files[0], await openAsBlob('final-cut.mov'), {
  onProgress: (sent, total) => console.log(`${Math.round((sent / total) * 100)}%`),
});
const { transfer } = await yungle.finalizeTransfer(draft.transfer.id, {
  recipients: ['client@example.com'],
});
```

```python Python theme={null}
import os
from yungle import upload_file

draft = yungle.create_transfer(
    [{"name": "final-cut.mov", "size": os.path.getsize("final-cut.mov")}]
)
upload_file(
    draft["tusEndpoint"], draft["files"][0], "final-cut.mov",
    on_progress=lambda sent, total: print(f"{sent / total:.0%}"),
)
transfer = yungle.finalize_transfer(
    draft["transfer"]["id"], recipients=["client@example.com"]
)["transfer"]

# Or the whole thing in one call:
sent = yungle.send(["final-cut.mov"], to=["client@example.com"])
```

Both upload helpers:

* Continue from the offset the server last committed, after a dropped connection or a server error.
* Renew the two-hour upload token for as long as the upload runs.
* Return the upload URL. Pass it back (`uploadUrl` in JavaScript, `upload_url=` in Python) to resume the same upload from another process. Never start a second upload for the same file: it starts over.

In Node, `fs.openAsBlob(path)` streams from disk without loading the file into memory. [Upload large files](https://docs.yungle.co/guides/upload-large-files.md) explains the protocol underneath.

## Errors and retries

A failed request throws `YungleApiError` in JavaScript and raises `YungleError` in Python. Branch on `code`; the message is for people and may change.

```javascript JavaScript theme={null}
import { YungleApiError } from 'yungle-client';

try {
  await yungle.createCollection({ title: 'Client deliveries' });
} catch (err) {
  if (err instanceof YungleApiError && err.code === 'upgrade_required') {
    // Collections need Leaf or Tree.
  } else {
    throw err;
  }
}
```

```python Python theme={null}
from yungle import YungleError

try:
    yungle.create_collection("Client deliveries")
except YungleError as err:
    if err.code != "upgrade_required":  # collections need Leaf or Tree
        raise
```

| Field | JavaScript | Python |
| - | - | - |
| HTTP status | `status` | `status` |
| Error code, as on [Errors](https://docs.yungle.co/errors.md) | `code` | `code` |
| Human-readable message | `message` | `message` |
| Extra detail, such as `limitBytes` | `details` | `details` |
| Link to the code's explanation | `docs` | `docs` |
| Worth retrying (`429` or `5xx`) | `retryable` | `retryable` |
| Seconds to wait, from the body or `Retry-After` | `retryAfterSeconds` | `retry_after_seconds` |

Both clients retry for you, up to `maxRetries` / `max_retries` times:

* **`429`** is always retried, because a throttled request did nothing.
* **`5xx`** is retried for `GET`, `DELETE` and `POST`. Every `POST` carries an `Idempotency-Key` that stays the same across its retries, so a retried create returns the first result instead of making a second one. `PATCH` is not retried.
* **A dropped connection** is retried under the same rules as a `5xx`. In JavaScript, an abort you asked for (an `AbortSignal` or a timeout) is never retried. Before `yungle-client` 0.3.2, the JavaScript client threw the `fetch` error instead.
* The wait honors `Retry-After` when the server sends one, and otherwise backs off exponentially with jitter.

Set the retry count to `0` to handle retries yourself.

## Verify webhooks

Each SDK exports a helper that checks the `Yungle-Signature` header against the raw request body, and rejects a timestamp more than five minutes old.

```javascript JavaScript theme={null}
import { verifyWebhook } from 'yungle-client';

// rawBody: the request body as a string, exactly as received.
const ok = await verifyWebhook(rawBody, req.headers['yungle-signature'], process.env.YUNGLE_WEBHOOK_SECRET);
```

```python Python theme={null}
from yungle import verify_webhook

# raw_body: bytes or str, exactly as received.
ok = verify_webhook(raw_body, headers["Yungle-Signature"], os.environ["YUNGLE_WEBHOOK_SECRET"])
```

The JavaScript helper is `async` because it uses WebCrypto, which lets it run on edge runtimes. Change the window with `{ toleranceSeconds }` or `tolerance_seconds=`. See [Webhooks](https://docs.yungle.co/guides/webhooks.md) for the full receiver.

## What the SDKs cannot reach

Your vault and end-to-end encrypted transfers are encrypted with keys Yungle never holds, so the SDKs cannot read their contents. The JavaScript SDK can create an end-to-end encrypted transfer when you encrypt the files yourself with the `yungle-e2e` package. See [Unsupported](https://docs.yungle.co/unsupported.md).

## Versions

| | Package | Changes |
| - | - | - |
| JavaScript | [`yungle-client` on npm](https://www.npmjs.com/package/yungle-client) | [CHANGELOG](https://github.com/heindewilde/yungle-clients/blob/main/packages/api-client/CHANGELOG.md) |
| Python | [`yungle` on PyPI](https://pypi.org/project/yungle/) | [Releases](https://github.com/heindewilde/yungle-clients/releases) |

Both are versioned `0.x`. Pin the version you tested against, and read the changes before you upgrade. The API itself is versioned in its path (`/api/v1`); see the [changelog](https://docs.yungle.co/changelog.md).

## Next steps

> **[Send from Python](https://docs.yungle.co/guides/send-from-python.md)**
>
> A full script with the Python SDK.

> **[Send a transfer](https://docs.yungle.co/guides/send-a-transfer.md)**
>
> The transfer flow, with curl, JavaScript, Python and the CLI.

> **[Webhooks](https://docs.yungle.co/guides/webhooks.md)**
>
> Events, signatures, retries and a worked example.

> **[Errors](https://docs.yungle.co/errors.md)**
>
> Every error code and what to do about it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.