Watermark processing
Save source-version marks and retrieve detailed watermark analysis from asynchronous tasks.
Watermark workflow
Section titled “Watermark workflow”Use the same quote → submit → poll → result flow as other tasks. The tool catalog includes watermark-find (find candidates), watermark-mask (track and preview a mask), and the removal tools available for the source kind. Read the catalog’s parameter choices before submitting. Candidate marks require review; saving them does not itself create a processing task.
- Run
watermark-findon the chosen source version and readwatermarkFindfrom its result. - Review the suggested regions and save the confirmed marks on that version.
- Quote and submit
watermark-maskwith the confirmed regions to preview tracking. - Inspect
maskPreviewUrland the analysis report, then submit the applicable removal tool with the same source version and confirmed parameters.
Save watermark marks
Section titled “Save watermark marks”This replaces all marks on the specified version. Coordinates are normalized from 0 to 1 with the top-left as the origin; x + width and y + height must stay within 1. timeMs identifies a video frame in milliseconds; omit it for a fixed position over the whole clip. Marks with the same objectId describe the same watermark.
positive marks indicate a watermark; negative excludes an area; absent marks an interval without that object; suggested is an unconfirmed candidate and is not executed. Pass timeMs for negative/absent/suggested marks, and objectId for absent/suggested marks. endMs, when used, must be at least timeMs. Use at most 64 total marks and eight objects, with at most 32 positive regions for processing. Sending regions: [] clears all marks; omitting objects clears their names.
The example saves a confirmed rectangle for the first frame. This persists annotations only: pass your confirmed regions in the task’s params when requesting tracking or removal. It does not automatically start or configure a task.
/api/v1/assets/{assetId}/versions/{versionId}/watermark-marksPath, query and headers
| Parameter | Location | Type & constraints |
|---|---|---|
assetIdrequired | path | string |
versionIdrequired | path | string |
Request body fields · application/json
objectsobject[]optionalmaxItems: 8
Expand fields / variants
namestringrequiredminLength: 1 · maxLength: 24
objectIdinteger · int64requiredminimum: 0 · maximum: 7
regionsobject[]requiredmaxItems: 64
Expand fields / variants
colorstringoptionalpattern: "^#[0-9a-fA-F]{6}$"
endMsinteger · int64optionalminimum: 0
heightnumber · doublerequiredminimum: 0 · maximum: 1
kind"positive" | "negative" | "absent" | "suggested"optionalobjectIdinteger · int64optionalminimum: 0 · maximum: 7
strokesobject[]optionalmaxItems: 64
Expand fields / variants
pointsnumber · double[][]requiredminItems: 1 · maxItems: 4000
Expand fields / variants
number · doubleradiusnumber · doublerequiredminimum: 0 · maximum: 0.25
timeMsinteger · int64optionalminimum: 0
widthnumber · doublerequiredminimum: 0 · maximum: 1
xnumber · doublerequiredminimum: 0 · maximum: 1
ynumber · doublerequiredminimum: 0 · maximum: 1
Request example
curl --include --fail-with-body --silent --show-error --max-time 60 \ --request PUT 'https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks' \ --header "Authorization: Bearer $GLACIER_API_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}'// Node.js 22+; save as example.mjs, then: node example.mjsconst key = process.env.GLACIER_API_KEY;if (!key) throw new Error('Set GLACIER_API_KEY');const response = await fetch('https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks', { method: 'PUT', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}), signal: AbortSignal.timeout(60_000),});const text = await response.text();if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);console.log(response.status, text);# Python 3.10+; standard library; python example.pyimport osimport urllib.requestimport urllib.error
headers = {"Authorization": "Bearer " + os.environ["GLACIER_API_KEY"]}headers["Content-Type"] = "application/json"request = urllib.request.Request( "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks", method="PUT", headers=headers, data="""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""".encode("utf-8"),)try: with urllib.request.urlopen(request, timeout=60) as response: print(response.status, response.read().decode("utf-8"))except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from error// Go 1.22+; go run example.gopackage main
import ( "fmt" "io" "net/http" "os" "strings" "time")
func main() { key := os.Getenv("GLACIER_API_KEY") if key == "" { panic("Set GLACIER_API_KEY") } req, err := http.NewRequest("PUT", "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks", strings.NewReader(`{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}`)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer " + key) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 60 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { panic(err) } if res.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, data)) } fmt.Println(res.StatusCode, string(data))}// cargo add reqwest@0.13 --features blocking// Save in src/main.rs, then: cargo runuse std::{env, error::Error, time::Duration};
fn main() -> Result<(), Box<dyn Error>> { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(60)).build()?; let response = client.request(reqwest::Method::PUT, "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks") .bearer_auth(env::var("GLACIER_API_KEY")?) .header("Content-Type", "application/json") .body(r#"{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}"#) .send()?; let status = response.status(); let text = response.text()?; if !status.is_success() { return Err(format!("HTTP {status}: {text}").into()); } println!("{status} {text}"); Ok(())}// Java 17+; save as Example.java, then: java Example.javaimport java.net.URI;import java.net.http.*;import java.time.Duration;import java.util.Objects;
class Example { public static void main(String[] args) throws Exception { var key = Objects.requireNonNull(System.getenv("GLACIER_API_KEY")); var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var request = HttpRequest.newBuilder(URI.create("https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString("""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""")) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body()); System.out.println(response.statusCode() + " " + response.body()); }}// .NET 8+; dotnet new console; replace Program.cs; dotnet runusing System;using System.Net.Http;using System.Net.Http.Headers;using System.Text;
var key = Environment.GetEnvironmentVariable("GLACIER_API_KEY") ?? throw new InvalidOperationException("Set GLACIER_API_KEY");using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };using var request = new HttpRequestMessage(new HttpMethod("PUT"), "https://app.berserkapi.com/api/v1/assets/ASSET_ID/versions/VERSION_ID/watermark-marks");request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);request.Content = new StringContent("""{ "regions": [ { "x": 0.7, "y": 0.05, "width": 0.2, "height": 0.1, "timeMs": 0, "objectId": 0, "kind": "positive" } ], "objects": [ { "id": 0, "name": "Logo" } ]}""", Encoding.UTF8, "application/json");using var response = await client.SendAsync(request);var text = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {text}");Console.WriteLine($"{(int)response.StatusCode} {text}");Response fields · 204
No response body. Do not attempt to parse empty JSON.
Read detailed analysis
Section titled “Read detailed analysis”Use the analysis task ID or the watermarkAnalysisUrl returned with a result. This is an authenticated JSON API request, not a signed file download. Reports include objects, time intervals and regions that may need review. Use them together with the mask preview before removal.
/api/v1/tasks/{taskId}/watermark-analysisPath, query and headers
| Parameter | Location | Type & constraints |
|---|---|---|
taskIdrequired | path | string |
Request example
curl --include --fail-with-body --silent --show-error --max-time 60 \ --request GET 'https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis' \ --header "Authorization: Bearer $GLACIER_API_KEY"// Node.js 22+; save as example.mjs, then: node example.mjsconst key = process.env.GLACIER_API_KEY;if (!key) throw new Error('Set GLACIER_API_KEY');const response = await fetch('https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis', { method: 'GET', headers: { Authorization: `Bearer ${key}`, }, signal: AbortSignal.timeout(60_000),});const text = await response.text();if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);console.log(response.status, text);# Python 3.10+; standard library; python example.pyimport osimport urllib.requestimport urllib.error
headers = {"Authorization": "Bearer " + os.environ["GLACIER_API_KEY"]}request = urllib.request.Request( "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis", method="GET", headers=headers,)try: with urllib.request.urlopen(request, timeout=60) as response: print(response.status, response.read().decode("utf-8"))except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from error// Go 1.22+; go run example.gopackage main
import ( "fmt" "io" "net/http" "os" "time")
func main() { key := os.Getenv("GLACIER_API_KEY") if key == "" { panic("Set GLACIER_API_KEY") } req, err := http.NewRequest("GET", "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer " + key) client := &http.Client{Timeout: 60 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { panic(err) } if res.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, data)) } fmt.Println(res.StatusCode, string(data))}// cargo add reqwest@0.13 --features blocking// Save in src/main.rs, then: cargo runuse std::{env, error::Error, time::Duration};
fn main() -> Result<(), Box<dyn Error>> { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(60)).build()?; let response = client.request(reqwest::Method::GET, "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis") .bearer_auth(env::var("GLACIER_API_KEY")?) .send()?; let status = response.status(); let text = response.text()?; if !status.is_success() { return Err(format!("HTTP {status}: {text}").into()); } println!("{status} {text}"); Ok(())}// Java 17+; save as Example.java, then: java Example.javaimport java.net.URI;import java.net.http.*;import java.time.Duration;import java.util.Objects;
class Example { public static void main(String[] args) throws Exception { var key = Objects.requireNonNull(System.getenv("GLACIER_API_KEY")); var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var request = HttpRequest.newBuilder(URI.create("https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + key) .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 300) throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body()); System.out.println(response.statusCode() + " " + response.body()); }}// .NET 8+; dotnet new console; replace Program.cs; dotnet runusing System;using System.Net.Http;using System.Net.Http.Headers;using System.Text;
var key = Environment.GetEnvironmentVariable("GLACIER_API_KEY") ?? throw new InvalidOperationException("Set GLACIER_API_KEY");using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://app.berserkapi.com/api/v1/tasks/TASK_ID/watermark-analysis");request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);using var response = await client.SendAsync(request);var text = await response.Content.ReadAsStringAsync();if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {text}");Console.WriteLine($"{(int)response.StatusCode} {text}");Response fields · 200
detectionVersionstringrequiredframesinteger · int64requiredheightinteger · int64requiredmarksHashstringrequiredobjectsobject[]requiredExpand fields / variants
colorobjectrequiredExpand fields / variants
alphanumber · doublerequiredrgbinteger · int64[]requiredminItems: 3 · maxItems: 3
Expand fields / variants
integer · int64samplesinteger · int64required
intervalsobject[]requiredExpand fields / variants
endFrameinteger · int64requiredendMsinteger · int64requiredreasonsstring[]requiredExpand fields / variants
stringsourcestringrequiredstartFrameinteger · int64requiredstartMsinteger · int64requiredstate"present" | "bridge" | "manual" | "uncertain" | "absent"required
namestringrequiredobjectIdinteger | null · int64requiredposeobject[]requiredExpand fields / variants
detailstringrequiredokbooleanrequiredreasonstringrequiredsourcestringrequired
shapeobjectrequiredExpand fields / variants
qualitystringrequiredreasonstringrequired
policystringrequiredratenumber · doublerequiredreviewobject[]requiredExpand fields / variants
boxobjectrequiredExpand fields / variants
heightnumber · doublerequiredminimum: 0 · maximum: 1
widthnumber · doublerequiredminimum: 0 · maximum: 1
xnumber · doublerequiredminimum: 0 · maximum: 1
ynumber · doublerequiredminimum: 0 · maximum: 1
endFrameinteger · int64requiredendMsinteger · int64requirednamestringrequiredobjectIdinteger | null · int64requiredreasonstringrequiredscorenumber | null · doublerequiredstartFrameinteger · int64requiredstartMsinteger · int64required
staticobject[]requiredExpand fields / variants
namestringrequiredobjectIdinteger | null · int64requiredreasonstringrequiredshapestringrequired
versioninteger · int64requiredwidthinteger · int64required