Official Java SDK for dosya.dev - file storage, resumable uploads, sharing, webhooks and workspace management.
- Java 11+ (uses the JDK's
java.net.httpclient; stay on 0.2.x for Java 8) - Single runtime dependency (Gson)
- Optional SLF4J integration for structured logging
- Thread-safe after construction
- Nullability annotations via JetBrains Annotations
- Installation
- Quick Start
- Authentication
- Configuration
- Retries and timeouts
- Uploads
- Downloads
- Sharing
- Webhooks
- Error Handling
- Things worth knowing
- Resources
- Logging
- Observability
- Thread Safety
- Changelog
- License
<dependency>
<groupId>dev.dosya</groupId>
<artifactId>dosya-java</artifactId>
<version>0.3.0</version>
</dependency>implementation("dev.dosya:dosya-java:0.3.0")implementation 'dev.dosya:dosya-java:0.3.0'DosyaClient client = new DosyaClient(System.getenv("DOSYA_API_KEY"));
String workspaceId = client.workspaces().list().getWorkspaces().get(0).getId();
// Upload - small files go up in one request, large files resume part by part
UploadResult uploaded = client.upload().file(
UploadParams.fromPath(workspaceId, Paths.get("report.pdf"))
.onProgress(p -> System.out.println(p.getPercent() + "%")));
// List a folder
ListFilesResponse listing = client.files().list(new ListFilesParams(workspaceId).sort("modified_desc"));
// A short-lived download URL
DownloadLink link = client.download().getUrl(uploaded.getFile().getId(), new DownloadOptions().ttl(600));
// Share it
CreatedShareLink share = client.files().createShareLink(uploaded.getFile().getId(),
new CreateShareLinkParams().expiresInDays(7));
System.out.println(share.getUrl());Every request carries an API key (dos_...). Create one in the web app under Settings > API keys.
| Scope | Can call |
|---|---|
read |
GET endpoints, downloads, archive downloads |
upload |
Upload endpoints and folders().create only |
Any scope can call me().revokeCurrentKey().
| full | Everything an API key can reach |
A key can also be pinned to one workspace. Pinned keys are refused (403) on routes whose workspace cannot be checked up front, such as webhook management, comment writes, share bundles, remote downloads, and team or share-link changes by id.
Creating or listing API keys, 2FA, passwords and sessions require an interactive login, so they are not part of this SDK. client.me().revokeCurrentKey() is the one key operation a key can perform on itself.
DosyaClient client = new DosyaClient(new DosyaClientOptions("dos_...")
.baseUrl("https://api.dosya.dev") // default; HTTPS required (http://localhost allowed for tests)
.connectTimeout(10_000)
.readTimeout(30_000) // per attempt, JSON requests, including the body
.uploadTimeout(600_000) // per attempt, requests that carry file bytes
.maxRetries(3)
.baseDelay(500)
.maxDelay(30_000)
.readYourWrites(true) // echo X-D1-Bookmark so reads see your writes
.onRateLimit(info -> System.out.println(info.getRemaining() + "/" + info.getLimit()))
.debug(System.out::println));Pass your own java.net.http.HttpClient with .httpClient(...) for proxies or custom TLS.
- GET, HEAD, PUT and DELETE are retried on 5xx, timeouts and network errors, with exponential backoff and jitter.
- POST and PATCH are only retried on a rate-limit 429 carrying
Retry-After, which the API sends before any work is done. A failed POST is never replayed, so it cannot create a duplicate share link, webhook or download job. - A 429 without
Retry-Afteris a business limit (for example "Daily limit of 20 remote downloads") and is thrown immediately. - When
Retry-Afterasks for longer thanmaxDelay, the exception is thrown withgetRetryAfterSeconds()set instead of sleeping. - Operations whose repeat would change their meaning (trash then purge, restore, move) are never retried automatically.
- Uploads run their own retries: before repeating an upload whose outcome was not seen, the SDK asks the server how the first attempt ended, so a file is never stored twice.
client.upload().file(UploadParams.fromPath(workspaceId, Paths.get("clip.mp4"))
.folderId(folderId)
.sourceModifiedAt(Files.getLastModifiedTime(Paths.get("clip.mp4")).toInstant())
.computeSha256(true) // verified by the server on single-request uploads
.expectedVersion(3) // 409 version_conflict if someone else wrote first
.concurrency(4));- Files up to 50 MiB go up in one request; larger files use resumable 10 MiB parts, read from disk one part at a time.
- Uploading a name that already exists in the folder creates a new version of that file.
- The stored MIME type is derived from the file extension.
Resume after a crash with the session id from DosyaUploadException.getSessionId():
client.upload().resume(sessionId, UploadSource.ofPath(Paths.get("clip.mp4")));Many small files in few requests:
List<UploadBatchFile> files = ...; // UploadBatchFile.of(path).folderId(folderId)
for (UploadManyResult r : client.upload().many(workspaceId, files,
new UploadManyOptions().onProgress(p -> System.out.println(p.getFilesCompleted() + "/" + p.getTotalFiles())))) {
if (!r.isOk()) System.err.println(r.getName() + ": " + r.getError());
}many() packs files up to 5 MiB into batch() requests (up to 200 files each) and sends larger files through file(), reporting per-file failures in the results instead of throwing. batch(), init(), uploadPart(), complete() and status() are available for custom pipelines.
DownloadLink link = client.download().getUrl(fileId, new DownloadOptions().ttl(3600));
byte[] bytes = client.download().downloadBytes(fileId);
long written = client.download().downloadTo(fileId, Paths.get("out.bin"));
try (InputStream part = client.download().downloadStream(fileId, new DownloadOptions().range(0, 1023))) {
// ...
}
try (InputStream zip = client.download().archive(null, Arrays.asList(folderId)).body()) {
// streamed zip of a whole folder
}Password-locked (FULL_LOCK) items need an unlock token first:
UnlockGrant grant = client.files().unlock(fileId, "secret");
client.download().downloadBytes(fileId, new DownloadOptions().unlockToken(grant.getUnlockToken()));CreatedShareLink link = client.files().createShareLink(fileId, new CreateShareLinkParams()
.expiresInDays(14)
.password("at-least-8-chars")
.maxDownloads(50));
// Private link: only these recipients can open it, after an emailed code
client.folders().createShareLink(folderId, new CreateShareLinkParams()
.accessMode(ShareAccessMode.RESTRICTED)
.recipientEmails(Arrays.asList("client@example.com")));
ShareByEmailResult sent = client.files().shareByEmail(fileId,
new ShareByEmailParams(Arrays.asList("a@example.com")));
client.shares().update(link.getId(), new UpdateShareLinkParams().maxDownloads(100));
client.shares().revoke(link.getId());CreatedWebhookEndpoint endpoint = client.webhooks().create(new CreateWebhookParams(workspaceId,
"https://example.com/hooks/dosya", WebhookEventType.FILE_UPLOADED, WebhookEventType.FILE_DELETED));
String secret = endpoint.getSecret(); // shown only here and by rollSecret()Verify deliveries on your server with the raw request body. WebhookSignature needs no client:
WebhookEvent event = WebhookSignature.constructEvent(rawBody, request.getHeader("X-Dosya-Signature"), secret);
if (event.getEventType() == WebhookEventType.FILE_UPLOADED) {
System.out.println(event.getFileId());
}Payload keys in event.getData() are the documented snake_case names. X-Dosya-Event-Id is stable across retries; use it to deduplicate. share.accessed payloads include the share token, so treat webhook bodies as secrets.
All SDK exceptions extend DosyaException (unchecked).
try {
client.files().list(new ListFilesParams(workspaceId).folderId(folderId));
} catch (DosyaApiException e) {
e.getStatus(); // 403
e.getErrorMessage(); // the API's message
e.getCode(); // machine code when present: "folder_locked", "version_conflict", "quota", ...
e.getDetails(); // extra fields as a JsonObject, e.g. folder_id, lock_mode
e.getRetryAfterSeconds(); // when the API sent Retry-After
e.getRequestId(); // X-Request-Id, for support
} catch (DosyaTimeoutException e) {
e.getTimeoutMs();
} catch (DosyaNetworkException e) {
e.getCause();
} catch (DosyaUploadException e) {
e.getSessionId(); // pass to upload().resume()
e.getPartNumber();
}- Deleting is two-stage.
files().delete()andfolders().delete()move an item to the trash; calling delete again on a trashed item removes it permanently. Usefolders().purge()to empty a trashed folder completely. - Unlock does not remove a lock.
unlock(id, password)grants one hour of access to aFULL_LOCKitem. Remove a lock withlock(id, LockMode.NONE). - Deleting a workspace needs a person. Call
workspaces().requestDeletion(id), read the 6-digit code from the owner's email, thenworkspaces().delete(id, code, confirmName). - A workspace's region is fixed at creation. List valid codes with
client.regions().list(). - Addresses at dosya.dev are refused as share and file-request recipients (400) unless the sender is also a dosya.dev account.
| Accessor | Methods |
|---|---|
files() |
list, get, delete, restore, rename, move, copy, getLock, lock, unlock, getHide, hide, listVersions, restoreVersion, getShareLinks, createShareLink, shareByEmail, createShareBundle, batchDelete, duplicates |
folders() |
create, createBatch, get, rename, restore, delete, purge, move, tree, children, search, getLock, lock, unlock, getHide, hide, getShareLinks, createShareLink, shareByEmail |
upload() |
file, resume, many, batch, init, uploadPart, complete, status |
download() |
getUrl, downloadBytes, downloadStream, downloadTo, raw, thumbnail, archive, archiveEntries, archiveEntry |
favourites() |
list, add, remove |
shares() |
list, update, analytics, revoke, withMe |
workspaces() |
list, get, create, update, getSettings, updateSettings, uploadLimits, deletePreview, requestDeletion, delete, transfer, leave |
team() |
list, invite, resendInvite, revokeInvite, updateMember, removeMember, listInviteLinks, createInviteLink, revokeInviteLink |
roles() |
list, create, update, delete |
regions() |
list |
fileRequests() |
list, create, get, update, delete, listUploads, listRecipients, addRecipient, removeRecipient, resend |
search() |
query (supports an ext:pdf token) |
comments() |
list, listForFile, listForFolder, create, edit, delete |
activity() |
list |
webhooks() |
list, create, get, update, delete, rollSecret, test, listDeliveries, redeliver |
remoteDownloads() |
list, create, cancel, waitFor |
me() |
profile, permissions, updateName, revokeCurrentKey |
Method Javadoc notes the permission, limits and error codes that matter, and the key scope where it differs from the default (GET needs read, everything else full).
Upgrading from 0.2.x? See CHANGELOG.md for the breaking changes.
The SDK supports two logging mechanisms:
Add any SLF4J binding to your classpath (Logback, Log4j2, etc.) and the SDK will log automatically:
- DEBUG - every request/response with URL, status code, and duration
- WARN - retries after rate limits, server errors and network errors (timeout retries go to the debug callback)
<!-- Example: add Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>SLF4J is an optional dependency - if no binding is present, logging is silently disabled.
For quick debugging without a logging framework:
new DosyaClientOptions("dos_your_api_key")
.debug(msg -> System.out.println("[dosya] " + msg));The callback receives human-readable messages like:
GET https://api.dosya.dev/api/files?workspace_id=ws_1 (attempt 1/4)
GET https://api.dosya.dev/api/files?workspace_id=ws_1 -> 200
HTTP 429, retrying in 1000ms
Register a DosyaInterceptor to hook into every HTTP request and response - useful for OpenTelemetry, Micrometer, or custom metrics:
import dev.dosya.sdk.DosyaInterceptor;
DosyaClient client = new DosyaClient(
new DosyaClientOptions("dos_your_api_key")
.interceptor(new DosyaInterceptor() {
@Override
public void beforeRequest(String method, String url) {
// start a span, increment a counter, etc.
}
@Override
public void afterResponse(String method, String url,
int statusCode, String requestId,
long durationMs) {
System.out.printf("%s %s -> %d (%dms) [%s]%n",
method, url, statusCode, durationMs, requestId);
}
})
);The requestId parameter is the value of the X-Request-Id response header (may be null).
DosyaClient and all resource instances are thread-safe after construction. You can safely share a single client across multiple threads:
// Create once, use everywhere
DosyaClient client = new DosyaClient("dos_your_api_key");
// Safe to call from any thread
executor.submit(() -> client.files().list(new ListFilesParams("ws_id")));
executor.submit(() -> client.workspaces().list());Do not mutate DosyaClientOptions after passing it to the client constructor.
See CHANGELOG.md for a complete list of changes in each release.
If you have questions, found a bug, or need help integrating the SDK, reach out to the Dosya.dev team:
- Email: support@dosya.dev
- GitHub Issues: dosya-dev/dosya-java/issues
MIT - the SDK is free to embed in your own applications. It talks to the official dosya.dev service.
Found a vulnerability? Please report it privately via GitHub private vulnerability reporting rather than a public issue.
| Repository | What it is | License |
|---|---|---|
| desktop | Desktop client - sync, upload, manage | Source-available |
| cli | Command-line interface | Source-available |
| app.dosya.dev | Web application | Source-available |
| shared | Shared TypeScript types & utilities | Source-available |
| dosya-js | Official JavaScript SDK | MIT |
| dosya-java | Official Java SDK | MIT |