AiCitationChecker REST API — POST /api/verify, POST /api/find_references, and supporting endpoints.
Stateless JSON over HTTPS with Bearer-token auth, so it wraps cleanly as a tool for AI agents and writing assistants, a step in your own app, or a batch script. Manage your key on the API Access page (login required).
All API requests require a Bearer token in the Authorization header.
POST /api/token
Content-Type: application/json
{"email": "your@email.com", "password": "your-password"}
If this account has no key yet, the response is 200 {"api_key": "…43-char token…", "username": "…", "tier": "…"}. The raw key is returned only this once and is not stored in plaintext.
If a key already exists, the response is 409 {"error": "API_KEY_EXISTS", "detail": "Regenerate at /api-access or POST /api/generate_key; the old key stops working."}. The existing key is not returned. Regenerating invalidates the previous key.
POST /api/generate_key Authorization: Bearer <current-key>
The old key is immediately invalidated.
Pass the key in every subsequent request:
Authorization: Bearer <api-key>
Verify a batch of bibliographic references against CrossRef, Semantic Scholar, and OpenAlex.
{
"references": [
"Smith, J.; Jones, A. Title of paper. Journal 2020, 10, 123.",
"Doe, J. Another title. Journal 2021, 5, 456-470, https://doi.org/10.xxxx/yyyy."
],
"mode": "all",
"citation_style": "MDPI"
}
| Field | Type | Required | Options |
|---|---|---|---|
references | array of strings | yes | One reference per string |
mode | string | no (default all) | all · doi_only · no_doi_only |
citation_style | string | no (default APA) | APA · MDPI · Chicago · Harvard · IEEE · Vancouver |
Mode values:
all — verify every reference regardless of whether a DOI is presentdoi_only — only process references that contain a DOIno_doi_only — only process references without a DOI{
"credits_used": 10.0,
"credits_remaining": 98432.0,
"skipped": [],
"results": [
{
"index": 0,
"input": "Smith, J.; Jones, A. Title of paper. Journal 2020, 10, 123.",
"status": "OK",
"confidence": "EXCELLENT",
"doi": "10.xxxx/yyyy",
"matched_title": "Title of paper",
"formatted_citation": "Smith, J.; Jones, A. Title of paper. Journal 2020, 10, 123. https://doi.org/10.xxxx/yyyy",
"page_mismatch": false,
"pages_ref": null,
"pages_crossref": "123-131"
}
]
}
skipped lists input lines set aside during screening (not charged). Each item has input and reason (e.g. browser/file links, bare filenames, or looks like a paragraph of text, not a reference for pasted prose). results[].input echoes the repaired reference string sent to verification (after copy/paste repair). index corresponds to the position in the filtered billable list (0-based), not the raw request array.
page_mismatch is true when the page range extracted from the source differs from CrossRef — even for OK results. When true, use pages_crossref (the value in formatted_citation) as the authoritative page range.
Find and assign academic references for a short text claim (same pipeline as the in-app Find References tool).
Both modes use the same search-and-cite pipeline; Enhanced searches deeper and explains more. Pick Standard for a single focused claim, Enhanced for a multi-claim paragraph where you want broader coverage and reasoning.
| Standard (25 cr) | Enhanced / Deep (115 cr) | |
|---|---|---|
| References returned | up to 2 | up to 8 |
| Candidate pool searched | up to 12 papers | up to 32 papers (split across extracted topics, deduplicated) |
| Topics extracted from text | 1 | up to 2 |
| Explanation | one summary sentence | per-reference reasoning (why each paper fits) |
| Uncovered-claims note | — | included |
{
"text": "Phase change materials in building envelopes reduce thermal load.",
"enhanced": false,
"verbose": false
}
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
text | string | yes | — | 10–500 characters after HTML strip |
enhanced | boolean | no | false | true = premium / deep mode (115 credits) |
verbose | boolean | no | false | true adds query_used, pool_size, all_references |
{
"credits_used": 25,
"credits_remaining": 970.0,
"mode": "standard",
"status": "ok",
"cited_text": "Phase change materials ... [Cabeza, 2007].",
"references": [
{
"cite_key": "[Cabeza, 2007]",
"title": "...",
"authors": "...",
"year": 2007,
"doi": "10.1016/...",
"journal": "Applied Energy",
"url": "https://doi.org/10.1016/..."
}
],
"explanation": "Selected for direct relevance to thermal storage.",
"uncovered_claims": ""
}
status values: ok (citations assigned), selection_only, no_match (search found nothing), llm2_error (candidates found but assignment failed). When no reference is cited, the search costs less (keyword-only input, which gets a topic listing, pays full); credits_used shows what was charged.
| Limit | Value |
|---|---|
| Per minute | 2 requests |
| Per hour | 30 requests (all modes) |
| Per day (standard) | 50 requests |
| Per day (enhanced) | 20 requests |
| Duplicate text | Same text within 5 minutes returns cached response (credits_used: 0, cached: true) |
curl -X POST https://aicitationchecker.org/api/find_references \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Phase change materials in building envelopes reduce thermal load.", "enhanced": false}'
| Status | Meaning |
|---|---|
OK | Reference verified — DOI resolves and author/title match CrossRef. Page numbers are not checked by the status; always inspect page_mismatch and formatted_citation. |
E1 | DOI cannot be resolved. Common causes: truncated DOI (journal-prefix only), a DOI that is not indexed, or a broken resolver. When a title is available the API falls back to other identification strategies and includes a suggestion where one is found. |
E2 | Legacy author-mismatch code (still supported in responses from older paths). |
E3 | Year mismatch >1 year. Preprint vs. final publication is tolerated (±1 year). |
E4 | The cited title does not match the DOI record: similarity too low to accept the match, or a meaning-changing word was swapped (e.g. “Reduced” cited as “Increased”; the differing words are listed in title_word_changes). May also occur when a DOI resolves to a journal landing page rather than a specific article. |
DOI_NO_TITLE | DOI verified — the DOI resolves and author, year, and journal (or volume/pages) match CrossRef, but the reference text has no article title. Use formatted_citation for the correct title. |
WRONG_DOI | The correct article was found by title search, but the DOI in the source points to a different article. Can be a false positive — verify manually before changing the DOI. |
WRONG_AUTHOR | DOI resolves to a real record, but submitted author metadata disagrees with the resolved article (hallucination warning). |
WRONG_JOURNAL | Title and authors match but the journal differs. |
SUGGESTION | Possible match found for a reference without a DOI. Confidence level (HIGH/MEDIUM/LOW) indicates reliability. LOW confidence for web/government sources usually means the search misfired — ignore it. |
SUGGESTION_S2 | Match found via Semantic Scholar (broader coverage including arXiv and conference papers). Verify manually. |
SUGGESTION_OA | Match found via OpenAIRE (EU repositories, national journals, theses). Metadata quality may vary and DOI can be absent — verify manually. |
NO_DOI | No DOI found and no match in databases. For web, institutional, or legislative references this is expected and correct. |
| Confidence | Meaning |
|---|---|
EXCELLENT | ≥95% title similarity — high certainty |
GOOD | 85–95% — solid match with minor uncertainty |
ACCEPTABLE | 70–85% — review the formatted citation carefully |
WEAK | 50–70% — treat with caution; verify manually |
CRITICAL | <50% — requires manual review regardless of status |
NOT_FOUND | No match at all |
confidence is null when the input is a bare DOI
(just the identifier, with no title or authors alongside it) — there is nothing
submitted to compare the resolved record against. A
status of OK already tells you the DOI resolved, and
matched_title / formatted_citation carry the authoritative
CrossRef record.
| Combination | Recommended action |
|---|---|
OK + EXCELLENT | Accept as-is; also check page_mismatch |
OK + GOOD | Accept; spot-check if the reference is critical |
E1 + suggestion present | DOI is broken/truncated but article was found by title — verify suggestion then correct the DOI |
E4 + CRITICAL | Check both the DOI and the formatted citation carefully |
WRONG_DOI + GOOD | Verify manually — may be a false positive |
SUGGESTION + LOW | Ignore if source has no DOI (misfired); verify manually otherwise |
Errors are returned as JSON — {"error": "…message…"} — never as an HTML page. The HTTP status tells you whether and how to retry. Note that per-reference problems (a broken DOI, a hallucinated title) are not HTTP errors: the call returns 200 and each issue is reported inside results[] via the status field.
| Code | Meaning | Example body | Retry? |
|---|---|---|---|
200 | Success. Inspect each item's status for per-reference outcomes. | see response schema above | — |
400 | Bad request: body isn't JSON, references is missing/empty, no references match the selected mode, or the batch exceeds your tier's refs-per-run limit. | {"error": "\"references\" must be a non-empty list of strings"} | No — fix the request. An over-limit batch is rejected before any charge. |
401 | Missing or invalid API key. | {"error": "Invalid or missing API key"} | No — check the Authorization: Bearer <key> header. |
402 | Insufficient credits. The whole call is rejected before any work — nothing is charged, no partial results. | {"error": "Insufficient credits. Need 14, have 6.", "upgrade_url": "…"} | Yes — after topping up credits. |
429 | Rate limit or quota exceeded (per-minute, runs-per-hour, or the daily find-references quota). | {"error": "Rate limit exceeded: 60 runs per hour"} | Yes — wait for the window to reset, then retry. (No Retry-After header is sent; back off client-side.) |
500 | Server error mid-request. | {"error": "Internal error during verification"} | Yes — retry once. On /api/find_references the credit is auto-refunded. |
503 | Endpoint temporarily unavailable. | {"error": "Endpoint temporarily disabled"} | Yes — retry with backoff. |
Credits are deducted before processing, based on the references actually verified (after cleanup, screening, and the mode filter). Screened-out lines are returned in skipped and are not charged. Every response echoes credits_used and credits_remaining so you can reconcile exactly.
| Input | Endpoint | Credits | What you pay for |
|---|---|---|---|
| Reference with a DOI | /api/verify | 1.0 | One deterministic DOI lookup — the cheapest path. |
| Reference without a DOI | /api/verify | 2.75 | Bibliographic search (1.75) + a DOI lookup of the match (1.0). |
| Find references — standard | /api/find_references | 25 | Claim analysis plus a bibliographic search across indexed sources. |
| Find references — enhanced | /api/find_references | 115 | Deeper search, up to 8 results, per-reference reasoning. |
A mixed /api/verify batch is billed per reference. Example: 8 with a DOI + 2 without = 8 × 1.0 + 2 × 2.75 = 13.5 credits. To pay the deterministic rate only, send the DOI string alone (a "bare DOI") rather than a full citation.
The status reflects DOI resolution and title match — page numbers are not validated. When the source page range differs from CrossRef the response includes "page_mismatch": true along with pages_ref and pages_crossref. Always apply the page range from formatted_citation (which reflects CrossRef data).
| Cause | Example |
|---|---|
| Truncated DOI (journal prefix only) | 10.1680/ensu. instead of 10.1680/ensu.14.00017 |
| DOI valid but not indexed in CrossRef | Some regional journals and smaller publishers |
| DOI resolver link broken | Journal hosted on an alternative server |
Some publishers assign a DOI to the journal itself (e.g. the journal prefix without an article suffix). The API detects this case and returns E1 with a note, rather than E4. Check whether the PDF of the article actually contains a DOI before trusting the source.
WRONG_DOI with GOOD confidence can be a false positive. Always verify manually before changing the DOI in the document.
When a reference contains et al., the API expands the full author list in formatted_citation from the resolved record. This works reliably and is particularly useful for MDPI compliance.
The API handles mixed input styles. If a reference is in APA style but contains a valid DOI, the output is correctly reformatted to the requested citation style regardless of the input format. Author separators, initials, and journal capitalisation are all normalised.
If a reference was cited from an early-access version, the API may return final published metadata with an updated year, volume, and page range. Always apply these corrections from formatted_citation.
NO_DOI for these sources is expected and correct, not an error. If such a reference returns SUGGESTION with LOW confidence, the search misfired — ignore the result.
source_type)Crossref indexes journal articles, so books, book chapters and institutional reports are usually absent from it. When a reference looks like one of those, the response carries an extra field source_type with the value "book" or "report" (and null for everything else). It is informational: the verdict is unchanged, but a NO_DOI that also carries a source_type is an expected miss rather than a coverage failure, and should not be read as a signal against the reference. A reference carrying a DOI never gets a source_type.
catalog_match)For references detected as books (not chapters) that Crossref could not confirm, the API may search Crossref book records, Open Library (CC0), and the Italian OPAC SBN (ICCU catalogue records).
catalog_found: true means the principal author and title matched an institutional catalogue record or two independent catalogues. The row stays NO_DOI and is never counted as DOI-confirmed.
catalog_found: false with a catalog_match object is a possible match with insufficient evidence.
Fields: source, source_label, sources, record_id, url, title, author, year, strength, year_differs.
When a source lists only volume and issue number, the API derives the full page range from the resolved record wherever the publisher's metadata allows it.
curl -s -X POST https://aicitationchecker.org/api/verify \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{
"references": [
"Mealy, P.; Teytelboym, A. Economic complexity and the green economy. Res. Policy 2022, 51, 103948, https://doi.org/10.1016/j.respol.2020.103948."
],
"mode": "all",
"citation_style": "MDPI"
}'
import json, urllib.request
KEY = "your-api-key-here"
REFS = [
"Mealy, P.; Teytelboym, A. Economic complexity and the green economy. "
"Res. Policy 2022, 51, 103948, https://doi.org/10.1016/j.respol.2020.103948.",
]
payload = json.dumps({
"references": REFS,
"mode": "all",
"citation_style": "MDPI"
}).encode("utf-8")
req = urllib.request.Request(
"https://aicitationchecker.org/api/verify",
data=payload,
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json"
}
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode("utf-8"))
print(f"Credits used: {data['credits_used']} Remaining: {data['credits_remaining']}")
for r in data["results"]:
pm = " ⚠ page mismatch" if r.get("page_mismatch") else ""
print(f"[{r['index']+1}] {r['status']:12s} {r.get('confidence',''):10s} DOI: {r.get('doi','—')}{pm}")
import json, urllib.request
from docx import Document
DOCX = "path/to/paper.docx"
KEY = "your-api-key-here"
BATCH = 30 # max per tier (see Tier Limits below)
doc = Document(DOCX)
in_refs = False
refs = []
for para in doc.paragraphs:
t = para.text.strip()
if not in_refs:
if t.lower() == "references":
in_refs = True
continue
if t.lower().startswith(("disclaimer", "appendix", "supplementary")):
break
if t:
refs.append(t)
for i in range(0, len(refs), BATCH):
batch = refs[i:i+BATCH]
payload = json.dumps({
"references": batch,
"mode": "all",
"citation_style": "MDPI"
}).encode("utf-8")
req = urllib.request.Request(
"https://aicitationchecker.org/api/verify",
data=payload,
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode("utf-8"))
for r in data["results"]:
global_idx = i + r["index"] + 1
pm = " ⚠ PAGE MISMATCH" if r.get("page_mismatch") else ""
print(f"[{global_idx:03d}] {r['status']:12s} {r.get('confidence',''):10s} {r['input'][:60]}...{pm}")
$headers = @{ Authorization = "Bearer YOUR_API_KEY" }
$body = '{"references":["10.3390/ma16176015"],"mode":"all","citation_style":"APA"}'
$r = Invoke-RestMethod -Uri "https://aicitationchecker.org/api/verify" -Method Post `
-Headers $headers -ContentType "application/json" -Body $body
$r.results | Format-Table index, status, confidence, doi
Invoke-WebRequest / Invoke-RestMethod hangs on POST while curl works:
this is a known Windows PowerShell 5.1 symptom — and it's client-side, not the API. The server
returns a normal Content-Length response over HTTP/1.1; the hang comes from a local proxy or a
TLS-inspecting security product that doesn't answer the Expect: 100-continue handshake that
.NET's HttpWebRequest sends before the request body (a plain GET has no body, which
is why it isn't affected). Any one of these fixes it:
[System.Net.ServicePointManager]::Expect100Continue = $falsepwsh) — its HttpClient stack doesn't send
Expect: 100-continue by default.curl.exe (bundled with Windows 10/11) instead.| Tier | Runs / hour | Refs / run |
|---|---|---|
| free | 20 | 10 |
| regular | 60 | 20 |