Waystar Integration and API Guide: Clearinghouse Setup From a Live Build
CTO & Co-Founder
CTO & Co-Founder at Nirmitee.io. Architects healthcare integrations across FHIR, SMART on FHIR, ABDM and NHCX, writing from production experience taking hospital software from sandbox to go-live.

Updated November 2026
A Waystar integration connects your EHR or billing system to the Waystar clearinghouse so your claims go out as 837 files and the answers come back as 999, 277CA, 277 and 835 files, either as batch files over SFTP or, where your contract includes it, through the Waystar API. Getting it right takes four pieces of work: the contract and payer enrollment, the SFTP or API connection, a correct 837, and code that reads every file that comes back, including the claim status you have to ask for.
This guide is built from our own Waystar clearinghouse integration work. Where we describe how Waystar behaved, that is what we observed on our connection; your contract and Waystar's companion guide are the final word. For the full claim journey around it, see our rcm integration guide. If you want a team to build or fix the connection, see our ehr clearinghouse integration service or talk to our team.
Watch: 999 vs 277CA: The Most Confusing RCM Difference Explained for Engineers (9:46).
Watch: How 277CA Claim Rejections Actually Work: STC Codes and a Safe Rejection Workflow (9:12).
Key takeaways
- Start payer enrollment and the IP allowlist request before you write code. On our project both took longer than the engineering.
- Waystar runs a developer portal with API documentation, but the documentation pages need a login. Confirm whether the Waystar API is in your contract; ours covered SFTP batch only.
- A 999 "accepted" means the file parsed, not that your claims were accepted. Claim rejections arrive in the 277CA.
- The 277CA and the payer's 277 status response both start with ST*277. Route them by BHT06 (TH or DG) and the implementation guide version, never by "277" alone.
- Claim status does not arrive on its own. Build a 276 polling loop from day one.
- Save every submission before you send it and retry with the same control numbers, so a retry never becomes a duplicate claim.
What Waystar does in your claim flow
Waystar sits between your billing system and the payers as a clearinghouse. You send it your claims in one standard format, it checks them and forwards them to each payer, and it sends back acknowledgements, payer responses and remittances.
So, is Waystar a clearing house? Yes. On our connection it received our 837 claim files over SFTP and returned 999 file acknowledgements, 277CA claim acknowledgements and 835 remittances, which is the clearinghouse role. Waystar also sells revenue cycle software beyond the clearinghouse; this guide covers only the claims connection.
Without a clearinghouse, you would need separate enrollment, connectivity, testing and upkeep for every payer. That is why most providers and billing platforms use one for all but their very largest payers.
Waystar and ZirMed: why your files still say ZIRMED
Waystar is the company formed when Navicure and ZirMed merged in November 2017; the combined company took the Waystar name in February 2018, according to Waystar's own announcement. So if you are asking what ZirMed is today, it is part of Waystar.
The ZirMed name still shows up in the integration. On our connection, the X12 files Waystar sent us carried ZIRMED as the sender ID in the ISA envelope (ISA06, with qualifier ZZ), and the XML portal reports it dropped in our download folder referenced a zirmed.com report stylesheet. If you see ZIRMED in a file, it came from Waystar. Confirm the sender and receiver IDs for your own account in writing before your first test file.
Before you write code: contract, API access and enrollment
The slowest parts of a Waystar integration are paperwork and network access, not code. Start the contract questions, payer enrollment and IP allowlisting on day one.
- The contract was per EHR, not per clinic. On our project, one Waystar account covered many practices, each with its own tax ID. The billing provider's tax ID travels inside the 837, not in the connection, so we did not need separate SFTP credentials for each clinic.
- The API was a separate product. Our contract covered SFTP batch only. If you plan real-time eligibility or claim status calls, ask whether the API is entitled before you design screens around it.
- IP allowlisting took days. SFTP access was limited to registered IP addresses, and adding one was not same-day. It is the most common reason we have seen a first sprint stall.
- Payer enrollment took weeks. Enrollment is per payer, per provider and per transaction type: claims, electronic remittance (ERA) and electronic funds transfer (EFT) are separate. Some payers want test files first. Gate your go-live date on enrollment, not on the code.
Waystar API documentation: what is public and what needs a login
Waystar publishes a developer portal at developer.waystar.com, linked as "Developer Login" from waystar.com. The portal's home page is public; the documentation pages behind it ask you to register and log in. We checked this on November 4, 2026.
The public home page lists the ways Waystar connects: REST APIs, web services, SFTP, HL7, batch, X12 EDI and RPA bots. Its documentation menu is grouped by product, including integration basics (SSH FTP instructions, HMAC security for web services, a payer ID list, test and sandbox accounts), insurance verification, claim management, claim monitoring, remit management and denial management.
So for "waystar api documentation", the answer is: the documentation exists, but you read it as a registered developer, and what you can call in production depends on your contract. On our project the API was a separate product and was not in the contract, so we built on SFTP batch files. If your scope needs real-time eligibility or claim status, ask Waystar two things before you design screens around it: which transactions your contract includes by API, and whether a test account is available.
Connecting over SFTP: folders and file names
On our connection, Waystar exchanged files through three SFTP folders: one for uploads, one for downloads and one archive. You put 837 files in the upload folder and poll the download folder for everything that comes back.
| Folder | What we saw | What to build |
|---|---|---|
| /Upload | Outbound claim and eligibility files; write-only for our account | Keep your own record of every file sent; you cannot list it back |
| /Download | 999, 277CA, 277, 271, 835 and report files | Poll on a schedule, download, then route by content |
| /Download/Archive | Files already collected | Use it to recover a file your poller missed |
In our setup the file extension told Waystar what kind of file we were sending: .CLP for professional 837P claims, .CLI for institutional 837I, .CLD for dental 837D, and .ELG for 270 eligibility batches. The base file name could not contain periods. Treat these as the conventions in your companion guide, which may differ.
Do not route inbound files by their names. Read the envelope (ISA, then GS, then ST) and route by content. File names are a convenience, not part of the X12 standard, and they can change.
On our connection, uploading a file with a name that was already used replaced the earlier file without warning. Since the upload folder cannot be listed, put a timestamp or batch ID in every base file name, for example claims_20261104T1830Z_001.CLP.
Credentials and the host key
One set of SFTP credentials can submit claims for every practice on your platform, so treat it like a production database password. Keep it in a secrets manager or environment variables, never in committed configuration, and refuse to start in production if any value is missing.
# Startup check: refuse to boot in production with missing settings
required = ["SFTP_HOST", "SFTP_USERNAME", "SFTP_PASSWORD", "HOST_KEY_SHA256", "USAGE_INDICATOR"]
missing = [k for k in required if not config.get(k)]
if missing and env != "development":
raise ConfigError(f"Refusing to start. Unset: {', '.join(missing)}")
log.info("X12 usage indicator ISA15=%s", config["USAGE_INDICATOR"]) # T or P Pin the SFTP host key. Record its fingerprint on your first verified connection and refuse to connect in production when it does not match, so a changed key is a visible event and not a silent one. Make the ISA15 usage indicator (T for test, P for production) a setting rather than a constant, and log it at startup, so anyone can see which mode a running system is in.
Sending the 837 so every answer can find its claim
The 837 itself is standard X12, so the work is mostly in identifiers. Every file needs unique control numbers, and every claim needs a unique claim ID that stays the same if you resend it.
- ISA13 and GS06 are the interchange and group control numbers. The 999 refers back to them, so they must be unique per file.
- CLM01 is your patient control number for the claim. It comes back in the 277CA and as CLP01 in the 835, so it is your key for matching every answer to its claim. Keep it the same across retries.
- Provider loops name the billing provider (NM1*85), the rendering provider (NM1*82) and, when used, the referring provider (NM1*DN), with the NPI after the XX qualifier.
- The billing tax ID sits in the claim, which is why one Waystar account can carry many practices.
Our 835 vs 837 guide covers the segments in detail, and our guide to rendering vs billing provider covers the provider loops.
Submitting claims without duplicates
A duplicate claim is worse than a failed one, so save each submission before you send it and retry with the same control numbers. Transmission failures are routine on SFTP; the design has to make them recoverable.
def submit_claim(claim_id):
blockers = validate_submission_readiness(claim_id) # demographics, coverage, NPI, diagnosis
if blockers:
return Blocked(blockers[0])
if submissions.exists(claim_id=claim_id, status_in=["pending", "transmitted"]):
return Blocked("A live submission already exists")
# Save first, with control numbers fixed now and never regenerated
s = submissions.create(claim_id=claim_id,
isa13=next_interchange_control_number(),
gs06=next_group_control_number(),
clm01=f"PCN{claim_id}", status="pending",
filename=f"claims_{utc_now():%Y%m%dT%H%M%SZ}_{claim_id}.CLP")
send(s)
def send(s):
try:
transport.put(build_837p(s.claim_id, s.isa13, s.gs06), s.filename)
s.mark_transmitted()
except TransportError as e:
s.attempts += 1
s.last_error = str(e)[:1024]
if s.attempts >= MAX_ATTEMPTS:
s.abandon() # visible state, not silence
claims.revert_to_ready(s.claim_id)
else:
s.retry_at = utc_now() + backoff(s.attempts)
def retry_sweep():
for s in submissions.due_for_retry():
if acks_999.exists(isa13=s.isa13): # it did arrive; do not send it again
s.mark_transmitted()
else:
send(s) # same ISA13, GS06 and CLM01 Three rules carry most of the weight:
- Check before you build. Missing demographics, inactive coverage, a missing NPI or a non-billable diagnosis are cheaper to catch now than as a rejection days later. Eligibility is usually the best first fix; see our guide to automating eligibility verification.
- Save before you send. If the process dies mid-upload, the record exists and can be recovered. Without it you have a claim that may or may not have been filed.
- Keep delivery state apart from claim state. A claim can be "submitted" in your workflow while its file never reached Waystar. Track both, so an unsent claim never looks filed.
Matching answers to claims follows the same discipline. Never rewrite the payer's identifiers to force a match; record a manual match as a separate row. Never discard a response you cannot match; park it in a triage queue, because an unmatched file usually means a matching bug.
The files that come back: 999, 277CA, 277 and 835
Three kinds of file come back without you asking: the 999, the 277CA and the 835. The fourth, the payer's 277 claim status response, only comes when you send a 276.
| File | What it tells you | Match it by | What to do |
|---|---|---|---|
| 999 | The file and each transaction set passed or failed syntax checks | ISA13 and GS06 of the file you sent | If rejected, fix the syntax and resend the file |
| 277CA (005010X214) | Each claim was accepted into processing or rejected, with a reason | CLM01, returned in the 277CA | Send rejections to a worklist; a rejected claim never reached the payer |
| 277 (005010X212) | Where the payer is with a claim: pending, paid, denied | The trace number you sent in the 276 | Update status; answer P3 "provider requested information" quickly |
| 835 (005010X221) | What was paid, adjusted and denied, and why | CLP01 (your CLM01), then the payer claim number in CLP07 | Post payments, route denials, match the EFT by TRN |
A 999 "accepted" is the most misread file in claims work. In a live clearinghouse integration we hardened, every one of the 1,728 999s in our first review was accepted, while 277CAs were still rejecting claims for content problems, most often diagnosis codes. Our clearinghouse claims integration case study has the details.
Keep your own copy of every file that comes back. Download, store it durably with the claim it matched, then clear it from your working set. Waystar's archive folder is a convenience for recovering a missed file, not your record; if a payer disputes timely filing months later, the 277CA that proves it should be in your own storage.
Portal reports: when someone edits a claim inside Waystar
Not everything in the download folder is X12. On our connection Waystar also dropped XML portal reports (named like Prof_<customer id>_<date>.xml), and they recorded billers correcting claims by hand inside the Waystar portal, most often diagnosis codes. When that happens, your system's copy of the claim is out of date and the same mistake comes back on the next claim.
So do not let these files pile up as "unrecognized". Detect them in the router (they do not start with ISA), and raise a work item that says "this claim was edited at the clearinghouse, update it here". We do not have a published schema for these reports, so parse them defensively and keep the raw file.
277 vs 277CA: the routing trap
The 277CA and the payer's 277 claim status response share the same transaction code, ST01 = 277. They differ in BHT06 (TH for the 277CA, DG for a status response) and in the implementation guide version (005010X214 vs 005010X212), so route on those, not on "277".
The 277CA is an acknowledgement: it says whether each claim got into processing. The 277 status response is the payer's answer to your 276: where the claim is in adjudication. If both go to one parser, a payer status will be read as an acknowledgement or the reverse, and claim statuses stop making sense without any error being raised. On one platform we reviewed, routing on ST01 alone had not caused harm yet only because every 277 it had received was a 277CA; the first payer status response would have been misread.
Here is what the two headers look like in public sample files: a Medicare contractor's 277CA example and a payer's 277 status response example.
277CA (acknowledgement):
GS*HN*...*005010X214~
ST*277*0001*005010X214~
BHT*0085*08*19157*20190606*123747*TH~
277 (claim status response):
GS*HN*...*005010X212~
ST*277*5579*005010X212~
BHT*0010*08*1001*20100901*1200*DG~ A minimal router in Python looks like this. It splits segments on the terminator and elements on the separator declared in the ISA, so it does not assume "*" and "~".
def route(x12: str) -> str:
elem = x12[3] # element separator from ISA
term = x12[105] # segment terminator after ISA16
segs = [s.strip().split(elem) for s in x12.split(term) if s.strip()]
st = next(s for s in segs if s[0] == "ST")
gs = next(s for s in segs if s[0] == "GS")
if st[1] == "999":
return "ack_999"
if st[1] == "835":
return "remit_835"
if st[1] == "271":
return "eligibility_271"
if st[1] == "277":
bht = next(s for s in segs if s[0] == "BHT")
bht06 = bht[6] if len(bht) > 6 else ""
version = gs[8]
if bht06 == "TH" and version.startswith("005010X214"):
return "claim_ack_277ca"
if bht06 == "DG" and version.startswith("005010X212"):
return "claim_status_277"
return "quarantine" # unknown: hold it and alert a person Claim status: you have to send a 276
Payer claim status does not arrive by itself. The 999, 277CA and 835 come to you unprompted, but to learn where a claim stands at the payer you must send a 276 claim status request and read the 277 that answers it.
Without this loop, a claim the payer is sitting on looks the same as a claim that is fine until someone notices it is old. The platform in our case study never sent a 276 and learned status only from what arrived on its own. Build the loop from the start:
- Each day, select claims accepted on a 277CA with no 835 after the number of days you expect for that payer.
- Send a 276 for each, through the same clearinghouse connection.
- Read the STC segment of the 277: category A codes are acknowledgements, P codes pending, F codes finalized. P3 means the payer is waiting on information from you.
- Update the claim's status by rank, so a weaker, later message never overwrites a rejection or a payment, and keep the history.
Our guide to 277 claim status automation covers the worklist side.
Posting the 835: CAS, reversals and balances
The 835 explains each payment and adjustment with CAS segments, and it can also take money back. Three details caused most of the posting errors we have fixed.
- CAS is read as triplets. A CAS segment has one group code (CO, PR, OA or PI) followed by up to six sets of reason code, amount and quantity. A loop that expects a group code at the start of every set reads a quantity as a group code from the second adjustment on.
- Reversals are claims too. CLP02 = 22 marks a reversal of a previous payment, with the original amounts negated. It is usually followed by the corrected claim payment. Post both, linked by the payer claim number in CLP07.
- Balance math. Remaining balance is billed minus paid minus contractual (CO) adjustments. The patient's share is the PR adjustments. Charge minus paid makes the patient look like they owe the write-off. For example, on a $200 charge with a $150 contracted rate where the payer pays $120, the patient owes $30 (PR) and $50 is a contractual write-off (CO). The collectible balance is $30, not $80. The business cost of getting this wrong is covered in the real cost of getting claims integration wrong.
def read_cas(seg: list[str]) -> list[dict]:
# seg = ["CAS", group, reason1, amount1, qty1, reason2, amount2, qty2, ...]
group, rest = seg[1], seg[2:]
out = []
for i in range(0, len(rest), 3):
reason = rest[i] if i < len(rest) else ""
if not reason:
break
amount = float(rest[i + 1]) if i + 1 < len(rest) and rest[i + 1] else 0.0
qty = rest[i + 2] if i + 2 < len(rest) else ""
out.append({"group": group, "reason": reason, "amount": amount, "qty": qty})
return out To see a real file, read our 835 file example. To compare tools that automate this, see our review of payment posting software.
Testing and go-live checklist
Test the unhappy paths on purpose, because production will send them. A rejected 277CA, a denied claim and a reversal on an 835 are the three cases we always run before the first production file.
| Check | Done when |
|---|---|
| Enrollment | Claims, ERA and EFT enrollment confirmed for every payer and provider going live |
| Network | Production IP addresses allowlisted; test and production credentials and folders confirmed separately |
| Security | Host key pinned; credentials in a secrets store and absent from logs (check a real log line) |
| Identifiers | Sender and receiver IDs in ISA and GS confirmed with Waystar in writing |
| Envelopes | ISA15 = T on test files and P in production; unique ISA13 and GS06 on every file |
| Claims | CLM01 unique and stable across retries; billing tax ID and NPIs correct; a deliberate double submission is blocked |
| Storage | Every response file saved in your own storage with the claim it matched |
| Routing | 999, 277CA, 277, 271 and 835 routed by content; unknown files quarantined with an alert |
| Status | 276 loop running; STC category codes mapped to actions |
| Posting | Multi-reason CAS, PR and CO adjustments and a CLP02 = 22 reversal all post correctly |
| Monitoring | Alert when a file has no 999 or 277CA within its expected window; silence is a failure |
Go live in steps: one claim, followed through every response, then a small batch for one payer, then widen. The first production claims surface enrollment gaps and payer rules that test files do not.
One go-live trap we have seen: a payer whose ERA enrollment never completed looks like a payer that "never pays", when it is paying on paper. Check ERA enrollment before you open a denial investigation. Our article on EDI testing for clearinghouse integration covers the test plan, and our free EDI Inspector explains any 999, 277CA or 835 segment by segment.
Running it: the numbers to watch
A Waystar integration is finished when your team can answer these questions on demand. Most failures in claims are quiet: a claim sits in a state nobody watches until the filing deadline passes.
- Claims waiting to be sent: anything here is unbilled care.
- Failed and abandoned uploads: claims that never reached Waystar.
- First-pass acceptance rate: claims accepted on the 277CA without rework, the best single signal of upstream data quality.
- Claims with no payer answer past their expected window, which feed the 276 loop.
- Denials by payer and reason: they cluster, and clusters are fixable. Many trace back to eligibility; see why claims get denied before they are sent.
- Unmatched remittances: money received but not yet tied to a claim.
Symptoms and their usual causes
| Symptom | Usual cause |
|---|---|
| Connection refused from production but works from another server | That server's IP address is not allowlisted |
| File uploaded but no 999 comes back | Wrong file extension or naming for your companion guide, or the file was overwritten by a later upload with the same name |
| Claims accepted, then denied by the payer | Enrollment incomplete for that provider and payer |
| Statuses stale; the Waystar portal disagrees with your system | 277 routed on ST01 without checking BHT06, no 276 loop, or claims edited inside the portal |
| Duplicate claims at the payer | Control numbers regenerated on retry |
| Receivables higher than anything ever collected | Contractual (CO) adjustments counted as money owed |
For the same patterns without Waystar specifics, read our clearinghouse integration guide and the seven claims integration traps.
What we observed vs what to confirm with Waystar
Everything above about Waystar's folders, file names, contract and API is what we saw on one integration. Before you build, confirm each point with Waystar and in your own companion guide.
| We observed | Confirm with Waystar |
|---|---|
| SFTP batch only; the API was a separate product | Which transactions your contract includes by API, and developer portal access |
| Files from Waystar carried ZIRMED as the ISA06 sender ID | The sender and receiver IDs for your account |
| XML portal reports arrived next to the X12 files | Their format, and whether you can turn them on or off |
| Upload, Download and Archive folders; upload was write-only | Folder paths for test and production |
| File extension set the file type (.CLP, .CLI, .CLD, .ELG) | The current naming rules in your companion guide |
| IP allowlisting took days | The current lead time and the form to request it |
| Payer enrollment took weeks, per payer and transaction | Which of your payers need enrollment or test files before go-live |
Need help with your Waystar integration?
We build and repair clearinghouse connections inside EHRs and billing platforms: 837 generation, 999 and 277CA handling, 276 polling and 835 posting. If your integration "works" but claims keep going quiet or balances do not add up, the files coming back are usually where the money leaks. See our x12 edi integration services or talk to our team.
Ready to scale?
Talk to our healthcare engineering team about building, integrating, and shipping faster.
Frequently Asked Questions
Is Waystar a clearing house?
Does Waystar have public API documentation?
What is ZirMed, and is it part of Waystar?
Why do Waystar files say ZIRMED?
What is the difference between a 277 and a 277CA?
How do I check claim status through Waystar, by API or 276?
Where is the Waystar payer ID list?
How long does Waystar clearinghouse enrollment take?


