Enhancely API (1.4.2)

Download OpenAPI specification:

Overview

The Enhancely API provides automatic generation and retrieval of Schema.org JSON-LD structured data for your web pages. This structured data helps search engines, AI platforms, and AI search tools better understand and represent your content.

Quick Start Guide

How It Works

Enhancely API Workflow Sequence Diagram

Get Started in 3 Steps

1. Get Your API Key - Log in to the Enhancely Dashboard, add your domain, verify ownership, and copy your project API key.

2. Make Your First Request - POST your page URL to /api/v1/jsonld with your API key. First request returns 201 (queued for processing). Wait a few minutes, then request again to get 200 with your generated JSON-LD.

3. Integrate Into Your Website - Add the returned JSON-LD in a <script type="application/ld+json"> tag in your page's <head> section. Implement caching with ETags for production.

Key Benefits

  • Automatic Updates: Nightly crawls detect content changes and regenerate JSON-LD
  • Efficient Caching: ETag-based validation minimizes bandwidth and API calls
  • Always Fresh: Your structured data stays synchronized with page content
  • Minimal Maintenance: Automatic nightly updates with manual cache clearing only when you update content

Prerequisites

This API is exclusively available to Enhancely customers who have:

  1. Added their domain to the Enhancely platform
  2. Validated domain ownership
  3. Retrieved their project API key from the Enhancely Dashboard

Integration Approaches

POST vs GET: Which Endpoint to Use

Use POST /api/v1/jsonld for CMS & Website Integration:

  • Primary endpoint for page-level integration
  • Automatically creates records if they don't exist
  • Supports sending complex URLs in request body (no query string length limits)
  • Returns 412 Precondition Failed when ETag matches (cache valid)
  • Ideal for server-side rendering in templates

Use GET /api/v1/jsonld for:

  • Listing all JSON-LD records across your project
  • Filtering by status, URL, hash, ETag, readonly, or timestamps
  • Paginated results with cursor-based navigation

Use GET /api/v1/jsonld/{hash} for:

  • Polling processing status after receiving 201/202 from POST
  • Direct retrieval when you already have the MD5 hash
  • Returns 202 Accepted while JSON-LD generation is still in progress
  • Returns 304 Not Modified when ETag matches (cache valid)

For integrating Enhancely into your website or CMS, follow this pattern:

  1. Template Integration: Add the API call in your page template's <head> section
  2. URL Normalization: Strip query parameters, hashes, and trailing slashes before caching
  3. Cache-First Strategy: Check local cache before making API requests (recommended TTL: 1 week)
  4. Cache Key Generation: Use MD5 hash of the normalized URL as the cache key
  5. ETag Validation: Always send If-None-Match header with cached ETag
  6. Silent Degradation: Return empty output on errors to avoid breaking page rendering
  7. Cache Invalidation: Clear local cache when you manually update page content via CMS (use CMS hooks like page.update:before). Auto-clear cache in local/development environments for testing.

Note: Enhancely automatically checks all indexed URLs nightly. Normal content changes are detected via ETag validation on your next API call—manual cache clearing is only needed when you actively update content through your CMS.

Cache Strategy

Enhancely uses HTTP ETag-based caching:

HTTP-Level Caching (ETags)

Every JSON-LD response includes an ETag header—a unique identifier for that version of the content. Use ETags for efficient caching:

# First request
POST /api/v1/jsonld
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: application/json

{"url": "https://example.com/page"}

HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Content-Type: application/json

{
  "hash": "7c165c13f93d7ca168bcfbd73cf563e3",
  "url": "https://example.com/page",
  "jsonld": {...},
  "etag": "a1b2c3d4e5f6",
  "crawled_at": "2025-01-16T14:30:00Z",
  "http_status_code": 200,
  "status": "ready",
  "readonly": false
}
# Subsequent request with cached ETag
POST /api/v1/jsonld
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: application/json
If-None-Match: "a1b2c3d4e5f6"

{"url": "https://example.com/page"}

HTTP/1.1 412 Precondition Failed

Benefits:

  • 412 Precondition Failed responses include minimal Problem body indicating cache is valid
  • Avoids redundant processing when content hasn't changed
  • Standard HTTP caching mechanism supported by all HTTP clients
  • Learn more: MDN: HTTP Conditional Requests

Content Change Detection

When HTML content changes, Enhancely reprocesses the JSON-LD and generates a new ETag.

  1. Check local cache for ETag and JSON-LD data
  2. Make POST request with If-None-Match header containing cached ETag
  3. Handle 412 response by returning cached data (ETag still valid)
  4. Handle 200 response by caching new ETag and JSON-LD with 1-week TTL
  5. Handle errors by returning cached data or empty output (silent degradation)

Processing Model

JSON-LD generation is asynchronous. When you submit a URL:

  1. 201 Created: The URL is queued for processing (first-time submission)
  2. 202 Accepted: The record exists but its JSON-LD has not been generated yet (still processing)
  3. 200 OK: Generated JSON-LD exists and is returned in full (check the record's status field)

Processing typically takes 1-3 minutes depending on page complexity and queue depth.

Response Body Shapes

The response body shape depends on the HTTP status code — do not assume a uniform status field across all responses:

HTTP code Body shape status field contains
200 OK Full record (hash, url, jsonld, etag, ...) Record status string: ready, updating, failed, ...
201 Created { "status": "processing", "message": ..., "hash": ..., "url": ... } The fixed literal string processing
202 Accepted RFC 9457 Problem (type, title, status, detail) The numeric HTTP status code 202
4xx / 5xx RFC 9457 Problem (type, title, status, detail) The numeric HTTP status code (e.g., 400, 429)

To handle responses reliably, switch on the HTTP status code of the response first, then interpret the body using the shape that belongs to that code.

Record Status Lifecycle

The status field inside 200 responses (and list items) reflects the record's processing lifecycle:

  • created: Queued for processing, JSON-LD generation hasn't started yet
  • updating: Currently being (re)processed
  • ready: JSON-LD is available and ready to use
  • failed: Generation failed (e.g., invalid page, unreachable URL). This is a terminal record state delivered with HTTP 200 — it is unrelated to 400 Bad Request errors, which indicate an invalid API request.

Note: the http_status_code field in 200 responses is the HTTP status the crawler received from your page during the last crawl (e.g., 200, 404) — it is not the status code of the API response you are reading. The HTTP status of the API response itself is available in the response status line and, for Problem bodies, duplicated in the numeric status field.

For newly created records or records still being processed (201/202 responses), poll the GET /api/v1/jsonld/{hash} endpoint. It returns 202 Accepted while processing continues and 200 OK once the JSON-LD is available.

Rate Limiting

All endpoints are rate-limited per organization. Each response includes rate limit headers to help you track your usage.

Rate Limit Headers

RateLimit-Limit: 100        # Maximum requests allowed in the current window
RateLimit-Remaining: 75     # Requests remaining in the current window
RateLimit-Reset: 3600       # Seconds until the rate limit window resets

Handling Rate Limits

429 Too Many Requests Response:

{
  "type": "https://enhancely.ai/problems/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded your rate limit. Please try again later."
}

Best Practices for Rate Limit Management

1. Monitor Rate Limit Headers

Check RateLimit-Remaining and RateLimit-Reset headers to track usage and implement proactive backoff when limits are approaching.

2. Implement Retry Logic

For 429 responses, wait for the number of seconds indicated by the RateLimit-Reset header before retrying. For 5xx server errors, use exponential backoff (e.g., 1s, 2s, 4s).

3. Use Silent Degradation

When rate limited or encountering errors, return cached data or empty output rather than breaking page rendering.

Error Handling

The API uses standard HTTP status codes and RFC 7807 Problem Details format for errors.

Key Status Codes

Success States:

  • 200 OK - JSON-LD ready and returned
  • 201 Created - URL queued for first-time processing
  • 202 Accepted - Processing in progress

Cache States:

  • 304 Not Modified (GET) / 412 Precondition Failed (POST) - Use cached data

Client Errors (4xx):

  • 400 - Invalid URL or malformed request
  • 401 - Missing/invalid API key
  • 403 - Plan limit or domain validation limit exceeded
  • 404 - Resource doesn't exist
  • 415 - Wrong Content-Type header
  • 429 - Rate limit exceeded or monthly generation limit reached

Server Errors (5xx):

  • 500/502/503 - Retry with exponential backoff, use cached data

Error Response Format

{
  "type": "https://enhancely.ai/problems/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded your rate limit. Please try again later."
}

All error responses — and the 202 Accepted still-processing response — use this RFC 9457 Problem shape with the same four fields: type (URI identifying the problem), title (short summary), status (numeric HTTP status code, duplicating the response status line), and detail (human-readable explanation). Some problems add extra fields (e.g., limit, remaining, resetAt on monthly-limit errors).

Request IDs

Every response carries an X-Request-Id header (UUID). Problem bodies additionally include the same value in the instance field. Include this ID when reporting issues to Enhancely support — it lets us find the exact request in our logs.

Best Practices

  • Silent Degradation: Return cached data or empty output on errors—never break page rendering
  • Cache on Success: Only cache 200 responses with ETag
  • Use Cache on 412/5xx/429: Return cached data when receiving these status codes
  • Don't Retry Client Errors: Log and return empty for 400/401/404
  • Monitor in Production: Use tools like Sentry or New Relic

JSON-LD Endpoints

Endpoints for generating and retrieving Schema.org JSON-LD structured data from web page URLs.

These endpoints enable automatic creation of rich structured data that helps search engines, AI platforms, and AI search tools better understand and represent your content.

List JSON-LD records for all domains in your project

Returns a paginated list of JSON-LD records across all domains in the authenticated project.

Key Features:

  • Cursor-based pagination with configurable page size
  • Filterable by hash, URL, status, ETag, readonly, and timestamps
  • Efficient project-level index lookup
Authorizations:
BearerAuth
query Parameters
limit
integer [ 1 .. 1000 ]
Default: 100

Maximum number of records per page. Default: 100, Max: 1000.

next_token
string

Pagination cursor from a previous response. Pass this to retrieve the next page of results.

hash
string

Filter by exact hash (MD5 of normalized URL).

jsonld_id
string

Filter by exact JSON-LD record ID (jsonld_id as returned in list items).

domain_id
string

Filter to a single domain. Must be a domain ID belonging to the authenticated project (returns 400 otherwise). Recommended for large projects — it uses the domain index for efficient pagination.

sort
string
Default: "createdAt"
Enum: "url" "createdAt" "updatedAt" "status" "crawledAt" "httpStatusCode"

Sort field. Note that sort fields use camelCase (unlike the snake_case filter parameters).

order
string
Default: "desc"
Enum: "asc" "desc"

Sort direction.

url
string

Filter by URL. The hostname is used to narrow to the matching domain, then exact URL match is applied.

status
string (JsonLdStatus)
Enum: "created" "updating" "failed" "ready" "ignored" "generation_limit_reached" "redirect"

Filter by processing status.

etag
string

Filter by ETag value. Use * to match records that have any ETag (i.e., have been processed at least once).

readonly
boolean

Filter by readonly status.

created_at
string <date-time>

Filter records created after this timestamp (ISO 8601).

updated_at
string <date-time>

Filter records updated after this timestamp (ISO 8601).

crawled_at
string <date-time>

Filter records crawled after this timestamp (ISO 8601).

Responses

Response samples

Content type
application/json
{
  • "data": [
    • {
      • "jsonld_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      • "hash": "7c165c13f93d7ca168bcfbd73cf563e3",
      • "domain_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
      • "project_id": "p1r2o3j4-e5c6-7890-abcd-ef1234567890",
      • "organization_id": "o1r2g3a4-n5i6-7890-abcd-ef1234567890",
      • "etag": "a1b2c3d4e5f6",
      • "crawled_at": "2025-01-16T14:30:00Z",
      • "http_status_code": 200,
      • "status": "ready",
      • "readonly": false,
      • "redirected_from_urls": [],
      • "redirect_target_hash": null,
      • "redirect_target_url": null,
      • "redirect_http_status": null,
      • "created_at": "2025-01-15T10:00:00Z",
      • "updated_at": "2025-01-16T14:30:00Z"
      },
    • {
      • "jsonld_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      • "hash": "e4d909c290d0fb1ca068ffaddf22cbd0",
      • "domain_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
      • "project_id": "p1r2o3j4-e5c6-7890-abcd-ef1234567890",
      • "organization_id": "o1r2g3a4-n5i6-7890-abcd-ef1234567890",
      • "etag": "b2c3d4e5f6a7",
      • "crawled_at": "2025-01-16T15:00:00Z",
      • "http_status_code": 200,
      • "status": "ready",
      • "readonly": false,
      • "redirected_from_urls": [ ],
      • "redirect_target_hash": null,
      • "redirect_target_url": null,
      • "redirect_http_status": null,
      • "created_at": "2025-01-15T10:05:00Z",
      • "updated_at": "2025-01-16T15:00:00Z"
      }
    ],
  • "pagination": {
    • "count": 2,
    • "limit": 100
    }
}

Generate or retrieve JSON-LD for a URL (Primary Integration Endpoint)

Recommended endpoint for CMS plugins and website integrations.

Submits a URL for JSON-LD generation or retrieves existing data if already processed. Automatically creates new records if they don't exist.

Response Codes:

  • 200 OK - JSON-LD exists; full record body (check status: ready, updating, or failed)
  • 201 Created - URL queued for first-time processing; body is { "status": "processing", "message", "hash", "url" } (poll GET /{hash} for completion)
  • 202 Accepted - Currently being processed; body is an RFC 9457 Problem with numeric status: 202 (poll GET /{hash} for completion)
  • 403 Forbidden - Plan limit exceeded or domain not validated
  • 412 Precondition Failed - ETag matches, use cached data
  • 429 Too Many Requests - Monthly generation limit reached (includes Retry-After header)

Note that the body shape differs per status code — see the Processing Model → Response Body Shapes section above.

Key Features:

  • Asynchronous processing (typically a few minutes for new URLs)
  • ETag-based conditional requests for efficient caching
  • Supports complex URLs in request body (no query string limits)
  • Optionally accepts html in the request body to skip crawling
Authorizations:
BearerAuth
header Parameters
If-None-Match
string
Example: a1b2c3d4e5f6

Optional. The ETag value from a previous response. If provided and matches the current ETag, returns 412 Precondition Failed.

Accept
string
Enum: "application/json" "application/ld+json"
Example: application/json

Optional. Specify application/ld+json to receive only the JSON-LD content without wrapper metadata. Defaults to application/json which includes metadata fields (status, etag, hash, etc.).

Note: the header value must be exactly application/ld+json — media type lists or parameters (e.g. application/ld+json, */* or ;charset=utf-8) are treated as application/json.

Request Body schema: application/json
required

Required. JSON request body with the URL to generate JSON-LD for. The Content-Type header must be application/json. Optionally include html to provide page content directly (skip crawling).

url
required
string <uri>

The target page URL to generate JSON-LD for. Example: https://example.com/blog/automatic-jsonld-with-enhancely.

html
string

Optional. Raw HTML content of the page. When provided, the crawler will use this HTML instead of fetching the page, allowing you to generate JSON-LD for pages that require JavaScript rendering or are behind authentication.

Limits: Max 5 MB uncompressed; max 350 KB after server-side gzip compression (inline data: URIs and inline SVGs are stripped before the compressed check, so realistic page HTML rarely hits this limit).

Responses

Request samples

Content type
application/json
Example

Response samples

Content type
{}

Retrieve a specific JSON-LD document by URL hash

Retrieves JSON-LD for a URL using its MD5 hash. Primarily used for polling processing status after POST returns 201/202.

Primary use cases:

  • Polling processing status after POST returns 201/202
  • Direct retrieval when you have the MD5 hash
  • ETag-based caching (returns 304 when content unchanged)

Special Headers:

  • Send Cache-Control: no-cache to bypass the server-side Redis cache and force a fresh database lookup (useful for debugging).

URL Hash Calculation:

The hash parameter is the MD5 hash (lowercase hexadecimal) of the normalized URL. Normalization applies these steps before hashing:

  1. Force the https: scheme
  2. Strip query parameters
  3. Strip the hash/fragment
  4. Remove a trailing slash
import crypto from 'crypto';

function normalizeUrl(url) {
  const u = new URL(url);
  u.protocol = 'https:';
  u.search = '';
  u.hash = '';
  const s = u.toString();
  return s.endsWith('/') ? s.slice(0, -1) : s;
}

const hash = crypto.createHash('md5').update(normalizeUrl('https://example.com/page')).digest('hex');
// Result: "7c165c13f93d7ca168bcfbd73cf563e3"

Recommended: instead of computing the hash yourself, use the hash field returned by POST /api/v1/jsonld (present in 200 and 201 responses) — it is always the canonical hash, including after redirect resolution.

Alternative: the path parameter also accepts a URL-encoded absolute URL instead of an MD5 hash. The server normalizes and hashes it for you: GET /api/v1/jsonld/https%3A%2F%2Fexample.com%2Fpage.

Polling etiquette: repeatedly requesting hashes that do not exist (404) triggers a temporary block — after excessive 404s the endpoint returns 429 Too Many Requests (title Too Many Not Found Requests) with a Retry-After: 60 header. Verify your hashes and reduce polling frequency if you hit this.

Response Codes:

  • 200 OK - JSON-LD returned with metadata (includes status field)
  • 202 Accepted - Record exists but JSON-LD generation hasn't completed yet; body is an RFC 9457 Problem with numeric status: 202. Keep polling.
  • 304 Not Modified - Content unchanged (when If-None-Match header matches ETag)
  • 404 Not Found - Hash doesn't exist (use POST to create)

ETag behavior while processing:

  • While a record is being processed for the first time it has no ETag yet — If-None-Match has no effect and the endpoint keeps returning 202 Accepted until processing completes.
  • During regeneration of an existing record, the previous ETag stays valid: a matching If-None-Match returns 304 Not Modified until the new version replaces it. A non-matching (or absent) If-None-Match returns 200 OK with the current content and status: "updating".
Authorizations:
BearerAuth
path Parameters
hash
required
string^[a-f0-9]{32}$

MD5 of the absolute URL (32 lowercase hexadecimal characters). Example: 7c165c13f93d7ca168bcfbd73cf563e3.

header Parameters
If-None-Match
string
Example: a1b2c3d4e5f6

Optional. Return 304 Not Modified if the supplied ETag matches the current resource ETag.

Cache-Control
string
Example: no-cache

Optional. Send no-cache to bypass the server-side Redis cache and force a fresh database lookup. Useful for debugging or verifying that regeneration has completed.

Accept
string
Enum: "application/json" "application/ld+json"
Example: application/json

Optional. Specify application/ld+json to receive only the JSON-LD content without wrapper metadata. Defaults to application/json which includes metadata fields (status, etag, hash, etc.).

Note: the header value must be exactly application/ld+json — media type lists or parameters (e.g. application/ld+json, */* or ;charset=utf-8) are treated as application/json.

Responses

Response samples

Content type
{}