API reference/Affidavits

Apply log matches

confirm

Turn chosen matches from an upload into affidavits.

POSThttps://affiliates.radioworkflow.com/api/v1/log-uploads/{upload}/apply
Confirm: this records an affidavit, the proof behind the network's invoices.

Records the matches you accept from an uploaded log as affidavits. Send the lines you chose in items, or all_matched: true to apply every line in the matched group. Each item is checked again exactly as a typed report is, and the log is kept as evidence the network can see.

An upload is applied once. Sending the same choice again answers the first result with replayed: true; sending a different choice answers 409 already_applied, and the other times go through report-affidavits.

An outside item needs a reason. epoch is week.epoch from get-week. The key needs the report scope on an affiliation that takes reports: without it, and for a read-only affiliation, the answer is 403 forbidden.

Authentication

Send a station key as Authorization: Bearer rwaff_k_.... A key with the report scope. Keys and scopes

Path parameters

uploadstringrequired
The upload id upload-log returned. It names the affiliation and the week, so the path needs neither.

Request body (JSON)

epochintegerat least 0required
week.epoch from get-week, sent back unchanged. It moves when the network reopens the week, so a report built on an older read is refused with 409 epoch_changed.
itemsarray of objectsoptional
The matches to apply, 1 to 2,000. Send items or all_matched, not both.
unit_idstring1 to 20 charactersrequired
The unit's id.
statusenumrequired
What the log shows.
airedoutside
aired_datedaterequired
The day it aired, YYYY-MM-DD.
aired_attimerequired
Minutes after midnight (0 to 1439) or HH:MM, in the station's time zone.
reasonstring0 to 2000 charactersoptional
Required for outside: why it aired outside its window, 1 to 300 characters once trimmed. Any other outside reason refuses the whole apply with 422 validation_failed (reason_required). Ignored for aired.
all_matchedbooleanoptional
true applies every line in the matched group. Send it instead of items.
true
all_or_nothingbooleanoptional
Only false is accepted: the valid items are saved and the rest listed in errors.
false

Idempotency

Send a new Idempotency-Key header with every request (8 to 48 letters, digits, dashes or underscores; a UUID is a good choice). To retry a request that timed out, send it again with the same key: if the first attempt saved, the repeat answers its first result instead of recording twice. A reused key records nothing new: here it answers its first result even when the body changed, and after a request to another endpoint it usually fails with 502 upstream_failed. Never reuse one for a new request. How it works

Response

200application/jsonThe evidence upload id, the units saved and the items refused.

Response schema

uploadIdintegerrequired
savedAffiliateUnit[]required
errorsItemError[]required
replayedtrueoptional
AffiliateUnit27 fields
idstringrequired
datedaterequired
daypartstringrequired
windowStartinteger (0 to 1439)required
windowEndinteger (0 to 1439)required
creativeIdstringrequired
advertiserstringrequired
productstringrequired
titlestringrequired
iscistringrequired
lengthintegerrequired
cartstringrequired
status"pending" | "aired" | "outside" | "not_aired"required
airedAtinteger (0 to 1439)optional
airedDatedateoptional
reasonstringoptional
source"portal" | "log_upload" | "rw_log" | "network" | "api" | "mcp"optional
addedInVersionintegerrequired
removedInVersionintegeroptional
kind"dated" | "weekly"required
weekStartdaterequired
allowedDaysdate[]optional
revintegerrequired
copyStatus"ready" | "to_follow"required
instructionsstringoptional
rwState"confirmed" | "not_confirmed"optional
flagsstring[]required
ItemError6 fields
unit_idstringoptional
vehicle_idstringoptional
datedateoptional
error"rev_conflict" | "not_reportable" | "outside_window" | "invalid_time" | "invalid_date" | "reason_required" | "removed" | "not_found" | "break_not_found" | "local_break"required
messagestringrequired
currentAffiliateUnit or ShowAiringoptional
ShowAiring12 fields
keystringrequired
showIdstringrequired
datedaterequired
status"pending" | "carried" | "not_carried"required
startinteger (0 to 1439)optional
endinteger (0 to 1439)optional
uneditedbooleanoptional
dropped(object or object or object)[]optional
reasonstringoptional
source"portal" | "log_upload" | "rw_log" | "network" | "api" | "mcp"optional
airedDatedateoptional
revintegerrequired

Errors

400bad_requestThe request is not valid: a path value, a query value or a body field is missing, malformed or out of range.
403forbiddenThe key is valid but cannot do this for this affiliation: the affiliation is not one of the key's, or the person who made the key lacks the role.
403read_onlyThe affiliation is read-only: the network paused or ended it, or the station outlet is inactive. Its history stays readable.
404not_foundNothing was found for the values in the path: no audio is ready for the week yet, the upload id is unknown, or the record does not exist.
409not_readyThe uploaded log is still being read, or reading it failed, so there is nothing to apply yet.
409staleThe week changed after the log was read, so the matches were made against an older copy.
409already_appliedMatches from this upload were already applied, and that result does not cover the units you sent now.
409nothing_to_applyThe upload has no matched lines to apply.
409week_signedThe week is signed, so its affidavits can no longer change.
413payload_too_largeThe request body is over the limit: 256 KB of JSON (512 KB when applying log matches), or 10 MB for an uploaded log.
415unsupported_media_typeThe body was not sent as JSON.
422validation_failedItems were refused. With `all_or_nothing` true nothing was saved; `errors` says why for each item.
500not_savedThe change could not be saved, so nothing changed.
502upstream_failedThe service behind the API answered in a way the portal could not use.
Possible on every endpoint
401invalid_principalThe key is missing, malformed, unknown, revoked or expired; all of these answer the same way.
429rate_limitedToo many requests: the key's bucket, a daily limit or the failed sign-in brake refused this one.
500server_errorSomething went wrong on our side. The answer never carries internal detail.
503unavailableThe service is not available right now: it could not be reached, or it is not set up yet.

Every code, its fix and when to retry

Rate limits

Failed sign-ins per address10 per minuteportal:auth
Burst per key60 per minuteportal:key
Calls per key600 per 10 minuteskey:{prefix}
Writes per key120 per minutew:{prefix}

The limits and the RateLimit headers

MCP

An assistant calls this as the tool apply_log_matches, with the same input as one object. The MCP server
Request
# A new Idempotency-Key for every new request. Reuse it only to retry this same request.
IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST 'https://affiliates.radioworkflow.com/api/v1/log-uploads/u1207_20260921_4f1c2a9e8b7d4c3fa0e1d2c3b4a59687/apply' \
  -H "Authorization: Bearer $AFFILIATE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
  "epoch": 1,
  "items": [
    {
      "unit_id": "u-01oso1x",
      "status": "aired",
      "aired_date": "2026-09-22",
      "aired_at": 462
    }
  ]
}'
Request body
{
  "epoch": 1,
  "items": [
    {
      "unit_id": "u-01oso1x",
      "status": "aired",
      "aired_date": "2026-09-22",
      "aired_at": 462
    }
  ]
}
Response200
{
  "uploadId": 44,
  "saved": [
    {
      "id": "u-01oso1x",
      "date": "2026-09-22",
      "daypart": "ROS",
      "windowStart": 360,
      "windowEnd": 1139,
      "creativeId": "trhd",
      "advertiser": "Trailhead Outfitters",
      "product": "",
      "title": "Hunting Season Kickoff",
      "isci": "SR_TRHD0921060",
      "length": 60,
      "cart": "K90388",
      "status": "aired",
      "addedInVersion": 1,
      "kind": "dated",
      "weekStart": "2026-09-21",
      "rev": 2,
      "copyStatus": "ready",
      "flags": [],
      "airedAt": 462,
      "airedDate": "2026-09-22",
      "source": "api"
    }
  ],
  "errors": []
}

Try it

Demo data, no key, nothing saved