Skip to content

API documentation

A REST API over the same records the site uses: search government registers, read a school, and check names against the known diploma-mill list. JSON in, JSON out.

Base URL
https://verifyed.org
Version
v1
Format
JSON

Getting started

  1. Base URL

    All endpoints are relative to https://verifyed.org/api/v1

  2. Make a request

    Most endpoints are public with no authentication required. Send a GET or POST request and receive JSON.

  3. Rate limits

    Requests are rate-limited per IP. Check the X-RateLimit-* response headers for your current usage.

A first request
bash
curl "https://verifyed.org/api/v1/search?q=springfield+high&country=US"

Search & Discovery

GET/api/v1/autocomplete

PublicRate limit 60 / min anonymous; 120 / min Free; 600 / min Pro

Fast prefix autocomplete for search-as-you-type UIs.

Optimized for dropdown speed. Returns lightweight results suitable for typeahead suggestions. Results are cached for 30–60 minutes depending on prefix length.

Parameters
qstringrequired
Search prefix (min 2 characters).
countrystring
ISO 3166-1 alpha-2 country codes, comma-separated.
statestring
State or province filter (uppercase).
limitnumber= 8
Max results (1–10).
include_allboolean= false
Include non-high-school records.
Request
bash
curl "https://verifyed.org/api/v1/autocomplete?q=spring&country=US&limit=5"
Response
json
{
  "results": [
    {
      "id": "us-nces-170993000948",
      "name": "Springfield High School",
      "city": "Springfield",
      "state_province": "IL",
      "country_code": "US",
      "ceeb_code": "143280",
      "result_type": null
    }
  ],
  "cached": false,
  "query_ms": 45
}

School Details

GET/api/v1/schools/{id}

Public

Get detailed school information including verification and enrichment data.

Returns the full school record with verification checks, accreditation data, enrichment results (geocoding, Street View, AI research), and metadata. Schools that haven’t been enriched recently may trigger automatic background enrichment.

Parameters
idstringrequired
School ID (from search results). e.g. us-nces-170993000948
Request
bash
curl "https://verifyed.org/api/v1/schools/us-nces-170993000948"
Response
json
{
  "id": "us-nces-170993000948",
  "canonical_name": "Springfield High School",
  "website": {
    "url": "https://www.sps186.org/schools/SHS/",
    "source": "government_data"
  },
  "address": {
    "city": "Springfield",
    "state": "IL",
    "country": "US",
    "coordinates": {
      "lat": 39.7817,
      "lng": -89.6501
    }
  },
  "verification": {
    "status": "verified",
    "confidence_score": 0.9,
    "checks": {
      "government_registry": {
        "status": "pass",
        "details": "Found in NCES database"
      },
      "accreditation": {
        "status": "pass",
        "details": "NCA accredited"
      }
    }
  },
  "metadata": {
    "school_type": "High School",
    "grade_levels": "9-12",
    "enrollment": 1850,
    "ceeb_code": "143280",
    "source": "NCES",
    "data_year": 2023,
    "operating_status": "Open"
  },
  "accreditations": [
    {
      "accreditor_name": "NCA",
      "accreditor_type": "Regional",
      "status": "active"
    }
  ],
  "enrichment_pending": false
}
  • The response shape varies based on enrichment level. Newly discovered schools may have fewer fields until enrichment completes.

Enrichment

POST/api/v1/schools/{id}/enrich

Auth optionalRate limit 10 / min

Trigger enrichment to add verification data to a school record.

Kicks off background enrichment jobs that add geocoding, Street View imagery, address validation, and AI-powered research to a school record. Jobs run asynchronously by default — poll the job status endpoint to track progress.

Parameters
idstringrequired
School ID to enrich.
levelstring= standard
Enrichment depth. "basic" = geocoding + address only. "standard" = adds Street View + AI research. "deep" = full research with fraud analysis.
Request
bash
curl -X POST "https://verifyed.org/api/v1/schools/us-nces-170993000948/enrich?level=standard"
Response
json
{
  "status": "queued",
  "job_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "estimated_time_seconds": 30
}
  • Standard enrichment adds address validation, Street View checks, AI-assisted public web research, source-backed contacts, and fraud indicators when available.
  • If X-API-Key is provided and matches ENRICHMENT_API_KEY, auth checks pass. Without a key, the endpoint may still work if no key is configured server-side.
  • Each enrichment request counts toward API plan quota; deep enrichment consumes more AI credits and takes longer.

GET/api/v1/jobs/{job_id}

Public

Check the status of an enrichment job.

Poll this endpoint to track enrichment progress. Jobs transition through queued → running → completed (or failed).

Parameters
job_idstringrequired
Job ID returned from the enrich endpoint.
Request
bash
curl "https://verifyed.org/api/v1/jobs/f47ac10b-58cc-4372-a567-0e02b2c3d479"
Response
json
{
  "job_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "completed",
  "school_id": "us-nces-170993000948",
  "level": "standard",
  "started_at": "2025-01-20T10:30:00Z",
  "completed_at": "2025-01-20T10:30:28Z",
  "error_message": null,
  "sources_checked": [
    "geocoding",
    "street_view",
    "address_validation",
    "perplexity_research"
  ]
}

AI Research

POST/api/v1/research-school

Auth requiredRate limit 2 / min Free; 10 / min Pro

AI-powered fraud research for an unverified school name.

Uses Perplexity AI to research a school name and assess its legitimacy. Returns fraud risk indicators, red flags, sources, and an actionable recommendation. Results are cached for 7 days.

Request body
namestringrequired
School name to research (min 2 characters).
citystring
City to narrow the search.
countrystring
Country to narrow the search.
Request
bash
curl -X POST "https://verifyed.org/api/v1/research-school" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Pacific Coast Academy", "country": "US"}'
Response
json
{
  "found_online": true,
  "confidence": "high",
  "fraud_risk": "low",
  "red_flags": [],
  "sources": [
    {
      "url": "https://example.com/school-page",
      "title": "Pacific Coast Academy - Official Site"
    }
  ],
  "recommendation": "School appears legitimate with verified online presence and accreditation records.",
  "generated_at": "2025-01-20T10:30:00Z",
  "cached": false
}
  • This endpoint calls external AI APIs and may take 5–15 seconds to respond on cache miss.
  • Results are cached for 7 days based on normalized name + city + country.
  • A verified API key or verified browser session with CSRF protection is required, including on cache hits.

Batch screening

POST/api/v1/batch/screen

Auth required

Screen up to 1,000 schools from one CSV upload.

Upload a CSV and get back one job that looks up candidate institutions and reports the matched record's existing verification status. Ambiguous names require identity review. This lookup does not run a separate diploma-mill watchlist check. Send the file as a multipart “file” field or as a raw text/csv body. The header row is required; “school_name” is the only required column. Needs a Pro or Internal API key — lower key tiers get a 402 with an upgrade link.

Request body
school_nameCSV columnrequired
The name to screen, one school per row.
countryCSV column
ISO 3166-1 alpha-2 country code. Narrows matching for that row.
external_idCSV column
Your own record ID, carried through untouched. Without it the Slate export has nothing to match rows against.
Request
bash
curl -X POST "https://verifyed.org/api/v1/batch/screen" \
  -H "X-API-Key: sk_live_a1b2c3d4..." \
  -F "file=@schools.csv"
Response
json
{
  "job_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "school_count": 250,
  "external_id_count": 250,
  "status": "queued",
  "poll_url": "/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "results_url": "/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479/results.csv",
  "results_slate_url": "/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479/results.slate.csv"
}
  • Maximum 1,000 rows and 2 MiB per upload, including multipart overhead. Larger files are rejected before a job is created.
  • Bulk submissions share a separate limit of two per minute and ten per UTC day across the account or workspace. Admitted failed submissions count; rejected submissions create no job. Bulk rows are not counted in the search/detail monthly allowance.
  • The job belongs to the API key that submitted it. Other keys polling the same job ID get a 404, not a 403 — job IDs can't be probed.
  • external_id_count says how many rows the Slate export will be able to match. If it's 0, fix the CSV now rather than after a 1,000-row run.
  • If your workspace has a webhook configured, a batch.completed event is delivered when the job finishes.

GET/api/v1/batch/{id}

Auth required

Poll a batch job's progress.

Parameters
idstringrequired
Job ID returned by the screen endpoint.
Request
bash
curl "https://verifyed.org/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479" \
  -H "X-API-Key: sk_live_a1b2c3d4..."
Response
json
{
  "job_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "processing",
  "school_count": 250,
  "processed_count": 120,
  "error_count": 2,
  "percent_complete": 48,
  "created_at": "2026-07-20T10:30:00Z",
  "completed_at": null,
  "results_url": null
}
  • Jobs move through queued → processing → done (or failed).
  • results_url stays null until the job is done.

GET/api/v1/batch/{id}/results.csv

Auth required

Download the result CSV for a finished job.

One row per input row, in order, with the match, verification flag, confidence score and any per-row error. Results are kept for 7 days after the job finishes; after that the download answers 410 and the batch needs resubmitting.

Parameters
idstringrequired
Job ID returned by the screen endpoint.
Request
bash
curl -O "https://verifyed.org/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479/results.csv" \
  -H "X-API-Key: sk_live_a1b2c3d4..."
Response
json
row_index,school_name,country,matched_name,school_id,verification_flag,confidence_score,school_type,ceeb_code,website,error,external_id
1,Springfield High School,US,Springfield High School,us-nces-170993000948,verified,0.9,High School,143280,https://www.sps186.org/schools/SHS/,,SIS-0042
  • A job that isn't done yet answers 409 with its current status.
  • Flags are verified, unverified, inconclusive, not_found, ambiguous, error, fraudulent or suspicious. Ambiguous means multiple candidate institutions; error means retry the row. Neither is a finding against the school. A partial-name candidate stays inconclusive even if its record is verified.
  • The confidence score is the matched record's enrichment confidence, not a name-match probability. Review the matched identity and evidence before accepting or rejecting a credential.
  • The X-Export-Count and X-Export-Errors headers carry the row counts, so a script can sanity-check the download without parsing it.

GET/api/v1/batch/{id}/results.slate.csv

Auth required

The same results, laid out for a Slate import.

Column names and order match what Slate expects for an inbound file. “Unique ID” is your external_id — rows that were uploaded without one import cleanly and update nothing, so check the X-Export-Matchable header before importing. The “Checked On” date is the day the job finished, not the day the file was downloaded.

Parameters
idstringrequired
Job ID returned by the screen endpoint.
Request
bash
curl -O "https://verifyed.org/api/v1/batch/f47ac10b-58cc-4372-a567-0e02b2c3d479/results.slate.csv" \
  -H "X-API-Key: sk_live_a1b2c3d4..."
Response
json
Unique ID,School 1 Name,School 1 CEEB,VerifyED School ID,VerifyED Verification Flag,VerifyED Match Confidence,VerifyED School Type,VerifyED Website,VerifyED Country,VerifyED Submitted Name,VerifyED Checked On,VerifyED Note
SIS-0042,Springfield High School,"143280",us-nces-170993000948,verified,0.9,High School,https://www.sps186.org/schools/SHS/,US,Springfield High School,2026-07-20,
  • Import the file into Slate directly. A round trip through Excel strips the leading zeros Slate needs on CEEB codes, and no CSV encoding can prevent that.
  • X-Export-Matchable counts the rows Slate can actually match — the rows that carried an external_id.

Team webhooks

GET/api/v1/team/webhook

Auth required

See your workspace's webhook and its recent deliveries.

These three endpoints are authenticated by the web app's session cookie — an API key doesn't work here — and the two write endpoints also need an X-CSRF-Token header. Any workspace member can read the configuration and the delivery log; the signing secret is never in the response.

Request
bash
curl "https://verifyed.org/api/v1/team/webhook" \
  -H "Cookie: session=..." 
Response
json
{
  "webhook": {
    "id": "wh_550e8400",
    "url": "https://crm.example.edu/hooks/verifyed",
    "secret_prefix": "whsec_a1b2c3",
    "is_active": true,
    "created_at": "2026-07-01T09:00:00Z",
    "updated_at": "2026-07-01T09:00:00Z",
    "last_delivery_at": "2026-07-20T10:35:00Z",
    "last_delivery_status": "delivered"
  },
  "recent_deliveries": [
    {
      "id": "del_7f3a",
      "event": "batch.completed",
      "job_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "attempt": 1,
      "status": "delivered",
      "response_status": 200,
      "error": null,
      "created_at": "2026-07-20T10:35:00Z",
      "completed_at": "2026-07-20T10:35:01Z"
    }
  ]
}
  • webhook is null until someone sets one up.

PUT/api/v1/team/webhook

Auth required

Set or update where batch results are delivered.

Creates the webhook or updates it in place. Only the workspace owner can change it. The one event today is batch.completed: a JSON POST with the job's counts, the result URLs and the first rows, signed with the X-VerifyEd-Signature header so the receiver can verify it came from us.

Request body
urlstringrequired
Where to POST deliveries. Must be https, on a public host, with no embedded credentials.
rotate_secretboolean
Mint a new signing secret. Deliberately not a side effect of changing the URL — rotation breaks any delivery the receiver hasn't verified yet.
is_activeboolean
Set to false to pause deliveries without losing the endpoint or its secret.
Request
bash
curl -X PUT "https://verifyed.org/api/v1/team/webhook" \
  -H "Cookie: session=..." \
  -H "X-CSRF-Token: ..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://crm.example.edu/hooks/verifyed"}'
Response
json
{
  "webhook": {
    "id": "wh_550e8400",
    "url": "https://crm.example.edu/hooks/verifyed",
    "secret_prefix": "whsec_a1b2c3",
    "is_active": true,
    "created_at": "2026-07-01T09:00:00Z",
    "updated_at": "2026-07-01T09:00:00Z",
    "last_delivery_at": null,
    "last_delivery_status": null
  },
  "secret": "whsec_a1b2c3d4e5f6..."
}
  • secret appears exactly twice in a webhook's life: on creation and on rotation. It isn't stored in a readable form and can't be shown again — if it's lost, rotate.
  • A member who isn't the owner gets a 403; an unusable URL gets a 400 naming the rule it broke.

DELETE/api/v1/team/webhook

Auth required

Remove the webhook and its secret.

Request
bash
curl -X DELETE "https://verifyed.org/api/v1/team/webhook" \
  -H "Cookie: session=..." \
  -H "X-CSRF-Token: ..."
Response
json
{
  "deleted": true
}
  • Owner only. Deleting with no webhook configured answers 404.

Platform

GET/api/v1/stats

PublicRate limit 60 / min

Database coverage statistics and enrichment metrics.

Request
bash
curl "https://verifyed.org/api/v1/stats"
Response
json
{
  "database": {
    "total_schools": 184350,
    "country_count": 50,
    "by_country": [
      {
        "country_code": "US",
        "count": 120000
      },
      {
        "country_code": "GB",
        "count": 8500
      }
    ],
    "by_school_type": [
      {
        "school_type": "Public",
        "count": 150000
      },
      {
        "school_type": "Private",
        "count": 34350
      }
    ],
    "diploma_mills_tracked": 2592
  },
  "enrichment": {
    "jobs_last_24h": {
      "completed": 1250,
      "failed": 3
    }
  },
  "generated_at": "2025-01-20T10:30:00Z",
  "cached": true
}
  • Stats are cached for 10 minutes. The first request after cache expiry may be slower.

GET/api/v1/health

Public

Health check for database and cache dependencies.

Request
bash
curl "https://verifyed.org/api/v1/health"
Response
json
{
  "status": "healthy",
  "timestamp": "2025-01-20T10:30:00Z",
  "version": "1.0.0",
  "checks": {
    "database": "ok",
    "cache": "ok"
  }
}

API Key Management (Admin)

POST/api/v1/admin/api-keys

Auth required

Create a new API key.

Issues a new API key with the specified tier. The plaintext key is returned only once in the response — store it securely. The key is stored as a SHA-256 hash and cannot be recovered.

Request body
namestringrequired
Human-readable label for the key (e.g. "Acme Corp production").
owner_emailstring
Contact email for the key owner.
tierstring= free
Rate limit tier: free, pro, or internal.
expires_atstring
ISO 8601 expiry date. Omit for no expiry.
Request
bash
curl -X POST "https://verifyed.org/api/v1/admin/api-keys" \
  -H "X-Backfill-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Corp", "owner_email": "dev@acme.com", "tier": "pro"}'
Response
json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "key": "sk_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "key_prefix": "sk_live_a1b2",
  "name": "Acme Corp",
  "tier": "pro"
}
  • Requires admin authentication via X-Backfill-Key header or ?key= query parameter.
  • The 'key' field is only returned at creation time. Store it immediately.

GET/api/v1/admin/api-keys

Auth required

List API keys with pagination and filtering.

Parameters
limitnumber= 20
Results per page (1–100).
offsetnumber= 0
Pagination offset.
is_activeboolean
Filter by active (true) or revoked (false) keys.
Request
bash
curl "https://verifyed.org/api/v1/admin/api-keys?limit=10&is_active=true" \
  -H "X-Backfill-Key: YOUR_ADMIN_KEY"
Response
json
{
  "results": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "key_prefix": "sk_live_a1b2",
      "name": "Acme Corp",
      "owner_email": "dev@acme.com",
      "tier": "pro",
      "is_active": 1,
      "last_used_at": "2026-02-07T15:30:00Z",
      "request_count": 1250,
      "created_at": "2026-01-15T10:00:00Z",
      "revoked_at": null,
      "expires_at": null
    }
  ],
  "total": 5
}

GET/api/v1/admin/api-keys/{id}

Auth required

Get details for a specific API key.

Parameters
idstringrequired
API key UUID.
Request
bash
curl "https://verifyed.org/api/v1/admin/api-keys/550e8400-e29b-41d4-a716-446655440000" \
  -H "X-Backfill-Key: YOUR_ADMIN_KEY"
Response
json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "key_prefix": "sk_live_a1b2",
  "name": "Acme Corp",
  "owner_email": "dev@acme.com",
  "tier": "pro",
  "scopes": "[\"read\"]",
  "is_active": 1,
  "last_used_at": "2026-02-07T15:30:00Z",
  "request_count": 1250,
  "created_at": "2026-01-15T10:00:00Z",
  "revoked_at": null,
  "expires_at": null
}

PATCH/api/v1/admin/api-keys/{id}

Auth required

Update an API key's name, tier, or active status.

Parameters
idstringrequired
API key UUID.
Request body
namestring
New label for the key.
tierstring
New tier: free, pro, or internal.
is_activeboolean
Set to false to revoke the key.
Request
bash
curl -X PATCH "https://verifyed.org/api/v1/admin/api-keys/550e8400-..." \
  -H "X-Backfill-Key: YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tier": "internal"}'
Response
json
{
  "updated": true
}

DELETE/api/v1/admin/api-keys/{id}

Auth required

Revoke an API key (soft delete).

Marks the key as inactive and records the revocation timestamp. The key record is preserved for audit purposes. Cached key data is immediately invalidated.

Parameters
idstringrequired
API key UUID to revoke.
Request
bash
curl -X DELETE "https://verifyed.org/api/v1/admin/api-keys/550e8400-..." \
  -H "X-Backfill-Key: YOUR_ADMIN_KEY"
Response
json
{
  "revoked": true
}
  • Revocation takes effect within 5 minutes (KV cache TTL). For immediate effect, the cache is proactively invalidated.

Common patterns

Integrating with an admissions CRM

There is a worked recipe for screening institution names in bulk and importing the results back into Slate, including the source format and the column mapping, at /integrations/slate. It covers what Slate can and cannot call, and says which parts we have tested.

Rate limiting

Rate-limited endpoints include these headers when rate limiting is applied.

X-RateLimit-Limitheader
Maximum requests allowed in the current window.
X-RateLimit-Remainingheader
Requests remaining in the current window.
X-RateLimit-Resetheader
Unix timestamp (seconds) when the window resets.

Rate limits vary by endpoint and deployment. Read the response headers for your current limits rather than trusting the numbers in this document.

429 response
json
{
  "error": "Rate limit exceeded",
  "retry_after_seconds": 42
}

Error responses

Every error returns a JSON object with an error field.

json
{
  "error": "School not found"
}
400status
Invalid request parameters or missing required fields.
404status
Resource not found.
429status
Rate limit exceeded. Check retry_after_seconds in the response.
500status
Internal server error.

Authentication

Create an API key at /app/api-keys after registering and verifying your email. Public lookup has a small anonymous allowance and returns registration guidance; research requires a verified key or a verified browser session with CSRF protection. Pass keys in X-API-Key. Invalid, expired and revoked keys return 401. Monthly usage counts each admitted search and school-detail request separately, including cache hits, failed lookups and retries. Rate/quota denials do not consume monthly usage. All keys for an account or workspace share usage; rotation does not reset it. Quotas reset at 00:00 UTC on the first of the month. Responses expose X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset and Retry-After. Map proxies have per-client limits and a shared 1,000-provider-call UTC-day cap. Anonymous API responses include an api_access object with the registration URL.

bash
curl -H "X-API-Key: sk_live_a1b2c3d4..." "https://verifyed.org/api/v1/search?q=test"
Rate limits by tier
Rate limits by API key tier
TierSearchAutocompleteEnrichmentWho gets it
Anonymous30 / min60 / min10 / minNo API key (default)
Free60 / min120 / min30 / minVerified account: 1,000 shared search/detail requests per UTC calendar month
Pro300 / min600 / min100 / minEligible subscribed workspace: 25,000 shared search/detail requests per UTC calendar month
Internal5,000 / min5,000 / min500 / minInternal use

Admin endpoints for API key management take the X-Backfill-Key header instead of X-API-Key.