Shift SRT subtitle timing by milliseconds
Move every subtitle in an SRT file by one exact offset: a shift of -1500 milliseconds brings a line from 00:00:05,000 to 00:00:03,500, while the captions and their numbering stay untouched. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Offset ms
- -1,500
- Entries
- 2
Subtitle timing can be consistently early or late even when every individual cue is internally correct. This tool shifts a complete SRT file by one signed offset measured in milliseconds, making a precise global correction without rewriting captions or changing their order. Positive values move subtitles later; negative values move them earlier. Any shifted timestamp that would become negative is clamped to zero. The parser validates each numbered cue and reports malformed timestamps instead of quietly returning a damaged subtitle file.
Choose the offset from a reliable sync point
Start by finding a clear event near the beginning of the video, such as a door closing, a speaker beginning a sentence, or a title appearing. Compare that event with the matching subtitle cue and measure the difference in milliseconds. If the subtitle appears before the event, use a positive offset to move it later.
If the subtitle appears after the event, use a negative offset to move it earlier. Apply the same check near the end of the program before committing to a global shift. When the difference stays constant, one offset is the right correction.
When the difference grows over time, the problem is probably playback-rate drift or a frame-rate conversion, which a constant shift cannot repair. Submit the complete SRT text and the signed integer offset. The returned text retains cue numbering and caption lines while replacing every valid start and end timestamp with its shifted value.
Understand validation and zero clamping
Each SRT cue must contain a numeric index, a timing line, and at least one caption line. Minutes and seconds must be between zero and fifty-nine. The tool rejects a cue when its timing line does not follow that structure, when either timestamp is malformed, or when the original end precedes the original start.
This strict behavior prevents a partial transformation that looks successful but leaves broken cues hidden inside the output. A negative offset is applied independently to both boundaries. Consequently, a cue that originally starts and ends before the magnitude of a large negative offset can collapse to a zero-length cue at the beginning.
That is intentional clamping, not an arithmetic error. Review early cues after a large negative adjustment and remove or retime any that no longer have useful duration.
Use the shifted file safely in a subtitle workflow
Keep the original subtitle file and save the shifted result under a new name until it has been checked against the intended video. Test dialogue, music cues, and scene transitions at several points rather than relying on a single opening cue. The operation changes timestamps only: it does not translate text, renumber entries, correct spelling, infer missing cues, or compensate for timing drift.
Line breaks inside caption text remain intact, so multi-line subtitles continue to display as authored. Input line endings are normalized in the result, and cue blocks are separated consistently, which is accepted by common SRT consumers. Because the algorithm is deterministic and uses no network, random values, or model calls, the same input and offset always produce the same output.
For batch jobs, record the offset beside the source video version: a subtitle correction derived for one edit may be wrong after an intro, advertisement, or distributor slate is inserted into another cut.
What you can do with it
Correct a constant audio delay
Move all captions later when a muxing or playback setup makes subtitle cues appear ahead of the spoken audio.
Account for a removed intro
Move every cue earlier after a fixed-length leader, countdown, or title sequence is removed from the video.
Normalize a delivery file
Apply a measured editorial offset and receive consistently formatted SRT timing before packaging subtitles with a release.
FAQ
Should I use a positive or negative offset?
Use a positive offset to make subtitles appear later and a negative offset to make them appear earlier.
Does this fix subtitle drift?
No. A constant offset fixes a constant timing difference. A difference that grows through the video requires a rate or frame-timing correction.
Will caption text or numbering change?
No. Cue numbers and caption lines are retained; only timestamp lines are replaced, while line endings and block spacing are normalized.
What happens if one cue has an invalid timestamp?
The entire request fails with an invalid-input error identifying the affected entry, so no partially shifted file is returned.
What happens to subtitles that would start before zero?
They are clamped to 00:00:00,000. With a large negative shift, a line that starts and ends early can shrink to no duration at all, so review the first subtitles afterwards and remove or retime any that no longer show.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/video/subtitle-srt-shift \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"srt":"1\n00:00:01,250 --> 00:00:03,500\nOpening line.\n\n2\n00:00:05,000 --> 00:00:07,250\nSecond line.","offset_ms":-1500}'const res = await fetch("https://api.kit.forhosting.com/video/subtitle-srt-shift", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"srt": "1\n00:00:01,250 --> 00:00:03,500\nOpening line.\n\n2\n00:00:05,000 --> 00:00:07,250\nSecond line.",
"offset_ms": -1500
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/video/subtitle-srt-shift",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"srt": "1\n00:00:01,250 --> 00:00:03,500\nOpening line.\n\n2\n00:00:05,000 --> 00:00:07,250\nSecond line.",
"offset_ms": -1500
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/video/subtitle-srt-shift", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"srt":"1\\n00:00:01,250 --> 00:00:03,500\\nOpening line.\\n\\n2\\n00:00:05,000 --> 00:00:07,250\\nSecond line.","offset_ms":-1500}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"srt":"1\n00:00:01,250 --> 00:00:03,500\nOpening line.\n\n2\n00:00:05,000 --> 00:00:07,250\nSecond line.","offset_ms":-1500}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/video/subtitle-srt-shift", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"srt": "1\n00:00:01,250 --> 00:00:03,500\nOpening line.\n\n2\n00:00:05,000 --> 00:00:07,250\nSecond line.",
"offset_ms": -1500
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "video.subtitle_srt_shift",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 500 |
max_minutes | 60 |
max_megapixels | 3.9 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
Choose the offset from a reliable sync point
This makes the result easy to save as a new `.srt` file and compare with the original before it replaces anything in a production workflow.
Understand validation and zero clamping
Timing values use the standard `hours:minutes:seconds,milliseconds` form, with two or more hour digits, two minute digits, two second digits, and exactly three millisecond digits. If a result falls below zero, it becomes exactly `00:00:00,000`.
Use the shifted file safely in a subtitle workflow
You can run it interactively in the browser or automate it through the API for $0.002 per request.
Technical questions
What happens when a timestamp shifts below zero?
That individual start or end timestamp is clamped to `00:00:00,000`; it never becomes negative.
How much does an API request cost?
The API price is $0.002 per request. The same deterministic operation is also available in the browser.