Fast and efficient: export files to Vimeo in Java
Vimeo’s current resumable upload flow creates a video through POST /me/videos with
upload.approach set to tus, then sends bytes to the returned upload.upload_link. This replaces
the legacy com.clickntap:vimeo:2.0 example. The implementation below uses Java’s HTTP client and
Gson, following the Vimeo upload guide,
Vimeo’s maintained Python upload implementation,
and the tus protocol.
Prerequisites
Use JDK 21, a Vimeo application authorized to upload, and a user access token with the required
upload permissions. Confirm your application’s upload access and your account’s available quota
with Vimeo. This guide does not assume a particular paid plan, file-size allowance, or duration
limit. Those account-specific conditions require a live check.
Keep access tokens on the server. The returned upload URL is also sensitive: store it privately and do not put it in logs. A completed byte transfer does not establish that Vimeo has finished transcoding the video or that it is publicly playable.
Setting up your development environment
Save the Java class below as VimeoUploader.java. This standalone example pins Gson 2.14.0:
curl --fail --location --output gson-2.14.0.jar \
https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar
javac -cp gson-2.14.0.jar VimeoUploader.java
For Maven, use com.google.code.gson:gson:2.14.0. The class uses no unofficial Vimeo SDK.
Configuring and authenticating your application
Provide VIMEO_ACCESS_TOKEN through your server’s secret management. The create command makes
one API request and saves a private session file; resume reuses that session without creating
another video. The file hash prevents resuming with different content of the same size. Keep the
input file immutable throughout a transfer.
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.HexFormat;
public final class VimeoUploader {
public record Session(String uri, String uploadLink, long size, String sha256) {}
private static final Gson JSON = new Gson();
private final HttpClient http;
private final URI api;
public VimeoUploader() {
this(HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(15))
.followRedirects(HttpClient.Redirect.NEVER).build(), URI.create("https://api.vimeo.com"));
}
// A trusted alternate HTTPS origin permits local protocol verification.
VimeoUploader(HttpClient http, URI api) {
this.http = http;
this.api = https(api);
}
private static URI https(URI uri) {
if (!"https".equals(uri.getScheme()) || uri.getHost() == null
|| uri.getRawUserInfo() != null || uri.getFragment() != null) {
throw new IllegalArgumentException("An HTTPS URL without embedded credentials is required");
}
return uri;
}
private static String hash(Path file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (var input = Files.newInputStream(file)) {
byte[] buffer = new byte[65536];
int count;
while ((count = input.read(buffer)) != -1) digest.update(buffer, 0, count);
}
return HexFormat.of().formatHex(digest.digest());
}
public Session create(Path file, String token, String name) throws Exception {
long size = Files.size(file);
if (!Files.isRegularFile(file) || size == 0) throw new IOException("A nonempty file is required");
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
String checksum = hash(file);
JsonObject upload = new JsonObject();
upload.addProperty("approach", "tus");
upload.addProperty("size", size);
JsonObject body = new JsonObject();
body.add("upload", upload);
body.addProperty("name", name);
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve("/me/videos"))
.timeout(Duration.ofSeconds(60)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body))).build(),
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo create HTTP " + response.statusCode());
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
String uri = result.get("uri").getAsString();
if (!uri.matches("/videos/[0-9]+")) throw new IOException("Invalid video URI");
URI link = https(URI.create(result.getAsJsonObject("upload").get("upload_link").getAsString()));
return new Session(uri, link.toString(), size, checksum);
}
private static long offset(HttpResponse<?> response, long size) throws IOException {
if (!response.headers().firstValue("Tus-Resumable").orElse("").equals("1.0.0")) {
throw new IOException("Unexpected tus protocol version");
}
String value = response.headers().firstValue("Upload-Offset")
.orElseThrow(() -> new IOException("Missing upload offset"));
long offset;
try { offset = Long.parseLong(value); }
catch (NumberFormatException error) { throw new IOException("Invalid upload offset", error); }
if (offset < 0 || offset > size) throw new IOException("Upload offset out of range");
return offset;
}
public void resume(Path file, Session session) throws Exception {
if (session == null) {
throw new IOException("Missing upload session; reconcile your Vimeo account before creating another video");
}
if (Files.size(file) != session.size() || !hash(file).equals(session.sha256())) {
throw new IOException("The upload file changed");
}
URI link = https(URI.create(session.uploadLink()));
HttpResponse<Void> head = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofSeconds(30)).header("Tus-Resumable", "1.0.0")
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build(),
HttpResponse.BodyHandlers.discarding());
if (head.statusCode() != 200 && head.statusCode() != 204) {
throw new IOException("Upload HEAD HTTP " + head.statusCode());
}
long position = offset(head, session.size());
try (RandomAccessFile input = new RandomAccessFile(file.toFile(), "r")) {
input.seek(position);
byte[] buffer = new byte[1024 * 1024];
while (position < session.size()) {
int length = (int) Math.min(buffer.length, session.size() - position);
input.readFully(buffer, 0, length);
HttpResponse<Void> patch = http.send(HttpRequest.newBuilder(link)
.timeout(Duration.ofMinutes(2)).header("Tus-Resumable", "1.0.0")
.header("Upload-Offset", Long.toString(position))
.header("Content-Type", "application/offset+octet-stream")
.method("PATCH", HttpRequest.BodyPublishers.ofByteArray(buffer, 0, length)).build(),
HttpResponse.BodyHandlers.discarding());
if (patch.statusCode() != 204) throw new IOException("Upload PATCH HTTP " + patch.statusCode());
long confirmed = offset(patch, session.size());
if (confirmed != position + length) throw new IOException("Unexpected confirmed offset");
position = confirmed;
}
}
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume video-file session-file");
Path file = Path.of(args[1]);
Path state = Path.of(args[2]);
VimeoUploader uploader = new VimeoUploader();
if (args[0].equals("create")) {
Files.createFile(state, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
Session session = uploader.create(file, System.getenv("VIMEO_ACCESS_TOKEN"), file.getFileName().toString());
Files.writeString(state, JSON.toJson(session));
System.out.println("Created " + session.uri());
} else if (args[0].equals("resume")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
uploader.resume(file, session);
System.out.println("Bytes transferred for " + session.uri());
} else {
throw new IllegalArgumentException("Choose create or resume");
}
}
}
The private-file example targets macOS and Linux. On Windows, configure an equivalent restrictive ACL before persisting the session. Protect the directory too. Only resume session files created by your application; they contain a destination URL that can receive your video.
Error handling and retries
Do not retry create blindly: a lost response can leave a video on Vimeo even though the caller saw
a timeout. If the session file is empty after a failure, reconcile the account before creating a
replacement. After a transfer failure, rerun resume with the same session and unchanged input. Its
HEAD request reads the server’s confirmed offset, including bytes accepted before a lost response.
Authentication failures and invalid requests need correction. Respect Vimeo’s Retry-After or rate
limit guidance for throttling, and back off on transient service/network failures before resuming.
An expired upload URL needs explicit recovery; do not turn it into an automatic new-video request.
The example never attaches the Vimeo bearer token to the tus upload URL and never changes privacy.
Testing your implementation
After account authorization, these commands create a video and transfer its bytes:
java -cp '.:gson-2.14.0.jar' VimeoUploader create video.mp4 session.json
java -cp '.:gson-2.14.0.jar' VimeoUploader resume video.mp4 session.json
Local HTTPS fixtures can check the JSON request, resumable offsets, chunk bytes, missing headers, and error statuses. They cannot confirm your application’s upload access, quota, transcoding result, or playback. Verify those with a deliberately authorized live upload before deploying.
Best practices for video uploads
Keep the video immutable while uploading, persist the video URI for later status checks, and treat session URLs like credentials. Check Vimeo’s current account and upload documentation instead of hardcoding plan names or maximum file sizes.
Enhancing workflows with Transloadit's Vimeo export Robot
Use 🤖 /video/encode for encoding and watermarking, then pass its output to 🤖 /vimeo/store for export. The export Robot does not itself perform all of the preceding video transformations.
Conclusion
Create one Vimeo video, retain its upload session, and resume against the confirmed tus offset. For managed processing before export, see Transloadit’s video encoding service.
