Included with Scale
The API comes with the Scale plan at no extra cost. Everything below is open to you: watch artists, read what we find, answer it, and follow a case to its outcome. Nothing here contacts anybody: a case carries the evidence and who to write to, and that part is yours.
Authenticating
Every request takes your key as a bearer token. Keys are created in the app, on the Scale plan.
curl https://www.artistsentry.com/api/v1/detections \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"
Scopes
A key is limited to the scopes it was created with. Anything outside them returns 403.
-
artists:read - List artists and their linked profiles.
-
artists:write - Create artists, import rosters, link profiles, trigger scans.
-
detections:read - List detections and deliveries.
-
detections:write - Answer detections and acknowledge deliveries. Disputing opens cases.
-
disputes:read - List takedown cases, their evidence and who to contact.
-
disputes:write - Record that a service was contacted, and how a case ended.
Rate limits
120 requests a minute, shared across every key on the team.
Every response carries the two headers alongside. Once Remaining hits zero the next request is answered 429, with Retry-After giving the seconds to wait. If you need a higher limit, reach out to our team.
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
Pagination
Every list takes page and per_page, and answers with the same links and meta alongside the data. per_page tops out at 200 and defaults to 50.
Follow links.next until it is null. meta.links is there for drawing a pager and can be ignored.
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [ … ],
"links": {
"first": "https://www.artistsentry.com/api/v1/detections?page=1",
"last": "https://www.artistsentry.com/api/v1/detections?page=4",
"prev": null,
"next": "https://www.artistsentry.com/api/v1/detections?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 4,
"path": "https://www.artistsentry.com/api/v1/detections",
"per_page": 50,
"to": 50,
"total": 183,
"links": [ … ]
}
}
Errors
- 401
- Missing, malformed or revoked key.
- 403
- Plan does not include the API, or the key lacks the required scope.
- 404
- No such record on this team. Returned instead of 403 so a key cannot probe another team.
- 422
- Validation failed. The response names the fields.
- 429
- Rate limit exceeded. Retry-After says how long to wait.
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"message": "Invalid ability provided."
}
Webhooks
We POST to a URL you own when something happens. Each payload carries the same resource the matching GET returns, so a consumer written against the API already parses it.
Signature
Every request carries ArtistSentry-Signature: t=<unix>,v1=<hmac>. v1 is HMAC-SHA256 of <t>.<raw body>, keyed with the endpoint secret.
Hash the raw request bytes. Re-serialising the JSON reorders keys and changes whitespace, and the signature will not match.
There can be more than one v1. While a rotation overlap is open the header carries a signature under the new secret and one under the old, current first, so a receiver that has deployed the new secret and one that has not both verify. Treat a match against any v1 as valid rather than parsing only the first or the last.
Reject timestamps older than 5 minutes. The timestamp is inside the signed string so it cannot be rewritten, but replaying the original request verifies fine without an age check.
Responses
2XX is success. Any other status retries on widening backoff (1m, 5m, 15m, 45m, 2h, 4h, 8h) for 24 hours, then the event is dropped. 4XX retries the same as 5XX.
Three consecutive dropped events disable the endpoint and email the team owner. Any 2XX resets the counter, and one arriving after an automatic switch-off turns the endpoint back on: an endpoint that has recovered does not stay dark on the strength of failures from a day ago. A pause you set yourself is never lifted this way.
Everything sent is kept: both sides of the exchange, headers and bodies, browsable per endpoint in your console. Deliveries that were given up on are kept far longer than ones that landed, since they are the only record of an event you were never told about.
Read timeout is 10 seconds. Acknowledge first, process after.
Ordering and duplicates
No ordering guarantee. Deliveries are rate limited to 60/min per endpoint and retried independently. Sort on occurred_at.
Expect duplicates. A 2XX arriving after the 10 second timeout is recorded as a failure and retried. Deduplicate on id, which is stable across retries and across endpoints.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_5310
ArtistSentry-Topic: ping
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_5310",
"type": "ping",
"api_version": "v1",
"occurred_at": "2026-08-17T18:30:00+00:00",
"data": {
"message": "This is a test from your ArtistSentry console.",
"endpoint": "Catalogue pipeline"
}
}
Constant-time comparison throughout. A plain equality check leaks the signature a byte at a time to anyone measuring how long you take to reject it.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VerifyArtistSentrySignature
{
public function handle(Request $request, Closure $next): Response
{
[$timestamp, $signatures] = $this->parse($request->header('ArtistSentry-Signature', ''));
$expected = hash_hmac(
'sha256',
$timestamp.'.'.$request->getContent(),
config('services.artistsentry.webhook_secret'),
);
$matched = false;
// Every v1 is checked. During a rotation overlap there are two.
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
$matched = true;
}
}
abort_unless($matched, 403);
abort_if(abs(time() - $timestamp) > 300, 403);
return $next($request);
}
/** @return array{0: int, 1: array<int, string>} */
private function parse(string $header): array
{
$timestamp = 0;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, '');
match ($key) {
't' => $timestamp = (int) $value,
'v1' => $signatures[] = $value,
default => null,
};
}
return [$timestamp, $signatures];
}
}
$secret = getenv('ARTISTSENTRY_SECRET');
$body = file_get_contents('php://input');
$timestamp = 0;
$signatures = [];
foreach (explode(',', $_SERVER['HTTP_ARTISTSENTRY_SIGNATURE'] ?? '') as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, '');
if ($key === 't') {
$timestamp = (int) $value;
} elseif ($key === 'v1') {
$signatures[] = $value;
}
}
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
$matched = false;
// Check every v1: a rotation overlap sends the old and the new.
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
$matched = true;
}
}
if (! $matched || abs(time() - $timestamp) > 300) {
http_response_code(403);
exit;
}
http_response_code(200);
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Raw body, not express.json(). Parsing and re-serialising breaks the hash.
app.post('/hooks', express.raw({ type: 'application/json' }), (req, res) => {
const parts = (req.get('ArtistSentry-Signature') || '')
.split(',')
.map((p) => p.split('='));
const timestamp = parts.find(([k]) => k === 't')?.[1] ?? '';
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
const expected = crypto
.createHmac('sha256', process.env.ARTISTSENTRY_SECRET)
.update(`${timestamp}.`)
.update(req.body)
.digest();
// Any v1 may match: a rotation overlap sends the old and the new.
const matched = signatures.some((signature) => {
const received = Buffer.from(signature, 'hex');
return expected.length === received.length
&& crypto.timingSafeEqual(expected, received);
});
if (!matched) return res.sendStatus(403);
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(403);
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
});
// Web Crypto, so this runs on Workers, Deno, Bun and Node 18+.
const encoder = new TextEncoder();
export async function verify(request: Request, secret: string): Promise<string | null> {
const body = await request.text();
const parts = (request.headers.get('ArtistSentry-Signature') ?? '')
.split(',')
.map((p) => p.split('=') as [string, string]);
const timestamp = parts.find(([k]) => k === 't')?.[1] ?? '';
const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(`${timestamp}.${body}`));
const expected = [...new Uint8Array(mac)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
// Any v1 may match: a rotation overlap sends the old and the new.
if (!signatures.some((signature) => equals(expected, signature))) return null;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return null;
return body;
}
function equals(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
)
func Verify(r *http.Request, secret []byte) ([]byte, bool) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
var ts string
var sigs []string
for _, part := range strings.Split(r.Header.Get("ArtistSentry-Signature"), ",") {
switch k, v, _ := strings.Cut(part, "="); k {
case "t":
ts = v
case "v1":
sigs = append(sigs, v)
}
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(ts + "."))
mac.Write(body)
expected := []byte(hex.EncodeToString(mac.Sum(nil)))
// Any v1 may match: a rotation overlap sends the old and the new.
matched := false
for _, sig := range sigs {
if hmac.Equal(expected, []byte(sig)) {
matched = true
}
}
if !matched {
return nil, false
}
sec, err := strconv.ParseInt(ts, 10, 64)
if err != nil || time.Since(time.Unix(sec, 0)).Abs() > 5*time.Minute {
return nil, false
}
return body, true
}
import hashlib
import hmac
import time
def verify(header: str, body: bytes, secret: bytes) -> bool:
"""Pass the raw request body. Anything re-serialised will not match."""
timestamp = ""
signatures = []
for part in header.split(","):
key, _, value = part.partition("=")
if key == "t":
timestamp = value
elif key == "v1":
signatures.append(value)
expected = hmac.new(
secret,
f"{timestamp}.".encode() + body,
hashlib.sha256,
).hexdigest()
# Any v1 may match: a rotation overlap sends the old and the new.
if not any(hmac.compare_digest(expected, s) for s in signatures):
return False
return abs(time.time() - int(timestamp or 0)) <= 300
# Flask
# verify(request.headers.get("ArtistSentry-Signature", ""), request.get_data(), SECRET)
# Django
# verify(request.headers.get("ArtistSentry-Signature", ""), request.body, SECRET)
/api/v1/artists
List the artists on this team, with their linked profiles. Ordered by name.
artists:read
200
-
pageinteger - Which page to return. Default 1.
-
per_pageinteger - Max 200. Default 50.
- links and meta are the same on every list. See Pagination.
curl https://www.artistsentry.com/api/v1/artists \
-H "Authorization: Bearer YOUR_KEY"
{
"data": [
{
"id": 12,
"name": "Corrin Ashe",
"slug": "corrin-ashe",
"monitoring_enabled": true,
"known_distributors": ["Fairlane Digital"],
"baselined_at": "2026-07-02T11:20:41+00:00",
"pending_count": 1,
"profiles": [
{
"id": 41,
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/artist/4kPCyOKK…",
"watched": true,
"last_scanned_at": "2026-08-13T04:12:09+00:00",
"needs_channel_choice": false,
"channel_candidates": []
}
]
}
],
"links": { … },
"meta": { … }
}
/api/v1/artists
Start watching an artist. The link is opened against the service before the artist is created, so a mistyped id is refused here rather than becoming a profile that is scanned forever and never returns anything.
artists:write
201
-
namestring required - Max 255.
-
profile_urlstring required - Artist page on Spotify, Apple Music, Deezer, TIDAL or YouTube Music. Any other platform is rejected.
-
https://open.spotify.com/artist/4kPCyOKKQXCPOOO4gMzOSj https://music.apple.com/gb/artist/corrin-ashe/1544912847 https://www.deezer.com/en/artist/8675309 https://tidal.com/browse/artist/39109089 https://www.youtube.com/@corrinashe https://www.youtube.com/channel/UC8B7Iq8sy2QuBpZpTfsjLmA -
known_distributorsstring[] - Distributors already in use. Releases from these are not questioned. Max 255 each.
- pending_count is only on the list.
curl -X POST https://www.artistsentry.com/api/v1/artists \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Corrin Ashe",
"profile_url": "https://open.spotify.com/artist/4kPCyOKK…"
}'
{
"data": {
"id": 12,
"name": "Corrin Ashe",
"slug": "corrin-ashe",
"monitoring_enabled": true,
"known_distributors": [],
"baselined_at": null,
"profiles": [
{
"id": 41,
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/artist/4kPCyOKK…",
"watched": true,
"last_scanned_at": null,
"needs_channel_choice": false,
"channel_candidates": []
}
]
}
}
/api/v1/artists/import
Create many artists at once. Each artist carries every link they are found under, so the same act on Spotify and TIDAL is one artist rather than two. Every link is opened against its service before the artist is created, so this is queued rather than answered here: poll the status url.
artists:write
202
-
artistsarray required - Max 500 per request.
-
artists[].namestring - Max 255. Read off the first link if you leave it out, and replaced by the real one at the first scan.
-
artists[].urlsarray required - One or more artist pages on Spotify, Apple Music, Deezer, TIDAL or YouTube Music. An artist with any unreadable link fails whole rather than being created half watched.
curl -X POST https://www.artistsentry.com/api/v1/artists/import \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"artists": [
{
"name": "Corrin Ashe",
"urls": [
"https://open.spotify.com/artist/4kPCyOKKQXCPOOO4gMzOSj",
"https://tidal.com/browse/artist/39109089"
]
},
{ "urls": ["https://www.deezer.com/en/artist/8675309"] }
]
}'
{
"data": {
"id": 4021,
"status": "queued",
"artists": 2,
"status_url": "https://www.artistsentry.com/api/v1/artists/imports/4021",
"added": [],
"failed": [],
"completed_at": null
}
}
/api/v1/artists/imports/{id}
Check how a roster is getting on. Answers while it runs, not only once it is over: added and failed fill in as the roster is worked through, so you can act on the artists already watching. Either artists scope reads it, so the key that started the import can always follow it. Status is queued, running, completed or failed.
artists:read
200
curl https://www.artistsentry.com/api/v1/artists/imports/4021 \
-H "Authorization: Bearer YOUR_KEY"
{
"data": {
"id": 4021,
"status": "completed",
"artists": 2,
"status_url": "https://www.artistsentry.com/api/v1/artists/imports/4021",
"added": [
{
"id": 12,
"name": "Corrin Ashe",
"slug": "corrin-ashe",
"monitoring_enabled": true,
"known_distributors": [],
"baselined_at": null,
"profiles": [
{
"id": 41,
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/artist/4kPCyOKK…",
"watched": true,
"last_scanned_at": null,
"needs_channel_choice": false,
"channel_candidates": []
}
]
}
],
"failed": [
{
"name": "Nobody",
"url": "https://example.com/nope",
"reason": "That doesn't look like an artist page link."
}
],
"completed_at": "2026-08-13T10:44:02+00:00"
}
}
/api/v1/artists/{id}/profiles
Link another service profile to an artist already being watched. Returns the artist with every profile. Linking one that is already there changes nothing and answers 200.
artists:write
201 · 200 if already linked
-
profile_urlstring required - Same platforms as above.
curl -X POST https://www.artistsentry.com/api/v1/artists/12/profiles \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"profile_url": "https://tidal.com/browse/artist/39109089"}'
{
"data": {
"id": 12,
"name": "Corrin Ashe",
"slug": "corrin-ashe",
"monitoring_enabled": true,
"known_distributors": [],
"baselined_at": "2026-07-02T11:20:41+00:00",
"profiles": [
{
"id": 41,
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/artist/4kPCyOKK…",
"watched": true,
"last_scanned_at": "2026-08-13T04:12:09+00:00",
"needs_channel_choice": false,
"channel_candidates": []
},
{
"id": 63,
"service": "tidal",
"service_name": "TIDAL",
"url": "https://tidal.com/browse/artist/39109089",
"watched": true,
"last_scanned_at": null,
"needs_channel_choice": false,
"channel_candidates": []
}
]
}
}
/api/v1/artists/{id}/profiles/{profileId}/rescan
Queue an immediate scan of one profile, rather than waiting for the next scheduled one. Returns the artist as it stands now; last_scanned_at moves once the scan runs.
artists:write
202
- The same artist object as everywhere else.
curl -X POST https://www.artistsentry.com/api/v1/artists/12/profiles/41/rescan \
-H "Authorization: Bearer YOUR_KEY"
{
"data": { "id": 12, "name": "Corrin Ashe", "…": "…" }
}
/api/v1/detections
List releases found on this team, newest first. Everything by default, not only what is waiting on an answer.
detections:read
200
-
statusstring - baseline, pending_review, confirmed, disputed, removed
-
artist_idinteger - Only releases by this artist.
-
sincedate - Only releases first seen on or after this date.
-
pageinteger - Which page to return. Default 1.
-
per_pageinteger - Max 200. Default 50.
- enriched_at null means we have not looked yet. distributor null with enriched_at set means nobody publishes one. The difference is reported rather than flattened.
- links and meta are the same on every list. See Pagination.
curl "https://www.artistsentry.com/api/v1/detections?status=pending_review" \
-H "Authorization: Bearer YOUR_KEY"
{
"data": [
{
"id": 4821,
"title": "Paper Streets",
"type": "album",
"status": "pending_review",
"needs_answer": true,
"artist": { "id": 12, "name": "Corrin Ashe" },
"released_on": "2026-08-11",
"first_seen_at": "2026-08-11T06:02:18+00:00",
"enriched_at": "2026-08-11T09:14:44+00:00",
"upc": "850214907733",
"isrc": "GBKQU2600114",
"label": "Broadwater Records",
"distributor": "Fairlane Digital",
"copyright": "℗ 2026 Broadwater Records",
"artwork_url": "https://i.scdn.co/image/ab67616d…",
"credited_artists": ["Corrin Ashe"],
"recordings": [
{ "isrc": "GBKQU2600114", "title": "Paper Streets" }
],
"matches_known_distributor": false,
"listings": [
{
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/album/…",
"distributor": null,
"first_seen_at": "2026-08-11T06:02:18+00:00",
"removed_at": null,
"needs_answer": false
}
]
}
],
"links": { … },
"meta": { … }
}
/api/v1/detections/{id}
Retrieve one detection.
detections:read
200
- The same fields as on the list.
curl https://www.artistsentry.com/api/v1/detections/4821 \
-H "Authorization: Bearer YOUR_KEY"
{
"data": { "id": 4821, "title": "Paper Streets", "…": "…" }
}
/api/v1/detections/{id}/answer
Confirm a release, or dispute it. Disputing opens a case for every service it is live on and returns them with their ids. Each case carries the evidence and who to contact; contacting them is yours to do.
detections:write
200
-
answerstring required - mine or not_mine.
-
claim_typestring - Required when answer is not_mine. misattribution, impersonation, infringement.
-
notestring - Kept on the release as your own note. Max 2000.
- data is the detection. disputes are whole cases, each with the evidence gathered for it and who to contact.
- Answering also settles any delivery on that release that was waiting on one, so those rows leave /deliveries. One release, one question.
- Answering again with a different claim_type re-points the open cases in place rather than adding more, since a different claim goes to a different desk. Cases you have already recorded contact on are left alone.
curl -X POST https://www.artistsentry.com/api/v1/detections/4821/answer \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"answer": "not_mine",
"claim_type": "misattribution",
"note": "Never heard of it."
}'
{
"data": { "id": 4821, "status": "disputed", "…": "…" },
"disputes": [
{ "id": 331, "status": "open", "service": "spotify", "…": "…" },
{ "id": 332, "status": "open", "service": "apple_music", "…": "…" }
]
}
/api/v1/deliveries
List deliveries waiting on an explanation: a second distributor turning up on a release the artist already has out. incumbent_distributor is the one that already held it.
detections:read
200
-
pageinteger - Which page to return. Default 1.
-
per_pageinteger - Max 200. Default 50.
- links and meta are the same on every list. See Pagination.
curl https://www.artistsentry.com/api/v1/deliveries \
-H "Authorization: Bearer YOUR_KEY"
{
"data": [
{
"id": 918,
"service": "youtube_music",
"service_name": "YouTube Music",
"distributor": "Northgate Media",
"incumbent_distributor": "Fairlane Digital",
"acknowledged_at": null,
"first_seen_at": "2026-08-12T22:41:07+00:00",
"release": {
"id": 4821,
"title": "Paper Streets",
"artist": "Corrin Ashe"
}
}
],
"links": { … },
"meta": { … }
}
/api/v1/deliveries/{id}/explain
Say a delivery was the artist's own doing. settled names every delivery the answer covered, including ones you have not seen yet.
detections:write
200
-
moving_libraryboolean required - true settles every other delivery from this distributor and remembers it against the artist, so releases that move across next week are never questioned. false settles this release only.
curl -X POST https://www.artistsentry.com/api/v1/deliveries/918/explain \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"moving_library": true}'
{
"data": { "id": 918, "acknowledged_at": "2026-08-13T…", "…": "…" },
"settled": [
{ "id": 918, "release_id": 4821,
"distributor": "Northgate Media" },
{ "id": 927, "release_id": 4833,
"distributor": "Northgate Media" }
]
}
/api/v1/disputes
List cases, newest first. One per service, because a correction has to reach each of them and each one answers separately. A case carries the evidence and who to contact; contacting them is yours to do.
disputes:read
200
-
statusstring - open, contacted, resolved, refused, dropped
-
openboolean - true for cases still in play, false for closed ones.
-
pageinteger - Which page to return. Default 1.
-
per_pageinteger - Max 200. Default 50.
- recipient is who the case was addressed to at the time, kept on the case itself rather than read back off a channel that can be re-pointed.
- links and meta are the same on every list. See Pagination.
curl "https://www.artistsentry.com/api/v1/disputes?open=true" \
-H "Authorization: Bearer YOUR_KEY"
{
"data": [
{
"id": 331,
"status": "open",
"is_open": true,
"claim_type": "misattribution",
"service": "spotify",
"service_name": "Spotify",
"reference": null,
"release": {
"id": 4821,
"title": "Paper Streets",
"artist": "Corrin Ashe",
"distributor": "Fairlane Digital"
},
"recipient": {
"name": "Fairlane Digital",
"address": "claims@fairlane.example",
"method": "email"
},
"evidence": { … },
"opened_at": "2026-08-11T09:20:03+00:00",
"contacted_at": null,
"closed_at": null,
"outcome_note": null
}
],
"links": { … },
"meta": { … }
}
/api/v1/disputes/{id}
Retrieve one case, including the evidence gathered for it.
disputes:read
200
- The same fields as on the list.
curl https://www.artistsentry.com/api/v1/disputes/331 \
-H "Authorization: Bearer YOUR_KEY"
{
"data": { "id": 331, "status": "open", "…": "…" }
}
/api/v1/disputes/{id}/contacted
Record that you got in touch. Nothing is sent from here, so this is your note of what you did.
disputes:write
200
-
referencestring - Their ticket or case number. Max 255.
curl -X POST https://www.artistsentry.com/api/v1/disputes/331/contacted \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"reference": "TICKET-4821"}'
{
"data": { "id": 331, "status": "contacted",
"reference": "TICKET-4821" }
}
/api/v1/disputes/{id}/outcome
Record how a case ended, wherever the reply landed.
disputes:write
200
-
statusstring required - resolved, refused, dropped. Anything else is refused: these are the states a case can end at, not every status it has held.
-
notestring - What they said. Max 2000.
curl -X POST https://www.artistsentry.com/api/v1/disputes/331/outcome \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "note": "Re-tagged at source."}'
{
"data": {
"id": 331,
"status": "resolved",
"is_open": false,
"closed_at": "2026-08-13T11:02:55+00:00",
"outcome_note": "Re-tagged at source."
}
}
detection.created
New release, awaiting an answer.
- Not raised on a profile's first scan. Baselining marks the existing catalogue as pre-existing, so linking a profile with a large back catalogue raises nothing.
- Sent after enrichment, so distributor, label, upc and isrc are already populated.
- Answer with POST /v1/detections/{id}/answer.
- data matches GET /v1/detections/{id}.
-
idinteger - Release id. Stable across every event about this release, and the id for /v1/detections/{id}.
-
titlestring - As the service reports it.
-
typestring - album, single, ep, compilation, appears_on.
-
statusstring - Always pending_review.
-
needs_answerboolean - Always true.
-
artistobject - id and name of the artist the release was found under.
-
released_onstring|null - YYYY-MM-DD. Services disagree by a day or so, so do not key on it.
-
first_seen_atstring - ISO 8601. First detection, not the release date.
-
enriched_atstring|null - When provenance was last resolved. Null when the lookup has not succeeded, which separates "no distributor published" from "not looked up yet".
-
upcstring|null - Barcode. The same release carries different barcodes on different services, so do not key on it.
-
isrcstring|null - Track level. On a multi-track release this is the lead recording; use recordings[] for the rest.
-
labelstring|null - Label as published.
-
distributorstring|null - Null when no service publishes one. Check enriched_at before treating null as absence.
-
copyrightstring|null - Copyright line as published.
-
artwork_urlstring|null - Hosted by the service. Not guaranteed to stay valid.
-
credited_artistsstring[] - Every credited name, including features. Empty when the service publishes none.
-
recordingsobject[] - Per track: title and isrc. Empty when none were resolved.
-
matches_known_distributorboolean - True when distributor appears in the artist's known_distributors.
-
listingsobject[] - One per service the release is live on.
-
listings[].servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik.
-
listings[].service_namestring - Display name for the service.
-
listings[].urlstring - The release on that service.
-
listings[].distributorstring|null - Who delivered it to this service. Can differ per service for one release.
-
listings[].first_seen_atstring - ISO 8601.
-
listings[].removed_atstring|null - Set once a scan no longer finds it. Null while live.
-
listings[].needs_answerboolean - True while this delivery is unacknowledged and still live.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4812
ArtistSentry-Topic: detection.created
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4812",
"type": "detection.created",
"api_version": "v1",
"occurred_at": "2026-08-17T04:12:11+00:00",
"data": {
"id": 991,
"title": "Cold Harbour",
"type": "single",
"status": "pending_review",
"needs_answer": true,
"artist": { "id": 12, "name": "Corrin Ashe" },
"released_on": "2026-08-14",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"enriched_at": "2026-08-17T04:12:11+00:00",
"upc": "0198004119353",
"isrc": "GBKQU2680114",
"label": "Fairlane Digital",
"distributor": "The state51 Conspiracy",
"copyright": "© 2026 Corrin Ashe",
"artwork_url": "https://i.scdn.co/image/ab67616d0000b273…",
"credited_artists": ["Corrin Ashe"],
"recordings": [{ "title": "Cold Harbour", "isrc": "GBKQU2680114" }],
"matches_known_distributor": false,
"listings": [
{
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/album/2xQ1mR…",
"distributor": "The state51 Conspiracy",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"removed_at": null,
"needs_answer": false
}
]
}
}
detection.answered
Release confirmed or disputed.
- Also raised for answers made over the API, so you will see your own writes.
- A disputed release additionally raises dispute.opened, one per live service.
- Confirming also raises delivery.explained for any delivery still open against the release.
-
idinteger - Release id. Stable across every event about this release, and the id for /v1/detections/{id}.
-
titlestring - As the service reports it.
-
typestring - album, single, ep, compilation, appears_on.
-
statusstring - confirmed or disputed.
-
needs_answerboolean - Always false.
-
artistobject - id and name of the artist the release was found under.
-
released_onstring|null - YYYY-MM-DD. Services disagree by a day or so, so do not key on it.
-
first_seen_atstring - ISO 8601. First detection, not the release date.
-
enriched_atstring|null - When provenance was last resolved. Null when the lookup has not succeeded, which separates "no distributor published" from "not looked up yet".
-
upcstring|null - Barcode. The same release carries different barcodes on different services, so do not key on it.
-
isrcstring|null - Track level. On a multi-track release this is the lead recording; use recordings[] for the rest.
-
labelstring|null - Label as published.
-
distributorstring|null - Null when no service publishes one. Check enriched_at before treating null as absence.
-
copyrightstring|null - Copyright line as published.
-
artwork_urlstring|null - Hosted by the service. Not guaranteed to stay valid.
-
credited_artistsstring[] - Every credited name, including features. Empty when the service publishes none.
-
recordingsobject[] - Per track: title and isrc. Empty when none were resolved.
-
matches_known_distributorboolean - True when distributor appears in the artist's known_distributors.
-
listingsobject[] - One per service the release is live on.
-
listings[].servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik.
-
listings[].service_namestring - Display name for the service.
-
listings[].urlstring - The release on that service.
-
listings[].distributorstring|null - Who delivered it to this service. Can differ per service for one release.
-
listings[].first_seen_atstring - ISO 8601.
-
listings[].removed_atstring|null - Set once a scan no longer finds it. Null while live.
-
listings[].needs_answerboolean - True while this delivery is unacknowledged and still live.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4813
ArtistSentry-Topic: detection.answered
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4813",
"type": "detection.answered",
"api_version": "v1",
"occurred_at": "2026-08-17T09:31:02+00:00",
"data": {
"id": 991,
"title": "Cold Harbour",
"type": "single",
"status": "disputed",
"needs_answer": false,
"artist": { "id": 12, "name": "Corrin Ashe" },
"released_on": "2026-08-14",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"enriched_at": "2026-08-17T04:12:11+00:00",
"upc": "0198004119353",
"isrc": "GBKQU2680114",
"label": "Fairlane Digital",
"distributor": "The state51 Conspiracy",
"copyright": "© 2026 Corrin Ashe",
"artwork_url": "https://i.scdn.co/image/ab67616d0000b273…",
"credited_artists": ["Corrin Ashe"],
"recordings": [{ "title": "Cold Harbour", "isrc": "GBKQU2680114" }],
"matches_known_distributor": false,
"listings": [
{
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/album/2xQ1mR…",
"distributor": "The state51 Conspiracy",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"removed_at": null,
"needs_answer": false
}
]
}
}
release.removed
Confirms a takedown landed.
- Derived from a scan rather than reported by a service, so it lags the actual removal by up to one scan interval.
- Only disputed releases reach this state. A confirmed release disappearing is not reported.
-
idinteger - Release id. Stable across every event about this release, and the id for /v1/detections/{id}.
-
titlestring - As the service reports it.
-
typestring - album, single, ep, compilation, appears_on.
-
statusstring - Always removed.
-
needs_answerboolean - Always false.
-
artistobject - id and name of the artist the release was found under.
-
released_onstring|null - YYYY-MM-DD. Services disagree by a day or so, so do not key on it.
-
first_seen_atstring - ISO 8601. First detection, not the release date.
-
enriched_atstring|null - When provenance was last resolved. Null when the lookup has not succeeded, which separates "no distributor published" from "not looked up yet".
-
upcstring|null - Barcode. The same release carries different barcodes on different services, so do not key on it.
-
isrcstring|null - Track level. On a multi-track release this is the lead recording; use recordings[] for the rest.
-
labelstring|null - Label as published.
-
distributorstring|null - Null when no service publishes one. Check enriched_at before treating null as absence.
-
copyrightstring|null - Copyright line as published.
-
artwork_urlstring|null - Hosted by the service. Not guaranteed to stay valid.
-
credited_artistsstring[] - Every credited name, including features. Empty when the service publishes none.
-
recordingsobject[] - Per track: title and isrc. Empty when none were resolved.
-
matches_known_distributorboolean - True when distributor appears in the artist's known_distributors.
-
listingsobject[] - One per service the release is live on.
-
listings[].servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik.
-
listings[].service_namestring - Display name for the service.
-
listings[].urlstring - The release on that service.
-
listings[].distributorstring|null - Who delivered it to this service. Can differ per service for one release.
-
listings[].first_seen_atstring - ISO 8601.
-
listings[].removed_atstring - ISO 8601. Non-null on every listing, which is what makes the release removed.
-
listings[].needs_answerboolean - True while this delivery is unacknowledged and still live.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_5104
ArtistSentry-Topic: release.removed
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_5104",
"type": "release.removed",
"api_version": "v1",
"occurred_at": "2026-08-29T03:14:55+00:00",
"data": {
"id": 991,
"title": "Cold Harbour",
"type": "single",
"status": "removed",
"needs_answer": false,
"artist": { "id": 12, "name": "Corrin Ashe" },
"released_on": "2026-08-14",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"enriched_at": "2026-08-17T04:12:11+00:00",
"upc": "0198004119353",
"isrc": "GBKQU2680114",
"label": "Fairlane Digital",
"distributor": "The state51 Conspiracy",
"copyright": "© 2026 Corrin Ashe",
"artwork_url": "https://i.scdn.co/image/ab67616d0000b273…",
"credited_artists": ["Corrin Ashe"],
"recordings": [{ "title": "Cold Harbour", "isrc": "GBKQU2680114" }],
"matches_known_distributor": false,
"listings": [
{
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/album/2xQ1mR…",
"distributor": "The state51 Conspiracy",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"removed_at": "2026-08-29T03:14:55+00:00",
"needs_answer": false
}
]
}
}
delivery.detected
Ambiguous delivery, awaiting an answer.
- Suppressed on baselines, on the first distributor for a release, and for distributors already in the artist's known_distributors.
- Scoped per service. Two services naming different distributors for one release is ordinary and is not reported.
- data matches an entry from GET /v1/deliveries.
-
idinteger - Release listing id. Pass this to POST /v1/deliveries/{id}/explain.
-
servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik.
-
service_namestring - Display name for the service.
-
distributorstring|null - Who made this delivery.
-
incumbent_distributorstring|null - Who already held the release on this service. distributor and incumbent_distributor differing is what raised the question.
-
acknowledged_atnull - Always null. The delivery is the question.
-
first_seen_atstring - ISO 8601.
-
releaseobject - id, title and artist of the release delivered.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4877
ArtistSentry-Topic: delivery.detected
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4877",
"type": "delivery.detected",
"api_version": "v1",
"occurred_at": "2026-08-18T02:44:18+00:00",
"data": {
"id": 2210,
"service": "spotify",
"service_name": "Spotify",
"distributor": "Symphonic Distribution",
"incumbent_distributor": "The state51 Conspiracy",
"acknowledged_at": null,
"first_seen_at": "2026-08-18T02:44:18+00:00",
"release": { "id": 991, "title": "Cold Harbour", "artist": "Corrin Ashe" }
}
}
delivery.explained
Delivery accounted for.
- One event per listing. A library-move answer settles every outstanding delivery from that distributor, so one answer can raise hundreds.
- Also raised when confirming a detection settles the deliveries still open against it.
- Disputing settles those deliveries too but raises nothing here, since a disputed delivery is not the artist's own. Read them off the detection.answered payload, where listings[].needs_answer is false.
- Never batched. Throughput is held by the endpoint rate limit.
-
idinteger - Release listing id. Pass this to POST /v1/deliveries/{id}/explain.
-
servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik.
-
service_namestring - Display name for the service.
-
distributorstring|null - Who made this delivery.
-
incumbent_distributorstring|null - Who already held the release on this service. distributor and incumbent_distributor differing is what raised the question.
-
acknowledged_atstring - ISO 8601. Always set, since the answer is what raised this.
-
first_seen_atstring - ISO 8601.
-
releaseobject - id, title and artist of the release delivered.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4901
ArtistSentry-Topic: delivery.explained
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4901",
"type": "delivery.explained",
"api_version": "v1",
"occurred_at": "2026-08-18T10:02:41+00:00",
"data": {
"id": 2210,
"service": "spotify",
"service_name": "Spotify",
"distributor": "Symphonic Distribution",
"incumbent_distributor": "The state51 Conspiracy",
"acknowledged_at": "2026-08-18T10:02:41+00:00",
"first_seen_at": "2026-08-18T02:44:18+00:00",
"release": { "id": 991, "title": "Cold Harbour", "artist": "Corrin Ashe" }
}
}
dispute.opened
Evidence pack and contact route for one service.
- One per live service. A release on nine services raises nine, each with its own id and recipient.
- Nothing is sent on your behalf. You contact the service and record the outcome.
- data matches GET /v1/disputes/{id}.
-
idinteger - Case id. Pass this to POST /v1/disputes/{id}/contacted and /outcome.
-
statusstring - Always open.
-
is_openboolean - Always true.
-
claim_typestring - misattribution, impersonation, infringement. Decides both the wording and which desk it goes to.
-
servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik. One case per service.
-
service_namestring - Display name for the service.
-
referencestring|null - Their ticket number, once you record one via /v1/disputes/{id}/contacted.
-
releaseobject - id, title, artist and distributor of the disputed release.
-
recipientobject - Where the claim goes. Snapshotted when the case opened, so it does not move if the channel is re-pointed later.
-
recipient.namestring|null - Name of the desk.
-
recipient.addressstring|null - An email address or a URL, depending on method.
-
recipient.methodstring|null - email, form, portal.
-
evidenceobject - The evidence pack, snapshotted when the case opened. Keys below.
-
evidence.captured_atstring - ISO 8601. The pack is accurate as of this moment and is never refreshed.
-
evidence.artistobject - name, plus profiles[] of dsp, url and external_id for every profile being watched.
-
evidence.releaseobject - title, type, release_date, upc, recordings[], label, distributor, copyright, credited_artists[] and artwork_url.
-
evidence.listingobject|null - The listing on this case's service: dsp, url, external_id, first_seen_at, last_seen_at. Null where the release was resolved onto the service but never enumerated there.
-
evidence.also_live_onobject[] - dsp and url for every other service still carrying the release.
-
evidence.detectionobject - first_seen_at, reviewed_at, reviewed_by, note and appeared_after_monitoring_began. reviewed_by is null when the answer came through the API.
-
evidence.claimobject - type, label and is_copyright_claim. is_copyright_claim is false for misattribution, which is a metadata correction rather than a takedown.
-
evidence.routingobject - primary and fallback channel snapshots, plus unrouted. Each snapshot carries recipient_type, name, method, address, instructions, source_url and verified.
-
evidence.routing.unroutedboolean - True when we hold no confirmed channel for this service and claim. recipient will be null and you are finding the desk yourself.
-
opened_atstring - ISO 8601.
-
contacted_atnull - Always null. Nobody has been contacted yet.
-
closed_atnull - Always null.
-
outcome_notestring|null - Whatever you recorded via /v1/disputes/{id}/outcome.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4820
ArtistSentry-Topic: dispute.opened
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4820",
"type": "dispute.opened",
"api_version": "v1",
"occurred_at": "2026-08-17T09:31:03+00:00",
"data": {
"id": 331,
"status": "open",
"is_open": true,
"claim_type": "impersonation",
"service": "spotify",
"service_name": "Spotify",
"reference": null,
"release": {
"id": 991,
"title": "Cold Harbour",
"artist": "Corrin Ashe",
"distributor": "The state51 Conspiracy"
},
"recipient": {
"name": "Spotify Content Policy",
"address": "https://support.spotify.com/contact-artist-claim/",
"method": "form"
},
"evidence": {
"captured_at": "2026-08-17T09:31:03+00:00",
"artist": {
"name": "Corrin Ashe",
"profiles": [
{ "dsp": "Spotify", "url": "https://open.spotify.com/artist/4kPCyOKK…", "external_id": "4kPCyOKK" }
]
},
"release": {
"title": "Cold Harbour",
"type": "Single",
"release_date": "2026-08-14",
"upc": "0198004119353",
"recordings": [{ "title": "Cold Harbour", "isrc": "GBKQU2680114" }],
"label": "Fairlane Digital",
"distributor": "The state51 Conspiracy",
"copyright": "© 2026 Corrin Ashe",
"credited_artists": ["Corrin Ashe"],
"artwork_url": "https://i.scdn.co/image/ab67616d0000b273…"
},
"listing": {
"dsp": "Spotify",
"url": "https://open.spotify.com/album/2xQ1mR…",
"external_id": "2xQ1mR",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"last_seen_at": "2026-08-17T09:02:44+00:00"
},
"also_live_on": [
{ "dsp": "Apple Music", "url": "https://music.apple.com/gb/album/1799…" }
],
"detection": {
"first_seen_at": "2026-08-17T04:12:09+00:00",
"reviewed_at": "2026-08-17T09:31:02+00:00",
"reviewed_by": "Dana Vos",
"note": "Not ours, never licensed this.",
"appeared_after_monitoring_began": true
},
"claim": {
"type": "impersonation",
"label": "Someone releasing under my name",
"is_copyright_claim": false
},
"routing": {
"primary": {
"recipient_type": "service",
"recipient_type_label": "Streaming service",
"name": "Spotify Content Policy",
"method": "form",
"method_label": "Web form",
"address": "https://support.spotify.com/contact-artist-claim/",
"instructions": "Use the artist impersonation form.",
"source_url": "https://support.spotify.com/article/impersonation/",
"verified": true
},
"fallback": null,
"unrouted": false
}
},
"opened_at": "2026-08-17T09:31:03+00:00",
"contacted_at": null,
"closed_at": null,
"outcome_note": null
}
}
dispute.updated
Existing case moved. Carries current state.
- Read is_open rather than comparing status strings if you only need open or closed.
-
idinteger - Case id. Pass this to POST /v1/disputes/{id}/contacted and /outcome.
-
statusstring - contacted, resolved, refused or dropped. Also open, when a case was re-pointed because its claim changed.
-
is_openboolean - False for resolved, refused and dropped.
-
claim_typestring - misattribution, impersonation, infringement. Decides both the wording and which desk it goes to.
-
servicestring - spotify, apple_music, deezer, tidal, amazon_music, youtube_music, soundcloud, anghami, audiomack, awa, iheartradio, kkbox, line_music, netease, qq_music, seven_digital, telmore_musik, yousee_musik. One case per service.
-
service_namestring - Display name for the service.
-
referencestring|null - Their ticket number, once you record one via /v1/disputes/{id}/contacted.
-
releaseobject - id, title, artist and distributor of the disputed release.
-
recipientobject - Where the claim goes. Snapshotted when the case opened, so it does not move if the channel is re-pointed later.
-
recipient.namestring|null - Name of the desk.
-
recipient.addressstring|null - An email address or a URL, depending on method.
-
recipient.methodstring|null - email, form, portal.
-
evidenceobject - The evidence pack, snapshotted when the case opened. Keys below.
-
evidence.captured_atstring - ISO 8601. The pack is accurate as of this moment and is never refreshed.
-
evidence.artistobject - name, plus profiles[] of dsp, url and external_id for every profile being watched.
-
evidence.releaseobject - title, type, release_date, upc, recordings[], label, distributor, copyright, credited_artists[] and artwork_url.
-
evidence.listingobject|null - The listing on this case's service: dsp, url, external_id, first_seen_at, last_seen_at. Null where the release was resolved onto the service but never enumerated there.
-
evidence.also_live_onobject[] - dsp and url for every other service still carrying the release.
-
evidence.detectionobject - first_seen_at, reviewed_at, reviewed_by, note and appeared_after_monitoring_began. reviewed_by is null when the answer came through the API.
-
evidence.claimobject - type, label and is_copyright_claim. is_copyright_claim is false for misattribution, which is a metadata correction rather than a takedown.
-
evidence.routingobject - primary and fallback channel snapshots, plus unrouted. Each snapshot carries recipient_type, name, method, address, instructions, source_url and verified.
-
evidence.routing.unroutedboolean - True when we hold no confirmed channel for this service and claim. recipient will be null and you are finding the desk yourself.
-
opened_atstring - ISO 8601.
-
contacted_atstring|null - Set when you record that you got in touch.
-
closed_atstring|null - Set when status reaches an outcome.
-
outcome_notestring|null - Whatever you recorded via /v1/disputes/{id}/outcome.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_5099
ArtistSentry-Topic: dispute.updated
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_5099",
"type": "dispute.updated",
"api_version": "v1",
"occurred_at": "2026-08-28T16:20:07+00:00",
"data": {
"id": 331,
"status": "resolved",
"is_open": false,
"claim_type": "impersonation",
"service": "spotify",
"service_name": "Spotify",
"reference": "TICKET-4821",
"release": {
"id": 991,
"title": "Cold Harbour",
"artist": "Corrin Ashe",
"distributor": "The state51 Conspiracy"
},
"recipient": {
"name": "Spotify Content Policy",
"address": "https://support.spotify.com/contact-artist-claim/",
"method": "form"
},
"evidence": {
"captured_at": "2026-08-17T09:31:03+00:00",
"artist": {
"name": "Corrin Ashe",
"profiles": [
{ "dsp": "Spotify", "url": "https://open.spotify.com/artist/4kPCyOKK…", "external_id": "4kPCyOKK" }
]
},
"release": {
"title": "Cold Harbour",
"type": "Single",
"release_date": "2026-08-14",
"upc": "0198004119353",
"recordings": [{ "title": "Cold Harbour", "isrc": "GBKQU2680114" }],
"label": "Fairlane Digital",
"distributor": "The state51 Conspiracy",
"copyright": "© 2026 Corrin Ashe",
"credited_artists": ["Corrin Ashe"],
"artwork_url": "https://i.scdn.co/image/ab67616d0000b273…"
},
"listing": {
"dsp": "Spotify",
"url": "https://open.spotify.com/album/2xQ1mR…",
"external_id": "2xQ1mR",
"first_seen_at": "2026-08-17T04:12:09+00:00",
"last_seen_at": "2026-08-17T09:02:44+00:00"
},
"also_live_on": [
{ "dsp": "Apple Music", "url": "https://music.apple.com/gb/album/1799…" }
],
"detection": {
"first_seen_at": "2026-08-17T04:12:09+00:00",
"reviewed_at": "2026-08-17T09:31:02+00:00",
"reviewed_by": "Dana Vos",
"note": "Not ours, never licensed this.",
"appeared_after_monitoring_began": true
},
"claim": {
"type": "impersonation",
"label": "Someone releasing under my name",
"is_copyright_claim": false
},
"routing": {
"primary": {
"recipient_type": "service",
"recipient_type_label": "Streaming service",
"name": "Spotify Content Policy",
"method": "form",
"method_label": "Web form",
"address": "https://support.spotify.com/contact-artist-claim/",
"instructions": "Use the artist impersonation form.",
"source_url": "https://support.spotify.com/article/impersonation/",
"verified": true
},
"fallback": null,
"unrouted": false
}
},
"opened_at": "2026-08-17T09:31:03+00:00",
"contacted_at": "2026-08-19T08:15:00+00:00",
"closed_at": "2026-08-28T16:20:07+00:00",
"outcome_note": "Re-tagged at source."
}
}
artist.import.completed
Replaces polling GET /v1/artists/imports/{id}.
- status is completed or failed. queued and running never arrive here, since the event fires once the import stops.
- A failed import keeps whatever it created. Read added rather than assuming nothing happened.
- artists is the number submitted. Compare it against the lengths of added and failed to find partial runs.
- data matches GET /v1/artists/imports/{id}.
-
idinteger - Import id.
-
statusstring - completed or failed. An import only raises this event once it stops.
-
artistsinteger - How many entries were submitted. Compare against added and failed lengths.
-
status_urlstring - GET /v1/artists/imports/{id}, same payload.
-
addedobject[] - The artists created, as full artist objects with their profiles.
-
failedobject[] - Per entry: name, url and reason. Reasons are human readable strings, not codes.
-
completed_atstring|null - ISO 8601.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_4790
ArtistSentry-Topic: artist.import.completed
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_4790",
"type": "artist.import.completed",
"api_version": "v1",
"occurred_at": "2026-08-16T22:08:30+00:00",
"data": {
"id": 57,
"status": "completed",
"artists": 120,
"status_url": "https://artistsentry.com/api/v1/artists/imports/57",
"added": [
{
"id": 12,
"name": "Corrin Ashe",
"slug": "corrin-ashe",
"monitoring_enabled": true,
"known_distributors": [],
"baselined_at": null,
"profiles": [
{
"id": 41,
"service": "spotify",
"service_name": "Spotify",
"url": "https://open.spotify.com/artist/4kPCyOKK…",
"watched": true,
"last_scanned_at": null,
"needs_channel_choice": false,
"channel_candidates": []
}
]
}
],
"failed": [
{ "name": "Unknown Act", "url": "https://open.spotify.com/artist/bad", "reason": "No such artist on Spotify." }
],
"completed_at": "2026-08-16T22:08:30+00:00"
}
}
ping
Check your signature verification before going live.
- Not subscribable. Sending a test ignores the subscription list.
- Never raised by the product, so it will not arrive unprompted.
- Signed and retried exactly like every other topic.
-
messagestring - Fixed string.
-
endpointstring - The endpoint name from your console.
POST /your-endpoint HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ArtistSentry-Webhooks/1.0
ArtistSentry-Event-Id: evt_5310
ArtistSentry-Topic: ping
ArtistSentry-Attempt: 1
ArtistSentry-Signature: t=1755455400,v1=8f2a…c91d
{
"id": "evt_5310",
"type": "ping",
"api_version": "v1",
"occurred_at": "2026-08-17T18:49:47+00:00",
"data": {
"message": "This is a test from your ArtistSentry console.",
"endpoint": "Catalogue pipeline"
}
}