Report affidavits
confirmReport times, outside-window airings and misses for a batch of units.
POST
https://affiliates.radioworkflow.com/api/v1/affiliations/{id}/weeks/{week}/affidavitsConfirm: this records an affidavit, the proof behind the network's invoices.
Records what happened to a batch of ROS units: the time each aired, an airing outside its window, or a miss. The network's invoices list these reports under each unit as proof, and a change after an invoice is sent becomes an adjustment, so every item is checked:
aired:aired_dateis the unit's day (a weekly unit: one of its allowed days whose window has closed) andaired_atis inside its window.outside: aired on a day of the broadcast week but not inside the window on one of the unit's own days, with areasonof 1 to 300 characters.not_aired: with areason.pending: clears an earlier report, until the week is signed.
A unit takes a time only once its window has closed. Send each unit's rev and the week's epoch from get-week unchanged. With all_or_nothing false (the default) the valid items are saved and errors lists the others with a code each; with true any refusal saves nothing and answers 422. At most 2,000 items per call, each unit once.
Authentication
Send a station key as
Authorization: Bearer rwaff_k_.... A key with the report scope. Keys and scopesPath parameters
idstring1 to 64 charactersrequiredThe affiliation id from
GET /v1/affiliations: one station's relationship with one network.weekdaterequiredMonday of the broadcast week,
YYYY-MM-DD. Any other day answers 400.Request body (JSON)
epochintegerat least 0requiredweek.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.all_or_nothingbooleanoptionaldefault falsetrue saves nothing when any item is refused (422 validation_failed). The default false saves the valid items and lists the others in errors.itemsarray of objectsrequiredThe units to report, 1 to 2,000, each once.
unit_idstring1 to 20 charactersrequiredThe unit's
id from get-week.revintegerat least 0requiredThe unit's
rev from get-week.statusenumrequiredWhat happened.
airedoutsidenot_airedpendingaired_datedateoptionalThe day it aired,
YYYY-MM-DD. Required for aired and outside.aired_attimeoptionalMinutes after midnight (0 to 1439) or
HH:MM, in the station's time zone. Required for aired and outside.reasonstring1 to 300 charactersoptionalRequired for
outside and not_aired.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 worksResponse
200
application/jsonThe units saved, the items refused and the week's new state.Response schema
weekobjectrequiredweek.state"signed" | "ready_to_sign" | "in_progress" | "published"requiredweek.epochintegerrequiredweek.revintegerrequiredAffiliateUnit27 fields
idstringrequireddatedaterequireddaypartstringrequiredwindowStartinteger (0 to 1439)requiredwindowEndinteger (0 to 1439)requiredcreativeIdstringrequiredadvertiserstringrequiredproductstringrequiredtitlestringrequirediscistringrequiredlengthintegerrequiredcartstringrequiredstatus"pending" | "aired" | "outside" | "not_aired"requiredairedAtinteger (0 to 1439)optionalairedDatedateoptionalreasonstringoptionalsource"portal" | "log_upload" | "rw_log" | "network" | "api" | "mcp"optionaladdedInVersionintegerrequiredremovedInVersionintegeroptionalkind"dated" | "weekly"requiredweekStartdaterequiredallowedDaysdate[]optionalrevintegerrequiredcopyStatus"ready" | "to_follow"requiredinstructionsstringoptionalrwState"confirmed" | "not_confirmed"optionalflagsstring[]requiredItemError6 fields
unit_idstringoptionalvehicle_idstringoptionaldatedateoptionalerror"rev_conflict" | "not_reportable" | "outside_window" | "invalid_time" | "invalid_date" | "reason_required" | "removed" | "not_found" | "break_not_found" | "local_break"requiredmessagestringrequiredShowAiring12 fields
keystringrequiredshowIdstringrequireddatedaterequiredstatus"pending" | "carried" | "not_carried"requiredstartinteger (0 to 1439)optionalendinteger (0 to 1439)optionaluneditedbooleanoptionaldropped(object or object or object)[]optionalreasonstringoptionalsource"portal" | "log_upload" | "rw_log" | "network" | "api" | "mcp"optionalairedDatedateoptionalrevintegerrequiredErrors
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.
403insufficient_scopeThe key does not carry the scope this endpoint needs (`read`, `report` or `acknowledge`).
409epoch_changedThe network reopened or changed the week after you read it, so the `epoch` you sent is out of date.
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.
422validation_failedItems were refused. With `all_or_nothing` true nothing was saved; `errors` says why for each item.
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.
503unavailableThe service is not available right now: it could not be reached, or it is not set up yet.
Rate limits
Failed sign-ins per address10 per minute
portal:authBurst per key60 per minute
portal:keyCalls per key600 per 10 minutes
key:{prefix}Writes per key120 per minute
w:{prefix}MCP
An assistant calls this as the tool
report_affidavits, with the same input as one object. The MCP server# 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/affiliations/srn-ksmt/weeks/2026-09-21/affidavits' \
-H "Authorization: Bearer $AFFILIATE_API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"epoch": 1,
"items": [
{
"unit_id": "u-01oso1x",
"rev": 1,
"status": "aired",
"aired_date": "2026-09-22",
"aired_at": "07:42"
}
]
}'{
"epoch": 1,
"items": [
{
"unit_id": "u-01oso1x",
"rev": 1,
"status": "aired",
"aired_date": "2026-09-22",
"aired_at": "07:42"
}
]
}{
"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": [],
"week": {
"state": "in_progress",
"epoch": 1,
"rev": 2
}
}