---
title: "SEVIS & Government"
slug: sevis-government
section: Integrations
status: stable
description: "Government-mandated compliance integration — DS-2019 lifecycle, batch processing, and status management"
last-updated: 2026-10-07
---

# SEVIS & Government

Government-mandated compliance integration — DS-2019 lifecycle, batch processing, and status management

## Overview

SEVIS integration is a government-mandated system connection. Globus submits sponsor-issued data to SEVIS for DS-2019 lifecycle management, including creation, validation, amendments, and termination.

> **Authority: Government Sits Above Globus**
>
> SEVIS is the one system that sits above Globus in the authority hierarchy. Government compliance requirements override internal business rules. Globus must conform to SEVIS data formats, submission windows, and status codes.

## DS-2019 Lifecycle

The DS-2019 is the core compliance document for J-1 exchange visitors. Globus manages the full lifecycle from initial creation through program completion or termination. Every SEVIS gate in this lifecycle is enforced by the Rules Engine using structured rule definitions with typed conditions and immutable evaluation records.

DS-2019 creation and issuance upon participant acceptance and placement confirmation — gated by Rules Engine compliance rules (payment status, background check recency, placement confirmation) / SEVIS ID assignment and tracking — each participant receives a unique SEVIS identifier / Program dates, category codes, and sponsor information submitted per SEVIS format requirements / Document versioning — amendments generate new DS-2019 versions linked to the original / Status synchronization — Globus reflects SEVIS-confirmed status, not internal assumptions

> **Host verification is re-validated at batch confirmation**
>
> Confirming a SEVIS batch re-runs the host_verifications check for every placement in the batch (see Job Orders &rarr; Host Verification ), not just the job-order-approval-time check. A batch confirm fails with 403 HOST_VERIFICATION_REQUIRED — "DS-2019 issuance blocked: N placement(s) belong to Camp hosts that have not passed host verification" — if any placement's host verification has lapsed or was never completed, even if the underlying job order was already approved.
>
> Confirmation also fails with 403 `OPERATING_AGREEMENT_REQUIRED` ("DS-2019 issuance blocked: N placement(s) lack an active operating agreement on the host organization") when a placement in the batch has no confirmed, active or completed placement, no host organization, no active operating agreement, or conflicting active agreements. `details.failures` lists every failing application (including any host verification failures) with `applicationId`, `hostOrgId` and a `reason` (`no_placement`, `no_host_org`, `no_operating_agreement`, `operating_agreement_invariant_violation` or `host_verification_required`), plus `conflictingAgreementIds` where relevant. When any failure is an agreement failure this code is returned in preference to `HOST_VERIFICATION_REQUIRED`.

### Issuance, the stored DS-2019, and where staff and applicants see it

An application is marked issued only after three things are true: SEVIS reported the per-record result as successful, a durable SEVIS ID is recorded on the batch item, and the matching DS-2019 PDF is stored and verified for the application. A successful upload, or a successful record with no stored PDF, is not issuance — such records stay visibly retryable and are reported as awaiting a document (check-results reports issuedCount, awaitingDocumentCount and documentsStored / documentsFailed).

The stored PDF carries provenance in its review notes (the batch id and normalized SEVIS ID). A persisted DS-2019 satisfies issuance for a batch item only when that provenance matches the same batch and SEVIS ID, so a stale DS-2019 from an earlier batch cannot. The file is only persisted when it is a real PDF (.pdf name, %PDF- header, non-empty) whose SEVIS ID maps to exactly one application in the batch; in production, object storage must be available (there is no inline fallback).

- **Applicant Portal delivery.** Once issuance is projected, the Applicant Portal lists the principal applicant's verified DS-2019 from GET /api/portal/applications/:appId/documents as a row with type "ds-2019", the PDF fileName, status "verified" and a same-origin downloadUrl (/api/portal/documents/:id/download). The download is ownership-checked; another applicant receives a non-disclosing 404, and raw storage paths or external portal URLs are never returned.
- **Applicant Profile SEVIS summary.** GET /api/applications/:id/sevis returns an application-scoped, entity-scoped summary for staff: sevisId (from the latest successful submission), issued, issuedAt, latestSubmission (state, error code/message, processedAt and the batch number/state/SEVIS batch id) and ds2019 (verified documents with a proxy downloadUrl at /api/documents/:id/download — raw storage references are never returned). An issued SEVIS ID is kept even when a later resubmission fails. On the Applicant Profile the summary drives the SEVIS ID in the identity header and the SEVIS issuance card, which shows a "DS-2019 Form" block: "Verified PDF on file" with a View DS-2019 PDF action, or "No verified PDF on file".
- **Issued submission vs current readiness.** On an issued applicant the profile deliberately keeps current readiness blockers separate from the completed SEVIS submission: current blockers do not reverse an issued DS-2019. The summary also carries submissionDecision — { status, decidedAt, reason } with status override_used, passed_without_override or unknown — derived from the successful batch item matched through the verified DS-2019's provenance, combining batch-correlated override-use audit events with the complete durable override set. Missing provenance, incomplete chronology, uncorrelated use events and overlapping approved grants whose use cannot be proven yield unknown rather than claiming no override was used (the durable-override test in that derivation counts an override of any scope other than pool as approved when its `approverUserId` is set, a field kept from the retired two-person workflow: a one-step application records the applying user there and a legacy pending request leaves it empty; the window runs from the override's creation to its expiry). It is null for an application that is not issued. Override-use audit events carry the batchId of the operation that used the override.
- **Batch participants.** In Batch Detail each participant row shows the assigned SEVIS ID and, when one exists, an Open DS-2019 action. The item's ds2019 metadata (id, fileName, contentType, fileSize, downloadUrl) is present only when a verified PDF's stored provenance exactly matches this batch item's batch and normalized SEVIS ID; otherwise it is null. Raw storage references are never returned.

## Batch Processing

SEVIS operations are submitted via batch API. Globus queues submissions, sends them in batch windows, and processes responses asynchronously.

Batch queue — individual SEVIS operations are collected and submitted in scheduled batches / Response parsing — SEVIS batch responses are parsed and matched to originating records / Error handling — failed submissions are flagged for staff review with SEVIS error codes / Retry logic for transient failures (network timeouts, SEVIS service unavailability) / Batch status dashboard — staff can monitor pending, submitted, and completed batches

### Exchange-Visitor-Only Submission

Globus files J-1 exchange visitor batches only. A batch containing any record that is not exchange-visitor shaped, whose records name more than one J-1 program sponsor, or whose sponsor id cannot be resolved (from the records or SEVIS_PROGRAM_SPONSOR_ID) is rejected 400 EV_ONLY_BATCH_REQUIRED, with details.reason (non_exchange_visitor_records, program_sponsor_conflict or program_sponsor_unresolved) and the offending 0-based details.recordIndexes. A record that declares a non-exchange-visitor recordType is rejected even if it carries J-1 fields. There is no fall-back to the SDK's F/M student schema, and the rejection is treated as caller input (no SEVIS incident ticket is filed). Two further batch-scoped validation errors are not tied to a record field: `XML_GENERATION_FAILED` when the batch XML could not be generated at all (so it could not be schema-validated) and `RECORD_CONSTRUCTION_FAILED` when SEVIS records could not be built from the supplied data (the message says why; the verdict is invalid with no records). Both come back as validation failures rather than a 500.

### Record Payload Rules

Record-level validation runs before any XML is generated or any network call, and the same verdict (record checks plus SEVIS Release 6.83 schema validation) backs the XML preview, batch validate, debug preview and send. Nothing on these federal-filing fields is defaulted or guessed: an absent value is an empty field that validation blocks.

- **Exchange-visitor category.** The category code comes from the program's configured SEVIS category, which sits beside the program's sponsor program number because SEVIS validates them as a pair. A program with no category configured is not given a default: it fails REQ_CATEGORY_CODE before any network call, so staff see a configuration error rather than a participant filed under another category.
- **Occupation category code.** <OccupationCategoryCode> accepts exactly 01–12 and 99 (EVOccupationCategoryCodeType). The only source is the job order's sevisOccupationCategoryCode, used verbatim after trimming; the program category codes 1D (Intern) and 1E (Trainee) are never substituted. Intern and Trainee records with no value fail REQ_OCCUPATION_CODE; a supplied value outside the 13 schema values (including 1D/1E) fails INVALID_OCCUPATION_CODE on every category and never reaches the XML; a schema-valid code left on a category that does not use the element is dropped without error.
- **Host US address.** The participant US address and the site-of-activity address are each canonicalized before filing, because SEVIS accepts only a USPS two-letter state code and a five-digit ZIP. A full state name ("Colorado") or lower-case code maps to its USPS code; ZIP+4 reduces to its five digits. Anything else (an unknown state, a four-digit international code, a ZIP with text appended) is rejected rather than truncated or guessed, with a named error that identifies the host organization, the address and the field: REQ_/INVALID_ US_ADDRESS_STATE, US_ADDRESS_POSTAL_CODE, SITE_STATE or SITE_POSTAL_CODE. A host with no resolvable name, street address or city for the placement cannot be built into a record at all.
- **Birth city.** Applicants often give birth city only on the passport step. The personal step's own birth-city input wins; when it is empty the passport step's Place of Birth (City/County) (passport_place_of_birth) is promoted into the applicant's birth_city through the biographical sync, keeping only the segment before the first comma ("Bogota, Colombia" becomes "Bogota") and ignoring a blank value rather than clearing the column. At filing the value is folded to ASCII (accents removed, letters such as Ł, Ø, Æ and ß transliterated, hyphens, periods and apostrophes kept) and limited to 100 characters. REQ_BIRTH_CITY means nothing is on file; INVALID_BIRTH_CITY means the stored value is in a script SEVIS cannot accept (for example Cyrillic) and must be re-recorded transliterated as on the passport.
- **Batch size and schema errors.** The SEVIS schema allows at most 250 `<ExchangeVisitor>` records per batch under both CreateEV and UpdateEV. A larger batch fails with a single batch-scoped BATCH_RECORD_LIMIT_EXCEEDED error (split the applications across multiple batches) ahead of the per-record checks, so validate, preview and send block on it identically. Schema violations come back as XSD_SCHEMA_VIOLATION errors carrying the offending element or attribute name as the field, a schemaPath (the XPath-style location in the generated batch XML) and, when the element sits inside one exchange visitor, that record's 0-based recordIndex; a violation that cannot be tied to one record stays batch-scoped with no recordIndex. Diagnostics are sanitized so applicant values are redacted. The validator fails closed: empty XML (XSD_EMPTY_DOCUMENT), XML over 8 MB (XSD_INPUT_TOO_LARGE), a missing vendored schema set (XSD_VALIDATOR_UNAVAILABLE), a run that exceeds 30 seconds (XSD_VALIDATION_TIMEOUT) or any other engine failure (XSD_VALIDATION_FAILED) report the batch invalid and nothing is submitted.
- **Send, result and amendment failures.** Sending a batch whose items reference applications that no longer exist is refused 409 `SEVIS_BATCH_ITEM_ORPHANED` with `details.missingApplicationIds`, because skipping them would shift the record-to-item mapping and misfile SEVIS IDs; remove the orphaned items and send again. When a results check reports the batch status as failed (or an error status) and no per-record results are found, every item is marked failed with error code `SEVIS_BATCH_FAILED` and the SEVIS error text, the batch state becomes failed, and the response returns `status: failed` with `reconciled: true`. Creating an amendment batch (`POST /api/sevis/ds-resubmission/create-amendment-batch`, `sevis.ds_resubmission.manage`) is refused 400 `VALIDATION_ERROR` with `details.code` `SEVIS_AMENDMENT_BATCH_INVALID_CANDIDATE` and a `rejected` list of application ids and reasons when any application does not exist, is outside the requested program and season, or has no issued SEVIS ID to amend; no batch is created.
- **Per-record results.** SEVIS per-record results are stored on each batch item (state, SEVIS ID, error code, error message, retry count). Batch Detail shows each item's submission status, attempts and last result, and the batch reconciliation report lists failed items with their SEVIS error codes and messages.
- **Manual response import.** `POST /api/sevis/batches/:batchId/process-response` (permission sevis.batch.retry_manage; body `responseXml` only) reconciles a pasted SEVIS response. It is allowed only for batches in sent, processing, submission_uncertain, completed, completed_with_errors or failed (otherwise 409 `BATCH_STATE_CONFLICT`). Each result is matched to its batch item by request id, and the import is refused with 409 when that cannot be done safely: `SEVIS_RESPONSE_CORRELATION_MISSING` (some batch items have no stored request id; `missingRequestIdApplicationIds` lists them), `SEVIS_RESPONSE_CORRELATION_DUPLICATE` (two batch items share a request id; `duplicateRequestIds`) and `SEVIS_RESPONSE_CORRELATION_INVALID` (the response repeats a request id or names one that is not in the batch; `duplicateRequestIds` and `unknownRequestIds`). On success it returns the reconciliation result with `issuedCount` and `awaitingDocumentCount`.
- **Preflight error and warning codes.** Every record is checked before XML is generated; each finding carries a `code`, the `field` and the 0-based `recordIndex`. Required-field codes: `REQ_LAST_NAME`, `REQ_FIRST_NAME`, `REQ_BIRTH_DATE`, `REQ_GENDER` (must be M, F or U), `REQ_COUNTRY_OF_BIRTH`, `REQ_CITIZENSHIP`, `REQ_PERM_RESIDENCE`, `REQ_PROGRAM_NUMBER`, `REQ_START_DATE`, `REQ_END_DATE`, `REQ_SEVIS_USER_ID`, `REQ_POSITION_CODE`, `REQ_SUBJECT_FIELD` and `REQ_SUBJECT_REMARKS`. Format codes: `INVALID_COUNTRY_CODE` (country of birth, citizenship or permanent residence is not a two-letter FIPS 10-4 code), `INVALID_USER_ID` (the SEVIS user id is not exactly 10 characters) and `INVALID_SUBJECT_CODE` (the subject field code is not in XX.XXXX form, or is the non-enumerated 99.9999). A missing email raises only the non-blocking warning `WARN_EMAIL`. Birth city, category code, occupation category, address and batch-size codes are described in the bullets above. Two codes are batch-scoped instead of per-record: `ORG_ID_MISSING` (no program sponsor id) and `NO_RECORDS` (the batch has no records).

### Batch Lifecycle States

A SEVIS batch moves through draft &rarr; confirmed &rarr; sent, with per-item results captured separately once SEVIS responds.

| State | Meaning | Next Action |
| --- | --- | --- |
| draft | Applicants added, not yet confirmed. Items can still be added or removed. | Confirm the batch. |
| confirmed | Every item re-passed readiness and host-verification at confirm time; batch is locked for submission. | Send to SEVIS. |
| submission_uncertain | POST .../send returned a server error, but the batch's state had already been mutated before the failure — the batch is neither confirmed nor sent. | No UI-driven recovery. An engineer must reset sevis_batches.state back to confirmed directly before the batch can be resent; retrying Send from the UI returns 409 BATCH_STATE_CONFLICT ("Cannot send batch in 'submission_uncertain' state. Must be 'confirmed'.") until that reset happens. |
| sent | Successfully submitted to SEVIS. Per-item results (Success / Validation Error / Partial Success / System Error) are recorded as they come back. | See Batch Participant Results. |

> **submission_uncertain is not the same as a SEVIS-side ambiguous response**
>
> The Duplicate Submission failure mode in Error Handling &rarr; SEVIS Failure Modes describes an idempotency-key-guarded retry after SEVIS itself gives an ambiguous answer. submission_uncertain is different: it is an application-layer failure in the /send handler itself (observed as a bare 500 INTERNAL_ERROR with an opaque traceId and no idempotency signal), before or during the point where the batch's own state row is updated. Treat it as a distinct, currently UI-unrecoverable failure mode, not as the documented duplicate-submission case.

### Batch History & Batch Detail

Batch History (GET /api/sevis/batches, also the legacy GET /api/sevis-batches) returns only the caller's entity-scoped batches, newest first, each with programName, seasonName and creatorDisplayName (shown as "Not recorded" when no reliable staff attribution exists) alongside the persisted scope, counts, state and timestamps. Confirmed means approved and locked for submission; it is not a live readiness verdict.

Two further batch routes. `POST /api/sevis/batches/:batchId/export` (`sevis.batch.view_results`, idempotency key required, entity-scoped) returns `batchId`, `batchName`, `exportDate`, `recordCount` and `records`; because the records carry applicant identity data (name, date of birth, nationality) the call is written to the PII access log as `sevis_batch_export` and audited as `sevis.batch_export`, and an unknown batch is 404. `DELETE /api/sevis/batches/:batchId/items/:itemId` (`sevis.batch.create`, entity-scoped, audited) removes one item from a draft batch and returns `removed`, `itemId`, `batchId` and `remainingItemCount`; a batch in any other state is refused 409 `BATCH_STATE_CONFLICT` with `details.currentState` and `details.requiredState` of `draft` (also returned when the batch changed state during the removal), and an unknown item is 404.

Batch Detail (GET /api/sevis/batches/:batchId) returns resolved program/season and actor labels (creatorDisplayName, confirmedByDisplayName), a lifecycle-specific summary, a chronological activity list (creation, confirmation, submission to SEVIS, processing/results, retry) and per-item readiness presentation. The summary differs by lifecycle: a draft batch reports Applicants, Ready, Blocked, Warnings/review and Blocking issues; a submitted or later batch reports Submitted, Successful, Failed and Processing. Each item carries sevisReadinessPresentation (ready, blocked or warning/review, the authoritative requirements with next action and whether each is overridable, override eligibility, the applicant profile path and a server-owned submissionStatus of unsubmitted, submitted, processing, successful, failed, retrying or uncertain), plus attempts, lastResult and ds2019. A submission_uncertain batch does not claim prepared request IDs were dispatched.

**Batch admission errors.** Creating a batch from applications (`POST /api/sevis/batches/create`, permission sevis.batch.create) re-checks every application for scope, issuance state and SEVIS readiness and rejects the whole request if any fails, listing them in `details.rejected`. When any rejection is not a readiness failure (application missing, already issued, program or season mismatch, review required or stale) the response is 400 `VALIDATION_ERROR` with `details.code` `SEVIS_BATCH_SCOPE_MISMATCH` (a program or season mismatch is among them) or `SEVIS_BATCH_INVALID_CANDIDATE`. When every rejection is a readiness failure the response is 422 `SEVIS_NOT_READY` if a pre-SEVIS track blocker exists, otherwise 403 `SEVIS_COMPLIANCE_BLOCKED` (compliance or passport blockers only); send applies a similar split (422 `SEVIS_NOT_READY` when any rejection is not a readiness failure or a pre-SEVIS track blocker exists, otherwise 403 `SEVIS_COMPLIANCE_BLOCKED`), while confirm returns 422 `SEVIS_NOT_READY` for any rejected application. Batch validate and XML preview do not fail on admission: they add errors to the validation result, `SEVIS_CANDIDATE_ADMISSION_BLOCKED` for each rejected application (with its `applicationId` and `reason`) and `SEVIS_BATCH_EMPTY` when the batch holds no applications.

The `sevis_status_history` table records each observed change of a batch item's SEVIS status. A row is written only when the status SEVIS reports differs from the one on record, in the same transaction that updates the item's current status, so a status-sync pass that sees the same status again writes nothing. Key columns: `batch_item_id` (deleted with the item), `application_id`, `sevis_id`, `previous_status` (null for the first observation), `new_status` (verbatim from SEVIS), `observed_at` (when Globus observed the change, not when SEVIS made it) and `observation_source` (for example `status_sync`).

### Checking Results — "Results Already Downloaded"

When staff check results (POST /api/sevis/batches/:batchId/check-results), the batch-level download result code in the SEVIS transaction log is read before the SDK's derived status, because the SDK reports every download code other than S0000 as "processing". A download reporting S0002 returns the terminal status already_downloaded with resultCode and an explanatory message: SEVIS will not send these results again, so this is not a processing delay, and the outcome must be recovered from what was captured the first time — re-sending the batch is never the remedy. A download reporting S0030 still returns processing and now names the code. Neither response writes batch or item state, and nothing is reconciled off a download-level code. Responses carrying per-record results are unaffected. If the per-record results do not map one-to-one onto the batch's items (a result for an application not in the batch, an item with no result, or duplicate results), reconciliation is refused with 409 `SEVIS_RECONCILIATION_MISMATCH`. The check runs inside the reconciliation transaction before any item is updated, so nothing is written. The thrown error carries the item and result counts and the unknown, missing and duplicate application ids, but the generic API error handler returns the response with `details: null`. (Vendored SDK (1.5.6 and later; currently 1.6.1) also classifies S0002 as failed with status source download_S0002:results_already_downloaded. Since 1.6.0 it exposes the batch-level `downloadResultCode` (which batch check-results reads, as described above) and structured per-record `errorCode` and `errorMessage`; since 1.6.1 it parses the transaction log structurally instead of by regex, so a malformed log surfaces a retryable error rather than marking a batch completed.)

### Staff SEVIS Routes

Staff routes not described above. All require an authenticated session (most also accept an API key) plus the permission shown; most are audited.

| Route | Permission | Behavior |
| --- | --- | --- |
| `POST /api/sevis/batches/:batchId/validate` | sevis.validation.run | Read-only batch verdict: record checks, XSD validation and admission errors (see Batch admission errors). 404 for an unknown batch; 400 when the program has no SEVIS Program Number. |
| `POST /api/sevis/batches/:batchId/process-response` | sevis.batch.retry_manage | Manual reconciliation of a pasted SEVIS response (see Manual response import). |
| `POST /api/sevis/batch/submit` | sevis.batch.create | Compliance-gated submission of records (body `records`, optional `applicationIds`); 201 on success. Rate limited. |
| `POST /api/sevis/validate/student` and `POST /api/sevis/validate/exchange-visitor` | sevis.validation.run | Validate one student or exchange-visitor record through the SDK. Rate limited. |
| `POST /api/sevis/search/students` and `POST /api/sevis/search/exchange-visitors` | sevis.realtime.search | Search by criteria; results are served through a cache. Rate limited. |
| `DELETE /api/sevis/cache` | sevis.status.view | Clears that cache and returns the number of entries cleared. |
| `POST /api/sevis/ds-resubmission/flag` | sevis.ds_resubmission.manage | Flags an application for DS-2019 resubmission. Body: `applicationId`, `changedFields` (string array) and optional `category` (`bio` or `site_of_activity`; the latter records a site change, otherwise a bio change). 404 when the application is not found. |
| `PATCH /api/sevis-batches/:id` | sevis.batch.confirm_issue | Legacy hyphen family. Only a draft batch can be moved to `confirmed`; any other state change returns 409 `BATCH_STATE_CONFLICT` (use the dedicated operation endpoints). |
| `POST /api/sevis-batches/:id/export` | sevis.batch.create | Legacy hyphen family, session only (no API key). Returns `batchId`, `batchName`, `exportDate`, `recordCount` and `records` for the batch. |
| `POST /api/sevis-readiness-overrides/:overrideId/reject` | none | Retired (Task #2375) together with approve: overrides are applied in one step, so there is nothing to reject and no handler is mounted. An override that should not stand is revoked. The path is still listed in the mutation registry. |

### Governed Overrides & Emergency Access

Staff have two independent mechanisms to push a batch or an individual applicant past a SEVIS blocker; neither bypasses the underlying data problem, and both are fully audited.

**Governed readiness override (SEVIS Ready Pool).** Revoking, shortening the expiry of (an extension beyond the recorded expiry is refused 409 LEGACY_SEVIS_OVERRIDE_EXTENSION_RETIRED; revoke the record or request a rule-level Compliance exception instead), reading the audit history of, and relying on an override at a pool or batch chokepoint all require the dedicated sevis.override.manage permission, held only by Super Admin, Compliance Manager and Compliance Officer; everyone else gets 403 naming the permission. New broad overrides can no longer be created: POST /api/applications/:id/sevis-readiness-overrides (still gated by sevis.override.manage and entity scope) always returns 409 LEGACY_SEVIS_OVERRIDE_RETIRED, directing staff to request a governed Compliance override for the specific failed rule instead. Existing overrides are grandfathered until they expire or are revoked. Overrides written under the retired request-and-approve workflow remain readable and are labelled legacy (legacyOrigin retired_status or two_person_approval); nothing new is created in those states.

- **Scopes.** Authority is granted separately for pool, add, confirm and send, and an override on one scope never authorizes another. Each existing scope record keeps its own expiry, revocation and audit trail. New scopes can no longer be applied (the apply endpoint always returns 409 LEGACY_SEVIS_OVERRIDE_RETIRED), so no submission, including the former "Complete SEVIS flow" choice, can create overrides, and an existing override is never broadened.
- **What can be bypassed.** The server classifies blockers at use time; callers do not supply blocker keys. Bypassable: compliance/gate failures, passport-evidence blockers, and the four pre-SEVIS lifecycle track blockers. Never overridable: first name, surname, contact number, email address, date of birth, the minimum-age determination, and a terminal application state, plus program/season scope binding, actor permission at use time, expiry, batch scope, host and operating-agreement requirements, payload/XSD validation, duplicate protection and external SEVIS integrity checks. An existing override is bound to the program and operational cycle captured when it was applied, and is revalidated on every use against the live program and cycle (a mismatch, expiry or revocation fails closed). The immutable requirements (nonblank names, usable phone, valid email, date of birth, minimum age) stay enforced even under an active override.
- **Compensating controls.** Apply-time controls (24-hour default lifetime, compliance notification, the sevis_readiness_override.applied audit entry and the SEVIS_READINESS_CHANGED 409) belong to the retired apply flow. For existing overrides, PATCH expiry can only shorten: any expiry later than the recorded one (or on a record with no recorded expiry) is refused 409 LEGACY_SEVIS_OVERRIDE_EXTENSION_RETIRED, and a new expiry must be in the future and within the maximum lifetime measured from when the override was applied (otherwise 400). Revocation and expiry changes are written to the audit trail as sevis_readiness_override.revoked and sevis_readiness_override.expiry_changed.
- **Managing and overseeing.** POST /api/sevis-readiness-overrides/:overrideId/revoke, PATCH /api/sevis-readiness-overrides/:overrideId/expiry and GET /api/sevis-readiness-overrides/:overrideId/audit act on one operation-scoped override; GET /api/applications/:id/sevis-readiness-overrides lists covered scopes (optional `status` filter `active`, `revoked` or `expired`, plus the legacy `pending` and `rejected` values, which only select historical rows left by the retired request-and-approve workflow; includeBaseline=true adds the no-override baseline). GET /api/sevis-readiness-overrides is the scoped, filtered, paged cross-applicant Override Register (status active, lapsing_soon, expired or revoked; CSV export via format=csv, capped at 5000 rows), and the Override Register page shows it in the staff UI. See the SEVIS Readiness Criteria feature documentation for the full contract.
- **Compliance case references in the readiness review.** `GET /api/applications/:id/sevis-review` (permission `sevis.ready_pool.view`, entity-scoped) marks each entry of `readiness.data.complianceRuleComposition.requirements` whose `source` is `compliance_override` with `complianceCaseReferenceStatus` (`available` or `unavailable`) and `complianceCase` (null when unavailable). An available reference carries `number` (the Compliance case number), `itemId`, `decidedAt` and `decisionProvenance`: `status`, the decision `reason`, `approvers` (name, role and a `deleted` flag for removed users) and `evidence` entries (`documentId`, `fileName`, `documentType`, `contentType`, `versionNumber`, `status`, `submittedAt`, `availability` and a `viewUrl`, null without `document.view` or when the file is unavailable). Reason, approvers and evidence are filled only while the override is active and its case item is approved; a reference whose case binding does not match the application, program and season is reported as unavailable.

**Emergency SEVIS Access (System Admin).** A separate, per-action confirmation gate that appears for System Admin users specifically on Create Batch, Add Applicants to Batch, Confirm Batch, and Send Batch. Each of these four actions requires its own independent Emergency Reason (minimum 20 characters) and its own "Confirm Emergency Action" click — confirming one action (e.g. Add Applicants) does not carry the confirmation forward to the next action (e.g. Confirm Batch) in the same batch workflow, even performed back-to-back in the same session.

## Check-In & Validation

SEVIS requires periodic check-in validation to confirm that participants are active and in compliance. Globus automates check-in submissions and tracks validation status. Check-in timing and missed-check-in thresholds are enforced by the Rules Engine using configurable parameters (sevis_checkin_due_days, missed_checkin_threshold) that can vary by brand, program, or season.

Automated check-in submissions based on program-specific schedules / No-show confirmation processing for participants who do not arrive / Validation status tracking — pending, confirmed, failed / Compliance alerts for missed or overdue check-in deadlines — threshold configurable via Rules Engine parameters / Integration with notification system for check-in reminders to staff

**No-show detection job.** The server starts an hourly in-process scheduler at boot (`noShowDetectionScheduler`), and the same job can be run by hand with `server/scripts/runNoShowDetection.ts`. Each pass takes SEVIS arrival check-ins whose `check_in_date` is at least `NO_SHOW_LOOK_BACK_HOURS` hours in the past (optional, default 24) and that have no `actual_arrival_date`. For each application it enqueues a scheduled rule evaluation (idempotent per application, rule and scheduled-arrival day), opens a high-severity compliance case with reason `no_show_detected` (an open case for the same scheduled arrival is reused), and moves the check-in to `no_show_review` with decision status `under_review` (a check-in already in `no_show_review` or `no_show_reported` is left as it is). `NO_SHOW_RULE_DEFINITION_ID` names the no-show rule definition: the manual script requires it and exits with code 2 when it is unset, while the scheduler treats it as an override and otherwise looks up the rule definition with key `no_show`, skipping the tick if none exists.
### Check-Ins Are Globus-Owned (Portal Sync Retired)

Shipped Sep 6 2026. The applicant-profile SEVIS check-in section no longer offers a manual "sync from portal" control, and the endpoint behind it — `POST /api/checkins/sevis/sync/:applicationId` — has been removed (recorded in the Globus endpoint-removal allowlist, `removedIn: 2026-09-06`). The control was a non-functional tombstone: SEVIS check-in records are created and transitioned in Globus, and there is no external system to pull them from. The check-in header now reports the count of Globus SEVIS check-in records, and the two remaining staff actions are the status transition and the no-show confirmation.

Flight and arrival information is likewise not synced onto the check-in: the legacy flight columns were dropped from `sevis_check_ins`, and arrival details are owned exclusively by the participant's `flight_arrival_details` workflow step, hydrated onto the check-in view at read time. Any page or runbook that still describes a portal-sync button for SEVIS check-ins is describing superseded behavior.

The staff check-in routes require `applicant.manage` and are audited. `PATCH /api/checkins/sevis/:id` updates a check-in (404 when unknown); flight and arrival keys in the body are discarded because arrival is read from the participant's portal step, and when the arrival date changes the arrival-based deadlines are recalculated. `POST /api/checkins/sevis/generate` is the operator trigger for the check-in generation job (optional `programId` and `seasonId` narrow it); it shares the scheduler's advisory lock, so a run already in progress returns 409. `POST /api/checkins/sevis/:id/address-review` records a staff-reviewed address change: `addressType` (`current_us_address`, `mailing_address` or `site_of_activity`) and `newAddress` are required, `effectiveDate` is optional; it writes an address-history row with source `staff_review`, updates the check-in and reconciles the exception queues (400 for a missing or invalid `addressType`, 404 for an unknown check-in). `POST /api/checkins/sevis/portal-event` is the inbound applicant-portal event endpoint (not the removed `sync/:applicationId` control): body `eventType` (`draft_saved`, `submitted` or `exception_flagged`; anything else is 400), `applicationId`, optional `portalCheckInId` and `data`; it updates or creates (201) the matching check-in, and `exception_flagged` with no matching check-in is 404. `PATCH` and `portal-event` also accept an API key with the `checkins.read` scope; `generate` and `address-review` need a session.

**Action log.** Each check-in has an action log in `sevis_checkin_actions`: `checkin_id` (foreign key to `sevis_check_ins`), `application_id`, `action_type`, `performed_by`, `performed_at`, `values_used` (JSON), `data_source`, `confirmation_notes`, `is_manual` (default true), `previous_status` and `new_status`. The check-in routes write rows through the engine's `logSevisAction` helper as check-ins are transitioned, and staff can read them with `GET /api/checkins/sevis/:id/actions` (permission `applicant.view`) or append one with `POST /api/checkins/sevis/:id/actions` (permission `applicant.manage`, 404 if the check-in does not exist).

## Amendments & Program Extensions

Program changes that affect the DS-2019 — such as site changes, category changes, or program extensions — require formal amendments submitted to SEVIS.

Program Extension requests submitted with updated program end dates and justification / Site of activity changes submitted when participant placement changes / Category changes for participants switching program types / Amendment tracking — each amendment linked to the original DS-2019 with status history / Staff approval workflow before amendments are submitted to SEVIS
## Termination Processing

Early termination of a participant’s program requires submission to SEVIS with a reason code. This is a compliance-critical operation gated by the Rules Engine — early termination requires compliance review before SEVIS record closure, and J-1 termination triggers automatic J-2 terminal closure via cascade dependency rules.

Early termination submission with SEVIS-defined reason codes / Termination approval workflow — requires staff authorization before submission, compliance review via Rules Engine / Grace period tracking for participants with pending termination / Termination confirmation — Globus updates participant status only after SEVIS confirmation / Audit trail for all termination actions with timestamps and authorizing staff
See the Ecosystem Model for how SEVIS fits into the authority hierarchy.

## Vendored SDK Boundary

> **Inviolable Law: Never Modify Vendored SDKs**
>
> The SEVIS SDK is vendored at server/vendor/sevis-sdk/. This code is maintained by the SDK team and must never be modified directly. If changes are needed, report them back to the SDK team for incorporation into the next release.

Vendored SDK lives at server/vendor/sevis-sdk/ and is treated as an external dependency / The SDK provides XML generation, SEVIS-format validation, and government endpoint communication / Globus wraps the SDK through its own service layer — the SDK is never called directly from routes / SDK updates are applied by replacing the vendored directory, not by patching individual files / Any bugs or feature requests for the SDK are tracked separately and reported to the SDK team
## Certificate-Based Authentication

All communication with SEVIS government endpoints uses certificate-based authentication. The SEVIS Service layer manages certificate loading, validation, and attachment to outbound requests.

Digital certificates issued by the Department of State authenticate all SEVIS API calls / Certificate files are uploaded via the Globus admin interface and stored securely / The active certificate determines the SEVIS environment (production, beta, alpha, test or demo) when one is active, otherwise the SEVIS_ENVIRONMENT setting applies (default test) — no manual toggle. A certificate that disagrees with an explicitly set SEVIS_ENVIRONMENT is a configuration conflict that blocks submission / Certificate expiry is monitored with 30/14/7 day advance warning notifications to compliance staff / Only one certificate is active per environment at any time; previous certificates are archived / If the active certificate expires, all SEVIS batch processing is automatically paused until renewed
### Verified TLS, Environment Guard & Connection Preflight

- **Verified connection.** Every call Globus makes to SEVIS verifies the government server's certificate chain and requires TLS 1.2 or better, in every environment including test. A private certificate authority, where an organisation uses one, is installed as an additional trusted root and never replaces the public ones. GET /api/sevis/health reports the TLS posture (certificateVerification is always "enabled", minTlsVersion, whether an approved CA bundle is installed and how many certificates it holds, or customCaBundleError when one is configured but unusable) with no certificate material, passphrase or file content.
- **Credential environment variables.** The client uses an admin-uploaded database certificate first; otherwise the certificate comes from `SEVIS_CERT_PATH` (file path) or, when that is unset, `SEVIS_CERT_BASE64` (base64 .p12, decoded to `/tmp/sevis-cert.p12` with mode 0600), and its passphrase from `SEVIS_CERT_PASSWORD`. Without a certificate and passphrase the client refuses to start with a not-configured error. The program sponsor id is read from `SEVIS_PROGRAM_SPONSOR_ID` and falls back to `SEVIS_SCHOOL_CODE` when it is unset. An approved private CA bundle is supplied as a PEM file in `SEVIS_CA_BUNDLE_PATH` or as base64 PEM in `SEVIS_CA_BUNDLE_BASE64` (the path wins when both are set); it must contain at least one PEM certificate block, and a bundle that is set but unreadable or empty is reported as `customCaBundleError` on the health endpoint, never as a reason to skip verification.
- **Submission guard.** Alpha, Beta, Test and Demo are recognised non-production rails. Submission is refused 403 SEVIS_SUBMISSION_BLOCKED, before any network call, when the resolved environment is production and SEVIS_ALLOW_PRODUCTION_SUBMIT is not explicitly enabled (details.code SEVIS_PRODUCTION_SUBMIT_DISABLED), when the active certificate and SEVIS_ENVIRONMENT disagree (SEVIS_ENVIRONMENT_CONFLICT), or when a configured upload/download endpoint does not belong to the resolved environment, including any override that targets the production gateway from a non-production environment (SEVIS_GATEWAY_ENDPOINT_MISMATCH). Gateway hosts are owned by a platform allowlist in `server/services/sevisGatewayBinding.ts`, not by the vendored SDK's environment constants: production is `batch.sevis.ice.gov` (government-provided), beta is `batch.beta.sevis.ice.gov` and alpha is `batch.alpha.sevis.ice.gov`; demo reaches no gateway. The default endpoints are `https://<environment host>/sevisbatch/action/batchUpload` and `.../batchDownload`. `SEVIS_BATCH_UPLOAD_URL` and `SEVIS_BATCH_DOWNLOAD_URL` are optional overrides that the binding guard judges: an override must resolve to the resolved environment's own host over `https` on the default port, otherwise it is a mismatch, and an unparseable override is treated as a mismatch. Read-only operations (health, status, preflight) are not guarded so a misconfiguration can be seen.
- **Connection preflight (sandbox validation).** The SEVIS Diagnostics page (POST /api/sevis/preflight/runs, gated on sevis.validation.run; run history via GET /api/sevis/preflight/runs and GET /api/sevis/preflight/runs/:runId, gated on sevis.status.view) runs a non-mutating validation and persists the result in sevis_preflight_runs. It runs five ordered checks: environment_resolution (effective environment, its source, any certificate-vs-SEVIS_ENVIRONMENT conflict, and an optional expectedEnvironment assertion), production_submit_safeguard (proves the guard refuses a production submission while the opt-in is off), certificate_material (sponsor certificate and private key load), tls_connectivity (a real TLS handshake with chain verification and the approved CA bundle) and controlled_request (a batch status query for a fixture identifier, where a protocol-level rejection counts as success). Network checks are skipped for demo and refused for a production environment without the opt-in. Each run records a pass, warn or fail status and sanitized evidence only (hostnames, negotiated protocol, certificate fingerprint and expiry); nothing is filed and no participant is read. Development and QA are configured to use the ICE Alpha endpoints, but readiness still depends on the active certificate, the resolved environment, TLS verification and a successful Alpha connectivity check.

**Data-retention policy routes.** `GET /api/sevis/compliance/retention-policies` (`sevis.status.view`) returns the policies and the entity types that can carry one. The writes require `sevis.batch.create` and are audited: `POST /api/sevis/compliance/retention-policies` creates a policy (201, body validated against the data-retention policy schema), `PATCH /api/sevis/compliance/retention-policies/:id` updates one (404 when the id is unknown), `POST /api/sevis/compliance/retention-policies/evaluate` evaluates the retention policies and returns the results with an `evaluatedAt` timestamp, and `POST /api/sevis/compliance/retention-policies/seed-defaults` creates the default policy for each entity type that has none and returns how many were seeded. All accept a session or an API key with the `sevis.read` scope.
## Rate Limiting

SEVIS API calls are rate-limited via the sevisRateLimiter.ts middleware to prevent exceeding government-imposed submission quotas and to ensure fair pacing of batch submissions.

sevisRateLimiter.ts is a dedicated middleware applied to all SEVIS-bound API routes / Enforces per-window submission caps aligned with SEVIS batch processing constraints / Automatic backoff with queue-based submission pacing when limits are approached / Prevents burst submissions that could trigger government-side throttling or rejection / Rate limit status is visible on the SEVIS batch processing dashboard for operational awareness
## SEVIS Service Layer

Globus wraps the vendored SEVIS SDK through a multi-layered service architecture. Routes never call the SDK directly — all SEVIS operations flow through these service modules:

| Service | File | Purpose |
| --- | --- | --- |
| SEVIS Service | sevisService.ts | Wraps the vendored SEVIS SDK. Handles certificate auth, XML generation, and government endpoint communication. |
| SEVIS Batch Service | sevisBatchService.ts | Manages the batch lifecycle: creation, submission, result processing, and retry logic. |
| SEVIS Compliance | sevisComplianceService.ts | Evaluates data readiness for SEVIS reporting. Checks that all required fields are present before submission. |
| SEVIS Check-in Engine | sevisCheckinEngine.ts | Business logic for regulatory check-ins, deadline tracking, and compliance status updates. |

> **Service Layer Boundary**
>
> Routes call services. Services call the vendored SDK. The SDK calls government endpoints. This layering ensures that business logic, transport concerns, and government protocol details remain cleanly separated and independently testable.

### Sponsor & Batch-User Settings Are Required

Shipped Sep 11 2026 (production deployment). The SEVIS program sponsor identifier and batch-user identifier (`SEVIS_PROGRAM_SPONSOR_ID`, `SEVIS_BATCH_USER_ID`) are **required** in the production deployment manifest (`k8s/prod/deployment.yaml`): both are non-optional `secretKeyRef` entries, so a pod whose secret mapping lacks either key fails at container creation (`CreateContainerConfigError`) rather than booting with undefined submission identity. This is a manifest-level guard only: the server code has no startup check, `SEVIS_PROGRAM_SPONSOR_ID` falls back to `SEVIS_SCHOOL_CODE` when unset, and `SEVIS_BATCH_USER_ID` falls back to a built-in default. This matched the live production posture after a Sep 11 2026 configuration outage showed that a silent fallback to undefined sponsor/batch-user data in SEVIS submissions is worse than a fast, visible start-up failure. Environment set-up and deployment runbooks must treat both values as mandatory for every environment that submits to SEVIS.
