USCIS Case Status Lookup
Purpose
Given a 13-character USCIS receipt number (e.g. MSC2190012345, EAC2290098765, IOE0123456789), look it up on the official Case Status Online portal and return: the current case-status heading, the next-step / status-description paragraph, the form type when surfaced (I-130, I-485, N-400, I-765, etc.), the service-center code (derived from the first three letters of the receipt — no scraping needed), the last-updated date when shown, and the canonical case-status URL. Read-only — never logs in, never updates anything, never subscribes.
When to Use
- A user supplies a receipt number ("
MSC2190012345", "my receipt is EAC22...") and wants the current status. - A workflow polls case status over time (rate-limit yourself — see Gotchas).
- An agent needs to disambiguate whether a receipt is on the legacy
.doflow or the modern Next.js portal (this skill always uses the modern path).
Workflow
The portal moved to a Next.js SPA at the root of egov.uscis.gov. The legacy /casestatus/landing.do form action endpoints (displayMyCaseStatus.do, mycasestatus.do) are either gone (404) or Cloudflare-blocked from any non-Verified client (403, Cf-Mitigated: challenge). Use the modern URL https://egov.uscis.gov/?localeLang=en and drive the SPA — there is no public unauthenticated JSON API, and the internal csol-api/case/{receipt} backing endpoint that the SPA XHRs to is Cloudflare-blocked from any out-of-browser caller (verified 403 from browse cloud fetch --proxies).
1. Create a Verified + residential-proxy + captcha-solving session
SID=$(browse cloud sessions create \
--keep-alive \
--verified \
--proxies \
--solve-captchas \
| jq -r '.id')
All three flags are required:
--verified— Cloudflare's managed-challenge engine fingerprints headless Chrome aggressively on this domain. Without it, the SPA shell may load but the moment its client-side JS XHRs tocsol-api/..., Cf responds with a 403 challenge page that breaks the SPA state silently (the form just spins).--proxies— USCIS is unusually aggressive about data-center IP ranges. Bare AWS/GCP/Azure egress trips an immediate 403.--solve-captchas— if Cloudflare elevates to a Turnstile checkbox (rare on the SPA root, common when retrying after a failed lookup), Browserbase's solver handles it. If it elevates to hCaptcha (very rare; observed historically on the legacy.doflow), the same flag handles it.
2. Validate the receipt number locally before opening a session
Format: ^([A-Z]{3})(\d{10})$ (exactly 13 chars)
Parse the prefix into service_center_code immediately — no scraping needed:
| Prefix | Service center |
|---|---|
EAC | Vermont Service Center (Eastern Adjudication Center, St. Albans VT) |
WAC | California Service Center (Western Adjudication Center, Laguna Niguel CA) |
LIN | Nebraska Service Center (Lincoln NE) |
SRC | Texas Service Center (Southern Regional Center, Dallas TX) |
MSC | National Benefits Center (Missouri Service Center, Lee's Summit MO) |
NBC | National Benefits Center (modern label) |
NSC | Nebraska Service Center (modern label, used alongside LIN) |
YSC | Potomac Service Center (Arlington VA) |
IOE | USCIS Electronic Immigration System (ELIS) — online-filed cases, no physical center |
The next two digits are the federal-fiscal-year (21 = FY2021, runs Oct 1 2020 → Sep 30 2021). The next three are a workday index. The last five are the case sequence. Surface these as opaque integers if needed; the FY mapping is the only one worth deriving without extra work.
Reject early any input that doesn't match ^[A-Z]{3}\d{10}$ — emit { "success": false, "error_reasoning": "Invalid receipt format" } without spinning up a session. The submit button on the SPA stays disabled for non-13-char input anyway, so a malformed receipt will hang the click step forever.
3. Open the modern SPA and wait for hydration
browse cloud browse --connect "$SID" open "https://egov.uscis.gov/?localeLang=en"
browse cloud browse --connect "$SID" wait load
browse cloud browse --connect "$SID" wait timeout 2500 # Next.js hydration; the input is in the DOM
# at load time but its React event handlers
# are not attached until hydration completes.
browse cloud browse --connect "$SID" snapshot
The snapshot must show a textbox ref labelled "Enter Another Receipt Number" with placeholder EAC1234567890, and a button ref labelled "Check Status". If the snapshot instead shows a "Just a moment…" body or only a Cloudflare challenge widget, the Verified+proxy combination is failing — see "Cloudflare wall" branch below.
4. Type the receipt and confirm the button enables
browse cloud browse --connect "$SID" fill @receipt_number "$RECEIPT"
browse cloud browse --connect "$SID" wait timeout 500
browse cloud browse --connect "$SID" snapshot
After 13 valid characters land in the input, the initCaseSearch button transitions out of disabled. Verify the snapshot shows it un-disabled before clicking. If it's still disabled after typing, the React controlled-input event didn't fire — re-snapshot, get the latest ref, and re-fill (do not type per-character; the spinner-styling masking in the input's CSS class can drop keystrokes when typed too fast).
5. Submit and wait for the in-place result render
browse cloud browse --connect "$SID" click @initCaseSearch
browse cloud browse --connect "$SID" wait timeout 4000 # XHR to csol-api/case/{receipt} round-trip
browse cloud browse --connect "$SID" snapshot
The page does not navigate — the URL stays at https://egov.uscis.gov/?localeLang=en (it may pick up &caseId={receipt} via history.replaceState, but the path doesn't change). The <div class="caseStatus-container"> block re-renders in place. Don't poll the URL; poll the snapshot for either the status heading or a known not-found / error string.
6. Branch on what the result block shows
- Status heading + paragraph rendered → success path. Extract the heading (an
h1/h2in the container) and the paragraph beneath it. The paragraph almost always opens with"On <Month Day, Year>, we <verb> your Form <type>…"— regex that forform_typeandlast_updated:form_type:/\bForm (I-\d{3}[A-Z]?|N-\d{3}|G-\d{3}|FOIA|EOIR-\d+)\b/last_updated:/^On ([A-Z][a-z]+ \d{1,2}, \d{4}),/m
- "Receipt Number Can Not Be Found" or "Your case status could not be found" → emit
{ success: true, status: "Not Found", status_description: <verbatim site text>, form_type: null, last_updated: null }. This is a legitimate outcome, not an error. - "Validation Error" / "Please enter a valid receipt number" → the SPA's client-side check rejected the input. Should never happen if step 2's regex passed; if it does, treat as
{ success: false, error_reasoning: "USCIS-side validation rejected receipt" }. - Cloudflare wall — snapshot shows a Turnstile widget, "Just a moment…", or a
cf-mitigatedbody, or the result container never updates after 15s → emit{ success: false, error_reasoning: "Cloudflare challenge — not bypassed" }. Recreate the session with--solve-captchasif not already set, optionally--region us-east-1to swap the proxy egress region, and retry once. After two consecutive Cf walls, give up and surface the error — do not loop.
7. Construct the canonical URL
case_status_url = "https://egov.uscis.gov/?caseId=" + receipt + "&localeLang=en"
This is the link a user can paste into a fresh browser to reproduce the lookup. The caseId param is read client-side by the SPA on load.
8. Release the session
browse cloud sessions update "$SID" --status REQUEST_RELEASE
Site-Specific Gotchas
- Use
https://egov.uscis.gov/?localeLang=en, NOT/casestatus/landing.do. The legacy.doURL still exists but every request to it returns403 Cf-Mitigated: challengeregardless of Verified/proxy flags — Cloudflare treats it as a deprecated honeypot path. The modern Next.js SPA at the root is the only working entry. Confirmed 2026-05-16: legacylanding.do403 even with--proxies --allow-redirects; modern/?localeLang=en200 from the same fetcher. - Primary anti-bot is Cloudflare's managed challenge, NOT hCaptcha. Task-prompt assumptions notwithstanding, evidence is
Server: cloudflare,Cf-Mitigated: challenge, JS-challenge body with__cf_chl_tk+__cf_chl_rt_tktokens. hCaptcha may appear as a Cloudflare escalation on retries —--solve-captchascovers both. Don't waste time looking for an hCaptcha sitekey at first contact. - The internal
csol-api/case/{receipt}backing endpoint is Cf-blocked. Verified 403 frombrowse cloud fetch --proxies --allow-redirectson bothcsol-api/case/{receipt}andcsol-api/cases/{receipt}(plural). The 403 status (vs 404) confirms the path exists but is firewalled — do not try to hit it directly even with a Browserbase Function or curl. The SPA is the only way in. Also tested-blocked:egov/api/casestatus/{receipt}(403). Definitely-non-existent:casestatus/displayMyCaseStatus.do?appReceiptNum=...(404),api/casestatus?receiptNumber=...(404). Don't probe further. ?caseId=...is read client-side only. GETtinghttps://egov.uscis.gov/?caseId=MSC2190012345&localeLang=enreturns the same generic landing-form HTML as/?localeLang=enwith no server-rendered status. The SPA readscaseIdfromwindow.location.searchafter hydration and then fires the XHR. Server-side scraping by URL won't work.- The Check Status button is
disableduntil exactly 13 chars are typed. The receipt input hasmaxLength="13". The button's enabled state is React-controlled —fillwill fire the input event correctly; per-charactertypecan race the spinner-styling masking class. Always snapshot-verify the button is un-disabled before clicking, otherwise the click is a no-op. - The lookup result renders in place — no navigation. URL stays at
/?localeLang=en(possibly with&caseId=appended viahistory.replaceState). Wait for the.caseStatus-containerDOM mutation, not a page load. ~3–5s wall after click is typical; budget 8s before declaring failure. - Status-description text is the primary source of truth for
form_typeandlast_updated. USCIS does NOT expose form type as a structured field on the result page — it's embedded in the paragraph:"On January 15, 2024, we received your Form I-130, Petition for Alien Relative…". Regex it out. Same for the date. If the form type isn't in the paragraph (rare — happens on someIOEELIS cases that pre-date the form-name templating), emitform_type: nullrather than guessing from the receipt prefix. IOE(ELIS) receipts may be longer than 13 characters in some historical exports (sometimes seen as 13–15). The modern SPA's input is hard-capped atmaxLength="13", so anything longer will be truncated client-side. If your input is a 14- or 15-charIOEstring, drop trailing chars to fit 13 only if the trailing chars are zeros; otherwise reject as malformed.- Service-center codes are not always present in the URL after submit. Parse them from the receipt prefix locally (see Workflow step 2 table). Do not try to extract them from the result page — USCIS surfaces the office name in the status paragraph sometimes ("at our National Benefits Center"), but it's an inconsistent natural-language mention, not a reliable field.
- Rate-limit yourself to ≤ 1 lookup / 5 seconds across a session. USCIS doesn't publish a limit, but Cloudflare's challenge engine elevates after ~10 rapid lookups from the same session, and the elevation can persist across session renewal from the same proxy IP for ~15 minutes. For bulk lookups, rotate proxy egress (
--region us-east-1/us-west-2/eu-central-1round-robin) and add a 5–8s sleep between receipts. - Don't reuse a session across many lookups. Recreate the session every ~5 lookups even if it stayed healthy. Long-lived sessions accumulate Cloudflare cookies (
__cf_bm,__cflb) that eventually trip the challenge engine when their fingerprint drifts from the proxy fingerprint. - READ-ONLY. There are no buttons to click that would mutate state on this page (no login required for the lookup), but do not click the "Login" button or the "Sign up for Case Status Online" CTA — both lead into the MyAccount flow which is an entirely different skill scope.
localeLang=esis the Spanish UI. Same SPA, samecsol-apibacking endpoint, but all status text comes back translated (e.g."Caso fue recibido"instead of"Case Was Received"). Always pinlocaleLang=enfor consistent text extraction.
Expected Output
Five distinct outcome shapes.
// 1. Found — status retrieved successfully
{
"success": true,
"receipt_number": "MSC2190012345",
"service_center_code": "MSC",
"service_center_name": "National Benefits Center",
"status": "Case Was Received",
"status_description": "On January 15, 2024, we received your Form I-130, Petition for Alien Relative, Receipt Number MSC2190012345, and mailed you a receipt notice that describes how we will process your case. Please follow any instructions in the notice. If you do not receive your receipt notice by February 15, 2024, please call Customer Service at 1-800-375-5283.",
"form_type": "I-130",
"last_updated": "January 15, 2024",
"case_status_url": "https://egov.uscis.gov/?caseId=MSC2190012345&localeLang=en",
"error_reasoning": null
}
// 2. Not found — receipt is well-formed but USCIS has no record
{
"success": true,
"receipt_number": "EAC9999999999",
"service_center_code": "EAC",
"service_center_name": "Vermont Service Center",
"status": "Not Found",
"status_description": "Validation Error: This receipt number does not exist in our database, or it has not been updated recently. Please re-check that you have entered the correct receipt number from your USCIS receipt notice and try again.",
"form_type": null,
"last_updated": null,
"case_status_url": "https://egov.uscis.gov/?caseId=EAC9999999999&localeLang=en",
"error_reasoning": null
}
// 3. Invalid format — rejected pre-session (no Browserbase cost)
{
"success": false,
"receipt_number": "ABC123",
"service_center_code": null,
"service_center_name": null,
"status": null,
"status_description": null,
"form_type": null,
"last_updated": null,
"case_status_url": null,
"error_reasoning": "Invalid receipt format — expected 3 uppercase letters followed by 10 digits (e.g. EAC1234567890)."
}
// 4. Cloudflare wall — anti-bot not bypassed even with Verified + proxies + captcha solver
{
"success": false,
"receipt_number": "MSC2190012345",
"service_center_code": "MSC",
"service_center_name": "National Benefits Center",
"status": null,
"status_description": null,
"form_type": null,
"last_updated": null,
"case_status_url": "https://egov.uscis.gov/?caseId=MSC2190012345&localeLang=en",
"error_reasoning": "Cloudflare challenge — not bypassed. Tried --verified + --proxies + --solve-captchas across 2 sessions; result container never populated within 15s of submit and snapshot showed Turnstile widget."
}
// 5. Approved / other state — same shape as #1, just different status heading + paragraph
{
"success": true,
"receipt_number": "LIN2090012345",
"service_center_code": "LIN",
"service_center_name": "Nebraska Service Center",
"status": "Case Was Approved",
"status_description": "On March 3, 2024, we approved your Form I-765, Application for Employment Authorization, Receipt Number LIN2090012345. We will mail your approval notice. Please follow any instructions in the notice. If you move, go to www.uscis.gov/addresschange to give us your new mailing address.",
"form_type": "I-765",
"last_updated": "March 3, 2024",
"case_status_url": "https://egov.uscis.gov/?caseId=LIN2090012345&localeLang=en",
"error_reasoning": null
}