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. Starter is the entry tier for developer access.
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
MusicShield protects audio against unauthorized model training and music generation misuse. The API supports a single-pass flow: submit a remote HTTPS audio URL and MusicShield will ingest it and queue protection in one call.
Supported formats: WAV, MP3, FLAC. Remote file size limits are 25MB on free plans and 100MB on paid plans. Free-plan uploads must be MP3.
| 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
- • MusicShield protection is billed at 20 credits per minute.
- • If
analysis: true, add 15 credits per track for protection analysis. - • ArtyShield estimates the initial charge when the URL is queued and reconciles final billing from the processed audio duration on completion.
- • If balance is insufficient, queueing returns or completes with 402 Payment Required.
MusicShield Quickstart
Submit a remote HTTPS audio URL and queue MusicShield protection in one request.
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);
VoiceShield
VoiceShield protects speech and voice clips against unauthorized voice cloning. The API supports a single-pass flow: submit a remote HTTPS audio URL and VoiceShield will ingest it and queue protection in one call.
Supported formats: WAV, MP3, FLAC. File size limits match MusicShield: 25MB on free plans and 100MB on paid plans. Free-plan uploads must be MP3.
| Method | Path | Description |
|---|---|---|
POST |
/voiceshield-from-url |
Single-pass endpoint: ingest URL + queue VoiceShield protection + return protectionId and version. |
GET |
/protections-get/{id} |
Poll one protection and read the signed protected voice download URL. |
GET |
/credits-balance |
Current credits, plan, and timestamp. |
VoiceShield Pricing Logic
- • VoiceShield protection is billed at 10 credits per minute.
- • ArtyShield estimates the initial charge when the URL is queued and reconciles final billing from the processed audio duration on completion.
- • If balance is insufficient, queueing returns or completes with 402 Payment Required.
VoiceShield Quickstart
Submit a remote HTTPS audio URL and queue VoiceShield protection in one request.
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}/voiceshield-from-url",
headers=headers,
json={
"url": "https://cdn.example.com/voice/voice_sample.wav",
"filename": "voice_sample.wav",
# 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 runVoiceShield() {
const queueResp = await fetch(`${base}/voiceshield-from-url`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://cdn.example.com/voice/voice_sample.wav",
filename: "voice_sample.wav",
// Optional request-level callback:
// webhookUrl: "https://your-app.example.com/webhooks/artyshield",
// webhookSecret: "whsec_your_signing_secret",
}),
});
if (!queueResp.ok) {
throw new Error(`Queue failed (${queueResp.status}): ${await queueResp.text()}`);
}
const queue = await queueResp.json();
console.log("fileId:", queue.fileId);
console.log("protectionId:", queue.protectionId);
console.log("version:", queue.version);
}
runVoiceShield().catch(console.error);
VoiceShield Results Polling
Poll /protections-get/{id} until the VoiceShield protection reaches completed or failed. Completed VoiceShield results include a signed protected voice download URL; there is no noise footprint or protection analysis output.
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"
while True:
response = requests.get(f"{base}/protections-get/{protection_id}", headers=headers, timeout=60)
response.raise_for_status()
data = response.json()
status = (data.get("status") or "").lower()
print("status:", status or "unknown")
if status in {"completed", "failed"}:
break
time.sleep(4)
if status == "completed":
print("version:", data.get("version"))
print("duration_seconds:", data.get("duration_seconds"))
print("cost_credits:", data.get("cost_credits"))
print("protected_download_url:", data.get("protected_download_url"))
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 pollVoiceShield(protectionId) {
while (true) {
const response = await fetch(`${base}/protections-get/${protectionId}`, { headers });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const data = await response.json();
const status = String(data.status || "").toLowerCase();
console.log("status:", status || "unknown");
if (status === "completed" || status === "failed") return data;
await sleep(4000);
}
}
async function run() {
const protectionId = "PUT_PROTECTION_ID_HERE";
const data = await pollVoiceShield(protectionId);
if (data.status === "completed") {
console.log("version:", data.version);
console.log("duration_seconds:", data.duration_seconds);
console.log("cost_credits:", data.cost_credits);
console.log("protected_download_url:", data.protected_download_url);
} else {
console.log("error:", data.error_msg);
}
}
run().catch(console.error);
VeriTune
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 clip
- • Explainability add-on: +15 credits only when:
- - explainability is requested, and
- - the clip 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
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
- • VeriVoice detection: 5 credits per clip
- • Explainability add-on: +15 credits only when:
- - explainability is requested, and
- - the clip is detected as AI and explainability is generated.
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, MusicShield, and VoiceShield 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 or VoiceShield",
"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).