Fast and efficient: export files to Vimeo in Java
Create one private Vimeo video, save its upload session, and resume an interrupted transfer from Vimeo’s confirmed byte offset. The Java program below uses the HTTP client and Gson to implement Vimeo’s resumable upload flow, with separate commands for creation, transfer, and processing status.
Prerequisites
This walkthrough targets Linux with Bash, cURL, and a maintained JDK 21 distribution. It was tested
with OpenJDK 21.0.12.1; use your distributor’s current security updates for the
maintained Java 21 line. Set JAVA_HOME to that JDK’s installation
directory. The commands select its compiler and runtime explicitly.
You also need a Vimeo application with upload access, a user access token with the public,
private, and upload scopes, and room in the
receiving account’s quota. Confirm those prerequisites
before creating a test video. Use a nonempty, valid video that you are allowed to upload, and run
one uploader at a time against its session file.
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
In a private working directory, save the complete class below as VimeoUploader.java. This
standalone setup downloads Gson 2.14.0
and compiles it. Each command must succeed before the next runs:
test -x "$JAVA_HOME/bin/java" &&
test -x "$JAVA_HOME/bin/javac" &&
command -v curl >/dev/null &&
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 &&
"$JAVA_HOME/bin/javac" -cp gson-2.14.0.jar VimeoUploader.java
If downloading or compiling fails, fix the reported problem and rerun this setup block. It replaces the downloaded JAR and rebuilds the class; do not run an older class after a failed build.
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"));
}
// Tests supply a trusted HTTPS receiver instead of the Vimeo API.
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);
JsonObject privacy = new JsonObject();
privacy.addProperty("view", "nobody");
body.add("privacy", privacy);
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")
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.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("Accept", "application/vnd.vimeo.*+json;version=3.4")
.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 JsonObject status(Session session, String token) throws Exception {
if (session == null || session.uri() == null || !session.uri().matches("/videos/[0-9]+")) {
throw new IOException("Missing or invalid video URI");
}
if (token == null || token.isBlank()) throw new IllegalArgumentException("Missing Vimeo token");
HttpResponse<String> response = http.send(HttpRequest.newBuilder(api.resolve(session.uri()
+ "?fields=uri,upload.status,transcode.status,privacy.view"))
.timeout(Duration.ofSeconds(30)).header("Authorization", "Bearer " + token)
.header("Accept", "application/vnd.vimeo.*+json;version=3.4")
.GET().build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) throw new IOException("Vimeo status HTTP " + response.statusCode());
JsonObject video = JsonParser.parseString(response.body()).getAsJsonObject();
if (!session.uri().equals(video.get("uri").getAsString())) {
throw new IOException("Unexpected status video URI");
}
JsonObject result = new JsonObject();
result.add("uri", video.get("uri"));
result.addProperty("upload", statusValue(video, "upload"));
result.addProperty("transcode", statusValue(video, "transcode"));
result.add("privacy", video.getAsJsonObject("privacy").get("view"));
return result;
}
private static String statusValue(JsonObject video, String field) {
if (!video.has(field) || !video.get(field).isJsonObject()) return "unavailable";
var value = video.getAsJsonObject(field).get("status");
return value == null || value.isJsonNull() ? "unavailable" : value.getAsString();
}
public static void main(String[] args) throws Exception {
if (args.length != 3) throw new IllegalArgumentException("create|resume|status video-file session-file");
for (int i = 1; i < args.length; i++) {
if (!args[i].chars().allMatch(c -> c >= 32 && c <= 126)) {
throw new IllegalArgumentException("Use printable ASCII paths for this command-line example");
}
}
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 if (args[0].equals("status")) {
Session session = JSON.fromJson(Files.readString(state), Session.class);
System.out.println(JSON.toJson(uploader.status(session, System.getenv("VIMEO_ACCESS_TOKEN"))));
} else {
throw new IllegalArgumentException("Choose create, resume, or status");
}
}
}
The session file is created exclusively with owner-only permissions. An existing file is never
overwritten by create. Protect its directory too, and only resume sessions created by your
application: their upload URLs can receive your video. This command-line example accepts printable
ASCII paths to avoid ambiguous decoding of native filenames. Rename other paths before use, and
quote paths containing spaces or shell characters.
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. For API throttling, follow Vimeo’s
rate-limit guidance and the X-RateLimit-Reset
response header. 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 attaches the bearer token only to API requests and requests privacy.view: nobody when
creating the video. It does not publish the video. Its 1 MiB chunks make the example easy to interrupt;
for larger uploads, review Vimeo’s chunk-size recommendations before tuning throughput.
Testing your implementation
Put your video at video.mp4, or substitute its path in each command. With VIMEO_ACCESS_TOKEN
available in the environment, create the private video once:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader create "video.mp4" "session.json"
Then transfer its bytes. If the process stops or a request fails, rerun this same command with the same file and session:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader resume "video.mp4" "session.json"
The success message identifies the video whose bytes were transferred. Each resume begins with a
tus HEAD request containing both headers in Vimeo’s HEAD table: Tus-Resumable: 1.0.0 and
Accept: application/vnd.vimeo.*+json;version=3.4. PATCH sends only the bytes after the confirmed
offset. The example also sends Accept on PATCH, as shown in Vimeo’s resume instructions.
Check the receiving video separately:
"$JAVA_HOME/bin/java" -cp '.:gson-2.14.0.jar' VimeoUploader status "video.mp4" "session.json"
This prints a JSON snapshot with the video URI, upload, transcode, and privacy values from
Vimeo. An upload status of complete records provider acceptance; a transcode status of complete
records processing completion. Neither makes this private video publicly playable. Repeat status
later if processing is still underway. Requests time out while waiting for response headers after
60 seconds for creation, 30 seconds for HEAD or status, and two minutes per PATCH. Reading a response
body can take longer than that timeout. The program has no whole-upload deadline or automatic retry
loop; use a separate job deadline if your service needs one.
Best practices for video uploads
To check stored content, download the original source file and compare every byte with your input.
Vimeo’s API file links
require the public, private, and video_files token scopes and an eligible account plan.
Availability of the original also
depends on source-file storage,
which requires a paid plan and must be enabled before uploading. A transcoded rendition is a different
file and cannot establish an original-byte match. Keep your own original, and delete the private
test video from your Vimeo account when you finish testing.
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
Keep the saved session until you have checked the receiving video and finished any recovery. For managed processing before export, see Transloadit’s video encoding service.
