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 120 / min

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

PublicRate limit 5 / min

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 "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.

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 screens every row against the registry and the diploma-mill watchlist. 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 an API key on a Team plan or above — lower 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 "[email protected]"
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 per upload. Larger files are rejected before a job is created.
  • 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.
  • 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": "[email protected]", "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": "[email protected]",
      "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": "[email protected]",
  "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

API keys are optional but unlock higher rate limits. Pass your key via the X-API-Key header on every request.

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
Anonymous100 / min120 / min10 / minNo API key (default)
Free300 / min400 / min30 / minFree-tier API key
Pro1,000 / min1,500 / min100 / minPro-tier API key
Internal5,000 / min5,000 / min500 / minInternal use

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