Skip to content

Language Check

A language check assesses the writing quality of a manuscript and returns a scored report. This page is the integration contract for the language check: the event ChronosHub publishes when a manuscript is ready, and the events the partner publishes back as the check progresses.

Unlike the scope check, this is asynchronous. There is no request/response pair: messages flow over the ChronosHub Integration Framework in both directions, and a single check produces one outbound event and one or more inbound ones. All messages are JSON. The schemas below are JSON Schema (draft 2020-12).

This contract describes the check payloads only — not the transport-level concerns (topic provisioning, authentication) which are agreed per partner during onboarding.

Envelope

Every message is wrapped in an Integration Framework envelope. payload is a JSON-encoded string, not a nested object — the payload schemas in the rest of this page describe the document inside that string.

Outbound (ChronosHub → Integration Framework → partner):

json
{
  "eventId": "0c5a6bd7-77b5-5c3e-9c0e-1f8b0f5a2e11",
  "eventType": "manuscript.uploaded",
  "partnerName": "Enago",
  "payload": "{\"correlationId\":\"lc_01J...\",\"fileUrl\":\"https://...\"}"
}

Inbound (partner → Integration Framework → ChronosHub):

json
{
  "eventId": "3f1d1f1e-9a4b-4c2f-8a1e-6d5c4b3a2f10",
  "partnerId": "b1e2d3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
  "eventType": "language.report.status.updated",
  "eventContractVersion": 1,
  "payload": "{\"correlationId\":\"lc_01J...\",\"status\":\"PROCESSING\", ...}",
  "receivedAt": "2026-08-07T12:37:28.844Z"
}

correlationId inside every payload is the ChronosHub language check id. It is the only thing tying the exchange together and must be echoed back unchanged on every inbound event.

Outbound: manuscript.uploaded

Published when a manuscript has been uploaded and is ready to check.

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ManuscriptUploadedPayload",
  "type": "object",
  "additionalProperties": false,
  "required": ["correlationId", "fileUrl"],
  "properties": {
    "correlationId": {
      "type": "string",
      "description": "ChronosHub language check id. Echo this back on every inbound event.",
      "minLength": 1
    },
    "fileUrl": {
      "type": "string",
      "format": "uri",
      "description": "Time-limited, IP-restricted download link for the manuscript. Accepted formats are .docx, .pdf and .tex."
    }
  }
}

Example

json
{
  "correlationId": "lc_01JQ8Z3M6T7B9C0D1E2F3G4H5J",
  "fileUrl": "https://stchronosmcprod.blob.core.windows.net/manuscripts/lc_01JQ8Z.../manuscript.docx?sv=..."
}

eventId is a deterministic UUIDv5 derived from the correlation id, so an at-least-once redelivery carries the same eventId. Partners should de-duplicate on it rather than starting a second check.

Inbound: language.report.status.updated

Progress signals while the check is running. Sent zero or more times.

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ReportStatusPayload",
  "type": "object",
  "required": ["correlationId", "status", "reportId"],
  "properties": {
    "correlationId": { "type": "string", "minLength": 1 },
    "status": {
      "type": "string",
      "description": "Progress state. A successful outcome is not reported here — it arrives as language.report.generated.",
      "enum": ["RECEIVED", "PROCESSING", "FAILED"]
    },
    "message": {
      "type": ["string", "null"],
      "description": "Human-readable detail, typically the reason on FAILED."
    },
    "reportId": {
      "type": "string",
      "description": "Partner identifier for the report."
    },
    "processStartTime": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "Null until processing begins."
    },
    "processEndTime": {
      "type": ["string", "null"],
      "format": "date-time",
      "description": "Null unless the check has finished."
    }
  }
}

Example

json
{
  "status": "PROCESSING",
  "message": null,
  "reportId": "d3ba4535-9036-4192-b760-2ae44d56c484",
  "correlationId": "lc_01JQ8Z3M6T7B9C0D1E2F3G4H5J",
  "processEndTime": null,
  "processStartTime": "2026-08-07T12:37:28.844Z"
}

Inbound: language.report.generated

The terminal success event, carrying the finished report.

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ReportGeneratedPayload",
  "type": "object",
  "required": [
    "correlationId", "reportId", "reportLink", "token",
    "processStartTime", "processEndTime", "data"
  ],
  "properties": {
    "correlationId": { "type": "string", "minLength": 1 },
    "reportId": { "type": "string" },
    "reportLink": {
      "type": "string",
      "format": "uri",
      "description": "Link to the hosted report."
    },
    "token": {
      "type": "string",
      "description": "Access token for the report link. May be an empty string when the link needs none."
    },
    "processStartTime": { "type": "string", "format": "date-time" },
    "processEndTime": { "type": "string", "format": "date-time" },
    "data": {
      "type": "object",
      "required": ["totals", "summary"],
      "properties": {
        "totals": { "$ref": "#/$defs/documentCounts" },
        "summary": {
          "type": "object",
          "required": ["score", "alertCounts", "globalCounts", "recommendation"],
          "properties": {
            "score": {
              "type": "object",
              "required": ["finalScore", "finalScoreBifurcation"],
              "properties": {
                "finalScore": {
                  "type": "number",
                  "description": "Overall language score."
                },
                "finalScoreBifurcation": {
                  "type": "object",
                  "description": "Per-dimension breakdown, keyed by dimension name.",
                  "additionalProperties": { "type": "number" }
                }
              }
            },
            "alertCounts": {
              "type": "object",
              "description": "Alert totals keyed by category name.",
              "additionalProperties": { "$ref": "#/$defs/alertCategory" }
            },
            "globalCounts": { "$ref": "#/$defs/documentCounts" },
            "recommendation": {
              "type": "object",
              "required": ["service", "description", "website"],
              "properties": {
                "service": { "type": "string" },
                "description": { "type": "string" },
                "website": { "type": "string", "format": "uri" }
              }
            }
          }
        }
      }
    }
  },
  "$defs": {
    "documentCounts": {
      "type": "object",
      "required": [
        "totalParagraphCount", "totalSentenceCount", "totalAlerts", "totalCriticalAlerts",
        "totalWordsCount", "totalLongParagraphCount", "totalLongSentenceCount"
      ],
      "properties": {
        "totalParagraphCount": { "type": "integer" },
        "totalSentenceCount": { "type": "integer" },
        "totalAlerts": { "type": "integer" },
        "totalCriticalAlerts": { "type": "integer" },
        "totalWordsCount": { "type": "integer" },
        "totalLongParagraphCount": { "type": "integer" },
        "totalLongSentenceCount": { "type": "integer" }
      }
    },
    "alertCategory": {
      "type": "object",
      "required": ["total", "categories"],
      "properties": {
        "total": { "type": "integer" },
        "categories": {
          "type": "object",
          "description": "Shape varies per category — flat counts for some, nested groupings for others. Stored verbatim by ChronosHub and left open by this contract."
        }
      }
    }
  }
}

Example

json
{
  "data": {
    "totals": {
      "totalAlerts": 53,
      "totalWordsCount": 758,
      "totalSentenceCount": 58,
      "totalCriticalAlerts": 22,
      "totalParagraphCount": 31,
      "totalLongSentenceCount": 0,
      "totalLongParagraphCount": 0
    },
    "summary": {
      "score": {
        "finalScore": 54.310344827586206,
        "finalScoreBifurcation": {
          "conciseness": 100,
          "correctness": 8.62068965517241,
          "formal_tone": 100,
          "readability": 100,
          "inclusive_language": 100
        }
      },
      "alertCounts": {
        "correctness": {
          "total": 53,
          "categories": {
            "Grammar": { "tense": 2, "verbs": 1, "articles": 6 },
            "Punctuation": { "punctuation": 6 }
          }
        },
        "consistency": { "total": 0, "categories": {} }
      },
      "globalCounts": {
        "totalAlerts": 53,
        "totalWordsCount": 758,
        "totalSentenceCount": 58,
        "totalCriticalAlerts": 22,
        "totalParagraphCount": 31,
        "totalLongSentenceCount": 0,
        "totalLongParagraphCount": 0
      },
      "recommendation": {
        "service": "Substantive Editing",
        "website": "https://www.enago.com/pub/oup/substantive-editing.htm",
        "description": "Your document requires advanced language check."
      }
    }
  },
  "token": "",
  "reportId": "d3ba4535-9036-4192-b760-2ae44d56c484",
  "reportLink": "https://reports.example.com/report/9d42be0d/abc123",
  "correlationId": "lc_01JQ8Z3M6T7B9C0D1E2F3G4H5J",
  "processEndTime": "2026-08-07T12:37:41.876Z",
  "processStartTime": "2026-08-07T12:37:28.844Z"
}

Notes

  • All property names are case-sensitive. Both directions use camelCase — unlike the scope check, whose response is snake_case.
  • A successful outcome never arrives as a status update. status carries only RECEIVED, PROCESSING and FAILED; success is language.report.generated. An unrecognised status value is rejected rather than ignored, so it dead-letters instead of silently dropping a signal.
  • Status updates need not arrive in order, and may be skipped. ChronosHub advances a check monotonically, so a late RECEIVED after a PROCESSING is discarded rather than regressing it.
  • Extra properties are tolerated. The report-generated payload observed in practice also carries a top-level status of SUCCESS, which ChronosHub ignores; the event type already conveys it.
  • fileUrl is time-limited and pinned to the partner's egress IP, so it must be fetched from the agreed source address and before it expires.
  • A partner failure, a malformed payload, or an inbound event whose correlationId matches no known check causes the check to be recorded as failed (or ignored, for an unknown id) on the ChronosHub side; it does not corrupt the manuscript's record.