ArtyShield APIs
This page is the shared API hub for ArtyShield services.
Start with quickstart + results polling for each product. Use the Results Webhooks section when you are ready for push-based result delivery.
You can create API keys and manage webhook subscriptions in your Dashboard under API Keys and Webhooks.
Base URL: https://api.artyshield.ai/functions/v1
Authentication
Get both keys from Dashboard → API Keys:
- •
<USER_API_KEY>: create it in the API Keys table (full value is shown only once at creation). - •
<ANON_KEY>: copy it from the Gateway Key panel on Dashboard → API Keys.
- •
X-API-Key: <USER_API_KEY> - •
apikey: <ANON_KEY> - •
Authorization: Bearer <ANON_KEY>
Header example:
X-API-Key: USER_API_KEY
apikey: ANON_KEY
Authorization: Bearer ANON_KEY
MusicShield (Remote URL Ingestion)
MusicShield API supports a single-pass flow with one request. Submit a remote HTTPS audio URL and MusicShield will ingest it and queue protection in one call. Completed jobs provide a protected track download URL, noise footprint URL, and protection metrics.
Supported formats: WAV, MP3, FLAC. Remote file size limit is 100MB.
| Method | Path | Description |
|---|---|---|
POST |
/musicshield-from-url |
Single-pass endpoint: ingest URL + queue MusicShield protection + return protectionId and version. |
GET |
/protections-get/{id} |
Poll one protection and read signed download URLs for protected track/noise footprint. |
GET |
/credits-balance |
Current credits, plan, and timestamp. |
MusicShield Pricing Logic
- • Protection cost is computed as
ceil(durationSeconds / 60) * 25 credits. - • If
analysis: true, add 15 credits per track for protection analysis. - • MusicShield protection is billed at 25 credits per minute.
- • If balance is insufficient, queueing returns 402 Payment Required.
MusicShield Quickstart
Select Python or JavaScript and run URL ingestion + protection queue in one script.
If you send analysis: true, the protection job may complete before analysis is finalized. Continue polling /protections-get/{id} and check protection_metrics.protection_analysis.status.
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
}
queue = requests.post(
f"{base}/musicshield-from-url",
headers=headers,
json={
"url": "https://cdn.example.com/catalog/mixdown.wav",
"filename": "mixdown.wav",
"protectionStrength": 5,
"analysis": True,
# Optional request-level callback:
# "webhookUrl": "https://your-app.example.com/webhooks/artyshield",
# "webhookSecret": "whsec_your_signing_secret",
},
timeout=300,
)
queue.raise_for_status()
data = queue.json()
print("fileId:", data["fileId"])
print("protectionId:", data["protectionId"])
print("version:", data.get("version"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
};
async function runMusicShield() {
const queueResp = await fetch(`${base}/musicshield-from-url`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://cdn.example.com/catalog/mixdown.wav",
filename: "mixdown.wav",
protectionStrength: 5,
analysis: true,
// Optional request-level callback:
// webhookUrl: "https://your-app.example.com/webhooks/artyshield",
// webhookSecret: "whsec_your_signing_secret",
}),
});
if (!queueResp.ok) {
const body = await queueResp.text();
throw new Error(`Queue failed (${queueResp.status}): ${body}`);
}
const queue = await queueResp.json();
console.log("fileId:", queue.fileId);
console.log("protectionId:", queue.protectionId);
console.log("version:", queue.version);
}
runMusicShield().catch(console.error);
MusicShield Results Polling
Poll /protections-get/{id} until protection reaches completed or failed. When analysis: true, keep polling after protection completion until protection_metrics.protection_analysis.status is completed or failed.
import time
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
}
protection_id = "PUT_PROTECTION_ID_HERE"
analysis_requested = True # set to False if you queued with analysis=False
while True:
r = requests.get(f"{base}/protections-get/{protection_id}", headers=headers, timeout=60)
r.raise_for_status()
data = r.json()
status = (data.get("status") or "").lower()
metrics = data.get("protection_metrics") or {}
analysis = metrics.get("protection_analysis") or {}
analysis_status = (analysis.get("status") or "").lower() if isinstance(analysis, dict) else ""
print("status:", status or "unknown", "| analysis_status:", analysis_status or "n/a")
if status == "failed":
break
if status == "completed":
if not analysis_requested:
break
if analysis_status in {"completed", "failed"}:
break
time.sleep(4)
if status == "completed":
print("version:", data.get("version"))
print("snr_db:", data.get("snr_db"))
print("protected_download_url:", data.get("protected_download_url"))
print("noise_footprint_download_url:", data.get("noise_footprint_download_url"))
if analysis_requested:
if analysis:
print("analysis_status:", analysis.get("status"))
print("analysis_summary:", analysis.get("summary") or analysis.get("text"))
else:
print("analysis_status: not_available_yet")
else:
print("error:", data.get("error_msg"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function pollProtection(protectionId, { analysisRequested = true } = {}) {
while (true) {
const response = await fetch(`${base}/protections-get/${protectionId}`, { headers });
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const data = await response.json();
const status = String(data.status || "").toLowerCase();
const metrics = data.protection_metrics || {};
const analysis = metrics.protection_analysis || {};
const analysisStatus = String(analysis.status || "").toLowerCase();
console.log("status:", status || "unknown", "| analysis_status:", analysisStatus || "n/a");
if (status === "failed") return data;
if (status === "completed") {
if (!analysisRequested) return data;
if (analysisStatus === "completed" || analysisStatus === "failed") return data;
}
await sleep(4000);
}
}
async function run() {
const protectionId = "PUT_PROTECTION_ID_HERE";
const analysisRequested = true; // set false if queued with analysis: false
const data = await pollProtection(protectionId, { analysisRequested });
if (data.status === "completed") {
console.log("version:", data.version);
console.log("snr_db:", data.snr_db);
console.log("protected_download_url:", data.protected_download_url);
console.log("noise_footprint_download_url:", data.noise_footprint_download_url);
if (analysisRequested) {
const metrics = data.protection_metrics || {};
const analysis = metrics.protection_analysis || {};
if (Object.keys(analysis).length) {
console.log("analysis_status:", analysis.status);
console.log("analysis_summary:", analysis.summary || analysis.text);
} else {
console.log("analysis_status: not_available_yet");
}
}
} else {
console.log("error:", data.error_msg);
}
}
run().catch(console.error);
VeriTune (Remote URL Ingestion)
Integrations can POST HTTPS audio URLs directly. ArtyShield fetches each URL in the Edge Function, stores it in your project storage, then queues detection.
Minimum duration requirement: submitted audio must be longer than 10 seconds. Remote file size limit is 100MB.
| Method | Path | Description |
|---|---|---|
POST |
/veritune-detect-from-url |
One-call flow: ingest URL + queue detection + return detectionId and version. |
GET |
/detections-get/{id} |
Poll one detection. |
GET |
/detections-list?status=&limit=&cursor= |
Paginated history (newest first). |
GET |
/credits-balance |
Current credits, plan, and timestamp. |
VeriTune Pricing Logic
- • VeriTune detection: 5 credits per track
- • Explainability add-on: +15 credits only when:
- - explainability is requested, and
- - the track is detected as AI and explainability is generated.
VeriTune Quickstart
Select Python or JavaScript and run remote URL ingestion + detection.
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
}
upload = requests.post(
f"{base}/veritune-detect-from-url",
headers=headers,
json={
"url": "https://cdn.example.com/track.wav",
"filename": "track.wav",
"explain": True
},
timeout=180,
)
upload.raise_for_status()
result = upload.json()
print("detectionId:", result["detectionId"])
print("version:", result.get("version"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
};
async function runVeriTune() {
const response = await fetch(`${base}/veritune-detect-from-url`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://cdn.example.com/track.wav",
filename: "track.wav",
explain: true
}),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const data = await response.json();
console.log("detectionId:", data.detectionId);
console.log("version:", data.version);
}
runVeriTune().catch(console.error);
VeriTune Results Polling
Poll a VeriTune detection until completion, then read detection results and explainability outputs if present.
import time
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
}
detection_id = "PUT_DETECTION_ID_HERE"
while True:
r = requests.get(f"{base}/detections-get/{detection_id}", headers=headers, timeout=60)
r.raise_for_status()
data = r.json()
status = data.get("status")
print("status:", status)
if status in {"completed", "failed"}:
break
time.sleep(4)
details = data.get("details") or {}
detection = details.get("detection") or {}
print("version:", data.get("version"))
print("ai_probability:", detection.get("ai_probability"))
print("human_probability:", detection.get("human_probability"))
artifacts = detection.get("explainability_artifacts") or {}
analysis = detection.get("analysis") or {}
if artifacts:
print("slice_mp3_path:", artifacts.get("slice_mp3_path"))
print("figure_path:", artifacts.get("evidence_areas_png_path"))
if analysis:
print("analysis_text:", analysis.get("text"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function pollDetection(detectionId) {
while (true) {
const response = await fetch(`${base}/detections-get/${detectionId}`, { headers });
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const data = await response.json();
const status = data.status;
console.log("status:", status);
if (status === "completed" || status === "failed") {
return data;
}
await sleep(4000);
}
}
async function run() {
const detectionId = "PUT_DETECTION_ID_HERE";
const data = await pollDetection(detectionId);
const details = data.details || {};
const detection = details.detection || {};
console.log("version:", data.version);
console.log("ai_probability:", detection.ai_probability);
console.log("human_probability:", detection.human_probability);
const artifacts = detection.explainability_artifacts || {};
const analysis = detection.analysis || {};
if (Object.keys(artifacts).length) {
console.log("slice_mp3_path:", artifacts.slice_mp3_path);
console.log("figure_path:", artifacts.evidence_areas_png_path);
}
if (Object.keys(analysis).length) {
console.log("analysis_text:", analysis.text);
}
}
run().catch(console.error);
VeriVoice (Remote URL Ingestion)
Integrations can POST HTTPS audio URLs directly. ArtyShield fetches each URL in the Edge Function, stores it in your project storage, then queues voice-clone detection.
Minimum duration requirement: submitted audio must be longer than 3 seconds. Remote file size limit is 100MB.
| Method | Path | Description |
|---|---|---|
POST |
/verivoice-detect-from-url |
One-call flow: ingest URL + queue detection + return detectionId and version. |
GET |
/detections-get/{id} |
Poll one detection. |
GET |
/detections-list?status=&limit=&cursor= |
Paginated history (newest first). |
GET |
/credits-balance |
Current credits, plan, and timestamp. |
VeriVoice Pricing Logic
- • Detection is queued through
/verivoice-detect-from-url. - • Credit policy is controlled by your deployment configuration and backend env vars.
- • Recommended: verify your current credits behavior in staging before production rollout.
VeriVoice Quickstart
Select Python or JavaScript and run remote URL ingestion + queue detection.
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
}
upload = requests.post(
f"{base}/verivoice-detect-from-url",
headers=headers,
json={
"url": "https://cdn.example.com/voice.wav",
"filename": "voice.wav",
"explain": True
},
timeout=180,
)
upload.raise_for_status()
result = upload.json()
print("detectionId:", result["detectionId"])
print("version:", result.get("version"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
"Content-Type": "application/json",
};
async function runVeriVoice() {
const response = await fetch(`${base}/verivoice-detect-from-url`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://cdn.example.com/voice.wav",
filename: "voice.wav",
explain: true
}),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const data = await response.json();
console.log("detectionId:", data.detectionId);
console.log("version:", data.version);
}
runVeriVoice().catch(console.error);
VeriVoice Results Polling
Poll a VeriVoice detection until completion, then read detection results and explainability outputs if present.
import time
import requests
base = "https://api.artyshield.ai/functions/v1"
headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
}
detection_id = "PUT_VERIVOICE_DETECTION_ID_HERE"
while True:
r = requests.get(f"{base}/detections-get/{detection_id}", headers=headers, timeout=60)
r.raise_for_status()
data = r.json()
status = data.get("status")
print("status:", status)
if status in {"completed", "failed"}:
break
time.sleep(4)
details = data.get("details") or {}
detection = details.get("detection") or {}
print("version:", data.get("version"))
print("ai_probability:", detection.get("ai_probability"))
print("human_probability:", detection.get("human_probability"))
artifacts = detection.get("explainability_artifacts") or {}
analysis = detection.get("analysis") or {}
if artifacts:
print("slice_mp3_path:", artifacts.get("slice_mp3_path"))
print("figure_path:", artifacts.get("evidence_areas_png_path"))
if analysis:
print("analysis_text:", analysis.get("text"))
const base = "https://api.artyshield.ai/functions/v1";
const headers = {
"X-API-Key": "USER_API_KEY",
"apikey": "ANON_KEY",
"Authorization": "Bearer ANON_KEY",
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function pollDetection(detectionId) {
while (true) {
const response = await fetch(`${base}/detections-get/${detectionId}`, { headers });
if (!response.ok) {
const body = await response.text();
throw new Error(`HTTP ${response.status}: ${body}`);
}
const data = await response.json();
const status = data.status;
console.log("status:", status);
if (status === "completed" || status === "failed") {
return data;
}
await sleep(4000);
}
}
async function run() {
const detectionId = "PUT_VERIVOICE_DETECTION_ID_HERE";
const data = await pollDetection(detectionId);
const details = data.details || {};
const detection = details.detection || {};
console.log("version:", data.version);
console.log("ai_probability:", detection.ai_probability);
console.log("human_probability:", detection.human_probability);
const artifacts = detection.explainability_artifacts || {};
const analysis = detection.analysis || {};
if (Object.keys(artifacts).length) {
console.log("slice_mp3_path:", artifacts.slice_mp3_path);
console.log("figure_path:", artifacts.evidence_areas_png_path);
}
if (Object.keys(analysis).length) {
console.log("analysis_text:", analysis.text);
}
}
run().catch(console.error);
Results Webhooks
Use webhooks for push-based result delivery instead of polling. You can manage webhook subscriptions in Dashboard → Webhooks and API keys in Dashboard → API Keys.
- • Quickstart + polling remains the default integration path for all products.
- • For request-level callbacks, you can pass
webhookUrlandwebhookSecretin detection and MusicShield protection requests. - • Request-level callbacks currently deliver:
detection.completed,detection.failed,protection.completed,protection.failed. - • Dashboard-managed subscriptions currently deliver:
protection.completed,protection.failed.
Webhook Headers
| Header | Description |
|---|---|
X-ArtyShield-Event |
Event name from the supported webhook events currently delivered by ArtyShield. |
X-ArtyShield-Delivery-Id |
Unique delivery id for idempotency tracking. |
X-ArtyShield-Timestamp |
Unix timestamp used for signature calculation. |
X-ArtyShield-Signature |
Format: t=<timestamp>,v1=<hex_hmac_sha256>, generated from <timestamp>.<raw_body>. |
Webhook Payload Examples
Payload structure differs by event type. Use the detection shape for detection.* and the protection shape for protection.*.
Detection Event Example
{
"event": "detection.completed",
"deliveryId": "f42f2b14-6f59-46fd-95a7-c186f4093fb9",
"sentAt": "2026-02-25T22:00:00.000Z",
"data": {
"id": "result_uuid",
"status": "completed",
"product": "veritune",
"detector_version": "VeriTune",
"cost_credits": 5,
"details": {}
}
}
Protection Event Example
{
"event": "protection.completed",
"deliveryId": "f42f2b14-6f59-46fd-95a7-c186f4093fb9",
"sentAt": "2026-02-25T22:00:00.000Z",
"data": {
"id": "protection_uuid",
"status": "completed",
"protection_version": "MusicShield",
"snr_db": 24.7,
"duration_seconds": 95,
"cost_credits": 55,
"protected_expires_at": "2026-03-27T22:00:00.000Z",
"protection_metrics": {},
"input_file": { "id": "input_file_uuid", "filename": "mixdown.wav" },
"output_file": { "id": "output_file_uuid", "filename": "mixdown_protected.wav" },
"noise_footprint_file": { "id": "noise_file_uuid", "filename": "mixdown_noise.png" }
}
}
Signature Verification
import hashlib
import hmac
def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:
# header format: "t=1700000000,v1=abcdef..."
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
ts = parts.get("t", "")
received = parts.get("v1", "")
if not ts or not received:
return False
signed = f"{ts}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
import crypto from "crypto";
export function isValidSignature(rawBody, header, secret) {
// header format: "t=1700000000,v1=abcdef..."
const parts = Object.fromEntries(
String(header || "")
.split(",")
.map((item) => item.split("=", 2))
.filter(([k, v]) => k && v)
);
const ts = parts.t || "";
const received = parts.v1 || "";
if (!ts || !received) return false;
const signed = `${ts}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
if (expected.length !== received.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(received, "utf8"));
}
Minimal Receiver Endpoint
Return 2xx quickly after verification. Process heavy business logic asynchronously.
import hashlib
import hmac
import os
from flask import Flask, jsonify, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("ARTYSHIELD_WEBHOOK_SECRET", "")
def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
ts = parts.get("t", "")
received = parts.get("v1", "")
if not ts or not received:
return False
signed = f"{ts}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
@app.post("/webhooks/artyshield")
def artyshield_webhook():
raw_body = request.get_data() # exact bytes for signature check
signature = request.headers.get("X-ArtyShield-Signature", "")
if not is_valid_signature(raw_body, signature, WEBHOOK_SECRET):
return jsonify({"message": "invalid signature"}), 401
payload = request.get_json(silent=True) or {}
event = payload.get("event")
result = payload.get("data") or {}
# TODO: enqueue internal processing using deliveryId for idempotency
_ = event, result
return jsonify({"ok": True}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
import crypto from "crypto";
import express from "express";
const app = express();
const WEBHOOK_SECRET = process.env.ARTYSHIELD_WEBHOOK_SECRET || "";
function isValidSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header || "")
.split(",")
.map((item) => item.split("=", 2))
.filter(([k, v]) => k && v)
);
const ts = parts.t || "";
const received = parts.v1 || "";
if (!ts || !received) return false;
const signed = `${ts}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
if (expected.length !== received.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(received, "utf8"));
}
app.post("/webhooks/artyshield", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const signature = req.get("X-ArtyShield-Signature") || "";
if (!isValidSignature(rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ message: "invalid signature" });
}
const payload = JSON.parse(rawBody);
const event = payload.event;
const result = payload.data || {};
// TODO: enqueue internal processing using deliveryId for idempotency
void event;
void result;
return res.status(200).json({ ok: true });
});
app.listen(8080, () => {
console.log("Webhook receiver listening on :8080");
});
Error Handling Notes
- • 401 Unauthorized: API key / auth header mismatch.
- • 402 Payment Required: insufficient credits.
- • 415 Unsupported Media Type: only WAV/MP3/FLAC accepted.
- • 400 Bad Request: remote file exceeds 100MB (returns
File too large).