The Civly Intelligence API is JSON over HTTPS. Everything on this page is public, and the only thing you need from us is an API key. Keys are issued by hand to each partner and carry the products you have agreed with Civly.
https://app.civly.ai/api/v1/partner
Products
Each product is a small set of endpoints under the base URL. Your key lists the products it may call, and a call to any other product returns 403.
| Product | What it does | How it answers | Key access |
|---|---|---|---|
| Research reports | A full sourced research document on a candidate or public figure, plus the raw research data behind it. | Job: submit, then poll | big_book |
| Social media analysis | One analysis across up to fourteen social platforms and web archives, with a per-platform summary and a Word document of the whole run. | Job: submit, then poll | social_analysis |
| Bulk data access | Read-only access to Civly's full research database as a Parquet export you query with DuckDB or SQL. | Instant response | data_access |
| Court opinion search | Full-text search over Civly's copy of U.S. federal and state court opinions, and the full text of any one. | Instant response | opinion_search |
| Epstein corpus screening | Screen a person and their associates against the DOJ and House Oversight Epstein document release and a curated watchlist. | Instant response | epstein_screening |
| Adverse screening | Screen a person against federal and international exclusion, enforcement, debarment and sanctions lists, and the DOJ press-release archive. | Instant response | adverse_screening |
| Municipal meeting search | Search speaker-attributed transcripts of city council and county board meetings, with a link that plays each moment. | Instant response | meeting_search |
| Clip feed | Pull the clips in your own Clip Library into your dashboards or warehouse: one flat row per clip, with lasting play and thumbnail links that need no login. | Instant response | clip_feed |
| NIL deal pricing | Fair-market-value estimates for college athlete NIL deals, priced line by line against published, dated rates. | Instant response | nil_pricing |
Get access
- Request a key with the access form. Tell us what you are building and which products you need. You can also send the same request from code with
POST /access-request. - We review it. A person at Civly reads every request; there is no self-serve signup.
- You receive your key over a secure channel, never by plain email, along with a signing secret if you use webhooks. The key starts with
cvly_pk_and is shown once, so store it in a secrets manager when it arrives.
Each key carries its own access level: which products it may call, a monthly submission quota for research reports and another for social media analysis, and a cap on how many reports can run at once. Keys work against the production API at the base URL above.
Authentication
Send your key on every request, in either of these headers:
Authorization: Bearer cvly_pk_your_key
X-API-Key: cvly_pk_your_key
The one exception is the clip feed’s play and thumbnail links, which are signed for a single clip and need no key; see Clip feed. A missing, invalid or revoked key returns 401:
{"detail": "Partner API key required. Provide via Authorization: Bearer <key> or X-API-Key header."}
Keep keys on your server. Never put one in browser code, a mobile app or source control. If a key leaks, tell us and we revoke it; a revoked key fails on its very next request. Each key only ever sees its own company’s jobs.
Making requests
Request and response bodies are JSON, so send Content-Type: application/json with every POST. Dates are YYYY-MM-DD and timestamps are ISO 8601 in UTC.
The attestation field
Every endpoint that researches a named person requires this exact field in the body:
"attestation": "subject-is-candidate-or-public-figure"
Sending it affirms that the subject is a candidate for public office or a public figure and that the research is for a lawful purpose. A request without it is rejected with 422, and every submission is recorded in an audit log. The endpoint reference marks it as required wherever it applies. Searches of public records (court opinions, meeting transcripts) and NIL pricing do not take it.
Idempotency keys
Research reports and social media analysis are slow, and they are billed when they start. Send an Idempotency-Key header with a value you choose once per logical submission, such as your own order id:
Idempotency-Key: order-7f3a
If a request times out or your process restarts, retry with the same value and you get the original job back, marked "idempotent_replay": true, instead of starting and paying for a second one. The value is scoped to the product, so the same string on /reports and /social-analysis names two different submissions. Use a new value for a genuinely new submission, including a resubmission after a failure.
Long-running jobs
Two products run as jobs: research reports and social media analysis. Every other endpoint answers in the same request.
- Submit.
POST /reportsorPOST /social-analysisreturns202with ajob_idwithin a couple of seconds. Store it: it is your receipt, and it is what invoices reference. - Poll. Check
GET /reports/{job_id}orGET /social-analysis/{group_id}every 30 to 60 seconds, and no faster. A report’sstatusis one ofqueued,running,completedorfailed. For reports, a webhook can replace polling. - Fetch the result.
GET /reports/{job_id}/resultorGET /social-analysis/{group_id}/results.
A research report takes about 5 to 10 minutes. Its raw research data (raw_data_url) is ready the moment the job completes. The finished document goes through a human quality review first: until an analyst releases it, document_url is null and document_status reads in_review. Review runs on a human timescale, hours rather than seconds, so check back occasionally rather than polling hard. Download links are presigned and last an hour; call the result endpoint again for a fresh one.
Social media analysis usually finishes in a few minutes. Each platform runs on its own, so a run where one platform failed and the rest succeeded is normal: read the per-platform statuses. Once the run is done, the results also carry a link to a Word document covering the whole run.
Two 409 responses are part of the normal flow. Asking for a report result before the report completes returns 409; keep polling. Submitting a subject that already has a report running for your company also returns 409, naming the running job_id; wait for that one to finish.
Webhooks
Give us an HTTPS URL when you request access, or any time after, and Civly sends a signed POST to it when one of your reports finishes:
{
"event": "report.completed",
"delivery_id": "d6c1a0f2-...",
"product": "big_book",
"job_id": "1234",
"status": "completed",
"timestamp": "2026-10-02T14:12:40Z"
}
The events are report.completed and report.failed. The body never contains report content: fetch it from the result endpoint with your key. report.completed means generation finished, so the raw data is ready but the document may still be in review.
Every delivery carries an X-Civly-Signature header of the form t=<unix timestamp>,v1=<hex>: an HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with your webhook secret. Check it before trusting the body:
import hashlib
import hmac
import time
def verify(secret: str, header: str, body: bytes) -> bool:
"""True if X-Civly-Signature matches the raw request body and is under five minutes old."""
try:
parts = dict(item.split("=", 1) for item in header.split(","))
ts, sig = parts["t"], parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Respond with any 2xx within 10 seconds, and do the real work after you have answered. A failed delivery is retried after 1, 5, 15, 30 and 60 minutes, then dropped, so treat polling as the fallback. The same event can arrive twice; use delivery_id to discard repeats.
Pagination
Most responses are a complete object, so there is nothing to page through. Three endpoints return lists in pages:
POST /meetings/searchpages withoffset. The response’stotal_resultstells you how many matches there are; add the number of results you received tooffsetand ask again until you reach it.POST /opinions/searchreturns the top matches only. To see more, narrow the search with its court, judge, date and citation filters.GET /clipspages by clip id, oldest first, up to 500 clips a page. See Clip feed.
Both searches rank results best first and return up to max_results (1 to 50, default 15) per call.
For work across whole datasets, use bulk data access instead: your own Parquet export, queried with DuckDB or SQL, with no per-call limits.
Clip feed
The clip feed brings the clips in your own Clip Library into your dashboards or data warehouse. Each clip is one flat row with no nested objects, so it maps straight to a CSV row, and its clip_id never changes, so use it as the row key. Only your company’s own collections are included.
Pulling only what is new
- Call
GET /clipswithafter_id=0on the first run. - While
has_moreistrue, call again withafter_idset to thenext_after_idyou just received. - Store the last
next_after_id. Start the next run from it and you receive only clips added since. When nothing is new,clipsis empty andnext_after_idrepeats yourafter_id.
A clip enters the feed five minutes after it is created, so a run never sees one that is still being saved. Each clip is sent once: a clip edited later is not sent again. If you need later edits, re-pull from an older after_id now and then and deduplicate on clip_id. The Python examples include a scheduled pull that writes a CSV.
Play and thumbnail links
Every row carries a play_url and a thumbnail_url. They last, they need no key and no login, and you can store them and show them to your own clients. Opening one redirects to a video or image link that expires within an hour, so embed the links as they are and never store where they redirect. A play link opens the cut clip when there is one, and otherwise the full source video at the clip’s moment.
- Each link works only for its own clip. A changed or missing
sigreturns404. - A link stops working when the clip or its collection is hidden or removed in the Clip Library, or when your key is revoked or loses the
clip_feedproduct. - A TV clip whose video is still being converted answers
503withRetry-After: 60; open it again a minute later.
Rate limits and quotas
- 120 requests a minute per key, over a rolling 60-second window. Requests without a valid key share 60 a minute per IP address.
- Every response carries
X-RateLimit-LimitandX-RateLimit-Remaining. Past the limit you get429with aRetry-Afterheader giving the seconds to wait. - Clip play and thumbnail links carry no key, so they have their own limit: 600 opens a minute per viewer IP address, with the same
429andRetry-After. A redirect can be cached for five minutes, so reloading a page of thumbnails does not count again. - Poll any one job no more than once every 30 seconds.
Your key’s product limits also answer 429, with an error and a message that say which limit you hit:
{
"detail": {
"error": "Monthly quota reached",
"message": "This key has used 20 of 20 big_book submissions this month. Contact Civly to increase your quota.",
"product": "big_book",
"quota": 20,
"used": 20
}
}
- Monthly quota reached: the key has used its submissions for the calendar month. Wait for the next month or ask us to raise it.
- Concurrent job limit reached: too many of your reports are running at once. Wait for one to finish.
- Partner capacity reached: shared capacity is momentarily full. Retry in a few minutes.
Errors
Errors come back as JSON with a detail field. For a validation error (422), detail lists each problem and the field it is in:
{
"detail": [
{"loc": ["body", "attestation"], "msg": "Field required", "type": "missing"}
]
}
| Status | Meaning | What to do |
|---|---|---|
401 | Key missing, invalid or revoked | Check the header is Authorization: Bearer cvly_pk_... or X-API-Key, with no stray whitespace or newline in the key. |
402 | Account out of credit | Contact Civly. Retrying will not help. |
403 | Product not enabled for this key | Contact Civly to add the product to your key. |
404 | Not found | Check the id. Keys only see their own company’s jobs, so another company’s job id is a 404 too. |
409 | Not ready, or already running | A report result asked for too early, or a second report on a subject already in progress. See Long-running jobs. |
422 | Validation error | Most often a missing or misspelled attestation. The body names the field. |
429 | Rate limit or product limit | See Rate limits and quotas. Honor Retry-After when present. |
503 | Clip still being prepared | Only from a clip play link: the TV clip’s video is still being converted. Open the link again after Retry-After (60 seconds). |
5xx | Server error | Retry with exponential backoff. If a submission may have gone through, retry with the same Idempotency-Key. |
A job that seems stuck usually is not. A report queued for more than about five minutes is waiting on a busy queue and clears on its own. A failed report can be resubmitted with a new Idempotency-Key; if the same subject fails repeatedly, send us the job_id. An expired download link just needs a fresh call to the result endpoint.
Python examples
These use the requests library (pip install requests) and read your key from the CIVLY_API_KEY environment variable. Start with this helper, which every example below uses. It retries rate limits and server errors and raises on anything else.
import os
import time
import requests
BASE = "https://app.civly.ai/api/v1/partner"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['CIVLY_API_KEY']}"
def call(method, path, **kwargs):
"""Make one API call and return the parsed JSON.
Waits out rate limits and retries server errors. Send an Idempotency-Key header
on submissions, so a retried POST can never start a second job.
"""
for attempt in range(5):
resp = SESSION.request(method, BASE + path, timeout=120, **kwargs)
if resp.status_code == 429 and "Retry-After" in resp.headers:
time.sleep(int(resp.headers["Retry-After"]))
continue
if resp.status_code >= 500:
time.sleep(2 ** attempt)
continue
if resp.status_code >= 400:
raise RuntimeError(f"{resp.status_code} on {method} {path}: {resp.text}")
return resp.json()
resp.raise_for_status()
Screen a list of names and write the results to a CSV
Reads names.csv (one column headed name), screens each name with adverse screening, and writes one row per name with its strongest hit.
import csv
with open("names.csv", newline="") as f:
names = [row["name"].strip() for row in csv.DictReader(f) if row["name"].strip()]
disclaimer = ""
with open("screening_results.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"name", "matched", "hit_count", "top_list", "top_record_name",
"name_similarity", "record_link", "doj_press_mentions",
])
for name in names:
result = call("POST", "/adverse-screening/screen", json={
"subject_name": name,
"attestation": "subject-is-candidate-or-public-figure",
})
disclaimer = result["disclaimer"]
for row in result["results"]:
top = max(row["hits"], key=lambda hit: hit["confidence"], default=None)
writer.writerow([
row["name"],
row["matched"],
len(row["hits"]),
top["source_label"] if top else "",
top["full_name"] if top else "",
top["confidence"] if top else "",
top["detail_url"] if top else "",
row["doj_mention_count"],
])
# A hit is a possible same-name match, not proof. Show this with any match you surface.
print(disclaimer)
Run a research report from start to finish
import time
job = call(
"POST",
"/reports",
headers={"Idempotency-Key": "order-1042"}, # one value per submission, e.g. your order id
json={
"subject_name": "Jane Example",
"subject_type": "candidate",
"office": "US Senate",
"state": "OH",
"attestation": "subject-is-candidate-or-public-figure",
},
)
job_id = job["job_id"] # store this: it is your receipt
while True:
status = call("GET", f"/reports/{job_id}")
if status["status"] in ("completed", "failed"):
break
print(status.get("progress") or status["status"])
time.sleep(45) # every 30 to 60 seconds, no faster
if status["status"] == "failed":
raise SystemExit(f"Report {job_id} failed: {status.get('error_message')}")
result = call("GET", f"/reports/{job_id}/result")
# Presigned links: plain downloads, no API key, valid for an hour.
raw = requests.get(result["raw_data_url"], timeout=300)
raw.raise_for_status()
with open(f"report-{job_id}-data.json", "wb") as f:
f.write(raw.content)
if result["document_status"] == "available":
print("Document:", result["document_url"])
else:
print("The document is in quality review. Call the result endpoint again later.")
Page through meeting moments into a CSV
import csv
search = {"query": "\"short-term rentals\"", "state": "CA", "max_results": 50}
moments, offset = [], 0
while True:
page = call("POST", "/meetings/search", json={**search, "offset": offset})
moments.extend(page["results"])
offset += len(page["results"])
if not page["results"] or offset >= page["total_results"]:
break
columns = [
"event_date", "municipality", "state", "body_name", "speaker_name",
"timestamp", "quote", "clip_url", "citation",
]
with open("meeting_moments.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore")
writer.writeheader()
writer.writerows(moments)
print(f"Wrote {len(moments)} moments")
Pull new clips into a CSV on a schedule
Run it from cron or any scheduler. Each run picks up where the last one stopped and writes only the clips added since.
import csv
import json
from pathlib import Path
STATE = Path("civly_clips_state.json") # where the last run stopped
after_id = json.loads(STATE.read_text())["after_id"] if STATE.exists() else 0
rows = []
for _ in range(50): # at most 25,000 clips a run; a bigger backlog finishes over the next runs
page = call("GET", "/clips", params={"after_id": after_id, "limit": 500})
rows += page["clips"]
after_id = page["next_after_id"]
if not page["has_more"]:
break
if rows:
with open("civly_clips.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
STATE.write_text(json.dumps({"after_id": after_id})) # save only after the CSV is written
print(f"{len(rows)} new clips")
Receive a webhook
A minimal Flask receiver using the verify function from Webhooks:
import os
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/civly-webhook")
def civly_webhook():
signature = request.headers.get("X-Civly-Signature", "")
if not verify(os.environ["CIVLY_WEBHOOK_SECRET"], signature, request.get_data()):
abort(400)
event = request.get_json()
# Answer within 10 seconds: hand the event to a queue, then fetch the
# result with call("GET", f"/reports/{event['job_id']}/result") from there.
print(event["event"], event["job_id"], event["delivery_id"])
return "", 204
Endpoint reference
Every partner endpoint, with its parameters, request body, response fields and an example of each. This section is rebuilt from the API’s own OpenAPI spec, so it lists exactly what the API accepts today. Paths are relative to the base URL.
Research reports
A full sourced research document on a candidate or public figure, plus the raw research data behind it. Your key needs the big_book product.
POST/reports
Submit a subject for a full research report
Start a full research report (Big Book) for a candidate or public figure.
Requires the attestation field. Generation runs asynchronously for 5-10 minutes; poll GET /reports/{job_id} or register a completion webhook, then fetch the document from GET /reports/{job_id}/result.
Headers
Idempotency-Keystring or nullOptional client-chosen key; retries with the same key return the original job.
Request body
subject_namestringrequiredName of the research subject. 2–255 characters.
subject_typestringOne of candidate, donor, organization. Default "candidate".
officestring or nullOffice held or sought (e.g. 'US Senate'). Up to 255 characters.
districtstring or nullUp to 255 characters.
citystring or nullUp to 255 characters.
statestring or nullState (2-letter code or full name). Up to 50 characters.
partystring or nullUp to 50 characters.
contextstring or nullFree-text research guidance. Up to 5,000 characters.
date_range_startdate or nulldate_range_enddate or nulltwitter_handlearray of strings or nullyoutube_handlearray of strings or nullinstagram_handlearray of strings or nulltiktok_handlearray of strings or nullbluesky_handlearray of strings or nullfacebook_handlearray of strings or nullsubstack_handlearray of strings or nulltruthsocial_handlearray of strings or nullfec_committee_namestring or nullUp to 255 characters.
legislator_namestring or nullUp to 255 characters.
attestationstringrequiredRequired safety attestation. Must be the exact string 'subject-is-candidate-or-public-figure', affirming the subject is a candidate for public office or a public figure and the research is lawful.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/reports" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-7f3a" \
-d '{
"subject_name": "Jane Example",
"subject_type": "candidate",
"office": "US Senate",
"state": "OH",
"party": "Republican",
"twitter_handle": [
"janeexample"
],
"attestation": "subject-is-candidate-or-public-figure"
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/reports",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}", "Idempotency-Key": "order-7f3a"},
json={
"subject_name": "Jane Example",
"subject_type": "candidate",
"office": "US Senate",
"state": "OH",
"party": "Republican",
"twitter_handle": ["janeexample"],
"attestation": "subject-is-candidate-or-public-figure",
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 202
4 response fields
job_idstringrequiredproductstringrequiredOne of big_book, social_analysis.
statusstringrequiredOne of queued, running, completed, failed.
idempotent_replaybooleanTrue when this response replays a previously created job (Idempotency-Key hit). Default false.
Example response
{
"job_id": "1234",
"product": "big_book",
"status": "running",
"idempotent_replay": false
}
GET/reports/{job_id}
Get report status
Poll generation progress. Statuses: queued, running, completed, failed.
Path parameters
job_idintegerrequiredExample request
curl "https://app.civly.ai/api/v1/partner/reports/1234" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/reports/1234",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
4 response fields
job_idstringrequiredstatusstringrequiredOne of queued, running, completed, failed.
progressstring or nullHuman-readable progress message.
error_messagestring or nullExample response
{
"job_id": "1234",
"status": "running",
"progress": "Generating content... (12/45)",
"error_message": null
}
GET/reports/{job_id}/result
Get report download links
Fetch short-lived download links for a completed report. Returns 409 if the report is not finished yet - keep polling the status endpoint.
raw_data_url is ready as soon as the report completes. The polished document clears a human quality review first: document_url stays null and document_status reads "in_review" until it is released.
Path parameters
job_idintegerrequiredExample request
curl "https://app.civly.ai/api/v1/partner/reports/1234/result" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/reports/1234/result",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
6 response fields
job_idstringrequiredstatusstringrequiredOne of queued, running, completed, failed.
document_statusstringLifecycle of the polished document: 'in_review' while it is being prepared and quality-reviewed, 'available' once document_url is live, 'unavailable' when no document will be produced for this report. One of available, in_review, unavailable. Default "in_review".
document_urlstring or nullShort-lived presigned URL for the finished document; null until document_status is 'available'.
raw_data_urlstring or nullShort-lived presigned URL for the raw data appendix.
expires_in_secondsintegerLifetime of the presigned URLs. Default 3600.
Example response
{
"job_id": "1234",
"status": "completed",
"document_status": "in_review",
"document_url": null,
"raw_data_url": "https://storage.example/reports/1234/raw.json?signature=...",
"expires_in_seconds": 3600
}
Social media analysis
One analysis across up to fourteen social platforms and web archives, with a per-platform summary and a Word document of the whole run. Your key needs the social_analysis product.
POST/social-analysis
Submit social handles for multi-platform analysis
Run unified social analysis across the selected platforms. Requires the attestation field. The 202 returns as soon as the job is accepted; per-platform dispatch and billing run right after the response, so platforms_queued means accepted-for-dispatch - poll the status endpoint for live per-platform state.
Headers
Idempotency-Keystring or nullOptional client-chosen key; retries with the same key return the original job.
Request body
person_namestringrequiredplatformsarray of stringsPlatforms to analyze. Omit to run the full default set (twitter, youtube, instagram, tiktok, reddit, substack, bluesky, facebook, truthsocial, rumble, twitch, linkedin, archive). Billing is per platform queued, so omitting this field bills for the entire default set.
search_modestring or nullDefault "account".
search_querystring or nullobjectivestring or nullstart_datedate or nullend_datedate or nullscreen_for_concernsbooleanDefault false.
twitterobject or null4 fields inside twitter
usernamestring or nullmax_tweetsintegerDefault 20.
include_retweetsbooleanDefault false.
include_repliesbooleanDefault false.
youtubeobject or null6 fields inside youtube
youtube_handlestring or nullmax_videosintegerDefault 10.
auto_transcribebooleanDefault true.
auto_transcribe_countintegerDefault 10.
additional_contextstring or nulldate_presetstring or nullinstagramobject or null4 fields inside instagram
usernamestring or nullmax_reelsintegerDefault 10.
auto_transcribebooleanDefault true.
auto_transcribe_countintegerDefault 10.
redditobject or null5 fields inside reddit
topicstring or nullsubredditsarray of strings or nullhighlight_keywordsarray of strings or nullupvote_thresholdintegerDefault 10.
max_postsintegerDefault 20.
tiktokobject or null8 fields inside tiktok
usernamestring or nullmax_videosintegerDefault 10.
include_transcriptsbooleanDefault true.
include_liked_videosbooleanDefault true.
max_liked_videosintegerDefault 10.
include_repostsbooleanDefault false.
max_repostsinteger or nullFrom 1 to 10,000.
additional_contextstring or nullsubstackobject or null6 fields inside substack
analysis_modestringDefault "topic".
author_urlstring or nulltopic_keywordstring or nullmax_postsintegerDefault 10.
max_authorsintegerDefault 10.
research_objectivestring or nullblueskyobject or null2 fields inside bluesky
usernamestring or nullmax_postsintegerDefault 20.
instagram_imagesobject or null3 fields inside instagram_images
usernamestring or nullmax_postsintegerDefault 50.
additional_contextstring or nullyoutube_imagesobject or null5 fields inside youtube_images
youtube_handlestring or nullmax_videosintegerDefault 25.
frames_per_videointegerDefault 6.
additional_contextstring or nulldate_presetstring or nullfacebookobject or null3 fields inside facebook
usernamestring or nullusernamesarray of strings or nullmax_postsintegerDefault 20.
truthsocialobject or null2 fields inside truthsocial
usernamestring or nullmax_postsintegerDefault 20.
mastodonobject or null2 fields inside mastodon
usernamestring or nullmax_postsintegerDefault 20.
telegramobject or null2 fields inside telegram
usernamestring or nullmax_postsintegerDefault 20.
rumbleobject or null2 fields inside rumble
usernamestring or nullmax_postsintegerDefault 20.
twitchobject or null3 fields inside twitch
usernamestring or nullmax_postsintegerDefault 20.
transcribe_max_minutesinteger or nullsnapchatobject or null2 fields inside snapchat
usernamestring or nullmax_postsintegerDefault 0.
threadsobject or null2 fields inside threads
usernamestring or nullmax_postsintegerDefault 500.
linkedinobject or null1 field inside linkedin
usernamestring or nullpolitical_emailsobject or null1 field inside political_emails
max_postsintegerDefault 50.
archiveobject or null2 fields inside archive
handlesarray of strings or nullmax_postsintegerDefault 25.
venmoobject or null1 field inside venmo
usernamestring or nullattestationstringrequiredRequired safety attestation. Must be the exact string 'subject-is-candidate-or-public-figure', affirming the subject is a candidate for public office or a public figure and the research is lawful.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/social-analysis" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: social-7f3a" \
-d '{
"person_name": "Jane Example",
"platforms": [
"twitter",
"youtube",
"reddit"
],
"twitter": {
"username": "janeexample",
"max_tweets": 50
},
"attestation": "subject-is-candidate-or-public-figure"
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/social-analysis",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}", "Idempotency-Key": "social-7f3a"},
json={
"person_name": "Jane Example",
"platforms": ["twitter", "youtube", "reddit"],
"twitter": {
"username": "janeexample",
"max_tweets": 50,
},
"attestation": "subject-is-candidate-or-public-figure",
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 202
6 response fields
job_idstringrequiredAnalysis group id - use for status/results polling.
productstringDefault "social_analysis".
statusstringrequiredOne of queued, running, completed, failed.
platforms_queuedarray of stringsplatforms_skippedarray of stringsidempotent_replaybooleanDefault false.
Example response
{
"job_id": "6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55",
"product": "social_analysis",
"status": "queued",
"platforms_queued": [
"twitter",
"youtube",
"reddit"
],
"platforms_skipped": [],
"idempotent_replay": false
}
GET/social-analysis/{group_id}
Get analysis status
Poll normalized per-platform status (pending, in_progress, completed, failed).
Path parameters
group_idstringrequiredExample request
curl "https://app.civly.ai/api/v1/partner/social-analysis/6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/social-analysis/6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
8 response fields
group_idstringrequiredperson_namestringrequiredoverall_statusstringrequiredtotal_platformsintegerrequiredcompleted_countintegerrequiredfailed_countintegerrequiredin_progress_countintegerrequiredplatformsarray of objectsrequired4 fields inside platforms
platformstringrequiredanalysis_idintegerrequiredstatusstringrequirederror_messagestring or nullExample response
{
"group_id": "6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55",
"person_name": "Jane Example",
"overall_status": "in_progress",
"total_platforms": 3,
"completed_count": 2,
"failed_count": 0,
"in_progress_count": 1,
"platforms": [
{
"platform": "twitter",
"analysis_id": 9101,
"status": "completed",
"error_message": null
},
{
"platform": "youtube",
"analysis_id": 9102,
"status": "completed",
"error_message": null
},
{
"platform": "reddit",
"analysis_id": 9103,
"status": "in_progress",
"error_message": null
}
]
}
GET/social-analysis/{group_id}/results
Get analysis results
Per-platform summaries for all completed platforms in the analysis.
Path parameters
group_idstringrequiredExample request
curl "https://app.civly.ai/api/v1/partner/social-analysis/6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55/results" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/social-analysis/6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55/results",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
9 response fields
group_idstringrequiredperson_namestringrequiredoverall_statusstringrequiredplatformsarray of objectsrequired19 fields inside platforms
platformstringrequiredanalysis_idintegerrequiredstatusstringrequiredsummarystring or nullrisk_levelstring or nullkey_topicsarray of objects or nullposts_fetchedinteger or nulluploads_fetchedinteger or nullreposts_fetchedinteger or nullreposts_requestedboolean or nullreposts_completeboolean or nullerror_messagestring or nulldetail_urlstringrequiredcontent_itemsarray of objects or nullhandle_usedstring or nullhandle_sourcestring or nulldiscovery_confidencestring or nullidentity_matchstring or nullreview_itemsarray of objects or nullcreated_atdatetimerequiredcross_platform_summarystring or nullcombined_themesarray of objects or nulldocument_urlstring or nulldocument_url_expires_in_secondsinteger or nullExample response
{
"group_id": "6f1c2a9e-3b7d-4c51-9a0e-2d8f4b7c1e55",
"person_name": "Jane Example",
"overall_status": "completed",
"created_at": "2026-10-02T14:05:11Z",
"platforms": [
{
"platform": "twitter",
"analysis_id": 9101,
"status": "completed",
"summary": "Posts center on manufacturing jobs and a state budget fight...",
"risk_level": "low",
"key_topics": [
{
"topic": "manufacturing",
"count": 14
}
],
"posts_fetched": 50,
"handle_used": "janeexample",
"handle_source": "supplied",
"detail_url": "https://app.civly.ai/social-analysis/9101"
}
],
"cross_platform_summary": "Consistent message across platforms...",
"document_url": "https://storage.example/social/6f1c2a9e.docx?signature=...",
"document_url_expires_in_seconds": 3600
}
Bulk data access
Read-only access to Civly's full research database as a Parquet export you query with DuckDB or SQL. Your key needs the data_access product.
GET/data/dictionary
Data dictionary for the bulk Parquet export
The data dictionary for your read-only Parquet export: one entry per dataset with a plain description, where it comes from, and the exact exported columns. Requires the data_access product.
Example request
curl "https://app.civly.ai/api/v1/partner/data/dictionary" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/data/dictionary",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
A JSON object; the example shows its shape.
Example response
{
"schema_version": "...",
"generated_at": "2026-10-02T06:00:00+00:00",
"description": "Partner-facing data dictionary for the Civly Parquet export...",
"dataset_count": 1,
"datasets": [
{
"table": "...",
"description": "...",
"source_type_label": "...",
"expected_scale": "...",
"columns": [
"..."
],
"column_count": 1
}
]
}
GET/data/sources
Source catalog behind the bulk export
The catalog of data sources behind the bulk export, grouped by category with a plain description and rough scale for each. Built from Civly's source registry, so it is always current and never exposes internal table or pipeline names. Requires the data_access product.
Example request
curl "https://app.civly.ai/api/v1/partner/data/sources" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/data/sources",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
A JSON object; the example shows its shape.
Example response
{
"categories": [
{
"category": "...",
"sources": [
{
"name": "...",
"description": "...",
"scale": "..."
}
]
}
],
"note": "Read-only bulk access; query the parquet export with DuckDB."
}
GET/data/export
Your read-only Parquet export location
Where your read-only Parquet export lives, for querying directly with DuckDB/SQL. Returns 409 until Civly has provisioned your export bucket. Requires the data_access product.
Example request
curl "https://app.civly.ai/api/v1/partner/data/export" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/data/export",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
3 response fields
export_locationstringrequiredThe partner's Parquet export location (s3://bucket/prefix path or a long-lived URL).
formatstringDefault "parquet".
accessstringDefault "read-only".
Example response
{
"export_location": "s3://civly-partner-exports/your-company/",
"format": "parquet",
"access": "read-only"
}
Court opinion search
Full-text search over Civly's copy of U.S. federal and state court opinions, and the full text of any one. Your key needs the opinion_search product.
POST/opinions/search
Full-text search U.S. court opinions
Search Civly's copy of U.S. court opinions and get ranked results with snippets. Filter by court, judge, precedential status, date range, and minimum citation count. Requires the opinion_search product.
full_text_available is False in environments where only case-name/judge metadata is searchable; the search never silently pretends to have read the opinion body.
Request body
querystringrequiredWords or phrase to find in opinion body text. 2–500 characters.
max_resultsintegerHow many ranked results to return (1-50). From 1 to 50. Default 15.
court_idsarray of strings or nullRestrict to court ids, e.g. ['scotus', 'ca9'].
judgestring or nullFilter by judge name. Up to 200 characters.
precedential_statusstring or nullFilter by precedential status, e.g. 'Published'. Up to 50 characters.
date_fromstring or nullOnly opinions filed on or after this date (YYYY-MM-DD).
date_tostring or nullOnly opinions filed on or before this date (YYYY-MM-DD).
min_citationsinteger or nullOnly opinions cited at least this many times. At least 0.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/opinions/search" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "qualified immunity excessive force",
"max_results": 10,
"court_ids": [
"ca9",
"scotus"
],
"date_from": "2010-01-01",
"date_to": "2020-12-31",
"min_citations": 5
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/opinions/search",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"query": "qualified immunity excessive force",
"max_results": 10,
"court_ids": ["ca9", "scotus"],
"date_from": "2010-01-01",
"date_to": "2020-12-31",
"min_citations": 5,
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
4 response fields
querystringDefault "".
resultsarray of objects12 fields inside results
opinion_idinteger or nullStable id; pass to GET /opinions/{opinion_id}.
case_namestring or nullcourt_idstring or nullCourt id, e.g. 'scotus'.
court_namestring or nullcourt_jurisdictionstring or nulljudgesstring or nulldate_filedstring or nullDate the opinion was filed (YYYY-MM-DD).
precedential_statusstring or nullcitation_countinteger or nullHow many later opinions cite this one.
docket_numberstring or nullsource_urlstring or nullPublic web page for this opinion.
snippetstringText excerpt around the first query-term match. Default "".
total_resultsintegerDefault 0.
full_text_availablebooleanTrue when results were ranked over full opinion text; False when only case name and judges metadata was searched. Default false.
Example response
{
"query": "qualified immunity excessive force",
"total_results": 10,
"full_text_available": true,
"results": [
{
"opinion_id": 812345,
"case_name": "Doe v. City of Example",
"court_id": "ca9",
"court_name": "Court of Appeals for the Ninth Circuit",
"court_jurisdiction": "F",
"judges": "...",
"date_filed": "2014-06-02",
"precedential_status": "Published",
"citation_count": 42,
"docket_number": "12-56789",
"source_url": "https://...",
"snippet": "...the doctrine of qualified immunity does not shield..."
}
]
}
GET/opinions/{opinion_id}
Fetch one full opinion by id
Fetch one opinion's full body text plus case metadata, by an opinion_id from a search result. Very long opinions are truncated to a fixed size cap, with truncated=True set on the response. Returns 404 if the id is not in Civly's copy. Requires the opinion_search product.
Path parameters
opinion_idintegerrequiredExample request
curl "https://app.civly.ai/api/v1/partner/opinions/812345" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/opinions/812345",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
14 response fields
opinion_idinteger or nullStable id; pass to GET /opinions/{opinion_id}.
case_namestring or nullcourt_idstring or nullCourt id, e.g. 'scotus'.
court_namestring or nullcourt_jurisdictionstring or nulljudgesstring or nulldate_filedstring or nullDate the opinion was filed (YYYY-MM-DD).
precedential_statusstring or nullcitation_countinteger or nullHow many later opinions cite this one.
docket_numberstring or nullsource_urlstring or nullPublic web page for this opinion.
snippetstringText excerpt around the first query-term match. Default "".
opinion_textstringFull opinion body text; may be truncated (see 'truncated'). Default "".
truncatedbooleanTrue when opinion_text was cut to the response size cap. Default false.
Example response
{
"opinion_id": 812345,
"case_name": "Doe v. City of Example",
"court_id": "ca9",
"date_filed": "2014-06-02",
"citation_count": 42,
"source_url": "https://...",
"opinion_text": "Full opinion body text...",
"truncated": false
}
Epstein corpus screening
Screen a person and their associates against the DOJ and House Oversight Epstein document release and a curated watchlist. Your key needs the epstein_screening product.
POST/epstein-screening/screen
Screen a person (and optional associates) against the Epstein corpus
Screen subject_name (plus any additional_names) against the Epstein document corpus and curated associate watchlist. Returns, per name, a cited match or an explicit no-match.
Every match is a POTENTIAL same-name reference that MUST be identity-verified before it is asserted; presence in these records is not evidence of wrongdoing, and presenting a hit as fact is defamatory. Always surface the returned disclaimer with any result. Requires the epstein_screening product.
Request body
subject_namestringrequiredFull name of the person to screen. 2–200 characters.
additional_namesarray of strings or nullOptional associated names to screen alongside the subject (business associates, family, etc.). Up to 40 items.
use_corpusbooleanSearch the full corpus + watchlist (True) or only the curated watchlist (False). Default true.
attestationstringrequiredRequired affirmation (the standard attestation token) that the subject is a candidate or public figure and the screening is for lawful political research.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/epstein-screening/screen" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject_name": "Jane Q. Public",
"additional_names": [
"Acme Holdings LLC",
"John Public"
],
"use_corpus": true,
"attestation": "subject-is-candidate-or-public-figure"
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/epstein-screening/screen",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"subject_name": "Jane Q. Public",
"additional_names": ["Acme Holdings LLC", "John Public"],
"use_corpus": True,
"attestation": "subject-is-candidate-or-public-figure",
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
3 response fields
resultsarray of objectsrequired9 fields inside results
namestringrequiredmatchedbooleanrequiredactionstringrequired'flag' (assert-worthy hit), 'review' (weak, verify), or 'pass' (clean).
confidencenumberrequiredtierstringrequired'watchlist', 'corpus_keyword', or 'none'.
doc_typesarray of stringscitationsarray of objects3 fields inside citations
labelstringrequireddoc_typestring or nulldoc_idstring or nullEFTA document id where available.
connectionsarray of stringsnotestringDefault "".
corpus_document_countintegerrequireddisclaimerstringrequiredExample response
{
"results": [
{
"name": "Jane Q. Public",
"matched": false,
"action": "pass",
"confidence": 0.0,
"tier": "none",
"doc_types": [],
"citations": [],
"connections": [],
"note": "No matches across the 383,000-document Epstein corpus."
}
],
"corpus_document_count": 383000,
"disclaimer": "..."
}
Adverse screening
Screen a person against federal and international exclusion, enforcement, debarment and sanctions lists, and the DOJ press-release archive. Your key needs the adverse_screening product.
POST/adverse-screening/screen
Screen a person (and optional associates) against free federal / international bad-actor lists
Screen subject_name (plus any additional_names) against the warehoused adverse screening lists and the DOJ press-release archive. Returns, per name, cited hits or an explicit no-match, plus a sources_checked list with per-list freshness dates.
Every hit is a POTENTIAL same-name match that MUST be identity-verified before it is asserted; presence on a list is not evidence of wrongdoing by the person you screened, and presenting a hit as fact is defamatory. Always surface the returned disclaimer with any result. Requires the adverse_screening product.
Request body
subject_namestringrequiredFull name of the person to screen. 2–200 characters.
additional_namesarray of strings or nullOptional associated names to screen alongside the subject (employers, orgs, family). Up to 40 items.
attestationstringrequiredRequired affirmation (the standard attestation token) that the subject is a candidate or public figure and the screening is for lawful political research.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/adverse-screening/screen" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject_name": "Jane Q. Public",
"additional_names": [
"Acme Holdings LLC"
],
"attestation": "subject-is-candidate-or-public-figure"
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/adverse-screening/screen",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"subject_name": "Jane Q. Public",
"additional_names": ["Acme Holdings LLC"],
"attestation": "subject-is-candidate-or-public-figure",
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
3 response fields
resultsarray of objectsrequired6 fields inside results
namestringrequiredmatchedbooleanrequiredhitsarray of objects18 fields inside hits
sourcestringrequiredList slug, e.g. 'hhs_oig_leie', 'fec_enforcement'.
source_labelstringrequiredPlain-English list name.
record_typestringrequiredcategorystring or nullfull_namestringrequiredThe list entry's name (may be an alias row).
entity_typestringrequired'individual' or 'entity'.
confidencenumberrequiredName-similarity 0.0-1.0; identity is NOT verified.
org_namestring or nullcase_idstring or nulldetail_urlstring or nullCitation link for the record.
summarystring or nulldobstring or nullDate of birth where the list publishes one (verify identity).
dob_textstring or nullnpistring or nullstatestring or nullaction_datestring or nullend_datestring or nullis_aliasbooleanDefault false.
doj_mentionsarray of objects4 fields inside doj_mentions
titlestringrequiredurlstring or nullpr_datestring or nullcomponentsarray of stringsdoj_mention_countintegerDefault 0.
notestringDefault "".
sources_checkedarray of objects6 fields inside sources_checked
sourcestringrequiredlabelstringrequiredrecord_countinteger or nullas_ofstring or nulllast_success_atstring or nullis_stalebooleanDefault false.
disclaimerstringrequiredExample response
{
"results": [
{
"name": "Jane Q. Public",
"matched": false,
"hits": [],
"doj_mentions": [],
"doj_mention_count": 0,
"note": "No matches across the adverse screening lists or the DOJ press-release archive."
}
],
"sources_checked": [
{
"source": "hhs_oig_leie",
"label": "HHS OIG healthcare exclusions (LEIE)",
"record_count": 80000,
"as_of": "2026-10-01T05:15:00+00:00",
"last_success_at": "2026-10-01T05:15:00+00:00",
"is_stale": false
}
],
"disclaimer": "..."
}
Municipal meeting search
Search speaker-attributed transcripts of city council and county board meetings, with a link that plays each moment. Your key needs the meeting_search product.
POST/meetings/search
Search municipal meeting transcripts for attributed moments
Search Civly's diarized municipal-meeting archive by keyword and/or speaker, with meeting-level filters (state, municipality, governing body, date range). Returns ranked moments; each carries a verbatim quote, the resolved speaker name, the meeting and date, a timestamp, and a clip link that plays the exact moment. Requires the meeting_search product.
Only moments with a resolved real speaker name are returned — a keyword hit spoken by an unresolved diarization label is never surfaced. Quotes are auto-transcribed: verify against the linked source video before asserting.
Request body
querystring or nullKeyword or phrase to find in what was said (websearch syntax; "quoted phrases" supported). 2–500 characters.
speakerstring or nullSpeaker name, partial match (e.g. a council member). Up to 200 characters.
statestring or nullTwo-letter state, e.g. 'CA'. 2–2 characters.
municipalitystring or nullMunicipality name, partial match. Up to 200 characters.
body_namestring or nullGoverning body, partial match. Up to 200 characters.
date_fromdate or nullOnly meetings on or after this date (YYYY-MM-DD).
date_todate or nullOnly meetings on or before this date (YYYY-MM-DD).
max_resultsintegerHow many ranked moments to return (1-50). From 1 to 50. Default 15.
offsetintegerResult offset for paging through more than max_results moments. At least 0. Default 0.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/meetings/search" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "\"rezoning\" affordable housing",
"state": "CA",
"municipality": "Antioch",
"date_from": "2023-01-01",
"max_results": 10,
"offset": 0
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/meetings/search",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"query": "\"rezoning\" affordable housing",
"state": "CA",
"municipality": "Antioch",
"date_from": "2023-01-01",
"max_results": 10,
"offset": 0,
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
4 response fields
querystring or nullspeakerstring or nullresultsarray of objects11 fields inside results
quotestringrequiredVerbatim passage as spoken (auto-transcribed; verify against the video).
speaker_namestringrequiredResolved real speaker name; moments without one are never returned.
municipalitystring or nullstatestring or nullbody_namestring or nullGoverning body, e.g. 'City Council'.
event_datedate or nullDate of the meeting (YYYY-MM-DD).
timestampstringrequiredHuman-readable offset into the meeting video, e.g. '1:35:07'.
start_secondsintegerrequiredOffset into the meeting video in whole seconds.
source_urlstring or nullPublic meeting video the moment is drawn from.
clip_urlstring or nullLink that opens the meeting video at this exact moment (deep-link).
citationstringrequiredReady-to-quote citation, e.g. '[Antioch City Council, 12/19/23 @ 1:35:07]'.
total_resultsintegerDefault 0.
Example response
{
"query": "\"rezoning\" affordable housing",
"speaker": null,
"total_results": 12,
"results": [
{
"quote": "I move that we approve the rezoning for the affordable housing project on L Street.",
"speaker_name": "Lamar Thorpe",
"municipality": "Antioch",
"state": "CA",
"body_name": "City Council",
"event_date": "2023-12-19",
"timestamp": "1:35:07",
"start_seconds": 5707,
"source_url": "https://.../antioch-council-2023-12-19.mp4",
"clip_url": "https://.../antioch-council-2023-12-19.mp4#t=5707",
"citation": "[Antioch City Council, 12/19/23 @ 1:35:07]"
}
]
}
Clip feed
Pull the clips in your own Clip Library into your dashboards or warehouse: one flat row per clip, with lasting play and thumbnail links that need no login. Your key needs the clip_feed product.
GET/clips
List your clips, oldest first, for a dashboard feed
Page through your company's clips in clip_id order. Store next_after_id after each run and send it as after_id next time to get only clips added since. Clips appear five minutes after they are created. Requires the clip_feed product.
Query parameters
after_idintegerReturn clips with clip_id above this; 0 starts at the start. At least 0. Default 0.
limitintegerClips per page (max 500). From 1 to 500. Default 100.
collectionstring or nullOnly this collection (collection_slug).
Example request
curl "https://app.civly.ai/api/v1/partner/clips?after_id=18233&limit=500" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/clips?after_id=18233&limit=500",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
3 response fields
clipsarray of objects21 fields inside clips
clip_idintegerrequiredStable Civly clip id; use it as the row key.
collectionstring or nullCollection name (usually one per client).
collection_slugstring or nullCollection identifier for the collection filter.
titlestring or nullquotestring or nullWhat was said (auto-transcribed; verify against the video).
speakerstring or nullcategorystring or nullseverityinteger or nullreasonstring or nullOne-line reason the clip was flagged, when AI-found.
showstring or nullProgram or show the clip aired on.
channelstring or nullChannel or station of the source video.
platformstring or nullair_datedate or nullDate the clip aired (YYYY-MM-DD).
start_secondsnumber or nullClip start within the source video.
end_secondsnumber or nullClip end within the source video.
duration_secondsnumber or nulltagsstring or nullComma-separated tags.
source_urlstring or nullOriginal public video, when there is one.
play_urlstring or nullLasting link that plays the clip; no login. Null when the clip has no video.
thumbnail_urlstring or nullLasting link to a still image, when there is one.
created_atdatetimerequiredhas_morebooleanDefault false.
next_after_idinteger or nullHighest clip_id on this page; store it and send it as after_id next time.
Example response
{
"clips": [
{
"clip_id": 18234,
"collection": "Acme for Congress",
"collection_slug": "acme-for-congress",
"title": "Smith on the gas tax",
"quote": "We are not going to raise the gas tax. Period.",
"speaker": "Jane Smith",
"category": "taxes",
"severity": 3,
"reason": "Contradicts her 2024 vote on HB 112",
"show": "Evening News",
"channel": "WXYZ",
"platform": "tv",
"air_date": "2026-10-01",
"start_seconds": 1312.4,
"end_seconds": 1341.0,
"duration_seconds": 28.6,
"tags": "gas-tax, debate",
"source_url": "https://...",
"play_url": "https://app.civly.ai/api/v1/partner/clips/18234/play?sig=...",
"thumbnail_url": "https://app.civly.ai/api/v1/partner/clips/18234/thumbnail?sig=...",
"created_at": "2026-10-01T22:14:09Z"
}
],
"has_more": true,
"next_after_id": 18234
}
GET/clips/{clip_id}/play
Play a clip (signed link from the clip feed; no API key)
Redirects to the clip's video: the cut clip when there is one, otherwise the source video opened at the clip's moment. A TV clip whose video is still being converted answers 503 with Retry-After.
Path parameters
clip_idintegerrequiredQuery parameters
sigstringrequiredExample request
curl -sI "https://app.civly.ai/api/v1/partner/clips/18234/play?sig=..."
import requests
resp = requests.get("https://app.civly.ai/api/v1/partner/clips/18234/play?sig=...", allow_redirects=False, timeout=60)
print(resp.status_code, resp.headers.get("Location"))
Response 302
HTTP/2 302
location: https://... (the clip's video, valid for an hour)
cache-control: private, max-age=300
GET/clips/{clip_id}/thumbnail
Clip still image (signed link from the clip feed; no API key)
Redirects to the clip's own frame, or to its source video's thumbnail.
Path parameters
clip_idintegerrequiredQuery parameters
sigstringrequiredExample request
curl -sI "https://app.civly.ai/api/v1/partner/clips/18234/thumbnail?sig=..."
import requests
resp = requests.get("https://app.civly.ai/api/v1/partner/clips/18234/thumbnail?sig=...", allow_redirects=False, timeout=60)
print(resp.status_code, resp.headers.get("Location"))
Response 302
HTTP/2 302
location: https://... (the clip's still image, valid for an hour)
cache-control: private, max-age=300
NIL deal pricing
Fair-market-value estimates for college athlete NIL deals, priced line by line against published, dated rates. Your key needs the nil_pricing product.
POST/nil-pricing/price
Price a proposed NIL deal line by line against published market rates
Price each deliverable in a deal against a published rate for that kind of work, adjusted for this athlete, and total the result.
Every priced line returns the rate used, the benchmark it came from with its source URL and as_of date, the reasoning for where this athlete sits inside the published range, and the extension. Deliverables with no sourced rate return priced: false with the reason rather than an estimate. The quote is stored and replayable by its quote_ref. Requires the nil_pricing product.
Request body
athleteobjectrequired3 fields inside athlete
athlete_idinteger or nullseasoninteger or nullFrom 2,015 to 2,100.
followersmap of integer or nullPlatform -> follower count, when Civly does not hold the athlete.
deliverablesarray of objectsrequiredUp to 50 items.
10 fields inside deliverables
deliverable_typestringrequiredsocial_post, appearance, likeness_usage, tv_spot, ...
quantityintegerFrom 1 to 10,000. Default 1.
platformstring or nullinstagram, tiktok, x, youtube.
formatstring or nullstatic, video, story, reel.
geographystring or nulllocal, regional, national.
term_monthsinteger or nullFrom 1 to 120.
duration_minutesinteger or nullFrom 1 to 1,440.
exclusivitybooleanDefault false.
audienceinteger or nullFrom 1 to 1,000,000,000.
descriptionstring or nullUp to 1,000 characters.
payor_namestring or nullUp to 300 characters.
payor_typestring or nullcollective, business, associated_entity, unknown.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/nil-pricing/price" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"athlete": {
"athlete_id": 8812,
"season": 2025
},
"payor_name": "A regional bank",
"payor_type": "business",
"deliverables": [
{
"deliverable_type": "social_post",
"quantity": 4,
"platform": "instagram",
"format": "static"
},
{
"deliverable_type": "appearance",
"quantity": 2,
"duration_minutes": 120
},
{
"deliverable_type": "likeness_usage",
"geography": "regional",
"term_months": 12
}
]
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/nil-pricing/price",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"athlete": {
"athlete_id": 8812,
"season": 2025,
},
"payor_name": "A regional bank",
"payor_type": "business",
"deliverables": [
{
"deliverable_type": "social_post",
"quantity": 4,
"platform": "instagram",
"format": "static",
},
{
"deliverable_type": "appearance",
"quantity": 2,
"duration_minutes": 120,
},
{
"deliverable_type": "likeness_usage",
"geography": "regional",
"term_months": 12,
},
],
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
14 response fields
quote_refstringrequiredathleteobjectrequired29 fields inside athlete
athlete_idinteger or nullfull_namestring or nullsportstring or nullseasoninteger or nullposition_abbrstring or nullprogram_namestring or nullconference_namestring or nullprogram_supplied_by_callerbooleanDefault false.
followersmap of integerengagement_ratemap of numberfollowers_supplied_by_callerbooleanDefault false.
reach_captured_ondatetime or nullproduction_statstring or nullproduction_valuenumber or nullproduction_percentilenumber or nullproduction_peer_countintegerDefault 0.
production_seasoninteger or nullmarket_namestring or nullmarket_populationinteger or nullmarket_gdp_thousandsinteger or nullmarket_pricedbooleanDefault false.
market_not_priced_reasonstring or nullprogram_revenue_usdinteger or nullprogram_revenue_percentilenumber or nullprogram_revenue_peer_countintegerDefault 0.
program_revenue_peer_groupstring or nullprofile_tierstring or nullprofile_tier_reasonstring or nullunresolvedarray of stringslinesarray of objectsrequired26 fields inside lines
deliverable_typestringrequiredplatformstring or nullformatstring or nullquantityintegerDefault 1.
geographystring or nullterm_monthsinteger or nullexclusivitybooleanDefault false.
descriptionstring or nullpricedbooleanrequiredunpriced_reasonstring or nullcoverage_gapbooleanDefault false.
rate_lownumber or nullrate_highnumber or nullrate_basisstring or nullrate_open_endedbooleanDefault false.
rate_floor_boundbooleanDefault false.
benchmark_idinteger or nullbenchmark_rate_lownumber or nullbenchmark_rate_highnumber or nullbenchmark_tierstring or nullextended_lownumber or nullextended_highnumber or nullplacement_reasonstring or nullcitationsarray of objects6 fields inside citations
namestringrequiredurlstring or nullas_ofstring or nullrolestringrequiredfigurestring or nullnotestring or nullmax_per_seasoninteger or nullexceeds_capbooleanDefault false.
total_lownumberrequiredtotal_highnumberrequiredtotal_high_openbooleanDefault false.
unpriced_line_countintegerDefault 0.
priced_atdatetimerequiredengine_versionstringDefault "1.0.0".
rates_as_ofdate or nullbenchmark_checkobject or null10 fields inside benchmark_check
archetypestringrequiredprojection_periodstringrequiredcommercial_nil_usdintegerrequiredexcluded_roster_pay_usdintegerrequireddeal_share_low_pctnumberrequireddeal_share_high_pctnumberrequiredbasisstringrequiredsource_namestringrequiredsource_urlstring or nullas_ofdaterequiredclearinghouseobject or null16 fields inside clearinghouse
period_keystringrequiredperiod_labelstringrequiredcleared_countintegerrequiredcleared_total_usdintegerrequireddenied_countintegerrequireddenied_total_usdintegerrequiredapproved_mean_usdintegerrequireddenied_mean_usdintegerrequiredreview_threshold_usdintegerrequiredbandstringrequiredband_reasonstringrequiredcaveatstringrequiredsource_namestringrequiredsource_urlstringrequiredas_ofdaterequiredreconciliation_notestring or nullathlete_yearobject or null8 fields inside athlete_year
commercial_lownumber or nullcommercial_highnumber or nullschool_lownumber or nullschool_highnumber or nulltotal_lownumber or nulltotal_highnumber or nulldeal_share_of_commercial_pctnumber or nullnotestringrequireddisclaimerstringExample response
{
"quote_ref": "nilq_...",
"athlete": {
"athlete_id": 8812,
"sport": "football",
"season": 2025,
"followers": {
"instagram": 28000
}
},
"lines": [
{
"deliverable_type": "social_post",
"quantity": 4,
"priced": true,
"rate_low": 280.0,
"rate_high": 500.0,
"rate_basis": "per_unit",
"extended_low": 1120.0,
"extended_high": 2000.0,
"placement_reason": "28,000 followers. The per-follower anchor puts a post at 280...",
"citations": [
{
"name": "...",
"url": "https://...",
"as_of": "2026-08-02",
"role": "anchor"
}
]
}
],
"total_low": 8120.0,
"total_high": 17300.0,
"total_high_open": false,
"unpriced_line_count": 0,
"priced_at": "2026-10-02T14:05:11Z",
"rates_as_of": "2026-08-02",
"disclaimer": "..."
}
POST/nil-pricing/reverse
Show the work a target budget would require, and whether that volume is credible
Given a target budget, return the package of work that reaches it, with a warning on every deliverable whose volume exceeds a credible per-season maximum.
feasible, credible_ceiling_low/_high and shortfall_multiple quantify those warnings: they say what the athlete's whole season of inventory supports and by what multiple the target overshoots it. A package returned with feasible: false reaches the number only at volumes this athlete cannot credibly deliver, and presenting its total without the warnings misrepresents the answer. Requires the nil_pricing product.
Request body
athleteobjectrequired3 fields inside athlete
athlete_idinteger or nullseasoninteger or nullFrom 2,015 to 2,100.
followersmap of integer or nullPlatform -> follower count, when Civly does not hold the athlete.
target_usdnumberrequiredAt most 100,000,000.
deliverable_mixarray of strings or nullUp to 11 items.
geographystring or nullDefault "regional".
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/nil-pricing/reverse" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"athlete": {
"athlete_id": 8812
},
"target_usd": 200000,
"geography": "regional",
"deliverable_mix": [
"social_post",
"appearance",
"likeness_usage"
]
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/nil-pricing/reverse",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"athlete": {
"athlete_id": 8812,
},
"target_usd": 200000,
"geography": "regional",
"deliverable_mix": ["social_post", "appearance", "likeness_usage"],
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
17 response fields
quote_refstringrequiredtarget_usdnumberrequiredpackage_total_usdnumberrequiredlinesarray of objectsrequired26 fields inside lines
deliverable_typestringrequiredplatformstring or nullformatstring or nullquantityintegerDefault 1.
geographystring or nullterm_monthsinteger or nullexclusivitybooleanDefault false.
descriptionstring or nullpricedbooleanrequiredunpriced_reasonstring or nullcoverage_gapbooleanDefault false.
rate_lownumber or nullrate_highnumber or nullrate_basisstring or nullrate_open_endedbooleanDefault false.
rate_floor_boundbooleanDefault false.
benchmark_idinteger or nullbenchmark_rate_lownumber or nullbenchmark_rate_highnumber or nullbenchmark_tierstring or nullextended_lownumber or nullextended_highnumber or nullplacement_reasonstring or nullcitationsarray of objects6 fields inside citations
namestringrequiredurlstring or nullas_ofstring or nullrolestringrequiredfigurestring or nullnotestring or nullmax_per_seasoninteger or nullexceeds_capbooleanDefault false.
warningsarray of stringsfeasiblebooleanrequiredcredible_ceiling_lownumberrequiredcredible_ceiling_highnumberrequiredshortfall_multiplenumber or nullvolume_constraintsarray of objects9 fields inside volume_constraints
deliverable_typestringrequiredplatformstring or nullmax_per_seasoninteger or nullrequired_for_targetintegerrequiredexceedsbooleanrequiredrationalestring or nullsource_kindstring or nullsource_namestring or nullsource_urlstring or nullpackage_total_lownumberrequiredpackage_total_highnumberrequiredathleteobjectrequired29 fields inside athlete
athlete_idinteger or nullfull_namestring or nullsportstring or nullseasoninteger or nullposition_abbrstring or nullprogram_namestring or nullconference_namestring or nullprogram_supplied_by_callerbooleanDefault false.
followersmap of integerengagement_ratemap of numberfollowers_supplied_by_callerbooleanDefault false.
reach_captured_ondatetime or nullproduction_statstring or nullproduction_valuenumber or nullproduction_percentilenumber or nullproduction_peer_countintegerDefault 0.
production_seasoninteger or nullmarket_namestring or nullmarket_populationinteger or nullmarket_gdp_thousandsinteger or nullmarket_pricedbooleanDefault false.
market_not_priced_reasonstring or nullprogram_revenue_usdinteger or nullprogram_revenue_percentilenumber or nullprogram_revenue_peer_countintegerDefault 0.
program_revenue_peer_groupstring or nullprofile_tierstring or nullprofile_tier_reasonstring or nullunresolvedarray of stringspriced_atdatetimerequiredengine_versionstringDefault "1.0.0".
clearinghouseobject or null16 fields inside clearinghouse
period_keystringrequiredperiod_labelstringrequiredcleared_countintegerrequiredcleared_total_usdintegerrequireddenied_countintegerrequireddenied_total_usdintegerrequiredapproved_mean_usdintegerrequireddenied_mean_usdintegerrequiredreview_threshold_usdintegerrequiredbandstringrequiredband_reasonstringrequiredcaveatstringrequiredsource_namestringrequiredsource_urlstringrequiredas_ofdaterequiredreconciliation_notestring or nulldisclaimerstringExample response
{
"quote_ref": "nilq_...",
"target_usd": 200000.0,
"package_total_usd": 202960.0,
"package_total_low": 190400.0,
"package_total_high": 215520.0,
"feasible": false,
"credible_ceiling_low": 13960.0,
"credible_ceiling_high": 28800.0,
"shortfall_multiple": 6.94,
"lines": [
{
"deliverable_type": "social_post",
"quantity": 104,
"priced": true,
"max_per_season": 7,
"exceeds_cap": true
}
],
"warnings": [
"No package at credible volume reaches 200,000..."
],
"athlete": {
"athlete_id": 8812
},
"priced_at": "2026-10-02T14:05:11Z"
}
POST/nil-pricing/annual
Value a full year of an athlete: what a school would pay, and what brands would pay
Value a whole season, in two halves that are reported separately and never blended.
What a school would pay (roster_pay) is a published annual pay range for the athlete's position, from a survey of 20+ college general managers and agents, with the athlete placed inside it by production percentile against peers at that position. Audience is not an input: schools pay to win games. Read placement_reason for the published range, the athlete's rank and the arithmetic between them.
What brands would pay (annual_low/annual_high) prices every kind of commercial work at a published rate and multiplies by how much of it a season holds.
Built bottom up: each line is a published rate for that work, times a per-season count for the athlete's sport. Nothing is a share of any published annual valuation. The social-post and appearance counts are derived from transaction data covering 125,000+ student-athletes; a line whose count is still ours reports cap_source_kind: "civly_assumption", and assumption_backed_usd totals how much of the figure rests on those.
excluded_work names every kind of work left out and why, and activity_count_check sets the package against published annual activity counts for the sport.
annual_low/annual_high are null when the year would be short a line Civly could not price, either because no sourced rate covers the athlete or because no sourced count says how much of that work a season holds. A total missing a line reads as complete and is not, and it errs in one direction only: the athlete looks cheaper, so any ratio built on the figure rises. annual_unavailable_reason says which lines are missing and incomplete_lines names them. Do not sum the lines to recover a total. A line unpriced because an input about the athlete was missing does not withhold the year, and reports coverage_gap: false. Requires the nil_pricing product.
Request body
athleteobjectrequired3 fields inside athlete
athlete_idinteger or nullseasoninteger or nullFrom 2,015 to 2,100.
followersmap of integer or nullPlatform -> follower count, when Civly does not hold the athlete.
seasoninteger or nullFrom 2,015 to 2,100.
geographystring or nullDefault "regional".
committedmap of integer or nulldeliverable_type -> count already signed this season.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/nil-pricing/annual" \
-H "Authorization: Bearer $CIVLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"athlete": {
"athlete_id": 8812
},
"season": 2025,
"geography": "national",
"committed": {
"social_post": 4,
"appearance": 1
}
}'
import os
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/nil-pricing/annual",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
json={
"athlete": {
"athlete_id": 8812,
},
"season": 2025,
"geography": "national",
"committed": {
"social_post": 4,
"appearance": 1,
},
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
29 response fields
quote_refstringrequiredathleteobjectrequired29 fields inside athlete
athlete_idinteger or nullfull_namestring or nullsportstring or nullseasoninteger or nullposition_abbrstring or nullprogram_namestring or nullconference_namestring or nullprogram_supplied_by_callerbooleanDefault false.
followersmap of integerengagement_ratemap of numberfollowers_supplied_by_callerbooleanDefault false.
reach_captured_ondatetime or nullproduction_statstring or nullproduction_valuenumber or nullproduction_percentilenumber or nullproduction_peer_countintegerDefault 0.
production_seasoninteger or nullmarket_namestring or nullmarket_populationinteger or nullmarket_gdp_thousandsinteger or nullmarket_pricedbooleanDefault false.
market_not_priced_reasonstring or nullprogram_revenue_usdinteger or nullprogram_revenue_percentilenumber or nullprogram_revenue_peer_countintegerDefault 0.
program_revenue_peer_groupstring or nullprofile_tierstring or nullprofile_tier_reasonstring or nullunresolvedarray of stringsseasoninteger or nulllinesarray of objectsrequired16 fields inside lines
deliverable_typestringrequiredlineobjectrequired26 fields inside line
deliverable_typestringrequiredplatformstring or nullformatstring or nullquantityintegerDefault 1.
geographystring or nullterm_monthsinteger or nullexclusivitybooleanDefault false.
descriptionstring or nullpricedbooleanrequiredunpriced_reasonstring or nullcoverage_gapbooleanDefault false.
rate_lownumber or nullrate_highnumber or nullrate_basisstring or nullrate_open_endedbooleanDefault false.
rate_floor_boundbooleanDefault false.
benchmark_idinteger or nullbenchmark_rate_lownumber or nullbenchmark_rate_highnumber or nullbenchmark_tierstring or nullextended_lownumber or nullextended_highnumber or nullplacement_reasonstring or nullcitationsarray of objects6 fields inside citations
namestringrequiredurlstring or nullas_ofstring or nullrolestringrequiredfigurestring or nullnotestring or nullmax_per_seasoninteger or nullexceeds_capbooleanDefault false.
max_per_seasoninteger or nullannual_lownumber or nullannual_highnumber or nullcap_source_kindstring or nullcap_source_namestring or nullcap_source_urlstring or nullcap_sportstring or nullvolume_notestring or nulldropped_reasonstring or nullcommittedinteger or nullremaininginteger or nullremaining_lownumber or nullremaining_highnumber or nulloversoldbooleanDefault false.
annual_lownumber or nullannual_highnumber or nullannual_unavailable_reasonstring or nullincomplete_linesarray of stringsannual_high_openbooleanDefault false.
assumption_backed_usdnumberDefault 0.0.
unpriced_line_countintegerDefault 0.
excluded_workmap of stringsport_activity_countnumber or nullactivity_count_checkstring or nullremaining_lownumber or nullremaining_highnumber or nulloversold_linesarray of stringstotal_lownumber or nulltotal_highnumber or nulltotal_unavailable_reasonstring or nulltotal_basisstring or nullarchetype_totalobject or null9 fields inside archetype_total
archetypestringrequiredtotal_usdintegerrequiredprecisionstringDefault "archetype average, not an estimate for this athlete".
periodstringrequiredpredates_revenue_sharingbooleanDefault false.
basisstringrequiredsource_namestringrequiredsource_urlstring or nullas_ofdaterequiredposition_archetypeobject or null10 fields inside position_archetype
archetypestringrequiredannual_lowintegerrequiredannual_highintegerrequiredprecisionstringcoarse_position_notestring or nullbasisstringrequiredcaveatstringrequiredsource_namestringrequiredsource_urlstring or nullas_ofdaterequiredroster_payobject or null13 fields inside roster_pay
archetypestringrequiredcoarse_position_notestring or nullannual_usdintegerrequiredannual_lownumber or nullannual_highnumber or nullplacement_reasonstring or nullprecisionstringDefault "published position range, placed by on-field production".
componentsstringrequiredcaveatstring or nullbasisstring or nullsource_namestringrequiredsource_urlstring or nullas_ofdaterequiredcampaign_geographystring or nullpriced_atdatetimerequiredengine_versionstringDefault "1.0.0".
rates_as_ofdate or nulldisclaimerstringExample response
{
"quote_ref": "nilq_...",
"athlete": {
"athlete_id": 8812
},
"season": 2025,
"lines": [
{
"deliverable_type": "social_post",
"max_per_season": 7,
"annual_low": 1960.0,
"annual_high": 3500.0,
"cap_source_kind": "derived",
"line": {
"deliverable_type": "social_post",
"priced": true
}
}
],
"annual_low": 13960.0,
"annual_high": 28800.0,
"assumption_backed_usd": 15000.0,
"excluded_work": {
"tv_spot": "no sourced broadcast rate yet..."
},
"priced_at": "2026-10-02T14:05:11Z"
}
GET/nil-pricing/rate-card
List every current NIL rate benchmark with its source
Every rate currently in force, with the bracket it applies to, what one unit buys, and the source it was read from. Superseded rows are excluded; a quote issued against an older survey still cites its own figures and replays unchanged.
Rows whose source licence permits citation but not reproduction are listed with their source and their figures withheld. Requires the nil_pricing product.
Example request
curl "https://app.civly.ai/api/v1/partner/nil-pricing/rate-card" \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/nil-pricing/rate-card",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
2 response fields
ratesarray of objectsrequired17 fields inside rates
idintegerrequireddeliverable_typestringrequiredplatformstring or nullformatstring or nullbasisstringrequiredtier_metricstring or nulltier_lownumber or nulltier_highnumber or nulltier_labelstring or nullrate_lownumber or nullrate_highnumber or nullunit_notestring or nullsource_namestringrequiredsource_urlstring or nullas_ofdaterequiredredistributionstringrequirednotesstring or nullcountintegerrequiredExample response
{
"count": 1,
"rates": [
{
"id": 3,
"deliverable_type": "social_post",
"platform": "instagram",
"basis": "per_unit",
"tier_low": 10000,
"tier_high": 100000,
"rate_low": 500.0,
"rate_high": 5000.0,
"unit_note": "One in-feed post to the athlete's own account...",
"source_name": "...",
"source_url": "https://...",
"as_of": "2026-08-02",
"redistribution": "..."
}
]
}
GET/nil-pricing/quotes/{quote_ref}
Replay a stored quote exactly as it was issued
Return a previously issued quote verbatim, including the rates and source dates it cited at the time. This does not recompute: the rates it was priced against may have been resurveyed since, and a recomputation would return a different number and prove nothing about the one being asked about. Requires the nil_pricing product.
Path parameters
quote_refstringrequiredExample request
curl "https://app.civly.ai/api/v1/partner/nil-pricing/quotes/nilq_..." \
-H "Authorization: Bearer $CIVLY_API_KEY"
import os
import requests
resp = requests.get(
"https://app.civly.ai/api/v1/partner/nil-pricing/quotes/nilq_...",
headers={"Authorization": f"Bearer {os.environ['CIVLY_API_KEY']}"},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 200
A JSON object; the example shows its shape.
Example response
{
"quote_ref": "nilq_...",
"total_low": 8120.0,
"total_high": 17300.0,
"lines": [
"..."
]
}
Access requests
Ask Civly for a key from your own code. The form linked under Get access does the same thing.
POST/access-request
Request partner API access
Ask Civly for a partner API key. Every request is reviewed by a human; approved partners receive their key and webhook secret over a secure channel. This endpoint never issues credentials.
Request body
company_namestringrequired2–200 characters.
contact_namestringrequired2–200 characters.
emailemailrequireduse_casestringrequiredWhat you plan to build and research with the API. 20–3,000 characters.
requested_productsarray of strings or nullProducts you want: big_book, social_analysis, data_access, opinion_search. Omit for all.
Example request
curl -X POST "https://app.civly.ai/api/v1/partner/access-request" \
-H "Content-Type: application/json" \
-d '{
"company_name": "Example Strategies",
"contact_name": "Jane Example",
"email": "[email protected]",
"use_case": "Screen our donor list each quarter and pull research reports on new candidates.",
"requested_products": [
"adverse_screening",
"big_book"
]
}'
import requests
resp = requests.post(
"https://app.civly.ai/api/v1/partner/access-request",
json={
"company_name": "Example Strategies",
"contact_name": "Jane Example",
"email": "[email protected]",
"use_case": "Screen our donor list each quarter and pull research reports on new candidates.",
"requested_products": ["adverse_screening", "big_book"],
},
timeout=60,
)
resp.raise_for_status()
print(resp.json())
Response 201
3 response fields
receivedbooleanDefault true.
request_idintegerrequiredmessagestringExample response
{
"received": true,
"request_id": 42,
"message": "Thanks - Civly reviews every request and will reply by email."
}
OpenAPI and llms.txt
- OpenAPI 3.1 spec: the partner endpoints only. Generate a typed client from it, or import it into Postman or Insomnia.
- Interactive reference: the same spec with a “try it” console, for sending real requests with your key.
- Guide for AI coding assistants: one plain-text file covering every product, written so an assistant such as Claude or ChatGPT can build and debug an integration from it. Paste the URL into your assistant and ask it to write your client.
Responsible use
- Subjects must be candidates or public figures, and the research must serve a lawful purpose. That is what the attestation affirms, and every submission is audited.
- A screening hit is a possible same-name match, not proof. Results from Epstein corpus screening and adverse screening name a record that resembles the name you sent. Confirm identity with the date of birth, state and employer fields before relying on one, never present a hit as established fact, and show the
disclaimerthe response returns alongside any match you display. - Meeting and clip quotes are machine-transcribed. Check a quote against the linked video before you publish it.
- NIL prices are estimates. They are not a prediction of whether a deal will clear review. Keep the caveats, warnings and reasons the response carries with any figure you show, and read
feasiblebefore any reverse-mode total.
Support
Questions about the API, your key or your limits: matthew@civly.ai. When something goes wrong, include the job_id and the X-Request-ID response header.
Civly’s Privacy Policy and Terms of Use apply to all use of the API. To see what the data behind it covers, visit the Data page.