Blog

Sep 17, 2025

Guide

The big takeaway: Manually entered payer names are often wrong. Use Stedi's Search Payers endpoint to find the closest matching payer ID. Then verify it's the right payer with an eligibility check.

Healthcare runs on forms. People often fill them out wrong.

One thing we commonly see is the plan name entered as the payer name. A tired nurse sees "UHC Open Access Plus" on an insurance card and enters it as the payer name. But that's actually the plan name. The payer is UnitedHealthcare.

It’s an honest mistake. But if you’re building on top of that data, it can break your workflow.

Clearinghouse transactions – like claims – need a payer ID. It’s like a routing number for the payer. Use the wrong ID, and your claims are likely to be rejected. And that may mean delayed payments for the provider.

When payer names are clean and trustworthy, finding their IDs is easy. You can just use the top result from Stedi’s Search Payers endpoint.

But when the data is dirty, you need a verification workflow. This guide lays out our recommended flow. It’s based on what we’ve seen work with customers.

Step 1. Run a payer search

Use the Search Payers endpoint to search for the payer name:

curl --request GET \
  --url "https://healthcare.us.stedi.com/2024-04-01/payers/search?query=uhc+open+access+plus" \
  --header "Authorization: <api_key>"

Results are sorted by relevance. Store the top result's primaryPayerId as the payer ID:

{
  "items": [
    {
      "payer": {
        "stediId": "KMQTZ",
        "displayName": "UnitedHealthcare",
        "primaryPayerId": "87726",            // Payer ID
        ...
      }
    },
    ...
  ],
  ...
}

The Search Payers endpoint’s search algorithm is greedy. It’ll almost always return something, even for an unmappable payer name like BCBS out of state.

It’s important to verify the patient has coverage with the payer before using the payer ID in a workflow.

Step 2. Verify the payer ID using an eligibility check

Eligibility checks are fast, inexpensive, and easy to automate. This makes them a good choice to verify a patient’s payer.

Run an eligibility check to verify the payer ID. For example, using the Real-Time Eligibility Check JSON endpoint:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
  "tradingPartnerServiceId": "87726",            // Payer ID
  "encounter": {
    "serviceTypeCodes": ["30"]
  },
  "provider": {                                  // Provider data
    "organizationName": "ACME Health Services",
    "npi": "2528442009"
  },
  "subscriber": {                                // Patient data
    "memberId": "1234567890",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19850101"
  }
}'

The results will tell you if the patient has active coverage with the payer. If so, you can use the payer ID in other workflows for the patient. For tips on interpreting the results, see Active and inactive coverage in the Stedi docs.

If the check returns AAA error 75 (Subscriber/Insured Not Found) or a similar error, it means the payer search likely matched the wrong payer. Move on to step 3.

Step 3. Manually check the rest

Some raw payer names can't be mapped automatically. They need human judgment to pick the right ID.

Many payers use separate payer names and payer IDs for different lines of business, states, or coverage types.

For example, searching "Aetna" returns their main commercial ID first, but they also have separate IDs for each state's Medicaid plan. Without more context, you can't pick the right one.

Other payer name strings can’t be mapped to a valid payer ID at all:

  • BCBS out of state (which state?)

  • Insurance pending (not a payer)

  • Typos that match nothing

Mark these for review or flag them with the provider. Don't guess.

Things to watch out for

Don’t send Medicare Advantage checks to CMS

Medicare Advantage isn't Medicare. It's commercial insurance that Medicare has approved.

Medicare Advantage plans and payers often have “Medicare” in their names. With typos, it’s easy to run a search for these payers that returns the National Centers for Medicare & Medicaid Services (CMS) – the government payer for Medicare – instead of the commercial payer you need.

CMS forbids using its system for Medicare Advantage checks. To avoid doing that, filter anything with "Medicare" in the name. Make sure it’s not a Medicare Advantage check before sending it to CMS.

Blue Cross Blue Shield eligibility responses

Blue Cross Blue Shield (BCBS) isn't one payer. It's a collection of 30+ separate entities. However, a BCBS payer can verify any other BCBS member through the BlueCard network.

For example, if you send an eligibility check to BCBS Texas for a BCBS Alabama member, you'll get benefits back. It may not be obvious that BCBS Alabama is the home payer.

To get the home payer, check the response's benefitsRelatedEntities for an entry with entityIdentifier = "Party Performing Verification". The entityIdentificationValue is the home payer’s ID. Use that payer ID for claims and other workflows.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "serviceTypeCodes": ["30"],
      ...
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Party Performing Verification",
          "entityType": "Non-Person Entity",
          "entityName": "Blue Cross Blue Shield of Alabama",
          "entityIdentification": "PI",
          "entityIdentificationValue": "00510BC"
        }
      ]
    },
    ...
  ],
  ...
}

Get help when you need it

We’ve seen the workflow above work about 80% of the time. For the remaining 20%, patterns often emerge.

If there’s a raw payer name string that’s giving you trouble, reach out. Our team can help with payer ID matching.

Sep 14, 2025

Guide

Big takeaway: Duplicate benefits often aren’t duplicates. They cover different scenarios, like in-network vs. out-of-network care.

Imagine you run an eligibility check. A second or two later, you get back the response. It lists three different co-pays – $15, $30, and $0 – each for a physician office visit.

Which one is right?

They all are. Each co-pay applies to a different situation:

  • $15 for in-network providers – providers who have a contract with the patient’s payer for their health plan.

  • $30 for out-of-network providers – providers without a contract.

  • $0 for specific services, like preventive care or maternity visits, with in-network providers.

If you don't know which fields to check in the response, it’s hard to tell them apart. This guide shows you what to look for with real-life examples.

Where to find benefits

If you’re using Stedi’s JSON eligibility APIs, most of a patient’s benefit information is in the response’s benefitsInformation object array.

Each benefitsInformation object – or benefit entry – tells you about one aspect of the patient’s coverage. One entry indicates active coverage. Another contains a co-pay.

For tips on reading the objects, see How to read a 271 eligibility response in plain English.

{
  ...
  "benefitsInformation": [
    {
      "serviceTypeCodes": ["30"],         // General medical
      "code": "1",                        // Active coverage
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network providers
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "serviceTypeCodes": ["30"],         // General medical
      "code": "C",                        // Deductible
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network providers
    },
    {
      "serviceTypeCodes": ["30"],
      "code": "D",                        // Benefit Description
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    },
    {
      "serviceTypeCodes": ["88"],         // Pharmacy
      "code": "B",                        // Co-pay
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network providers
    },
  ],
  ...
}

Fields to check

Benefit entries often look identical except for one or two fields. Check these fields first to spot the difference:

  • serviceTypeCodes
    The Service Type Code (STC) indicates what type of service the benefit applies to. If two benefitsInformation objects have different serviceTypeCodes values, they apply to different services – like pharmacy and mental health services.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include the same STC.


  • coverageLevelCode
    Whether the benefit applies to the plan’s subscriber, their family, etc. A $20 individual deductible and $50 family deductible aren't duplicates. They're separate buckets that apply to different members of the patient’s plan.


    If coverageLevelCode is missing, assume the benefit entry applies to individual coverage.

  • inPlanNetworkIndicatorCode
    Whether the benefit applies to in-network providers, out-of-network providers, or both. This often explains the biggest price differences in what the patient pays.

  • timeQualifierCode
    The time period for the benefits, such as calendar year, remaining year, or visit. A $500 calendar year maximum is different from a $500 per-visit limit.

  • additionalInformation.description
    Free-text notes – these act as fine print. Payers often use these to include specific procedure codes, exclusions, carve outs, or special rules. As a rule of thumb, more specific descriptions override less specific ones.

    In many cases, these descriptions will be in a separate entry for the STC. These entries typically have a code of 1 (Active Coverage) or  D (Benefit Description).

  • eligibilityAdditionalInformation.industryCode
    When eligibilityAdditionalInformation.codeListQualifierCode is set to ZZ (Mutually Defined), this field contains a code for where the service takes place. Some payers offer reduced co-pays or coinsurance for telehealth visits.


    See Place of Service Code Set on CMS.gov for a list of these codes and their descriptions.

Other fields

The above list isn’t exhaustive. If you’ve checked these fields and still can’t spot differences between similar benefit entries, try diffing the entries in a code editor or a similar tool.

Examples

Here are a few examples of near-identical benefit entries we’ve helped customers interpret.

Multiple co-pays for the same service
The following benefits cover mental health outpatient visits (STC CF).

The difference is in the additionalInformation.description field. Primary care providers (PCPs) have a $25 co-pay. Specialists and other providers have a $75 co-pay.

// PCP co-pay
{
  "serviceTypeCodes": ["CF"],              // Mental health outpatient
  "code": "B",                             // Co-pay
  "coverageLevelCode": "IND",              // Individual coverage
  "inPlanNetworkIndicatorCode": "Y",       // In-network
  ...
  "benefitAmount": "25",                   // $25 co-pay
  "additionalInformation": [
    {
      "description": "Provider Role PCP"    // Primary care provider only
    }
  ]
}

// Specialist co-pay
{
  "serviceTypeCodes": ["CF"],
  "code": "B",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "Y",
  ...
  "benefitAmount": "75",                   // $75 co-pay
  "additionalInformation": [
    {
      "description": "Provider Role OTHER"  // All other providers
    }
  ]
}

Different provider network status, different deductibles
The benefits below both cover general medical care (STC 30). Both have an annual deductible.

The only difference is the provider’s network status. In-network providers have a $1000 deductible. Out-of-network providers have a $2500 deductible.

// In-network deductible
{
  "serviceTypeCodes": ["30"],              // General medical
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "timeQualifierCode": "23",               // Calendar year
  ...
  "benefitAmount": "1000",                 // **$1000 deductible**
  "inPlanNetworkIndicatorCode": "Y",       // **In-network only**
}


// Out-of-network deductible
{
  "serviceTypeCodes": ["30"],
  "code": "C",
  "coverageLevelCode": "IND",
  "timeQualifierCode": "23",
  ...
  "benefitAmount": "2500",                 // **$2500 deductible**
  "inPlanNetworkIndicatorCode": "N",       // **Out-of-network only**
}

Co-insurance for different procedures
These dental benefits all cover adjunctive dental services (STC 28). The coinsurance percentage depends on which procedure codes are billed.

In this case, the procedure codes are CDT (Current Dental Terminology) codes, which are used for dental services.

The codes for each coinsurance are listed in the additionalInformation.description field.

// Fully covered procedures
{
  "serviceTypeCodes": ["28"],              // Adjunctive dental services
  "code": "A",                             // Co-insurance
  "coverageLevelCode": "IND",              // Individual coverage
  "inPlanNetworkIndicatorCode": "W",       // Not applicable
  "benefitPercent": "0",                   // Patient pays 0% (fully covered)
  "additionalInformation": [
    {
	// CDT codes for palliative treatment
      "description": "D9110 D9912"
    }
  ]
}

// 20% coinsurance procedures
{
  "serviceTypeCodes": ["28"],
  "code": "A",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "W",
  "benefitPercent": "0.2",                 // Patient pays 20%
  "additionalInformation": [
    {
	// CDT codes for consultation and diagnostic procedures
      "description": "D9910 D9911 D9930 D9942 D9943 D9950 D9951 D9952"
    }
  ]
}

// 50% coinsurance procedures
{
  "serviceTypeCodes": ["28"],
  "code": "A",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "W",
  "benefitPercent": "0.5",                 // Patient pays 50%
  "additionalInformation": [
    {
	// CDT codes for hospital/facility services and anesthesia
      "description": "D9938 D9939 D9940 D9944 D9945 D9946 D9947 D9948 D9949 D9954 D9955 D9956 D9957 D9959"
    }
  ]
}

Get help from eligibility experts

Sometimes, payers do return conflicting benefit entries. We've seen it. In other cases, descriptions aren’t clear about when a benefit applies.

If you need help, reach out. We offer real-time support and answer questions in minutes. Our experts have helped hundreds of teams interpret confusing eligibility responses.

Sep 11, 2025

Guide

Big takeaway: Use patient control numbers to track a claim from submission to remit.

A patient control number is a tracking ID for a claim.

You create a patient control number when you submit a claim. The payer sends the ID back in follow-up transactions: claim acknowledgments, Electronic Remittance Advice (ERAs), and claim status checks.

This guide gives you best practices for creating patient control numbers and where to find them in each transaction.

Patient control number locations

This table shows the location of patient control numbers across claim-related transactions in Stedi’s JSON API requests and responses.


837P, 837I, 837D claim submissions

277CA claim acknowledgments

835 ERAs

276/277 claim status checks

JSON API endpoint

Professional Claims JSON

Dental Claims JSON

Institutional Claims JSON

277CA Report

835 ERA Report

276/277 Real-Time Claim Status JSON

Location of the patient control number

Request:
claimInformation
└─patientControlNumber

Response:
claimReference
└─patientControlNumber

Response:
claimStatus
└─patientAccountNumber

or
claimStatus
└─referencedTransactionTraceNumber

Response:
claimPaymentInfo
└─patientControlNumber

Response:
claimStatus
└─patientAccountNumber

Best practices for creating patient control numbers

Here's what we found works best in practice:

  • Stick to 17 characters.
    X12 states patient control numbers can be up to 20 characters. But some payers cut off values longer than 17 characters in ERAs and claim acknowledgments.

  • Use a unique patient control number for each claim.
    If multiple claims have the same patient control number, you may match the claim to the wrong ERA or acknowledgment.

  • Use alphanumeric characters only.
    Patient control numbers can contain both letters and numbers. Avoid special characters. Many payers don’t handle them properly.

  • Use random strings.
    Predictable formats, like {patientInitials}-{DOS}, can create duplicates.

Our recommendation: Use nanoid or a similar library to create a strong, unique 17-character patient control number for each claim.

Claim submission

You set the patient control number when you submit the claim.

For JSON-based claims submission endpoints, pass the patient control number in the patientControlNumber field. For example, using the Professional Claims (837P) JSON endpoint:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/professionalclaims/v3/submission" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
  ...
  "claimInformation": {
    "patientControlNumber": "ABCDEF12345267890",  // Patient control number 
    "claimChargeAmount": "109.20",
    ...
  }
  ...
}'

Claim acknowledgments

A 277CA claim acknowledgment indicates whether the claim was accepted or rejected. Payers send claim acknowledgments to the clearinghouse that submitted the claim.

Listen for claim acknowledgments
Use a webhook or the Poll Transactions endpoint to listen for incoming 277 transactions. When a claim acknowledgment arrives, use the transaction ID to fetch the acknowledgment using Stedi’s Claims acknowledgment endpoint.

Retrieve claim acknowledgments
The claim’s claimStatus.patientAccountNumber and claimStatus.referencedTransactionTraceNumber fields contain the claim’s patient control number. You can use either of these fields in your application logic.

{
  "claims": [
    {
      "claimStatus": {
        "claimServiceBeginDate": "20250101",
        "claimServiceEndDate": "20250101",
        "clearinghouseTraceNumber": "01J1SNT1FQC8ABWD44MAMBDYKA",
        "patientAccountNumber": "ABCDEF12345267890", // Patient control number
        "referencedTransactionTraceNumber": "ABCDEF12345267890", // Patient control number
        ...
      },
      ...
    },
    ...
  ]
}

ERAs

An 835 ERA contains details about payments for a claim, including explanations for any adjustments or denials.

ERAs require transaction enrollment
Payers send ERAs to the provider's designated clearinghouse. The provider designates this clearinghouse through transaction enrollment. Providers can receive ERAs at one clearinghouse per payer.

Listen for ERAs
Use a webhook or the Poll Transactions endpoint to listen for incoming 835 transactions. When an ERA arrives, you can use the transaction ID to fetch the ERA using Stedi’s 835 ERA endpoint.

Retrieve ERAs
The endpoint’s response contains the patient control number in the claimPaymentInfo.patientControlNumber field.

{
  ...
  "transactions": [
    {
      ...
      "detailInfo": [
        {
          "assignedNumber": "1",
          "paymentInfo": [
            {
              "claimPaymentInfo": {
                "patientControlNumber": "ABCDEF12345267890", // Patient control number
                "patientResponsibilityAmount": "30",
                ...
              },
              ...
            },
            ...
          ]
        }
      ],
      ...
    }
  ]
}

Claim status checks

Unlike claim acknowledgments or ERAs, you run 276/277 claim status checks in real time.

Claim status requests
The Real-Time Claim Status JSON endpoint doesn’t accept a patient control number as a request parameter. Instead, you pass in information for the payer, provider, patient, and date of service. For detailed guidance, see our recommended base JSON request.

Claim status responses
If the payer has multiple claims on file that match the information you provided, the response may include multiple claims. Each claim’s claimStatus.patientAccountNumber field contains the claim’s patient control number.

{
  "claims": [
    {
      "claimStatus": {
        "patientAccountNumber": "ABCDEF12345267890",  // Patient control number
        "amountPaid": "95.55",
        ...
      },
      ...
    }
  ],
  ...
}

Other tracking IDs

Tracking service line items

This guide covers tracking at the claims level. However, you can also track service line items from a claim in claim acknowledgments and ERAs. For details, check our docs:

Payer claim control numbers

Don’t use payer claim control numbers for tracking.
Payers use payer claim control numbers to internally track claims. You can use them when talking to a payer, but they can’t easily be matched to a claim submission.

Payer claim control numbers are returned in the following fields:

X12 control numbers

If you’re using Stedi’s JSON APIs, you can safely ignore X12 control numbers.
X12 control numbers are used by Stedi and payers for general EDI file processing. If you’re using our JSON APIs, Stedi manages them for you. They’re not useful for tracking claims or individual transactions.

Process claims with Stedi

Stedi’s JSON Claims APIs are available on all paid Stedi developer plans.

To try it out, request a free trial. We get most teams up and running in less than a day.

Sep 10, 2025

Products

You can now submit 837I institutional claims as X12 EDI using the new Institutional Claims X12 API endpoint.

Most Stedi customers submit 837I claims using our JSON-based Institutional Claims endpoint. JSON is familiar and easy to work with. You can build an integration faster.

But if you already work with X12, converting to JSON wastes time. The new endpoint accepts X12 directly. 

Previously, X12 submissions of 837I claims required SFTP. With SFTP, you have to wait for files to get errors or validation. With the API, you get instant responses. Development is faster, and debugging is easier.

Use the Institutional Claims X12 endpoint

To use the endpoint, send a POST request to the https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/raw-x12-submission endpoint. Include an 837I X12 payload in the request body’s x12 field:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/raw-x12-submission" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "x12": "ISA*00*          *00*          *ZZ*001690149382   *ZZ*STEDITEST      *240630*0847*^*00501*000000001*0*T*>~GS*HC*001690149382*STEDITEST*20240630*084744*000000001*X*005010X223A2~ST*837*3456*005010X223A2~BHT*0019*00*0123*20061123*1023*CH~NM1*41*2*PREMIER BILLING SERVICE*****46*TGJ23~PER*IC*JERRY*TE*7176149999~NM1*40*2*AETNA*****46*AETNA~HL*1**20*1~NM1*85*2*TEST HOSPITAL*****XX*1234567890~N3*123 HOSPITAL ST~N4*NEW YORK*NY*10001~REF*EI*123456789~HL*2*1*22*1~SBR*P********CI~NM1*IL*1*DOE*JOHN****MI*123456789~NM1*PR*2*AETNA*****PI*AETNA~HL*3*2*23*0~PAT*19~NM1*QC*1*DOE*JOHN~N3*123 MAIN ST~N4*NEW YORK*NY*10001~DMG*D8*19800101*M~CLM*26403774*150***11>B>1*Y*A*Y*I~DTP*472*D8*20240101~REF*D9*17312345600006351~NM1*82*1*SMITH*JANE****XX*1234567890~PRV*AT*PXC*207Q00000X~LX*1~SV2*0450*HC>99213*150****1~DTP*472*D8*20240101~SE*29*3456~GE*1*000000001~IEA*1*000000001~"
  }'

Stedi validates the EDI and sends the claim to the payer.

The endpoint returns a response from Stedi in JSON. The JSON contains information about the claim you submitted and whether the submission was successful:

{
  "claimReference": {
    "correlationId": "01J1M588QT2TAV2N093GNJ998T",
    "formatVersion": "5010",
    "patientControlNumber": "26403774",
    "payerID": "AETNA",
    "rhclaimNumber": "01J1M588QT2TAV2N093GNJ998T",
    "serviceLines": [
      {
        "lineItemControlNumber": "1"
      }
    ],
    "timeOfResponse": "2024-07-10T22:05:32.203Z"
  },
  "controlNumber": "000000001",
  "httpStatusCode": "200 OK",
  "meta": {
    "traceId": "b727b8e7-1f00-4011-bc6e-e41444d406d8"
  },
  "payer": {
    "payerID": "AETNA",
    "payerName": "Aetna"
  },
  "status": "SUCCESS",
  "tradingPartnerServiceId": "AETNA"
}

For complete details, check out the Institutional Claims (837I) Raw X12 API reference.

Try it free

The Institutional Claims X12 endpoint is available for all paid Stedi developer accounts.

To try it out, request a free trial. Most teams are up and running in under a day.

Sep 8, 2025

Guide

Big takeaway: Most payers omit carveout benefits from eligibility responses, but many include the carveout administrator's information. Run a second eligibility check with the carveout admin for full benefits details.

Carveout benefits can leave gaps in your eligibility workflows.

For example, many Blue Cross Blue Shield (BCBS) plans carve out mental (behavioral) health benefits to Magellan, a mental health payer.

A BCBS eligibility check may confirm the patient has mental health coverage. But the response doesn’t contain details about mental health benefits. No co-pays, deductibles, or limitations – just basic information for Magellan.

To get the full benefit details, you need to run a separate check with Magellan. This guide shows you how and what to look for in the eligibility response.

What’s a carveout?

A carveout is when the primary payer for a plan lets another entity handle certain benefits.

Often, carveout administrators specialize in benefits for a particular service, such as mental health services or pharmacy benefits.

Carveouts in eligibility responses

Payers aren’t required to return carveout benefits in eligibility checks.
Most don't. If they do, Stedi passes along what the payer provides.

Many payers return the carveout admin’s information.
However, it’s not guaranteed. If you’re using Stedi’s JSON Eligibility API, the carveout admin’s information is typically included in a related benefitsInformation entry in the response. Look for:

  • code = U (Contact Following Entity for Eligibility or Benefit Information)
    OR
    code = 1 (Active coverage)

  • serviceTypeCodes containing a related Service Type Code (STC) 

  • benefitsRelatedEntities object containing contact information for the carveout admin.

  • If present, benefitsRelatedEntities.entityIdentificationValue contains the patient’s member ID for the carveout admin.

Also look for key phrases in additionalInformation.description. These may be in a separate benefitsInformation entry with code = D (Benefit Description).

For example:

{
  "benefitsInformation": [
    {
      "code": "U",	// Contact Following Entity for Eligibility or Benefit Information
      "serviceTypeCodes": [
        "MH"		// Mental Health
      ],
      ...
      "benefitsRelatedEntities": [
      {
        "entityIdentifier": "Third-Party Administrator",
        "entityType": "Non-Person Entity",
        "entityName": "Acme Health Payer",
        "entityIdentificationValue": "123456789", // Member ID for the carveout admin
        "contactInformation": {
          "contacts": [
            {
              "communicationMode": "Telephone",
              "communicationNumber": "1234567890"
            }
          ]
        }
      },
      ...
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["MH"],
      "additionalInformation": [
        {
          "description": "BEHAVIORAL HEALTH MANAGED SEPARATELY"
        }
      ]
    }
  ],
  ...
}

Tip: Don’t rely on benefitsRelatedEntities.entityIdentifier to identify carveout admins. The value can vary between payers.

The carveout benefits runbook

Payers don’t consistently return carveout admin information. But when they do, you follow these steps to get the full carveout benefits:

  1. Run an eligibility check for the primary payer.
    Use a related STC. For tips, see our STC testing docs.

  2. Look for the carveout admin’s information.
    Check for benefitsInformation entries with a related STC in serviceTypeCodes and a benefitsRelatedEntities section.

    The benefitsRelatedEntities.entityName field will contain the carveout admin’s name. If present, benefitsRelatedEntities.entityIdentificationValue contains the patient’s member ID for the carveout admin. See the above example.

  3. Get the carveout admin’s ID.
    Use Stedi's Payer Search API or Payer Network to get the payer ID for the carveout admin’s name.

  4. Run an eligibility check for the carveout admin.
    Use the patient’s member ID for the carveout admin. If you use the right STC, many carveout admins will return the missing carveout benefits.

    The STC may differ from the primary payer. See our STC testing docs for tips.

Other ways to get carveout benefit details

If checks alone can’t get you the carveout benefits you need, try one of these methods:

Check the member ID card.
The back often lists information for carveout benefits and payers. The card may provide enough information on its own. If not, it may give you enough to run an eligibility check for the carveout admin.

Make a telephone call to the primary payer.
Use an AI voice agent to do this programmatically.

Check the primary payer’s website or portal.
Some payers post plan coverage documents with carveout details on their public website. Others may require you to log in to their portal. For programmatic access, create a scraper or use a scraping vendor.

Claims for carveout benefits

Claims for carveout benefits are often a form of crossover claim.

Submit the claim to the primary payer first. If the primary payer supports crossover, they’ll automatically forward the claim to the carveout admin. If not, you’ll need to submit a separate claim directly to the carveout admin.

You may need to complete a separate transaction enrollment for the carveout admin. For more guidance, see our crossover claims docs.

Carveouts vs. secondary or tertiary insurance

Carveouts are different from secondary or tertiary insurance:

  • Carveouts are part of a single health plan.
    Secondary and tertiary insurance is when a patient has multiple, separate health plans.

  • Carveouts don’t show up in coordination of benefits (COB) checks.
    COB checks are intended for cases where a patient has multiple health plans.

  • You can have both carveout benefits and a secondary (or tertiary) plan.
    After the primary payer or carveout admin adjudicates a claim for carveout benefits, you can submit a claim for the remaining balance to the secondary health plan. Sometimes, the primary payer will automatically forward the claim to the secondary payer.

    If you’re unsure which plan is the primary or secondary, use a COB check to find out.

Get expert support

Carveouts – and how they show up in eligibility responses – vary widely by payer and plan. Knowledgeable support can make a difference.

Stedi offers real-time support from experts over Slack or Teams. Request a free trial and try it out.

Sep 4, 2025

Guide

Big takeaway: For Medicare Advantage plans, send eligibility checks to the commercial payer, not CMS.

Verification and billing for Medicare Advantage plans can be confusing. It’s easy to send a request to the wrong payer or miss an important detail.

This guide aims to make it simple. It covers how to spot Medicare Advantage plans, where to send eligibility checks, and how to submit claims.

What is Medicare Advantage?

A Medicare Advantage plan – also called Medicare Part C – is a health plan from a private payer that’s approved by Medicare. It serves as an alternative to Original Medicare. 

Medicare Advantage plans cover benefits included in Medicare Part A (hospital benefits) and Part B (medical benefits). Along with those benefits, Medicare Advantage plans often provide extra coverage like prescription drugs, vision, dental, and hearing. 

Eligibility checks for Medicare Advantage plans

When running eligibility checks for Medicare Advantage plans:

  • Send the check to the plan’s commercial payer.
    Don’t send the check to the National Centers for Medicare & Medicaid Services (CMS) – the government payer for Medicare.

  • Check the payer ID.
    Some payers have separate payer IDs for Medicare Advantage plans and other lines of business, like employer-sponsored plans. For example, CareFirst Medicare Advantage vs CareFirst Blue Cross Blue Shield Maryland.

    Use the payer ID for the Medicare Advantage payer. You can get the ID using the Stedi Payer Network or Payers API.

  • Transaction enrollment isn’t typically required.
    Most Medicare Advantage payers don’t require transaction enrollment for eligibility checks. You can check for transaction enrollment requirements using the Stedi Payer Network or Payers API.

  • Use the commercial plan’s member ID – if required.
    Many Medicare Advantage plans allow eligibility checks with just the patient’s first name, last name, and date of birth.

    If a member ID is required, use the commercial plan’s member ID, not the patient’s Medicare Beneficiary Identifier (MBI) used for Medicare.

How to spot a Medicare Advantage plan in an eligibility response

Providers often want to know if a plan is a Medicare Advantage plan. These plans often have different prior authorization requirements and reimbursement rates than traditional Medicare.

To spot a Medicare Advantage plan in a commercial payer’s eligibility response, look for either of the following indicators in the JSON Eligibility API's response:

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)

  • planInformation.hicNumber or benefitsInformation.benefitsAdditionalInformation.hicNumber, which contains the patient’s MBI.

{
  "benefitsInformation": [
    {
      "code": "1",                 // Active Coverage
      "serviceTypeCodes": ["30"],  // Health Benefit Plan Coverage
      "insuranceTypeCode": "MA",   // Medicare Part A
      ...
      "benefitsAdditionalInformation": {
        "hicNumber": "1AA2CC3DD45" // Patient's MBI
      },
      ...
    },
    ...
  ],
  ...
}

What is a HIC number?
The hicNumber property name refers to a Health Insurance Claim (HIC) Number, the old member ID system for Medicare. HIC numbers were usually a Social Security Number plus a letter, such as 123-45-6789A.

CMS now uses MBIs for member IDs – and that’s usually what fills these properties when present. MBIs are 11 characters: numbers and capital letters, no spaces. For MBI formatting rules, see CMS’s Understanding the MBI doc.

Medicare Advantage plans in CMS eligibility responses

Sometimes, a patient may be confused about whether they’re covered by Original Medicare or Medicare Advantage.

You may run an eligibility check with CMS (the Medicare government payer) or an MBI lookup (which returns a CMS eligibility response on a match), only to discover through the response that the patient actually has Medicare Advantage coverage.

If the patient has Medicare Advantage, the eligibility response from CMS will include a benefitsInformation object with the following:

  • code = U (Contact Following Entity for Eligibility or Benefit Information)

  • serviceTypeCodes = 30 (Health Benefit Plan Coverage)
    OR
    serviceTypeCodes = 30 AND CQ (Case Management

  • insuranceTypeCode = HM (HMO), HN (HMO - Medicare Risk), IN (Indemnity), PR (PPO), or PS (POS)

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Primary Payer

The name of the Medicare Advantage payer is usually in the object’s benefitsRelatedEntities.entityName property. For example:

{
  "benefitsInformation": [
    {
      "code": "U",	// Contact Following Entity for Eligibility or Benefit Information
      "serviceTypeCodes": ["30"],	 // Health Benefit Plan Coverage
      "insuranceTypeCode": "HM",	 // HMO
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Primary Payer",
          "entityName": "BLUE CROSS MEDICARE ADVANTAGE",
          ...
        }
      ],
      ...
    },
    ...
  ],
  ...
}

Note: Don’t use CMS eligibility checks to verify Medicare Advantage coverage. CMS prohibits this.

Coordination of benefits (COB) checks for Medicare Advantage

Medicare Advantage patients often have supplemental insurance or employer-sponsored coverage that could be primary or secondary.

A COB check can help you determine the correct billing order. Most Medicare Advantage plans are supported.

Medicare vs. Medigap

Although Medicare Advantage patients often have supplemental insurance, they won’t have Medigap.

Medigap – also called Medicare Supplement Insurance – is supplemental insurance that helps pay out-of-pocket costs (like deductibles and coinsurance) for people with Original Medicare. 

  • Medigap is completely separate from Medicare Advantage.

  • You can’t have Medigap and Medicare Advantage at the same time.
    Payers can’t sell someone Medigap if they’re on a Medicare Advantage plan.

  • Medigap only works with Original Medicare, not Medicare Advantage

Medicare Advantage claims

Submit claims for Medicare Advantage plans to the commercial payer – just as you would for any commercial health plan. Don’t send Medicare Advantage claims to CMS.

Get started

You can run eligibility checks, MBI lookups, and COB checks – and submit claims – using any paid Stedi developer plan.

If you don't have a paid developer plan, request a free trial. We get most teams up and running in under a day.

Sep 3, 2025

Spotlight

Federico Ruiz @ Puppeteer AI

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Federico Ruiz, Founder and CEO of Puppeteer AI.

What does Puppeteer do?

Puppeteer builds AI agents that handle the patient work that clogs your day: calls, follow-ups, symptom checks, scheduling, and routine questions, so clinicians can focus on care. Our agents hold natural phone conversations, ask clinically relevant questions, summarize to the chart, and keep checking in after the visit. Think continuous, proactive navigation of each patient’s journey, with humans in the loop for anything sensitive or complex. The outcome is less admin, fewer missed appointments, and more capacity without more headcount.

How did you end up working in health tech?

I was working at Meta at the time, but my head was already in the space of building something of my own. I had just launched LangAI, and I was really interested in how agents could evolve beyond the narrow use cases we were seeing then. Back in those days, “agents” weren’t a mainstream concept – it still felt like an open frontier.

I stayed close with Alan and the founders of Light-it, and through those conversations, the opportunity came up to work on a project together. Their background in healthcare and my focus on agents fit naturally, and that collaboration turned into the starting point for Puppeteer, a company built around the idea that agents could actually support clinicians and patients in meaningful ways.

So the path wasn’t a grand plan. It was Meta giving me exposure to big-scale problems, LangAI showing me the potential of agents, and trusted friendships that created the right conditions to start something new in healthcare.

How does your role intersect with revenue cycle management (RCM)?

When you think about RCM, a lot of the problems trace back to the same operational gaps: missed appointments, incomplete paperwork, delays in follow-up, or patients not having the right information at the right time. My role is to make sure our agents close those gaps.

For example, if someone misses an appointment, the agent can call right away to reschedule. If benefits need to be confirmed, the agent can collect and structure that information before the visit. And in value-based programs, our agents can keep nudging patients on adherence and preventive care, which not only improves outcomes but also makes sure organizations meet their quality metrics.

So my intersection with RCM isn’t on the billing side, it’s on the upstream side: making sure the right things happen with patients, consistently, so that revenue capture becomes a natural outcome of smoother operations.

What do you think RCM will look like two years from now?

I think we’ll see a shift from RCM being a back-office function to something that’s much closer to the point of care. Conversations with patients, whether over the phone, in the waiting room, or through an AI agent, will generate structured data that flows directly into eligibility checks, claims, and follow-ups. The gap between “talking to a patient” and “having everything ready for billing” will keep getting smaller.

At the same time, value-based care is going to push revenue away from just transactions and more toward outcomes. That means systems will need to stay connected to patients long after the visit, reminding them about meds, nudging them to book labs, checking in on recovery. The financial side will depend on how well organizations can keep patients engaged and adherent, not just how fast they code a claim.

In short, RCM will still be about dollars, but it will feel less like accounting and more like care continuity, because that’s where the revenue will come from.

Sep 3, 2025

Guide

You can turn payer names into payer IDs (or the reverse) in Google Sheets using the Search Payers API endpoint. This guide tells you how.

Tip: If you just want a full list of Stedi’s payers in a spreadsheet-friendly format, you can download the latest payer CSV from the Stedi Payer Network or use the List Payers CSV endpoint.

Requirements

To use the Search Payers API endpoint, you need a paid Stedi developer account. To try it out free, request a trial.

Step 1. Create a production API key

  1. Log in to Stedi.

  2. Click your account name at the top right of the screen.

  3. Select API Keys.

  4. Click Generate new API Key.

  5. Enter a name for your API key.

  6. Select Production mode.

  7. Click Generate. Stedi generates an API key and allows you to copy it.

If you lose the API key, delete it and create a new one.

Step 2. Open Google Sheets

In your Google Sheet spreadsheet, place your payer names or payer IDs in column A. You can mix them if desired. For example:



How this may look in your spreadsheet:

Google Sheets example

Step 3. Create the script

  1. In the Google Sheets menu, click Extensions > Apps Script.

  2. Delete what’s in the editor.

  3. Paste in the following code. Replace YOUR_STEDI_API_KEY with your Stedi API key.

    /**
     * Returns the payer name and primary payer ID from Stedi's Payer Search API.
     * Usage: =STEDI_PAYER_INFO(A2)
     * Output: [[payer name, primary payer ID]]
     */
    function STEDI_PAYER_INFO(query) {
      if (!query) return [["", ""]];
      const apiKey = "YOUR_STEDI_API_KEY"; // Replace with your Stedi API key
      const encodedQuery = encodeURIComponent(query);
      const url = "https://healthcare.us.stedi.com/2024-04-01/payers/search?query=" + encodedQuery;
      const options = {
        "method": "get",
        "headers": { "Authorization": apiKey },
        "muteHttpExceptions": true
      };
      try {
        const response = UrlFetchApp.fetch(url, options);
        const status = response.getResponseCode();
        if (status === 403) {
          return [["Error: Invalid or missing API key", ""]];
        }
        const result = JSON.parse(response.getContentText());
        if (
          result.items &&
          result.items.length > 0 &&
          result.items[0].payer &&
          result.items[0].payer.displayName &&
          result.items[0].payer.primaryPayerId
        ) {
          return [[
            result.items[0].payer.displayName,
            result.items[0].payer.primaryPayerId
          ]];
        } else {
          return [["No match found", ""]];
        }
      } catch (e) {
        return [["Error: " + e.message, ""]];
      }
    }
  4. Click the Save project to Drive icon or press Ctrl + S.

The script creates an =STEDI_PAYER_INFO() formula that outputs the payer name and primary payer ID from the Search Payers API endpoint.

Step 4. Use the Google Sheets formula

In your Google Sheets spreadsheet, use the =STEDI_PAYER_INFO() formula to get the payer name and primary payer ID for each value in column A. For example:

Google Sheets example

The search supports fuzzy matching. The formula returns the closest match available.

Note: The payer names returned by the Search Payers API endpoint are intended for routing transactions – not for display in patient-facing UIs. If you need a patient-facing name, build your own list and map the names to Stedi payer IDs.

Get started

The Payers API is available on Stedi’s paid developer plan.

To try it out, request a free trial. Most teams are up and running in less than a day.

Sep 2, 2025

Products

Today, we’re announcing Stedi integrated accounts and Stedi apps – a new set of capabilities that allows end customers of Stedi’s Platform Partners to have Stedi accounts of their own.

Integrated accounts drastically reduce the amount of clearinghouse functionality that partners need to build themselves.

Instead of partners having to replicate Stedi’s user interfaces within their own applications, providers can use their own dedicated Stedi accounts that are pre-integrated into partner platforms, such as Revenue Cycle Management (RCM) systems, Practice Management Systems (PMS), and Electronic Healthcare Record (EHR) systems. 

Making integrations easier

Platform Partners have used Stedi’s clearinghouse APIs to incorporate healthcare claims and eligibility functionality into their own systems. To build this functionality, a partner typically maintains a single Stedi account that powers all of the transactions for their provider customers. 

For example, an RCM system would submit claims to a single Stedi account using Stedi’s Claims API or SFTP, and would create transaction enrollment requests for each provider’s required ERA enrollments. In other words, the RCM system would use a single Stedi account in a multitenant fashion – that is, with multiple end provider customers (such as an independent practitioner, group practice, or health system) operating within a single Stedi account.

This multitenant pattern has meant that each Stedi partner needed to replicate much of Stedi’s functionality within their own platform, since providers needed the ability to perform all necessary actions within the partner’s system. For example:

  • Any partner that offered functionality related to ERAs needed to build out a complex set of user interfaces and email notifications for transaction enrollment.

  • Any partner that offered the ability to submit claims needed to also have the ability to manually modify and resubmit claims that are rejected or denied.

  • Any partner that offered eligibility checks needed to build out functionality for troubleshooting failed attempts and functionality for batch uploads. These partners were also on the hook for support, even if Stedi was better positioned to help fix the issue.

This functionality is complex and time-consuming to build, and each time Stedi launched a new feature like delegated signature authority or the Stedi Agent, it meant that our partners needed to dedicate engineering, product, and design resources to incorporate Stedi’s latest functionality. 

For these reasons, the number one feature request we’ve heard from our partners is that they want to allow their providers to have Stedi accounts of their own. This way, providers can use Stedi’s functionality directly, rather than partners needing to replicate it.

Integrated Stedi accounts give providers that access. Stedi apps connect integrated accounts to one or more partner platforms.

Integrated accounts

Integrated accounts are simplified Stedi accounts that are designed to be friendly to non-technical users. The more complex developer-focused functionality – such as configuring API access and webhooks, and viewing raw JSON payloads – has been removed. 

The interfaces for common tasks such as running and troubleshooting eligibility checks, modifying and resubmitting claims, and managing transaction enrollments have been streamlined, so platforms can direct providers to their Stedi accounts for this functionality rather than building and maintaining UIs for those workflows themselves. Integrated accounts also include other standard Stedi account features, including member access, role-based access control (RBAC), and multi-factor authentication (MFA).

Create an integrated account

Providers can create integrated accounts using a self-service signup flow:

  1. Create a Stedi sandbox account.

  2. In the sandbox, click Upgrade

  3. Select Integrated Account and follow the prompts.

Stedi apps

Integrated accounts can install Stedi apps, which are Stedi’s pre-built integrations to a growing list of third-party RCM, PMS, EHR, and other platforms.

Stedi apps directory

Stedi apps allow providers to quickly connect their Stedi account to these external systems using preconfigured SFTP credentials and API keys.

They can also grant Stedi portal access for external support and implementation teams to assist with setup and ongoing support.

Aug 28, 2025

Guide

Transaction enrollment registers a provider to exchange specific transaction types with a payer. For Electronic Remittance Advice (ERAs), enrollment is always required. For other types of transactions, enrollment requirements depend on the payer.

Most traditional clearinghouses treat transaction enrollment as a cost center. Because ERA revenue is small, they try to minimize their involvement and put the work on you. You end up filling out PDFs, chasing signatures, and checking portal statuses just to onboard a provider.

Many Stedi customers manage transaction enrollment for hundreds or thousands of providers. At that scale, enrollment can be an operational burden. One team told us that before switching to Stedi, their staff spent 25–30% of their time just managing enrollment requests.

At Stedi, we treat transaction enrollment as a core part of our product experience. We offer fully managed, API-based transaction enrollment designed to reduce operational overhead, eliminate manual steps, and improve visibility. So you can scale provider onboarding without scaling manual work.

This guide explains how transaction enrollment works at Stedi, how we designed it, and how to manage enrollments at scale. It incorporates practices we’ve seen high-scaling teams use to streamline provider onboarding.

Types of enrollment

Transaction enrollment is just one part of the broader enrollment process. It’s the final step – and the only one that involves working with your clearinghouse. Providers typically complete three types of enrollment to work with a payer:

  • Credentialing – Verifies licenses, training, and qualifications.

  • Payer enrollment – Registers the provider with specific insurance plans. This is typically when providers set rates with a payer.

  • Transaction enrollment – Enables the provider to exchange transactions through a clearinghouse.

This guide focuses only on transaction enrollment. For help with credentialing or payer enrollment, contact the payer or a credentialing service, like CAQH, Assured, or Medallion.

Transaction enrollment as a product

At Stedi, we take on as much of the transaction enrollment process as possible and work constantly to automate the rest. If a manual step is required, it’s built into the product with clear next steps and full visibility.

Our goal is to eliminate back-and-forth. You shouldn’t need to track spreadsheets or email threads to onboard a provider.

Key parts of this approach include:

  • API-first design – Submit and track enrollment requests programmatically using the Enrollments API. Built for automation at scale, with full visibility into status and next steps. We also support UI and bulk CSV upload.

  • One-click enrollment – For payers that support one-click enrollment, just submitting an enrollment request is enough. You don’t need to take any additional steps. One-click enrollment is available for 850+ payers. You can check support using the Payers API or the Payer Network.

  • Delegated signature authority – For payers that still require signed forms, you can authorize Stedi to sign on your behalf. It’s a one-time setup that can eliminate 90% of your enrollment paperwork.

  • Streamlined timelines – Enrollment timelines vary by payer, but most enrollments through Stedi complete in 24-48 hours.

When enrollment is required

Before exchanging transactions with a payer, check whether enrollment is required for the transaction type. Common requirement patterns:

  • 835 ERAs. Always require enrollment. Payers can only send a provider’s ERAs to one clearinghouse at a time, so they need to know where to route them.

  • 270/271 eligibility checks. A few payers, including Medicare, require enrollment. Most major payers don’t.

  • 837P/837D/837I claim submissions. Most major payers don’t require enrollment, but certain payers do – TRICARE West Region and TRICARE East are two examples that do.

Use the Stedi Payer Network or Payers API to see which payers require enrollment by transaction type.

For example, the following JSON payer record from the Payers API indicates enrollment is required for 835 ERAs (claimPayment):

{
  "displayName": "Blue Cross Blue Shield of North Carolina",
  "primaryPayerId": "BCSNC",
  ...
  "transactionSupport": {
    "claimPayment": "ENROLLMENT_REQUIRED",
   ...
  }
}

How to submit an enrollment request

If a transaction type requires enrollment, Stedi’s Enrollments API lets you automate related requests at scale. Here’s how it works:

Step 1. Create a provider record.
Use the Create Provider endpoint to submit the provider’s basic information:

  • Name

  • NPI

  • Tax ID – Either an Employer Identification Number (EIN) or Social Security Number (SSN), depending on whether the provider is a corporate entity or not.

  • Contacts – Information for one or more provider contacts.

    Ensure this contact information – excluding email and phone number – matches what the payer has on record for the provider. Some payers reject enrollment requests if the name or address doesn’t match what’s already on file.

    The payer may use the provided email and phone to reach out with enrollment steps. If you’re a vendor representing a provider, you can use your own email and phone to have the payer contact you instead.

    When creating an enrollment request in Stedi, you must select one of these contacts as the primary contact.

curl --request POST \
  --url "https://enrollments.us.stedi.com/2024-09-01/providers" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Bob Dental Inc",
    "npi": "1999999992",
    "taxIdType": "EIN",
    "taxId": "555123456",
    "contacts": [
      {
        "firstName": "Bob",
        "lastName": "Dentist",
        "email": "bob@example.com",
        "phone": "555-123-2135",
        "streetAddress1": "123 Some Street",
        "city": "Chevy Chase",
        "zipCode": "20814",
        "state": "MD"
      },
    ]
  }'

The request returns an id for the provider. You can use this ID to reference the provider record across multiple enrollment requests.

{
  ...
  "id": "10334e76-f073-4b5d-8984-81d8e5107857",
  "name": "Bob Dental Inc",
  "npi": "1999999992",
  ...
}

Step 2. Submit the enrollment request.
Use the Create Enrollment endpoint to submit the actual transaction enrollment request.

In the request, specify:

  • The provider ID, returned by the Create Provider endpoint or fetched using the List Providers endpoint.

  • The payer ID. You can get this using the Payers API.

  • The transaction types (for example,  claimPayment, eligibilityCheck) you want to enroll the provider for.

  • A primary contact – One of the contacts from the provider record.

  • A userEmail address. Stedi will send notifications for enrollment status updates to this email address. If you’re a vendor representing a provider, you can use your own email address here.

  • A status for the request. Set the status to DRAFT if you plan to work on the request later or want to wait to submit the request. Otherwise, set the status to SUBMITTED to submit the request.

curl --request POST \
  --url "https://enrollments.us.stedi.com/2024-09-01/enrollments" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": {
      "id": "db6665c5-7b97-4af9-8c68-a00a336c2998"
    },
    "payer": {
      "idOrAlias": "87726"
    },
    "transactions": {
      "claimPayment": {
        "enroll": true
      }
    },
    "primaryContact": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "test@example.com",
      "phone": "555-123-4567",
      "streetAddress1": "123 Some Str.",
      "city": "A City",
      "state": "MD",
      "zipCode": "20814"
    },
    "userEmail": "test@example.com",
    "status": "SUBMITTED"
  }'

Step 3. Track enrollment status
Each enrollment request moves through a defined set of statuses.

Transaction enrollment statuses

Status

What it means

DRAFT

The request hasn’t been submitted yet. You can still make changes.

SUBMITTED

The request is in our queue. We’re reviewing it and preparing to send it to the payer.

PROVISIONING

We’ve sent the request to the payer and are actively managing follow-up steps.

LIVE

The enrollment is complete. The provider can now exchange the specified transactions.

REJECTED

The payer rejected the request. We’ll include a reason and next steps.

CANCELED

The request was canceled before it was processed (only allowed in DRAFT or SUBMITTED states).

You can track the status of enrollment requests using the List Enrollments endpoint or the Stedi portal.

The endpoint lets you pull the status of every request and filter by status or transaction type. This lets you build custom views into your own systems or dashboards. For example:

curl --request GET \
  --url "https://enrollments.us.stedi.com/2024-09-01/enrollments?status=LIVE" \
  --header "Authorization: <api_key>"

You’ll also get email notifications whenever an enrollment status changes, like when it moves from PROVISIONING to LIVE, or if it's rejected. If an enrollment requires action on your part, we’ll reach out to you using Slack, Teams, or email with clear next steps.

Get started

Fully managed transaction enrollment is available on all paid Stedi plans.

If you’re not yet a customer, request a free trial. We get most teams up and running in under a day.

Aug 28, 2025

Spotlight

Cassandra Bahre @ Included Health

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Cassandra Bahre, Product Manager for Virtual Visit Revenue at Included Health.

What does Included Health do?

Included Health delivers personalized all-in-one healthcare to support members holistically – mind, body, and wallet. We offer a wide range of services, from care navigation to virtual care, including urgent care, primary care, and behavioral health to claims and member advocacy.

How did you end up working in health tech?

After several years in product consulting across numerous industries, I was personally struck by how insurance and benefits complexity directly influence the patient experience. I learned this firsthand when my child was diagnosed with cancer shortly after her first birthday (today she is cancer-free!). I faced numerous billing issues over the subsequent years, from a prior authorization being denied as not medically necessary to serious overbilling by the practice because prepaid funds weren't correctly applied to the claim balance.

My knowledge of revenue cycle management (RCM) granted me the ability to advocate for myself, but the billing process was still a source of unnecessary trauma during my family’s journey. My personal mission is to help others understand and navigate the healthcare system so they can advocate for themselves, too. Health tech is the perfect place to combine my skills and my goal!

How does your role intersect with revenue cycle management (RCM)?

As the Product Manager for Virtual Visit Revenue at Included Health, I'm the voice of our members and our RCM Operations team in our virtual care business.

For our members, I focus on the billing experience, advocating for cost transparency and reliable, secure payment processing. My goal is to help them focus on getting care without the stress of managing the financial side of their healthcare experience.

For our RCM Ops team, I support revenue realization for virtual visits. I collaborate with cross-functional teams across the entire revenue cycle – from eligibility and registration to coding, claims submission, denial management, and payment posting. By optimizing for an efficient claims experience, we reduce administrative overhead on the member, instilling trust that they can rely on us to effectively manage their benefits. 

When my team and I are successful, we provide high-quality, cost-transparent care to our members and recognize revenue for the business in ways that are scalable and sustainable for our operational and care delivery teams. 

What do you think RCM will look like two years from now?

I believe RCM operations are set for a deep evolution over the next couple of years. We're already seeing a rapid rise in AI usage to automate repetitive, manual tasks like data entry, eligibility verification, and payment posting. And AI is being used on the payer side by claims departments to review, approve, and deny claims, prior authorizations, and more.

This shift will allow RCM teams to focus on more complex, high-value work that requires critical thinking, such as managing complex denial appeals and analyzing data to find revenue leakages. The future of RCM will be a hybrid model where humans and AI work together, with humans providing the crucial oversight and strategic direction.

This shift will also require a renewed focus on data integrity for Health Tech teams. Healthcare data can no longer be managed in silos; it needs to be clean, accurate, and accessible across the entire revenue cycle to build effective AI agents and provide patients with a more efficient and transparent billing experience.

Lastly, I think members – as healthcare consumers – will increasingly expect cost transparency and accuracy for the care they receive. Included Health aims to make that the standard, and that’s always my north star.

Aug 27, 2025

Spotlight

Spotlight: Bruno Ferrari @ Light-it

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Bruno Ferrari, Head of Innovation at Light-it.

What does Light-it do?

Light-it is a digital product studio specialized in healthcare. We help healthcare organizations ideate, design, develop, and launch custom software solutions, which range from patient-facing apps all the way to complex internal systems. Our focus is on combining product strategy, technical expertise, and deep knowledge of the healthcare ecosystem to create solutions that are innovative, compliant, and impactful.

How did you end up working in health tech?

I’ve always been passionate about building products that make a real difference – not just nice-to-have tools but solutions that truly impact people’s lives. When I joined Light-it, I quickly realized how uniquely complex and rewarding the healthcare industry is. Unlike many other sectors, the work you do here has a direct influence on people’s well-being, outcomes, and even quality of life. That combination of high stakes and high potential drew me in.

Healthcare comes with its own set of challenges: regulatory requirements, fragmented systems, and a huge diversity of stakeholders. But I’ve always seen those challenges as opportunities for innovation. Technology, when designed right, can unlock new ways of delivering care.

Today, I lead our Innovation and AI initiatives at Light-it. My focus is on exploring how cutting-edge technologies can be applied responsibly to solve some of the industry’s biggest problems. From reducing administrative overhead and clinician burnout to improving clinical documentation and enabling smarter use of data, I’m constantly looking for ways to push the boundaries of what’s possible while keeping compliance and patient trust at the center.

For me, health tech is the perfect intersection of purpose and innovation: you get to experiment with the future of technology, but you also know that every advancement has the potential to make healthcare more humane, efficient, and accessible.

What’s one thing you wish you could change about U.S. healthcare?

I’d love to see more interoperability and the adoption of open, flexible systems across U.S. healthcare. Today, too many providers and platforms operate in silos, which not only leads to inefficiencies but also creates a fragmented, frustrating experience for both patients and clinicians. Every time data is locked within a single system, valuable context is lost, whether it’s a physician missing part of a patient’s history or patients having to repeatedly provide the same information.

If health data could flow more seamlessly and securely across the ecosystem, it would unlock enormous benefits: better coordination of care, reduced administrative costs, improved clinical decision-making, and ultimately healthier, more empowered patients. True interoperability would also accelerate innovation, giving startups and health systems the ability to build on shared infrastructure rather than having to reinvent the wheel each time.

At the core, healthcare should put patients first, and that means enabling them and their providers to access the right information at the right time without unnecessary barriers.

What do you think U.S. healthcare will look like two years from now?

Two years from now, I think U.S. healthcare will look very different. We’ll see a major shift toward AI-enabled workflows and automation. Especially in administrative and clinical documentation, freeing clinicians from routine tasks so they can focus more on patients. Healthcare organizations will increasingly adopt tools that not only reduce the burden on providers but also improve patient engagement and unlock richer, more actionable insights from data. At the same time, trust, safety, and compliance will remain non-negotiable, meaning the solutions that thrive will be those that strike the right balance between innovation and responsibility.

AI is an incredibly powerful force, but its impact depends on how humans guide, test, and validate it before it touches patients' lives. At Light-it, we don’t just watch these trends; we anticipate them. Through our Innovation Lab, we dedicate a team fully focused on exploring, testing, and validating emerging technologies. We run experiments, build proofs of concept, and pressure-test new models so that when clients face new challenges, we’re already a step ahead with proven solutions.

At the end of the day, healthcare is about people. Technology should never replace the human connection; it should empower it. That’s the future we’re working towards: a healthcare system where innovation makes care more human, not less.

Aug 27, 2025

Guide

If you want to get benefits data, eligibility checks are usually the best place to start. They’re fast, accurate, inexpensive, and easy to automate. In most cases, eligibility responses give you everything you need.

But not always. While there is some required data, payers mostly decide what benefits to include in their eligibility responses. Benefits for some services – like medical nutrition therapy – might be hidden in free-text fields or missing entirely. If the payer leaves information you need out, it can create a gap in your workflow.

Stedi has worked with teams who have filled these gaps – for medical nutritional therapy and other services –  to keep their workflows running.

This guide covers how you can follow their practices to get the most out of eligibility responses and what to do when you need more information. It uses medical nutrition therapy benefits as a running example.

Eligibility responses

Eligibility responses organize benefits by Service Type Codes (STCs), which group services into broad categories like "office visit" or "surgery." You can get the full STC list in our docs.

If you use Stedi’s JSON-based Eligibility API, these benefit details are in the benefitsInformation object array. Each object in the array includes a serviceTypeCodes field with the related STC:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["30"],         // General medical
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["30"],
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    }
  ],
  ...
}

For more information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

Why gaps can happen

Though not a one-to-one mapping, the STC you send in the eligibility request determines the benefits you get back in the response.

The problem is that X12 5010 – the X12 version mandated by HIPAA –  doesn’t have a specific STC for medical nutrition therapy or some other healthcare services.

If a payer includes benefits for these services in their eligibility responses, it’s usually under a generic STC like 30 (Health Benefit Plan Coverage) or 98 (Professional Physician Visit - Office), often as a free-text description.

These descriptions appear in the additionalInformation.description field of benefitsInformation objects. Look for them with a code of 1 (Active Coverage), D (Benefit Description), or F (Limitations), but they can also show up in other entries too.

Example: Co-pay with free-text note
For example, the following entries relate to a generic STC – BZ (Physician Visit - Office: Well). The additionalInformation.description values indicate they relate to nutritional therapy.

{
  "code": "1",                               // Active coverage
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "inPlanNetworkIndicatorCode": "W",         // Both in and out-of-network
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
  ]
},
{
  "code": "B",                               // Co-pay
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "benefitAmount": "20",                     // $20 co-pay amount
  "timeQualifierCode": "27",                 // Per visit
  "coverageLevelCode": "IND",                // Individual coverage
  "inPlanNetworkIndicatorCode": "Y",         // In-network benefits
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
    }
  ]
},
{
  "code": "B",                               // Co-pay
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "benefitAmount": "30",                     // $30 co-pay
  "timeQualifierCode": "27",                 // Per visit
  "coverageLevelCode": "IND",                // Individual coverage
  "inPlanNetworkIndicatorCode": "N",         // Out-of-network benefits
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
    }
  ]
}

Example: Service limitations using codes
The following example is a bit more complex. One entry (code = D) defines abbreviations like AQ and 086 for Nutritionist. Another entry (code = A) uses these codes to show that nutritionists and other services are excluded from co-insurance for emergency hospital coverage.

{
  "code": "D", // Benefit description. Contains details or notes about the coverage.
  "additionalInformation": [
    {
      // List of abbreviations and their definitions for various codes.
      "description": "BENEFIT ABBREVIATIONS - B0 = NAPRAPATH - IL1, BY = NAPRAPATH GRP - IL1, D0 = VIRTUAL VISITS VENDOR - IL1, D7 = TELEMEDICINE - IL1, MT1, NM1, OK1, TX1, 097 = NAPRAPATH - IL1, 0I = +NON-PLN FOREIGN CLMS - IL1, MT1, NM1, TX1,"
    },
    ...
    {
      // More abbreviations and definitions
      // Note `AQ` is used for nutritionist.
      "description": "AC = AMBULANCE SERV - IL1, MT1, TX1, AG = OPTICIAN (IND) - IL1, MT1, AH = HEARING AID SUPPLIER - IL1, MT1, AM = PHARMACY - IL1, AQ = NUTRITIONIST - IL1, MT1, AX = SKILLED NURSE GRP - IL1, MT1, BG = OPTICIAN GRP - IL1, MT1, 071 = AMBULANCE SERVICES - IL1, MT1, TX1,"
    },
    {
      // More abbreviations and definitions.
      // Note `086` is also used for nutritionist.
      "description": "076 = HEARING AID & SUPPLIES - IL1, MT1, 080 = REGISTERED NURSE (RN) - IL1, 081 = LICENSED PRACTICAL NURSE (LPN) - IL1, OK1, 086 = NUTRITIONIST - IL1, MT1, 094 = PHARMACY - IL1"
    }
  ]
},
{
  "code": "A",                         // Co-insurance
  "serviceTypeCodes": [
     "51",                             // Hospital - Emergency Accident
     "52"                              // Hospital - Emergency Medical
   ],    
  "benefitPercent": "0.2",             // 20% co-insurance
  "timeQualifierCode": "23",           // Calendar year
  "coverageLevelCode": "IND",          // Individual coverage
  "inPlanNetworkIndicatorCode": "N",   // Out-of-network benefits
  "additionalInformation": [
    ...
    {
      // These provider specialties are NOT covered under this benefit.
      // The list includes `086` (Nutritionist) from the prior entry.
      "description": "EXCLUDED PROVIDER SPECIALTIES - 071, 076, 080, 081, 086, 094, 097,"
    },
    {
      // More provider specialties NOT covered under this benefit.
      // The list includes `AQ` (Nutritionist) from the prior entry.
      "description": "EXCLUDED PROVIDER TYPES - 0I, 0N, 0Q, 0T, 0U, 0V, A9, AC, AG, AH, AM, AQ, AX, B0, BG, BY, D0, D7, D7,"
    }
  ],
  ...
}

Missing benefits
In some cases, eligibility responses like the ones above may provide everything you need. However, sometimes, the payer doesn’t include the benefit at all – even if it’s covered by the patient’s plan. This makes it hard to know what’s covered, which STC to check, or how to automate parsing.

How to get the most from eligibility responses

Eligibility responses vary by payer. The best way to check for the benefit details you need is to test likely STCs with each payer. For each payer:

  1. Compile a list of STCs to test.
    Use our list of STCs for common services as a starting point. For example, for medical nutritional therapy, try 98 (Professional Physician Visit - Office), MH (Mental Health), 1 (Medical Care),  and BZ (Physician Visit - Office: Well).

  2. Send a baseline eligibility request.
    Use STC 30 (Health Benefit Plan Coverage) for general medical benefits or 35 (Dental Care) for general dental benefits.

  3. Compare the baseline response with the response to the specific STC.
    Save the benefitsInformation array for each STC and diff them. If they're different, the payer may include information about your service in the response.

  4. Search the response.
    Look for keywords related to your service in free-text fields. For medical nutrition therapy, you can match on nutrition, dietitian, and MNT.

As a test, we checked recent responses from Stedi’s top eligibility payers to see which returned benefits related to medical nutrition therapy. Out of the 140 we checked, about 40% did. That group included several major payers – like UnitedHealthcare and Blue Shield of California – who make up about 82% of our transaction volume.

There are exceptions. For example, some major payers like Cigna and Blue Cross Blue Shield of Texas didn’t appear to include nutrition therapy benefits in their responses at all.

How to fill in gaps

If the eligibility response is missing benefits you need, you can still reliably get the information. Here’s what we’ve seen work with our most successful customers:

  1. Contact the payer.
    Call or use the payer’s portal to get any missing benefits information you need. You can do this manually or use an AI voice agent or screen scraper to do it programmatically.

  2. Record what you learn.
    Regardless of the method you use, create a system to track the information you get by payer and plan. Depending on your needs, this could just be a database, spreadsheet, or JSON file.

  3. Use the collected data to enrich your eligibility responses.
    Plan benefits for the same payer and plan usually don’t vary from member to member. You can reuse the information you collect to enrich eligibility responses across patients for that plan.

    Even when plan benefits are the same, eligibility checks are still useful for things that change by member like active coverage or service history.

Get expert support

Eligibility checks are a good first line of defense for fetching benefits data. They work for most cases and help automate your workflow. But when gaps appear, you can still get answers.

Support often plays a key role in filling those gaps. Stedi has helped several companies, like Berry Street and others, interpret eligibility responses and optimize their workflows for medical nutrition therapy and more.

Request a free trial and see our support for yourself.

Aug 26, 2025

Products

You can now correct and resubmit claims directly from the transaction detail page of any claim in the Stedi portal. 

We’ve also made other UI improvements that make the portal’s Claims section simpler and faster to use. Here’s what’s new:

  • Streamlined list views and filters on the Transactions and Files pages help you find what you need, fast.

  • Cleaner detail pages for transactions and files put the most important information front and center.

These changes cut the clutter and help anyone using the portal – developer or operator – resubmit claims quickly, which means faster payments for the provider.

Correct and resubmit claims

You can now correct and resubmit claims from the transaction details page in the Claims section of the Stedi Portal. Just open a claim from the Transactions page, and click Edit and resubmit.

Edit and resubmit

The claim opens in an interactive inspector, with the X12 EDI on the left and the EDI specification on the right. Make your changes, and click Review and submit. You can review a diff of the updated X12 before resubmitting.

X12 diff

Improved list page views and filters

The Transactions and Files list pages now have a simplified layout. We’ve removed unneeded columns and simplified each page’s filter. For example, the Transactions page:

Transactions page

The Files page:

Files page

Improved detail pages

We’ve revamped the File detail page so that you can see the file’s X12 alongside the EDI specification. You can also view related transactions in the Transactions tab.

File detail page

The Transaction detail page now includes tabs for X12 and JSON versions of the transaction, along with webhook deliveries, which are useful for 277CA and 835 transactions.

Transaction detail page

Get started

The improved claims UI is now available in the Stedi portal for all paid plans. If you’re not yet a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Aug 21, 2025

Products

You can now run 276/277 real-time claim status checks in the Stedi portal.

A real-time claim status check tells you if a claim was received, is in process, was denied, or has another status with the payer. You typically run claim status checks if you haven’t gotten a claim acknowledgment or an ERA for a claim within an expected timeframe – typically around 21 days.

Previously, you could only run these checks using Stedi’s Real-Time Claim Status API.

Now, you can send real-time claim status requests and view the responses right in the Stedi portal. This makes it easy for anyone – whether you’re an operator or a developer – to check on claims and debug issues. This can help speed up resubmissions and result in faster payments.

Submit a claim status request

You can submit a claim status check using the Stedi portal’s new Create claim status check page. You can access the page by clicking Claims > New claim status in the portal’s nav.

By default, the page shows fields from our recommended base request. Check our docs for best practices on filling out the request.

Create claim status check page

View claim status responses

Claim status checks run synchronously – so you get responses back quickly.

If the payer has multiple claims on file that match the information in the claim status request, the response may contain information about more than one claim. If so, you can use the dropdown to navigate between claims and quickly see each one’s status category code.

You’ll see claim-level statuses in the response, and when available from the payer, you’ll also see service-line level statuses.

Claim status response page

Get started

Claim status checks are available in the Stedi portal for all paid plans. Check the Stedi Payer Network or Payers API to see which payers support 276/277 real-time claim status checks.

If you’re not a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Aug 21, 2025

Guide

Figuring out the right steps for processing out-of-network claims can be confusing.

Some payers require provider registration. Others make you submit a claim before enrolling for Electronic Remittance Advice (ERAs). Some have few requirements at all.

If you’ve run into questions or hurdles, you’re not alone.

While requirements vary between payers, there are common patterns you can use to reliably submit out-of-network claims and – when possible – get back ERAs. 

This guide aims to answer your questions about out-of-network claims. It also covers patterns we’ve seen work.

Provider network status

A provider’s network status indicates whether a provider is in-network or out-of-network for a specific payer. If a provider is out-of-network, any claim they submit is also out-of-network.

Network status is determined by credentialing and payer enrollment, which are handled directly with the payer.

They’re separate from transaction enrollment, which determines what transactions you can exchange with a payer. Your clearinghouse only handles transaction enrollment.

For help with credentialing or payer enrollment, contact the payer or use a credentialing service like CAQH, Assured, or Medallion.

Checking a provider’s network status
You can’t reliably determine whether a provider is in-network or out-of-network using an eligibility check or other pre-claim clearinghouse transaction. However, there are options for checking this programmatically. For a rundown, see our Provider network status docs.

Out-of-network benefits
Even if a payer accepts out-of-network claims, not all patients have out-of-network benefits. You can determine if a patient has out-of-network benefits –and whether they’re eligible for reimbursement – using an eligibility check. See the In Plan Network Indicator docs

Requirements for out-of-network claims

Most payers accept out-of-network claims, but requirements can vary. The two main things to check are:

  • Whether the payer requires registration for out-of-network providers

  • Whether they require transaction enrollment for claims submission

Provider registration
Some payers require any out-of-network provider to register as a “non-participating” provider before they’ll accept claims. This registration is separate from transaction enrollment and happens outside of Stedi. 

You’ll need to contact the payer to confirm what’s needed and set up your provider if required.

Transaction enrollment for claims submission
Most payers don’t require transaction enrollment for claims submission – but some do. If a payer does and enrollment isn’t completed, the claim will be rejected.

Before submitting claims, check the payer’s enrollment requirements using the Payer API or Payer Network. You can filter payers by supported transaction type and enrollment requirements.

Once any needed provider registration or transaction enrollment is complete, the provider can submit out-of-network claims for the payer.

Submit an out-of-network claim

You submit out-of-network claims using the Claims API, the Stedi portal, or SFTP.

There are no special fields or requirements for out-of-network claims. Just submit them as you would an in-network claim. A provider doesn’t have to be enrolled for ERAs with a payer to submit claims.

You’ll receive claim acknowledgments for out-of-network claims. You can also run real-time status checks.

ERAs for out-of-network claims

Unlike claims, all payers require transaction enrollment for ERAs. Providers can only be enrolled for ERAs with one clearinghouse per payer at a time.

Transaction enrollment for ERAs
ERA enrollment requirements for out-of-network providers vary across payers.

Some payers treat ERA enrollment the same for all providers. Others require out-of-network providers to be "on-file" first. Some payers don’t allow out-of-network providers to enroll for ERAs at all.

“On-file” requirements
To become "on-file," out-of-network providers typically need to either:

  • Submit a claim to the payer.

  • Register as a “non-participating” provider with the payer.

Many payers require a submitted claim before ERA enrollment. However, claims submitted before or during enrollment won't generate ERAs. 

Many payers send Explanations of Benefits (EOBs) – typically snail mailed – for out-of-network claims or when no ERA enrollment is on file. EOBs contain the same information as ERAs, but if you and your providers rely on ERAs for reconciliation, this can create issues.

A straightforward workaround is to use Anatomy to convert your EOBs into ERAs. Anatomy sends the converted ERAs to Stedi. You can then use Stedi’s APIs or SFTP to fetch the ERAs as normal.

Set up Anatomy for EOB-to-ERA conversion

Setting up Anatomy is a one-time step. Once configured, you can use it for any EOBs sent to the provider from any payer.

Step 1. Create an Anatomy account
Contact Anatomy to get started. You can upload PDFs directly in Anatomy’s UI or redirect paper EOBs to a PO Box managed by Anatomy.

Step 2. Submit an ERA enrollment request for Anatomy in Stedi
Enroll the provider for ERAs using the Enrollments API or the Stedi portal, with ANAMY (Anatomy) as the payer ID. Enrollment typically takes 1-2 business days.

Step 3. Send EOBs to Anatomy
After enrollment completes, send any paper EOBs for the provider to Anatomy.

Step 4. Fetch the converted ERAs
You can fetch the converted ERAs as normal using our APIs or SFTP. See the ERA docs.

Pricing
Stedi doesn’t charge extra to receive ERAs from Anatomy. You pay the same as you would for any ERA. Contact Anatomy for pricing on their services.

The out-of-network claims runbook

There are several ways to handle out-of-network claims with Stedi. Here's practical steps you can follow based on what we’ve seen work.

Step 1. Set up Anatomy (optional).
Follow the instructions above. You only need to configure Anatomy once. You use the same Anatomy ERA enrollment across multiple payers.

Step 2. Check the payer’s requirements for out-of-network claims and ERAs.
Contact the payer to see if they require registration for “non-participating” for out-of-network claim submissions and ERAs. If so, complete any registration steps for the provider.

Step 3. Check if the payer requires transaction enrollment for claim submission.
Check the payer’s transaction enrollment requirements for claim submission using the Payer API or Payer Network.

If transaction enrollment is required, use the Enrollments API or the Stedi portal to submit a related enrollment request. Wait for enrollment to complete before submitting claims to the payer.

Step 4. Start submitting out-of-network claims.
Submit out-of-network claims using the Claims API, the Stedi portal, or SFTP.

Many payers require a claim on file before processing ERA enrollment. While these initial claims won't get ERAs, waiting risks cash flow delays and missed timely filing deadlines. We'll address this gap in step 6 below.

Step 5. Submit an ERA enrollment request for the payer.
Use the Enrollments API or the Stedi portal to submit an ERA enrollment request for the payer.

Most enrollment requests complete in 1-2 business days, but it varies by payer. You can monitor the enrollment status using the API or the portal.

Step 6. Monitor for ERAs from the payer.
Once enrollment completes, you can listen for and fetch ERAs using our APIs or SFTP.

ERA enrollment isn't retroactive. You'll only receive ERAs for claims submitted after enrollment completes. For claims submitted before or during the enrollment process, you have two options:

  • If you're using Anatomy and the payer sends paper EOBs, Anatomy converts these EOBs into ERAs. You can then fetch the ERAs as normal using our APIs or SFTP.

  • If the payer doesn’t send EOBs or you don’t use Anatomy, use real-time claim status checks and your provider’s actual payments for reconciliation.

Get started

We help teams set up claims processing workflows every day.

When you’re ready, start a free trial and see the workflow end-to-end.

Aug 19, 2025

Products

You can now use the Stedi Agent in sandbox accounts and test mode.

In sandbox accounts and test mode, you run predefined mock eligibility requests that return realistic responses. The Stedi Agent is the Stedi portal’s built-in AI assistant. It helps you recover failed eligibility checks.

Previously, you could only use the Stedi Agent for failed eligibility checks in production. Now, you can try the agent out in sandbox accounts and test mode using a predefined mock check.

Try Stedi Agent in Eligibility Manager

The mock eligibility request is preloaded in the Stedi portal’s Eligibility Manager. To run the check:

  1. Log in to your sandbox account. If you have a production account, switch to test mode.

  2. Create a new eligibility check and select Stedi Agent as the Payer. Leave the Person as is.

  3. Submit the check. It’s expected to fail.

  4. After the check fails, click Resolve with Stedi Agent.

The agent runs in Debug view, where you can watch it work – step by step, in real time.

Stedi Agent in Eligibility Manager

Run a mock API request

You can also run the mock request using the Real-Time Eligibility Check JSON endpoint and a test API key. Sandbox accounts can only create test API keys.

The following curl request uses the request body for the predefined mock request:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": {
      "organizationName": "STEDI",
      "npi": "1447848577"
    },
    "tradingPartnerServiceId": "STEDI",
    "controlNumber": "112233445",
    "subscriber": {
      "memberId": "23051322",
      "lastName": "Prohas",
      "firstName": "Bernie"
    },
    "stediTest": true
  }'

Try it today

Stedi Agent is available now and is completely free for sandbox accounts and in test mode.

To get access, create a sandbox account. It’s free and takes less than two minutes.

Aug 19, 2025

Guide

Stedi provides fast, reliable infrastructure for healthcare billing. Anyone doing RCM work could benefit from running on it.

But not everyone should build on Stedi themselves.

Stedi is designed for teams with developers. Think software platforms, MSOs, DSOs, and health tech startups. These teams are often building custom RCM functionality and want full control over their RCM stack. If you have engineers, Stedi’s APIs give you everything you need to do that.

But many healthcare companies don't have developers. Even those with dev teams may not want to build everything from scratch. Some use solutions from our Platform Partners directory. Others need something more custom.

That's where firms like Light-it come in. Light-it is a software development firm that specializes in healthcare. They help teams build on Stedi without writing code. Light-it handles development and other implementation details for them.

Here's how one provider worked with Light-it to verify insurance coverage for nutrition therapy.

Verifying eligibility for nutrition therapy

The provider offers virtual-first medical nutrition therapy. They work with a nationwide network of registered dietitians. Most visits are fully covered by insurance – 95% of patients pay nothing out of pocket.

To deliver that kind of experience, the provider needs to verify coverage before a patient books. That’s harder than it sounds.

Most payers don’t clearly state whether nutrition therapy is covered in their eligibility responses. The X12 270/271 standard lacks a specific Service Type Code (STC) for medical nutrition therapy. This makes it hard to get consistent data across payers.

Most clearinghouses add more friction on top of this. You have to deal with unreliable connections, outdated – or poorly documented – APIs, and lossy responses where the clearinghouse drops data from the payer’s responses. When you do get an unclear response, it can take days to get help. Many teams end up just calling the payer themselves.

Instead of adding friction, Stedi makes the process easier. We offer easy integration, reliable payer connections, and lossless eligibility responses. And we provide real-time support. When you get a confusing eligibility response, we’ll help you interpret or debug it.

From proof of concept to production

Light-it and the provider started small. They ran a proof of concept with real patient data. They tested which payers returned usable coverage.

The results were clear. With Stedi, Light-it and the provider got clean, lossless eligibility responses. When they got errors, there were predictable failure modes. Light-it built reliable logic around those failures. When responses were unclear, they turned to Stedi for help in real time.

After validating the approach, Light-it built a simple frontend. Then they connected it to Stedi's Real-Time Eligibility Check API. It’s modern, JSON-based, and well-documented. Because of that, Light-it was able to move from proof of concept to production in weeks, not months.

The result

Today, the provider runs eligibility checks before every visit. Patients see coverage results immediately.

Stedi handles the transaction layer and APIs. Light-it handles the business logic and patient experience. They tailored the workflow to match how the provider operates. This includes fallback paths when data is unexpected or incomplete.

When to use an implementation partner

If you're evaluating Stedi and don’t have developers – or don’t want to build from scratch – partnering with a healthcare software development service like Light-it might be a good fit. They offer free consultation calls with no commitment.

You can start with a proof of concept to test your specific use case. Then work with the firm to build what's missing. Stedi's modern APIs and real-time support help implementation teams ship faster.

Aug 18, 2025

Products

You can now filter results more precisely in the List Enrollments and List Providers API endpoints.

We’ve also updated the Enrollments page and Providers page in the Stedi portal to surface these filters.

Previously, you had to page through results and filter them in your client. These new options make it easier to find the data you want – faster, with less paging and smaller responses.

List Enrollments endpoint

We’ve added new query parameters to the List Enrollments endpoint. You can use the parameters to filter by:

  • Partial term: filter lets you search across multiple fields at once.

  • Status: Filter by one or more enrollment statuses—such as DRAFT, SUBMITTED, PROVISIONING, LIVE, CANCELED, or REJECTED.

  • Provider details: Filter by NPI (providerNpis), tax ID (providerTaxIds), or name (providerNames).

  • Payer: Filter by payer IDs (payerIds).

  • Source: Limit results by how the enrollment was created: API, UI, or IMPORT.

  • Transaction type: Filter by transaction types, like eligibilityCheck, claimStatus, or claimSubmission.

  • Date: Filter enrollments by when they were created (createdFrom, createdTo) or when their status last changed (statusUpdatedFrom, statusUpdatedTo).

  • Import ID: If an enrollment was created through CSV import, filter by the returned importId.

Several of these query parameters accept arrays. You can include an array parameter more than once in the URL to filter by multiple values.

For example, ?providerNames=John%20Doe&providerNames=Jane%20Doe&status=LIVE returns all enrollments in LIVE status that have either John Doe or Jane Doe as the provider:

https://enrollments.us.stedi.com/2024-09-01/enrollments?providerNames=John%20Doe&providerNames=Jane%20Doe&status=LIVE

List Providers endpoint

The List Providers endpoint now accepts a filter parameter for searching by provider name, NPI, or tax ID. Filtering is case-insensitive and supports partial matches.

For example, ?filter=2385442357 returns all providers whose name, NPI, or tax ID contains 2385442357:

https://enrollments.us.stedi.com/2024-09-01/providers?filter=2385442357

Updated filters in the Stedi portal

Alongside API improvements, we’ve updated the Stedi portal to surface these new filters on the Enrollments page and Providers page.

On the Enrollments page:

Enrollments page filters


On the Providers page:

Providers page search

Try it out

The new filters are available on all paid Stedi plans. For full details, check out the API documentation.

Aug 14, 2025

Company

In February of last year, I gathered our engineering team in a war room. Change Healthcare – the nation’s largest clearinghouse for healthcare claims processing – had been down for almost a full week due to a cyberattack that would ultimately render them unable to process many types of transactions for two months or longer. 

It’s hard to explain the magnitude of the Change Healthcare outage to people outside the healthcare industry. Nearly 40% of healthcare claims processed in the United States flowed through Change’s platform. They processed an aggregate $1.5 trillion of claims volume annually – 15 billion claims – and they were the exclusive designated clearinghouse for dozens of payers ranging from small regional payers to UnitedHealthcare, Change’s parent company. Healthcare spend in the US is $4.9 trillion annually – 18% of total GDP – which means that when Change went down, it was processing roughly 5.5% of US GDP

When the seriousness of Change’s situation became clear, we worked around the clock to accelerate the development of our own clearinghouse that we had planned to launch later that year. We announced a drop-in replacement for Change’s clearinghouse just a few days later, and the 7 weeks afterwards were unlike anything I had experienced in 20 years of business. We couldn’t leave our keyboards during any waking hour of the day for more than a few minutes at a time – 6 and 7 figure deals went from initial phone call or text message to signed terms in under an hour. 

Change Healthcare has long since come back online, but our growth trajectory has only steepened. In April 2025, we were named Ramp’s 3rd-fastest growing software vendor. Last month, we signed 5x the number of customers that we signed at the height of the Change outage. Stedi has become the de facto choice for virtually every new venture-backed health tech company – and as later-stage health tech companies and traditional institutions revisit their legacy clearinghouse dependencies in the wake of the Change outage, Stedi’s cloud-native, API-first platform has become the obvious choice. 

But more and more, our growth is driven by GenAI use cases from all segments of the market – from brand new startups to traditional companies coming to Stedi to build agentic functionality into their existing platform. One-third of our customer base is now made up of fully native GenAI companies, an extremely high-growth cohort that has collectively raised an astounding $5B in funding to date. 

Today, we’re announcing our own $70 million Series B fundraise, co-led by Stripe and Addition, with participation from USV, First Round, Bloomberg Beta, BoxGroup, Ribbit Capital, and other top investors, which includes a $50 million previously-unannounced round plus $20 million of new capital.

Our mission is to make healthcare transactions as reliable as running water. This new funding has allowed us to double down on rebuilding the backbone of healthcare transaction infrastructure as Revenue Cycle Management (RCM) undergoes an AI-driven transformation. In addition to our best-in-class APIs, we’ve made it even easier for development teams to integrate to our clearinghouse using the new MCP server launched last week. Yesterday, we launched the Stedi Agent to power AI functionality within our platform directly.

What’s driving the AI boom in RCM? 

RCM is practically designed to be automatable using AI. Every claim, remittance, and eligibility check is already transmitted in a well-structured transaction, giving AI models clean, labeled data to work with. RCM workflows are rule-bound – that is, every step is governed by explicit payer or regulatory logic. Eligibility follows coverage rules; claim submissions and resubmissions must meet payer-specific requirements; denial appeals hinge on predefined evidence thresholds and deadlines. Because each decision is effectively a yes/no test against a published rule set, accuracy can be measured objectively and improvements validated quickly – ideal conditions for AI agents to learn, iterate, and outperform manual processes.

But when it comes to implementing AI workflows, development teams hit frustrating roadblocks with legacy clearinghouses. Most of the functionality offered by legacy clearinghouses is not accessible programmatically. If you’re lucky, you might be able to do basic eligibility and claim submission using an API – a distant second-class citizen to the clearinghouse’s main focus: traditional EHR integration. More often, you’re relying on brittle portal scraping, email capture, and PDF parsing to attempt to stitch together a passable workflow. 

The legacy clearinghouses are unlikely to get much better. They were built pre-cloud computing (and in many cases, pre-internet), and most are the result of a series of private equity acquisitions with tech stacks that were never harmonized or modernized. As a result, the technology roadmaps move at a glacial pace – the people who built the systems have long since departed and most of the effort is expended on ‘keep the lights on’ maintenance. 

Stedi’s approach is API-first: every piece of functionality available through our user interface is available via API, from the basics like eligibility checks, claims, and remits to the often-neglected aspects like payer search and transaction enrollment. Alongside our APIs, we offer a full suite of modern user interfaces that are easy for non-technical users to use.

Our thesis is simple: as more and more aspects of RCM software are subsumed by agentic workflows, companies will shift ever-greater portions of their workloads to the platforms that offer the best accessibility and legibility to AI agents that are performing actions; since other clearinghouses don’t offer ways to perform tasks programmatically, customers will continue to migrate to Stedi as they build net-new workflows, or as they find that existing workflows come to exceed the requirements afforded by other clearinghouses.

We have a single question that we use to guide our roadmap decisions: does this make it easier for humans and agents to interact with our platform? This has led to dozens of small improvements and major launches over the past several months, and will lead to many more. This latest investment allows us to continue to expand the breadth and depth of our transaction functionality in order to serve the needs of the smallest providers and the largest health systems, and everyone in between.

Most importantly, it allows us to accelerate hiring of world-class talent across engineering, product, design, business operations, and more. If that sounds exciting to you, come work with us.

Aug 13, 2025

Products

Now available in Stedi's Eligibility Manager, the Stedi Agent brings AI-powered automation to healthcare clearinghouse workflows, beginning with eligibility check recovery.

Most failed eligibility checks are recoverable. Common causes include mismatched patient details, an incorrect payer ID, or a temporary outage on the payer’s side. In most cases, these errors have clear, automatable recovery steps.

Now, the Stedi Agent can run those steps for you.

In Eligibility Manager, each eligibility check and any related retries are grouped under a search. If a check fails with a known recoverable error, a Resolve with Stedi Agent option appears next to the related search. The agent runs in Debug view, where you can watch it work – step by step, in real time.

Stedi Agent

How it works

The Resolve with Stedi Agent option only appears where the agent can currently help. We’re starting with the most common errors and expanding coverage based on what works in practice.

When you start the agent, it examines the eligibility search’s failed checks and works through recovery strategies based on the error type. For example, for mismatched patient details, it might try different combinations of patient data or adjust name formats.

The agent can make API calls to the Real-Time Eligibility Check JSON and Search Payers endpoints.  

Each check it runs is added to the same eligibility search and appears in real time in the Debug view. The agent only uses data from the eligibility search it's running on.

Security

The Stedi Agent is hosted on Stedi's HIPAA-compliant infrastructure, which runs completely on AWS. The agent uses Stedi's existing security model, including role-based access control (RBAC) and support for multi-factor authentication (MFA). You must have the Operator role or above to use the agent.

The agent only accesses data from the eligibility search it's working on. It can’t access data from other searches, customers, or systems.

Pricing

The Stedi Agent is available on all Stedi accounts at no additional cost beyond those for additional eligibility checks.

As with our APIs, there’s no charge for non-billable requests. See Billing for eligibility checks.

Try it out

The Stedi Agent is available now. Look for the Resolve with Stedi Agent option next to failed eligibility checks.

If you’re not a Stedi customer, request a trial. Most teams are up and running in less than a day.

Aug 11, 2025

Products

Stedi now enriches most Blue Cross Blue Shield (BCBS) eligibility responses with the member’s home payer name and primary payer ID. 

BCBS is a collection of 33 entities that operate independently. BlueCard is BCBS’s national program that enables members of one BCBS plan to obtain healthcare service benefits while traveling or living in another BCBS plan’s service area.

Each BCBS plan has access to the BlueCard eligibility network, which means that a provider operating in one state can check eligibility for any nationwide BCBS member using the provider’s local BCBS plan payer ID, as long as the eligibility check includes the member’s first name, last name, birthdate, and full member ID (including the 3-character BCBS alpha prefix).

For example, a provider in Texas might send an eligibility check to “Blue Cross Blue Shield of Texas” for a member whose coverage is actually with “Blue Cross Blue Shield of Alabama.” BCBS of Texas will return a successful response as long as all of the member’s details were correct. 

The problem is that the returned eligibility response doesn’t say which BCBS payer is the patient’s home payer. It just lists the payer to whom the request was sent. In the example above, the eligibility response would have no indication that the member belonged to BCBS of Alabama. 

The reason that BCBS doesn’t include this information is that it’s irrelevant for most traditional care scenarios, since the BlueCard program instructs providers to always submit claims to their local payer – not the member’s home plan. However, for multi-state providers or telehealth scenarios, the rules can differ, and it becomes important for the provider to identify the actual home plan.

To simplify this process, Stedi has developed logic to automatically detect and include the home payer’s name and ID in the eligibility response whenever possible.

When detected, this info now appears in a related benefitsInformation.benefitsRelatedEntities entry in our JSON eligibility responses and in a 2120C or 2120D loop in X12 responses.

No action is needed to take advantage of this new functionality. This enhancement is already live for all related Real-Time Eligibility Check API endpoints.

How the data is included

If you’re using our JSON eligibility API, the home payer’s details appear as a benefitsInformation.benefitsRelatedEntities entry in the response. It’s included in the same benefitsInformation entry that includes the patient’s coverage status.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "serviceTypeCodes": ["30"],
      ...
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Party Performing Verification",
          "entityType": "Non-Person Entity",
          "entityName": "Blue Cross Blue Shield of Alabama",
          "entityIdentification": "PI",
          "entityIdentificationValue": "00510BC"
        }
      ]
    },
    ...
  ],
  ...
}

In X12 eligibility responses, the home payer’s information is included in Loop 2120C or 2120D.

LS*2120~
NM1*VER*2*Blue Cross Blue Shield of Alabama*****PI*00510BC~
LE*2120

Try it out

You can see home payer enrichment in action by running a real-time eligibility check for any BCBS member for whom you have the first name, last name, birthdate, and member ID.

If you don’t have production access, request a free trial. Most teams are up and running in less than a day.

Aug 8, 2025

Company

Stedi is now e1 certified by the HITRUST for foundational cybersecurity.

HITRUST e1 Certification demonstrates that Stedi’s healthcare clearinghouse platform is focused on the most critical controls to demonstrate that essential cybersecurity hygiene is in place. The e1 assessment is one of three progressive HITRUST assessments that leverage the HITRUST Framework (HITRUST CSF) to prescribe cyber threat adaptive controls that are appropriate for each assurance type.

“The HITRUST e1 Validated Assessment is a strong fit for cyber-conscious organizations like Stedi that are looking to establish foundational assurances and demonstrate ongoing due diligence in information security and privacy,” said Ryan Patrick, VP of Adoption at HITRUST. “We commend Stedi for their commitment to cybersecurity and congratulate them on successfully achieving their HITRUST e1 Certification.”

Aug 7, 2025

Guide

“How much will this cost?” It’s the first question many patients ask their provider.

Real-time eligibility checks return the data you need to estimate patient responsibility – co-pays, deductibles, limitations – but they don’t hand you the answer. The response is often nuanced and, in some cases, can seem contradictory.

With a few simple patterns in place, though, you can reliably extract the information needed to build cost estimates you – and your providers – can trust.

This guide shows you how. It walks through the structure of a 271 eligibility response and shares practical tips for using Stedi’s Real-Time Eligibility Check JSON API to estimate patient responsibility. It also gives you tips for improving cost estimates using data from Stedi’s 835 ERA Report API.

Choose the right STC

Eligibility responses organize benefits by Service Type Codes (STCs). STCs group services into broad categories like "office visit" or "surgery."

The STC you send in the eligibility request shapes what you get back in the response. Choose the right one, and you'll get the benefits you need. Choose the wrong one, and you might miss them entirely.

Here’s a reliable approach:

  • Use one STC per request. Many payers don’t support multiple STCs. To test payer support for multiple STCs, see Test payer STC support in the Stedi docs.

  • Don't use procedure codes (HCPCS/CPT/CDT) in eligibility requests. While Medicare and some dental payers accept them, most ignore them entirely. Map the procedure codes to STCs first.

  • Only send required patient data in eligibility requests. Payers require that eligibility checks match a single member. Extra data increases the risk of a mismatch. Stick to:

    • Member ID

    • First name

    • Last name

    • Date of birth

  • You may get benefits for more STCs than you request. For example, HIPAA requires medical payers to return benefits for applicable STCs for STC 30 (Health Benefit Plan Coverage).  See General benefit checks in the docs.

  • Missing benefits don't mean missing coverage. Payers aren't required to respond to every STC. Compare the response for your STC to STC 30 for medical or STC 35 for dental. If there's a difference in the response, the STC is likely supported. See Test payer STC support in the docs.

For example, to estimate costs for a level 3 established patient office visit (CPT code 99213), start by mapping the procedure code to the most appropriate STC. In this case, that’s STC 30.

Then, send a real-time eligibility request with a body similar to:

{
    ...
    "encounter": {
      "serviceTypeCodes": ["30"]
    },
    "provider": {
      "organizationName": "ACME Health Services",
      "npi": "1999999984"
    },
    "subscriber": {
      "dateOfBirth": "19900101",
      "firstName": "John",
      "lastName": "Doe",
      "memberId": "123456789"
    }
}

Where to find patient costs in the response

Most of a patient’s benefit information, including patient cost, is in the eligibility response’s benefitsInformation array. For example:

{
  ...
  "benefitsInformation": [
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // In-network only
    },
    {
      "code": "C",                         // Deductible
      "coverageLevelCode": "IND",          // Individual coverage
      "serviceTypeCodes": ["30"],          // General medical (used for CPT 99213)
      "timeQualifierCode": "23",           // Calendar year
      "benefitAmount": "1000",             // $1000 deductible
      "inPlanNetworkIndicatorCode": "Y",   // In-network only
    },
    {
      "code": "A",                         // Co-insurance
      "serviceTypeCodes": ["35"],          // Dental Care
      "benefitPercent": "0",               // 0% co-insurance    
      "inPlanNetworkIndicatorCode": "N",   // Out-of-network only
      "compositeMedicalProcedureIdentifier": {
        "productOrServiceIDQualifierCode": "AD", // American Dental Association (ADA)
        "procedureCode": "D0150"                 // Comprehensive oral evaluation
      },
      "benefitsDateInformation": {
        "latestVisitOrConsultation": "20240404" // Last service
      },
      "benefitsServiceDelivery": [           // 1 visit every 6 months
        {
          "quantityQualifierCode": "VS",     // Visits
          "quantity": "1",                   // 1 visit
          "timePeriodQualifierCode": "34",   // Months
          "numOfPeriods": "6"                // Every 6 months
        }
      ]
    }
    ...
  ],
  ...
}

Each benefitsInformation object includes a few key fields related to patient costs:

  • serviceTypeCodes - The services this benefit applies to.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include its STC.

  • code - What the benefit is. For patient costs, the relevant codes are:

    • ACo-insurance: Percentage the patient pays for the benefit.

    • BCo-pay: Fixed dollar amount the patient pays for the benefit.

    • CDeductible: Total amount the patient must pay before benefits begin.

    • FLimitations (Maximums): Maximum benefit amount. Typically used for dental and vision plans.

    • GOut of Pocket (Stop Loss): Maximum amount a patient can pay per year. Once reached, the plan pays 100% of covered services.

    • JCost Containment: Total amount the patient must pay before benefits begin, similar to a deductible. Typically used for Medicaid benefits.

    • YSpend Down: Total amount the patient must pay before they can receive benefits. Typically used for Medicaid benefits.

  • benefitAmount or benefitPercent - The dollar or percentage value of the patient costs. benefitPercent is used for co-insurance (code = A). All other patient cost types use `benefitAmount`.

  • timeQualifierCode - What the benefit amount represents. It’s often the time period it applies to. For example, if an entry has code = G (Out-of-pocket maximum) and timeQualifierCode = 29 (Remaining Amount), then benefitAmount contains the remaining out-of-pocket maximum.

For the full list of time qualifier codes, see Time Qualifier Codes in the docs.

  • coverageLevelCode - Code indicating the level of coverage for the patient. For example, IND (Individual) or FAM (Family).

  • inPlanNetworkIndicatorCode - Whether the benefit applies to in-network or out-of-network care – not whether the provider is in-network. Possible values are "Y" (In-network), "N" (Out-of-network), "W" (Both), and "U" (Unknown). For more details, see In Plan Network Indicator in the docs.

  • additionalInformation.description - Free-text notes from the payer. For patient costs, these notes often contain limitations and qualifications, as well as carve-outs that don’t align neatly to a full STC. For example for co-pays (code = B), this may contain VISIT OFFICE PCP,TELEHEALTH PRIMARY CARE, which indicates when the co-pay applies.

Service history fields
Some benefits have frequency limits. For example, “one visit every 6 months” or “two cleanings per year.” Others depend on when the patient last received the service.

To estimate patient cost for these types of benefits, look for:

  • benefitsDateInformation – Shows when a service (like a cleaning or exam) was last performed.

  • benefitsServiceDelivery – Indicates how often a service is allowed, such as once every 6 months or twice per year. Many payers don’t populate this field and instead return this information as free text in additionalInformation.description.

These fields show up in responses for dental, vision, and Medicaid. They also apply to some medical services, like annual wellness visits or therapy sessions.

If the patient has already reached the allowed frequency, the next visit may not be covered. In that case, they may owe the full amount.

Some plans, especially dental, apply shared frequency limits across a group of procedures. For example, a plan might allow one X-ray series per year, regardless of the procedure code used later in the claim. If a claim has already been paid for one of the codes in the group, subsequent claims for others may be denied.

Handling multiple benefit entries

The same STC often has different patient costs for different scenarios. When you see multiple benefit entries for the same STC, check these fields to understand which cost applies when:

  • coverageLevelCode - Coverage level, such as individual or family

  • inPlanNetworkIndicatorCode - In-network vs. out-of-network rates

  • additionalInformation.description - Specific limitations and exceptions.

These fields are easy to miss, but without them, entries with the same STC and benefits code can look contradictory when they’re actually describing different conditions.

Example: Multiple deductibles for the same STC
In the following example, both benefitsInformation entries apply to general medical coverage (STC 30), but the inPlanNetworkIndicatorCode differentiates them: the in-network deductible is $1000. The out-of-network deductible is $2500.

// In-network deductible
{
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "serviceTypeCodes": ["30"],              // General medical
  "timeQualifierCode": "23",               // Calendar year
  "benefitAmount": "1000",                 // **$1000 deductible**
  "inPlanNetworkIndicatorCode": "Y",       // **In-network only**
}
// Out-of-network deductible
{
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "serviceTypeCodes": ["30"],              // General medical
  "timeQualifierCode": "23",               // Calendar year
  "benefitAmount": "2500",                 // **$2500 deductible**
  "inPlanNetworkIndicatorCode": "N",       // **Out-of-network only**
}

Improve cost estimates with 835 ERAs

Eligibility responses return benefits at the service type level. But what payers actually pay shows up in the 835 ERA, broken down by procedure code.

You can retrieve ERAs as JSON using Stedi’s 835 ERA Report API endpoint. For example, the previous eligibility check above may correspond to a visit with the following ERA:

{
 ...
 "transactions": [
   {
     ...
     "detailInfo": [
       {
         "paymentInfo": [
           {
             "claimPaymentInfo": {
               "claimPaymentAmount": "500",           // What payer paid provider
               "patientResponsibilityAmount": "300",  // What patient owes
               "totalClaimChargeAmount": "800",       // Original charge
               "patientControlNumber": "1112223333"   // Your tracking number
             },
             "patientName": {
               "firstName": "JOHN",
               "lastName": "DOE",
               "memberId": "123456789"
             },
             "serviceLines": [
               {
                 "serviceAdjustments": [
                   {
                     "adjustmentAmount1": "300",         // Amount adjusted
                     "adjustmentReasonCode1": "1",       // Deductible
                     "claimAdjustmentGroupCode": "PR"    // Patient responsibility
                   }
                 ],
                 "servicePaymentInformation": {
                   "adjudicatedProcedureCode": "99213",   // CPT code
                   "lineItemChargeAmount": "800",         // Charged amount
                   "lineItemProviderPaymentAmount": "500" // Paid to provider
                 }
               }
             ]
           }
         ]
       }
     ],
     "financialInformation": {
       "totalActualProviderPaymentAmount": "1100"   // Total payment for all claims
     },
     ...
   }
 ]
}

To identify patient responsibility in an ERA, look for serviceLines entries with serviceAdjustments.claimAdjustmentGroupCode = PR (Patient responsibility). These adjustments are the amounts the patient is expected to pay. The adjustmentReasonCode tells you why. For example, Claim Adjustment Reason Codes (CARCs) are:

  • 1 – Deductible

  • 2 – Coinsurance

  • 3 – Copay

  • 119 – Benefit max reached for the period

For a full list, see the X12 Claim Adjustment Reason Codes list.

CARCs often align with benefitsInformation.code values in eligibility responses. For example, adjustmentReasonCode = 1 (Deductible) in the ERA corresponds to code = C (Deductible) in the eligibility response. By comparing both sources, you can refine your estimates over time.

Building a cost estimation engine
Several Stedi customers combine 271 eligibility checks with 835 ERAs to refine their patient cost estimates over time. The most reliable setups use a simple feedback loop:

  1. Estimate patient costs using eligibility responses and historical data from prior claims (see step 5).

  2. Submit claims and collect ERAs.

  3. Extract payment patterns by CPT code, payer, and plan.

  4. Compare actual payments to your original estimates.

  5. Update your logic for patient cost estimates based on actual payments.

ERAs don’t give you market-wide benchmarks – only what happened with your own claims. For common procedures with high-volume payers, that’s often enough to make confident estimates.

Real-time support for real-time eligibility

Even clean eligibility requests sometimes return unclear responses. When that happens, our support team can help make sense of it. Our average support response time is under 10 minutes.

It’s one reason teams trust Stedi to stay in the loop while they scale.

To see how it works, start a free trial.

Aug 6, 2025

Products

You can now run real-time eligibility checks using our CAQH CORE–compliant SOAP API endpoint.

CAQH CORE SOAP is a widely adopted XML-based interoperability standard for exchanging healthcare transactions, like eligibility checks. It defines how systems can connect and exchange that data in a consistent, reliable way.

If you're already using CAQH CORE SOAP, our SOAP endpoint is the fastest way to start running eligibility checks with Stedi. Just point your existing integration to our endpoint – no other changes are needed.

Most Stedi customers use our JSON API for real-time eligibility. JSON is familiar, fast to integrate, and easy to work with. But if you’ve already built on SOAP, switching to JSON adds unnecessary overhead.

This endpoint removes that step.

The endpoint supports CAQH CORE Connectivity Rule vC2.2.0. We plan to support CAQH CORE Connectivity Rule vC4.0.0 as industry adoption increases.

How it works

To use the endpoint, send a POST request to:

https://healthcare.us.stedi.com/2025-06-01/protocols/caqh-core

In the request body, use the standard CAQH CORE Connectivity Rule vC2.2.0 SOAP envelope. Wrap your X12 270 eligibility request in the Payload element as CDATA. Authenticate with WS-Security headers using your Stedi account ID and Stedi API key.

You can find your Stedi account ID at the end of any Stedi portal URL. For example, in https://portal.stedi.com/app/healthcare/eligibility?account=1111-33333-55555, the account ID is 1111-33333-55555.

<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"
  xmlns:cor="http://www.caqh.org/SOAP/WSDL/CORERule2.2.0.xsd">
  <soapenv:Header>
    <wsse:Security soapenv:mustUnderstand="true"
      xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
      xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
      <wsse:UsernameToken>
        <wsse:Username>STEDI-ACCOUNT-ID</wsse:Username>
        <wsse:Password>STEDI-API-KEY</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
  </soapenv:Header>
  <soapenv:Body>
    <cor:COREEnvelopeRealTimeRequest>
      <PayloadType>X12_270_Request_005010X279A1</PayloadType>
      <ProcessingMode>RealTime</ProcessingMode>
      <PayloadID>YOUR-PAYLOAD-ID</PayloadID>
      <TimeStamp>2024-07-29T12:00:00Z</TimeStamp>
      <SenderID>SENDER-ID</SenderID>
      <ReceiverID>RECEIVER-ID</ReceiverID>
      <CORERuleVersion>2.2.0</CORERuleVersion>
      <Payload><![CDATA[ISA*00*          *00*          *ZZ*SENDER         *ZZ*RECEIVER       *231106*1406*^*00501*000000001*0*T*>~GS*HS*SENDERGS*RECEIVERGS*20231106*140631*000000001*X*005010X279A1~ST*270*1234*005010X279A1~BHT*0022*13*10001234*20240321*1319~HL*1**20*1~NM1*PR*2*ABCDE*****PI*11122~HL*2*1*21*1~NM1*1P*2*ACME HEALTH SERVICES*****SV*1999999984~HL*3*2*22*0~TRN*1*11122-12345*1234567890~NM1*IL*1*JANE*DOE****MI*123456789~DMG*D8*19000101~DTP*291*D8*20240108~EQ*MH~SE*13*1234~GE*1*000000001~IEA*1*000000001~]]></Payload>
    </cor:COREEnvelopeRealTimeRequest>
  </soapenv:Body>
</soapenv:Envelope>

Stedi returns a synchronous XML response containing the X12 271 eligibility response or a 999 acknowledgement, depending on the payer's response.

Try it free

The Real-Time Eligibility Check SOAP endpoint is available on all paid Stedi plans. If you’re not a customer, request a trial. Most teams are up and running in under a day.

Aug 6, 2025

Guide

If you’re building a dental eligibility product, you want accurate benefits data fast.

Real-time eligibility checks are quick, reliable, and inexpensive. They return benefits data – like coverage status, deductibles, coinsurance, and plan dates – in seconds and cost pennies per transaction.

Eligibility checks can return any benefit data that a payer chooses to include. Some payers return complete data in every response. Many dental payers don’t.

To fill in those gaps, many teams turn to scraping payer portals. Scraping is quick to roll out and can surface data that checks can’t. But those scrapers require ongoing maintenance.

We’ve worked with dozens of teams building dental eligibility tools. The teams that scale best take a hybrid approach: They start with real-time checks and scrape only when it matters.

Why scraping alone doesn’t scale

In small doses, scraping is effective, but it comes with technical debt. That debt compounds faster than many teams expect. It shows up in a few ways:

1. It’s slow.
Real-time eligibility checks typically respond in 1-3 seconds. Scraping can take 10 times longer. The scraper must log in, navigate pages, and wait for load times. Multiply that by thousands or millions of requests.

Concurrency helps, but only to a point. Most portals rate-limit traffic. Some payers require monthly minimums for automated access. Those minimums often cost more than using their APIs.

2. It’s brittle.
Scrapers frequently break. Portals can make unannounced layout changes or require multi-factor authentication (MFA). When that happens, there are no structured errors. Just screenshots and HTML.

You can’t rely on retry logic. The failure modes are too varied and too unpredictable. That means near-constant patching.

3. It burns engineering time.
One CTO told us they had 60+ scrapers in production. Their best engineers weren’t building the product – they were fixing broken scrapers. Most of the data that those scrapers collected was available in eligibility checks with much lower cost and maintenance overhead.

Scraping isn’t just hard to maintain. For most teams, it’s rarely necessary.

The 85/15 rule

Teams that we’ve seen scale portal scraping follow a simple pattern:

  • They use real-time eligibility checks alone for about 85% of verifications.

  • For the remaining 15%, they combine checks with scraping or payer calls to fill in missing details.

Instead of scraping everything, they focus on high-impact payers: ones that don’t return key data in eligibility responses and that account for a large share of volume or revenue risk. They also use eligibility checks to verify scraper output and catch failures early.

What eligibility responses cover

To put the 85/15 rule to work, you need to know what data real-time eligibility checks reliably return.

We analyzed responses from major dental payers – including MetLife, Delta Dental, and UnitedHealthcare – to find out. Here’s what we saw:

Dental benefits information

In real-time eligibility response?

Requires scraping?

Active coverage

✅ Yes

🚫 Rarely

Coverage dates

✅ Yes

🚫 Rarely

Deductible

✅ Yes

🚫 Rarely

Co-pay

✅ Yes

🚫 Rarely

Coinsurance

✅ Yes

🚫 Rarely

Service History

⚠️ Sometimes

✅ Yes

Downgrade Logic

⚠️ Sometimes

✅ Yes

Frequency Limits

⚠️ Sometimes

✅ Yes

Missing Tooth Clause

❌ No

✅ Yes

Provider network status

❌ No

✅ Yes (may require a call to the payer)

Start with checks

Scraping is a valuable tool. It’s just not something you want to rely on for every verification.

If you’ve already built out scraping, keep it. But try running eligibility checks first. They may cover more than you expect, which can lower costs and reduce engineering effort.

Get started

Try Stedi’s real-time eligibility API for free in our sandbox. You’ll get instant access to run mock checks.

When you’re ready for production, request a free trial. Most teams are up and running in under a day.

Aug 5, 2025

Products

Today, Stedi announces the release of its Model Context Protocol (MCP) server. AI agents can use Stedi’s MCP server to run eligibility checks and search payers.

A third of Stedi’s customers are generative AI companies building AI agents for revenue cycle management (RCM). Until now, connecting those agents to Stedi’s eligibility API meant writing custom integration code or copying parts of Stedi’s docs into instructions.

Stedi’s MCP server changes that.

It gives agents plug-and-play access to Stedi’s Real-Time Eligibility and Search Payers API endpoints, along with built-in guidance for common errors. You can connect your agents without having to write integration code or copy documentation.

I connected our agent to Stedi’s MCP server in minutes. Updated the instructions, and it started running eligibility checks right away.
- Rambo Wu, Engineering at Stratus

How it works

Your AI agent connects to the server using an MCP client. Once connected, the server gives your agent access to two types of capabilities – tools and prompts – as defined by the MCP server spec.

Tools perform specific actions, like running an eligibility check. They’re thin wrappers that let your agent invoke Stedi APIs – currently, the Real-Time Eligibility Check API endpoint and the Search Payers API endpoint.

Prompts help your agent decide what to do next, including how to recover from a failed check.

The following diagram show how your agent can use the MCP server to connect to Stedi’s APIs.

Stedi MCP server diagram

How to connect

The MCP server is a Streamable HTTP MCP server hosted at https://mcp.us.stedi.com/2025-07-11/mcp.

To connect your agent, add this configuration to your agent’s MCP client:

{
  "mcpServers": {
    "stedi-healthcare": {
      "type": "http",
      "url": "https://mcp.us.stedi.com/2025-07-11/mcp",
      "headers": {
        "Authorization": "YOUR_STEDI_API_KEY"
      }
    }
  }
}

Replace YOUR_STEDI_API_KEY with your actual Stedi API key.

Tools

The server provides two tools:

Prompts

The server provides prompts to help your agent recover from common errors. The prompt instructions cover the most common recoverable scenarios we see in production:

  • How to handle common errors

  • When to retry eligibility checks

  • What to do when a payer isn't found

You and your agent stay in control of executing follow-up actions, such as troubleshooting and retries.

Tip: Many LLM clients skip MCP prompts unless explicitly instructed to read them. You'll often get better results by adding “Read the prompts from Stedi's MCP server” to the beginning of your agent’s instructions.

Example usage

You can instruct your agent to use the MCP server to run eligibility checks.

For example, if you’re building a voice agent, you might give your model an instruction like:

Read the prompts from Stedi's MCP server.Then use Stedi’s MCP server to check
the patient’s eligibility programmatically before making a telephone call to
the payer. Only call the payer if the response doesn’t include the benefits you need

You can also use the MCP server to perform one-off checks using MCP clients. If you're using a third-party tool like Claude or ChatGPT, follow your organization’s data handling policies to ensure that you stay compliant with HIPAA and other applicable requirements. For example, your organization likely requires a BAA with any third-party tool before using the tool with Stedi’s MCP server.

When to use the MCP server

The MCP server excels are one-off eligibility checks, especially when your agent needs to retrieve coverage data in real time.

For example:

  • If you're building a voice agent that calls payers for benefits, use the MCP server to check eligibility first. Call the payer only  if the response doesn’t have what you need.

  • If you're building an RCM workflow agent, use Stedi’s MCP server to validate a patient’s coverage before scheduling an appointment or submitting a claim.

For bulk eligibility checks, use our eligibility APIs directly. For tips, see When to use batch eligibility checks.

For benefit interpretation, like determining remaining visits, you'll want to layer your own logic on top, just as you would with our APIs.

Performance

The MCP server adds minimal overhead to our APIs. You’ll get the same fast response times with added intelligence for your agents.

Pricing

The MCP server is available on all paid Stedi plans at no additional cost beyond those for related API calls.

As with our APIs, there’s no charge for non-billable requests. See Billing for eligibility checks.

Security and compliance

The MCP server uses the same security model as our existing APIs, including TLS encryption and API key authentication. If you're using a third-party tool to interact with the MCP server, reach out to your security team and legal counsel to ensure you have the appropriate safeguards in place.

Try it out

If you’re a Stedi customer, you can start building with the MCP server today.

If you’re not, request a trial. We can get you up and running in less than a day.

Jul 31, 2025

Guide

Real-time eligibility checks are built for real-time scenarios: A patient's on the phone. Someone's at the front desk. You need an answer in seconds.

But if you’re checking coverage for upcoming appointments or refreshing eligibility for entire patient panels, the timeline shifts. You don’t need answers in seconds. You need them in hours.

Stedi’s real-time eligibility API works for these bulk use cases until you’re running thousands or millions of checks at once. That’s when teams usually start writing separate logic for large batches, like queuing requests and handling long-running retries.

Stedi's batch eligibility API handles that for you. You can submit multiple checks in one request. Stedi queues and retries them automatically. And batch checks run in a separate pipeline, away from your real-time traffic.

When to use real-time vs. batch checks

Real-time checks are the default. Use them for:

  • When a patient at the desk

  • Verification before a visit

  • Fast feedback loops while testing or debugging

  • Smaller sets of bulk checks

  • Any time-sensitive eligibility need

As your volume grows and you find yourself building custom logic to support it, use batch checks for bulk workflows that aren’t time sensitive:

  • Monthly or weekly coverage refreshes

  • Upcoming appointments

  • Sets of thousands or millions of checks that can run in the background

  • Any workflow where a timeline of minutes to hours works

Most teams start by using real-time checks for everything. They add batch checks when they start running several thousands of time-insensitive checks at once. Until then, using real-time checks is simpler.

Run a batch check

You submit batch checks using the Batch Eligibility Check API endpoint or a CSV upload. Each check in the batch uses the same fields as real-time requests. You can track individual checks in a batch using submitterTransactionIdentifier.

curl --request POST \
  --url "https://manager.us.stedi.com/2024-04-01/eligibility-manager/batch-eligibility" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "march-2024-eligibility-batch",
    "items": [
      {
        "submitterTransactionIdentifier": "ABC123456789",
        "controlNumber": "000022222",
        "tradingPartnerServiceId": "AHS",
        "encounter": {
          "serviceTypeCodes": [
            "MH"
          ]
        },
        "provider": {
          "organizationName": "ACME Health Services",
          "npi": "1234567891"
        },
        "subscriber": {
          "dateOfBirth": "19000101",
          "firstName": "Jane",
          "lastName": "Doe",
          "memberId": "1234567890"
        }
      },
      ...
    ]
  }'

The response includes a batchId you can use to check results:

{
  "batchId": "01928d19-df25-76c0-8d51-f5351260fa05",
  "submittedAt": "2023-11-07T05:31:56Z"
}

Get batch results

Use the Poll Batch Eligibility Checks API endpoint to poll the batchId:

curl --request GET \
  --url "https://manager.us.stedi.com/2024-04-01/eligibility-manager/polling/batch-eligibility?batchId=01928d19-df25-76c0-8d51-f5351260fa05" \
  --header "Authorization: <api_key>"

You can start polling immediately. After the initial poll, use exponential backoff with jitter. Start at 2 minutes and approximately double the wait between polls, up to 8 hours.

The endpoint returns results incrementally. The response's items array contains full 271 eligibility responses in the same JSON format as real-time checks. Failed checks appear alongside successful ones. You can debug failed checks using Stedi’s Eligibility Manager

You can track batch completion by counting returned results. You can also match submitterTransactionIdentifier values for each check.

Batch processing times

Most batches complete in 15-30 minutes. If a check in the batch fails due to payer connectivity, Stedi retries the check for up to 8 hours.

Pricing

A batch check costs the same as the equivalent number of real-time checks. For example, running a batch of 500 checks costs the same as running 500 real-time checks.

There’s no charge for non-billable checks in a batch. For details, see Billing for eligibility checks.

Get started

If you’re not currently using batch checks but are considering building logic for bulk eligibility, test the pattern with a small batch, then scale up.

Batch eligibility checks are available on all paid Stedi plans. If you don’t have a paid account, request a free trial.

Jul 29, 2025

Spotlight

A spotlight is a short-form interview with a leader in RCM or health tech. In this spotlight, you'll hear from Justin Liu, Co-founder and CEO of Charta.

What does Charta do?

Charta is a proprietary AI-powered platform that optimizes medical billing and coding workflows.

By running a pre‑bill review on every chart, Charta pinpoints documentation gaps, uncovers missed revenue opportunities, and heads off potential denials – boosting accuracy and compliance in one pass.

Our customers typically see up to a 15.2% lift in RVUs per encounter and an 11% increase in revenue, all delivered with a guaranteed ROI and less administrative drag on clinical teams.

How did you end up working in health tech?

My co-founder and CTO, Scott Morris, and I were fortunate to be part of a team at the forefront of AI infrastructure innovation at Rockset, which ultimately became OpenAI’s first ever product acquisition. We saw an opportunity to take what we had learned and apply it to a space where we could drive real, tangible change. AI infrastructure is an exciting field, but we wanted to build something where AI didn’t just optimize processes – it fundamentally improved outcomes.

Healthcare stood out because of the sheer scale of inefficiencies, particularly in administrative tasks like patient chart reviews and medical billing. What really struck us was that patient charts are essentially the data layer of the entire healthcare system – every clinical decision, every billing code, every compliance check is rooted in chart documentation. Yet, reviewing these charts remains a painfully manual process, bogging down providers and leading to lost revenue, denied claims, and time away from patient care.

Instead of optimizing AI for AI’s sake, we wanted to use our expertise to solve real-world problems in a way that directly impacted people’s lives. That perspective – combined with spending a year earning our medical coding credentials and speaking with over 100 healthcare professionals – helped us build a solution that directly addresses the root problems rather than just iterating on legacy systems.

One of those early conversations was with Dr. Caesar Djavaherian, co-founder and former Chief Medical Officer of Carbon Health. The challenges we were tackling – especially around documentation and billing – were so familiar to him that he not only invested in Charta, but ultimately joined the team as our Chief Medical Officer.

How does your role intersect with revenue cycle management (RCM)?

Charta sits squarely in the middle of the RCM stack – between clinical documentation and claim submission – by running pre‑bill AI audits that boost revenue integrity and slash denial risk. 

As CEO, I’m responsible for turning those RCM pain points into product advantages: I spent my first year interviewing medical professionals, diagramming every hand‑off from charge capture to payment posting, and hard‑coding those insights into our roadmap. 

Today we still meet often with rev‑cycle leaders and use real‑world payer & provider feedback to steer cutting-edge model training and new feature prioritization. 

What do you think RCM will look like two years from now?

CMS has widened its Medicare Advantage audit program, signaling increased scrutiny of billing practices, while new federal guidance from HHS and CMS outlines frameworks for responsible AI use in administrative systems. Together, these shifts are laying the groundwork for a new phase of operational automation across the healthcare sector.

For decades, healthcare operations have lagged behind clinical innovation. The revenue cycle – arguably the financial backbone of the healthcare system – remains manual, fragmented, and error-prone. Despite years of outsourced labor and legacy tools, the problems haven’t been solved – they’ve multiplied.

What’s different now is timing. Health systems are facing unprecedented margin pressure, workforce shortages, and growing regulatory scrutiny. At the same time, there's broad consensus that AI is not just viable – it’s urgently needed. Decision-makers are ready to buy. Infrastructure that can deliver step-change improvements, not just incremental gains, is finally in reach.

Charta already reviews 100% of charts in real time; the next wave is about extending that same AI-powered automation across the rest of the revenue cycle. 

Jul 28, 2025

Company

It’s easy to ruin your company’s support by trying to scale it too early – not carelessly or intentionally – but by following the standard support playbook.

That standard playbook – async tickets, chatbots, and rigid KPIs – is meant to improve the customer experience. In practice, it often makes it worse.

Stedi is a healthcare clearinghouse for developers. We sell APIs to highly technical buyers, and we differentiate based on our technology and products. But when we ask customers why they chose us – what we do better than anyone else – they almost always say the same thing:

Support.

As one customer put it:

"I wish I could tell you that I chose Stedi because you have the best APIs or documentation, but the reality is that I went with you because you answered every message I sent within 15 minutes.”

We hear versions of that all the time. It’s not because we’ve mastered support best practices. It’s because we ignore them. Instead, we focus on one thing: solving the customer’s problem as quickly and completely as possible.

When you do support this way – the way that supposedly doesn’t scale – something unexpected happens:

You fix the root causes of customer issues. When you fix root causes, fewer things break. And when fewer things break, you avoid the type of problems that the standard support playbook was designed to manage in the first place.

Why most customer support is bad

Bad customer support is everywhere: useless chatbots, long wait times, unhelpful canned responses. Almost every company offers support. So why is support usually bad?

It’s because typical support tools and practices aren’t designed to help customers. They’re designed to make support easier at scale.

Most support systems assume your company is drowning in support requests. That might be true for some. But, early on, most companies aren’t. So when you implement these systems too soon – to solve a problem you don’t have – you don’t make the customer experience better. You make it worse.

The scale trap

Most teams start with hands-on support. Founders talk to customers, fix their problems, and improve the product. Then they make their first support hire – and assume what they’ve been doing will no longer work. This is typically when companies adopt traditional support systems.

Regardless of intent, those systems cause the company’s incentives around support to shift. You build systems that make support easier to manage at scale – all your tools are optimized for it.

That’s the scale trap.

For example, a typical support workflow looks like this:

  1. The customer creates a ticket.
    A ticketing system makes support easier to manage. Agents can organize work into queues and juggle dozens of conversations at once.

    But those same ticketing systems make it harder for customers to reach out. To get help, customers have to find the (often buried) support portal, create an account, learn a new interface, file a ticket, and, finally, get in line – just when things are already going wrong for them.

  2. Then you make them wait.
    Support teams use queues to manage their workload. Queue-based metrics also help you staff support predictably.

    For the customer, a queue means waiting. You need a quick answer to finish your work. Two hours later, you're still waiting. So customers learn: Don't ask until you're desperate.

  3. Then you don’t fix their problem.
    Support KPIs track things like closed ticket counts and time to resolution, not solved problems. There is no “root causes fixed” metric. What gets measured gets done, so agents master quick workarounds and band-aid fixes.

    For customers, that means they wait days to get shallow responses and temporary fixes. It also means they end up running into the same issues over and over again. The root cause isn’t fixed. The tickets are closed, but the problems stay. Over time, customers stop trusting you to fix their problems at all – and stop reaching out.

If you look at typical support KPIs, this setup works: There are fewer tickets per customer and faster resolution times. But the metrics don’t answer two important questions:

  1. Is the root cause of the customer’s problem really solved?

  2. Does the customer trust you enough to bring you their next problem?

The job of customer support

Support exists to address customer pain. When something breaks, people want the same things anyone in pain wants:

  1. To stop the pain.

  2. To fix the root cause so it doesn’t happen again.

  3. If that’s not possible, to manage the symptoms.

  4. If that’s not possible, to get a direct, honest explanation so they can plan around it.

And they want those things fast. When you’re in pain, you want it to be over as soon as possible.

For support teams, speed isn't just about providing relief. It’s also about empathy. When you respond quickly to someone in pain, it shows you care.

Fixing the scale trap isn’t about tools – it’s about where you focus.

Delivering good support

At Stedi, the job of support is to eliminate customer pain as quickly and thoroughly as possible.

Most people brace for delays and friction when they contact support. Instead, we respond quickly and over-index on being helpful. The contrast often surprises them.

Any company can deliver good support. It just takes hard work, consistency, and the willingness to do things differently. Here’s what that looks like for us:

Work where your customers work.
Every customer gets a dedicated Slack or Microsoft Teams channel. No tickets, no help portals. Slack and Teams are already the customer’s virtual office. When they need help, they don't have to go anywhere. We're already there.

Respond quickly.
We’ve had customers say they were surprised to get a real human response within minutes. We don’t use bots or scripts. Just people who care and can help.

Anyone can help.
Any employee can answer any question in any customer channel. Engineers who built the feature. PMs who designed it. Even the CEO. We have customer success engineers, but support is everyone's job.

If something’s broken, fix it.
Everyone is empowered to make things better. If you have the answer, share it. If something's confusing, explain it and update the docs. If there's a bug, fix it. Don’t wait or assume someone else will do it.

Turn up the volume.
We want customers to bring us more problems, not fewer. Our best customers are often the loudest. They’re pushing our product to the limit, and the questions they ask make our product better.

Go deep.
We tackle questions most healthcare clearinghouses won't touch. We debug errors with customers. We explain complicated responses line by line.

No bots.
We use AI everywhere at Stedi – but not to deflect customer questions. When you need help or have an emergency, you want a human who understands your problem, not a chatbot suggesting knowledge base articles.

Be honest and direct.
When something's broken, we say so. We tell customers why it broke and when we'll fix it.

Do what you say.
If we commit to something, we give customers a date and hit it. If we can't make the deadline, we tell them early. 

Treat every customer the same.
We don’t sell support. Every Stedi customer gets the same level of support, whether you're a two-person startup or processing millions of transactions. There are no support tiers or upsells.

Hire the best people you can.
We hire smart, technical people to do support, and we pay them well. They work hard, understand systems, and can go deep into customer problems.

We didn't start with this playbook. When we started, we were just doing the obvious: trying to help.

Later, we found that our support model has a powerful side effect: it made our product much better.

The support-product flywheel

Good support means fixing things fast. More importantly, it means fixing root causes. We never want customers to hit the same problem twice.

To fix root causes, you have to understand the real problems. Where does our product break? Where do the docs confuse people? Where are users getting stuck?

You can't fix what you don't see, but if you respond quickly and fix things, customers will start to tell you where to look. They’ll point out issues. They’ll trust you to address their pain. And because they trust you, they reach out more. The feedback gets better. The signal gets stronger.

When you fix real problems, your product improves. Fewer customers hit snags. Your support team stops firefighting and starts building. They ship tools, chase harder problems, and spend less time repeating themselves. They’re happier because they’re doing work they’re proud of.

A better product with better support attracts more customers, including ones who push your product to the limit. They find new edge cases. Which you fix. Which makes the product better. Which attracts more customers.

That's the flywheel.

When the flywheel is in effect, the most important support-related metrics can't be measured in CSAT scores or ticket counts. They show up in net revenue retention, churn, and referrals.

But does it scale?

"Your support model won't scale. It works fine with a few customers, but it won’t work past that."

We hear this a lot.

When we were just getting started with our first customers, people would tell us that our approach wouldn’t scale past 10. When we had 10 and didn’t have any issues, people told us that it works fine with 10 customers, but it wouldn’t scale to 100 customers. We keep waiting for the wall, but it still hasn't shown up – even with hundreds of customers.

Why? Our guess is the flywheel is in full effect.

But, to be fair, we have some other things working in our favor. Stedi is built for developers. Our customers are smart and often technical. When they reach out to us, they've done their homework. They ask specific questions. They test things themselves. Smart customers asking real questions keeps our support manageable, even as we grow. Our model might not work if half our tickets were "How do I log in?"

People also assume our support team must cost a fortune. It doesn't. We pay our folks very well, and it pays off in spades. While good-enough support prevents churn, great support encourages customers to use our products even more. That makes for happier customers, and happy customers are our best salespeople.

Will we need to change certain aspects of our support eventually? Probably. But the idea that great human-to-human support can't scale assumes that every customer needs the same amount of help forever.

When you solve problems instead of managing them, support volume doesn't grow linearly with your customer count.

The hard choice

Here’s our pitch. If you run a support org, you have two options:

The safe path.
Buy the ticketing system. Track deflection rates. Answer customer questions with AI slop. This is what most companies do. No one gets fired for it. And your customers might not even complain – they’ve been trained to expect bad support. But no one will rave about it.

The harder path.
Ignore what everyone else does. Do the obvious thing: Be fast, be helpful, fix real problems. Don’t worry about whether it scales.

Our suggestion: Pick the harder path. When done right, support can become your company’s biggest competitive advantage.

Your competitors can copy your features. They can match your prices. But it takes courage to do support in a way that isn’t supposed to scale. Your customers will notice. 

See it for yourself

If you’re evaluating healthcare clearinghouses, give Stedi a try.

You can request a free trial and experience our support firsthand.

Jul 30, 2025

Spotlight

A spotlight is a short-form interview with a leader in RCM or health tech. In this spotlight, you'll hear from Dakota Haugen, Eligibility Lead at Grow Therapy.

What does Grow Therapy do?

Grow Therapy is a leading hybrid mental health company delivering both in-person and virtual therapy and medication management. With a nationwide network of over 20,000 rigorously vetted providers, Grow connects individuals with high-quality, affordable mental healthcare – often at little to no cost through insurance.

Currently available in all 50 states and accessible to 180 million Americans through their health plans, Grow is on a mission to make high-quality mental health care accessible and affordable to all. By removing barriers to care and empowering independent providers, Grow is transforming how people access and experience mental health support. Backed by top investors including Sequoia Capital and Goldman Sachs Alternatives, Grow reached a total of $178 million in funding in 2024 following a Series C raise of $88 million. The round was led by Sequoia and supported by PLUS Capital, alongside artists and athletes such as Anna Kendrick, Lily Collins, Dak Prescott, Joe Burrow, Jrue Holiday, and Lauren Holiday. They joined existing investors Transformation Capital, SignalFire, and TCV – underscoring a strong belief in Grow’s mission to become the most trusted destination for mental healthcare.

Why Grow?

  • Accessible & Affordable: More than 90% of visits are insurance-covered; many clients pay just $0–$20 per session.

  • Diverse & Inclusive Care: Providers reflect a wide range of identities, languages, and specialties – including trauma-informed, mindfulness, and faith-based care.

  • Tech-Enabled Progress: Industry-leading Measurement Informed Care (MIC) platform and AI-assisted tools ensure clients and providers track meaningful progress.

  • Trusted Relationships: 95% of clients report a strong therapeutic alliance; NPS of 85 signifies exceptional client satisfaction.

  • Support for Providers: Grow removes the administrative and financial barriers to starting a private practice, empowering clinicians to focus on care.

How did you end up working in health tech?

I’ve always known that I wanted to help people – especially by addressing barriers to healthcare and creating pathways to access. My career began in the healthcare space as a medical biller, where I saw firsthand just how complex and discouraging it can be for individuals to access care. I realized I wasn’t even well-versed in navigating my own insurance, and that experience fueled my drive to better understand the system and make it easier for others.

Working in billing gave me insight into the structural challenges that stand between people and the care they need. I became passionate about creating a seamless, informed experience for patients. That passion deepened when I began my own mental health journey and experienced the benefits of care personally.

This led me to pivot into mental health, where I found an intersection of purpose and personal connection. When I discovered Grow Therapy, I immediately aligned with the mission. Grow’s commitment to both providers and clients resonated deeply with me – it’s the kind of meaningful, impactful work I’ve always aspired to do.

How does your role intersect with revenue cycle management (RCM)?

As the Eligibility Lead, my role sits at the front line of RCM, where I help ensure that clients are accurately verified for insurance coverage before beginning care. This early step is foundational to the entire revenue cycle – by identifying coverage details, confirming benefits, and addressing potential issues upfront, we minimize claim denials, reduce billing delays, and support timely reimbursement for our providers. But eligibility is more than just a financial checkpoint – it’s a critical access point for care. When done right, it removes barriers that might otherwise prevent someone from starting or continuing treatment. It gives clients clarity and confidence about what their insurance covers and what to expect financially, creating a smoother, more equitable path to mental healthcare. My work in eligibility helps lay the groundwork for both a positive client experience and a sustainable, efficient billing process.

What do you think RCM will look like two years from now?

I believe RCM will continue to evolve into a more specialized, data-driven function – especially within mental health. As the industry matures, I see RCM playing a pivotal role in not just operational efficiency, but also in driving financial outcomes that reflect the value of care. With the integration of AI and automation, particularly in areas like eligibility verification, we’ll be able to optimize workflows, reduce manual errors, and deliver real-time insights that help clients get approved and connected to care faster.

RCM will increasingly align with performance-based and value-driven models, where financial success is tied to meaningful health outcomes. This will allow individuals to see clearer connections between what they’re paying for and the results they experience – ultimately building more trust in the mental healthcare system. As we continue to remove financial and administrative barriers, RCM will help make mental health care more accessible and affordable, reinforcing the overall client experience and encouraging more people to seek and sustain the care they need.

Jul 25, 2025

Guide

It’s 3 AM. You’re on-call and just got paged. A customer – a 24-hour hospital in Des Moines – can’t verify patient coverage. Every eligibility check they send is failing.

You’re staring at a cryptic AAA error. More failures, now from other customers, are starting to come in. Account management wants answers.

You don’t know where to start. Is it your system? Your clearinghouse? The payer?

We’ve been there. Healthcare billing is a distributed system with multiple single points of failure. Even if you do everything right, things break: a payer goes down, a provider enters the wrong data, a request times out.

Stedi’s helped hundreds of teams debug eligibility issues.

This guide outlines the real-world scenarios where we’ve seen eligibility checks fail – and what we’ve found works. You’ll find common failure patterns, diagnostic tips, and how Stedi helps resolve each scenario.

For more tips, check out Eligibility troubleshooting in the Stedi docs.

You’re getting AAA 42 errors – a payer is down

Payer outages are common and often opaque. When you get an AAA 42 (Unable to Respond at Current Time) error or see a spike in timeouts for a specific payer, it usually means one of the following:

  • The payer is having intermittent issues. They’re down for now but may be back up shortly.

  • The payer is having a prolonged outage. This can be because of planned maintenance or a system failure on their end.

  • An intermediary clearinghouse that connects to the payer – not the payer itself – is down.

These scenarios return the same errors, so it's hard to tell them apart.

What you’ll see

  • AAA 42 errors from the payer

  • A spike in timeouts for requests to the payer.

  • Requests that fail even after retries

  • Increased latency on failures

An example AAA 42 response:

{
 "errors": [
    {
      "field": "AAA",
      "code": "42",
      "description": "Unable to Respond at Current Time",
      "followupAction": "Please Correct and Resubmit",
      "location": "Loop 2100A",
      "possibleResolutions": "This is typically a temporary issue with the payer's system, but it can also be an extended outage or the payer throttling your requests (https://www.stedi.com/docs/healthcare/send-eligibility-checks#avoid-payer-throttling)."
    }
  ],
  ...
}

What helps
Whether it’s a full-blown outage or just a flaky payer, the remediation steps are the same:

  • Check with Stedi to confirm the issue.

  • If you’re using Stedi’s Eligibility API, retry the requests with exponential backoff. For suggested retry strategies, see Retry strategy in the Stedi docs.

    This is the most important remediation step. Most Stedi customers simply give up on retries too early. You’re not billed for eligibility checks that return a AAA 42 error, so there’s little incentive not to retry.

Avoid using cached or stale responses. This can cause false positives, which ultimately lead to denied claims.

What Stedi does
Where possible, Stedi uses multiple, redundant payer routes with automatic failover and retries to minimize the impact of intermediary clearinghouse outages. In many cases, our customers don’t notice an outage.

If the payer itself is down – or their designated gateway clearinghouse is down – there's no workaround. No clearinghouse can get traffic through. For widespread outages, we reach out to the payer or the gateway clearinghouse – and we notify affected customers in Slack or Teams.

A patient says they're covered, but eligibility checks return "Subscriber/Insured Not Found"

Payers can only return an eligibility response if the request matches exactly one member in their system.

If they can’t find a match, they return an AAA 75 (Subscriber/Insured Not Found) error. That usually means one of two things:

  • The patient isn’t in the payer’s system.

  • The patient information in the request doesn’t match what the payer has on file

What you’ll see
An AAA 75 error in the eligibility response. For example:

{
  "subscriber": {
    "memberId": "123456789",
    "firstName": "JANE",
    "lastName": "DOE",
    "entityIdentifier": "Insured or Subscriber",
    "entityType": "Person",
    "dateOfBirth": "19001103",
    "aaaErrors": [
      {
        "field": "AAA",
        "code": "75",
        "description": "Subscriber/Insured Not Found",
        "followupAction": "Please Correct and Resubmit",
        "location": "Loop 2100C",
        "possibleResolutions": "- Subscriber was not found."
      }
    ]
  }
}

You may also see:

  • AAA 15 – Required Application Data Missing

  • AAA 71 – Patient DOB Does Not Match That for the Patient on the Database

  • AAA 73 – Invalid/Missing Subscriber/Insured Name

What helps
In your app or support flow, prompt the provider to:

  • Check that the member ID matches what’s on the insurance card. Include any prefix or suffix.

  • Double-check the patient’s name and date of birth (DOB).

  • Use the full legal name. For example, “Robert,” not “Bob.” Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings. Ask about recent name changes.

  • If the details look correct, make sure you’re sending the request to the right payer.

Note: Some payers allow a match even if either the first name or DOB is missing (but not both). If you’re not sure about the first name’s spelling, omitting it may improve your chances of a match. Most payers aren’t strict about matching first names exactly.

What Stedi does
Stedi makes proactive edits to eligibility requests for certain payers that are known to respond incorrectly to specific commonly accepted input formats. These edits increase the likelihood of a valid match.

Stedi also offers real-time support for eligibility issues over Slack and Teams. We’ve helped customers quickly diagnose data mismatches by highlighting exactly where the payer couldn’t find a match. Our support team responds within minutes and can spot common formatting issues that might otherwise take hours to debug.

You get a valid eligibility response, but it’s missing benefits

This usually comes down to the Service Type Code (STC) in the request. In requests, the STC tells the payer what kind of benefits you’re asking for, like medical, dental, or vision.

STCs are standardized, but payers don’t have to support all of them. Some payers only respond to a small set. Payers also don’t return benefits for STCs not covered by the related plan.

The only way to know which STCs a payer or plan supports is to test. For tips, check out How to pick the right STC (when there isn’t one).

What you’ll see
If you’re using Stedi’s Eligibility API, most benefit details come back in the benefitsInformation object array. Each object in the array has a serviceTypeCodes field.

  • If no benefitsInformation objects include the requested STC, the payer or the plan probably doesn’t support the STC.

  • If the STC shows up but the details you need aren't there, be sure to check any other returned STCs. Some payers return patient responsibility information under STC 30, while others may use a more specific STC code. If you still can’t find the details, the payer may not return that data in eligibility responses. 

If the response includes your STC but still lacks enough detail to determine whether the service is covered and the patient responsibility, you may need to get the missing information by calling the payer or using their portal. Some teams do this programmatically with an AI voice agent or portal scraping.

What helps

  • Check that you're using the right STC.

    For a cheat sheet, see How to pick the right STC. For a full list, see Service Type Codes in the Stedi docs.

  • Re-run the request with a baseline STC (like STC 30 for medical or STC 35 for dental).

  • Test different STCs against the baseline to see what each payer supports.

What Stedi does
We’ve tested thousands of payer responses. In addition to our How to pick the right STC blog, we maintain internal payer-specific guidance on which STCs return which benefits. If a payer omits key details, we help teams explore alternatives – whether that’s trying another STC, making a payer call, using the payer portal, or a mix.

The patient doesn’t know their payer or member ID

Eligibility checks only work if the payer can match the request to one specific member. Some payers can match using just name and date of birth. Others require a member ID. All eligibility checks require a payer ID.

What you’ll see
If you don’t know the payer, you can’t submit a check – you don’t know which payer to send it to.

If you leave out the member ID and the payer requires it, you’ll likely get an AAA 72 (Invalid/Missing Subscriber/Insured ID) error.

What helps
Use insurance discovery. It lets you search for active coverage using basic demographic info like name, DOB, ZIP code, and SSN.

Success rates vary depending on the provided patient data – from 25% with basic info to 70%+ with more comprehensive details. For tips, see How to use insurance discovery.

What Stedi does
Getting insurance discovery right out of the gate can be tough. If you’re getting started, our team can help you set up workflows and provide prompts to ensure you get the most out of it.

How would I know if Stedi is down?

Outages are extremely rare for Stedi. We run entirely on AWS with a redundant, multi-region cloud architecture, and we have automated systems for deployment safety. We also maintain redundant routes for payers wherever possible, so a single intermediary clearinghouse or even a widespread payer outage won’t take us down.

What you’ll see

  • A notice on our status page

  • 500 HTTP status code errors when running eligibility checks.

What helps
Contact Stedi.

What Stedi does
If an outage does happen, we reach out to affected customers immediately in Slack and Teams. We also update our status page. You’ll get clear status updates, including timelines and next steps.

Behind the scenes, our team works across regions to fix the issue fast. Once it’s resolved, we share what went wrong with customers and what we’re doing to make sure it doesn’t happen again.

Don’t debug alone

Eligibility failures happen. With so many players – payers, intermediaries, gateways, providers – it’s not a matter of if, but when one makes a mistake. When your eligibility checks fail, your clearinghouse should help.

If you’re seeing one of these issues – or something new – reach out. We can help.

Jul 24, 2025

Guide

When you're evaluating a healthcare clearinghouse, there's one question that matters most:

Do they support my payers?

If you’re looking at Stedi, the Stedi Payer Network is how you check. It’s a public, searchable index of Stedi’s supported payers.

This short guide explains how to use the Payer Network and answers common questions about Stedi's payer coverage.

The Stedi Payer Network

The Payer Network includes every payer Stedi supports – currently 3,500+. Stedi covers virtually every U.S. healthcare payer that accepts electronic claims and eligibility checks.

If one’s missing:

  • It might be listed under a different name. Reach out and we’ll check.

  • If we truly don’t support the payer, submit a request to add it. We can usually connect to new payers quickly.

Some payers can’t be supported at all – by us or other clearinghouses. This may be because they’re low volume, mail-only, or only support portal-based operations.

What each Payer Network entry includes

Each payer entry includes:

  • Payer name.

  • Primary payer ID, plus historical and synonymous aliases. Payer ID aliases let you use the payer IDs you already use with no additional mapping.

  • Supported transaction types, like 270/271 eligibility checks and 837P/837D/837I claim submissions.

  • Transaction enrollment requirements for each transaction type

You can search payers by name or payer ID. You can also filter by supported transaction types.

Filters use AND logic – only payers that support all selected types will appear. For example, if you filter for 270/271 eligibility checks and 837P claim submissions, you’ll only see payers who support both.

How to check Stedi’s supported payers

There are three ways to check if your payers are covered by Stedi:

  1. Send us your payer list.
    This is typically the easiest and fastest way to check. We’ll run a script to check for you and manually review the results. You can send us a message to start.

  2. Download the full CSV.
    You can download our full payer list as CSV from the Payer Network site.

  3. Use the Payers API.
    If you're a Stedi customer on a paid plan, you can use the Payers API to search and fetch our payers programmatically.

Do I need to map my current payer IDs?

No. We support the payer IDs you already use – no mapping required.

Behind the scenes, we maintain an internal mapping of historical and current payer IDs. If a payer changes its ID or merges with another, we’ll handle it automatically.

Do I need a backup clearinghouse?

In most cases, no.

Stedi covers virtually every U.S. healthcare payer. Where possible, we set up redundant routes to each payer with automatic failover. We also use a multi-region cloud infrastructure for redundancy.

How does Stedi work with other clearinghouses?

All clearinghouses rely on other clearinghouses. Some payers have exclusive clearinghouse relationships. For example, Optum is the designated gateway clearinghouse for UnitedHealthcare. No matter which clearinghouse you use, all UnitedHealthcare traffic goes through Optum.

To offer the broadest payer coverage, Stedi integrates with every major clearinghouse. We also build direct connections to payers wherever possible.

Stedi uses multiple routes, direct connections, and built-in redundancy to provide fast, reliable payer access. We’re not a wrapper for any other clearinghouse. As a Stedi customer, you don’t need to worry about which path we take. We pass back full payer responses without dropped fields or lossy normalization.

Next steps

Once you've verified our payer coverage, the typical next step is to explore our docs. When you're ready, you can request a free trial. Setup typically takes less than a day.

If there's a specific payer giving you trouble – whether with claims, enrollments, or eligibility – reach out. We may be able to help.

Jul 21, 2025

Guide

Most payers expect a Service Type Code (STC) in eligibility requests. It tells the payer what kind of benefits you want back – mental health, urgent care, vision, and so on.

But not every healthcare service maps cleanly to an STC.

Take medical nutrition or ABA therapy. There’s no obvious STC. Which one should you use?

The answer depends on the payer. The only way to know which STCs a payer supports – and return the benefits you need – is to test them.

This guide shows how to test for STC support and includes a cheat sheet of STCs to try for services that don't have a clear match. It also gives a quick primer on how STCs work in 270/271 eligibility checks.

Just want the cheat sheet? Scroll to the bottom.

Note: This guide is for medical STCs only. It doesn't cover dental STCs or dental-only payers.

What’s an STC?

A Service Type Code (STC) is a two-character code that groups similar healthcare services into standard categories. For example:

  • 47 – Hospital

  • AL – Vision

  • UC – Urgent care

In eligibility requests, STCs tell the payer what type of benefits you're asking about. In responses, they indicate what type of service each returned benefit entry relates to.

The standard STC list

HIPAA standardizes the list of valid STCs in X12 version 005010. Medical payers should only send these STCs in responses – but they aren’t required to support every code in the list.

For the full set of X12 005010 STCs, see Service Type Codes in the Stedi docs.

Note: X12 maintains a broader Service Type Codes list for later X12 versions. X12’s list includes codes that aren’t part of 005010 and shouldn’t be used by medical payers.

STC 30 - The fallback

If you only send STC 30 (Health Benefit Plan Coverage) in the eligibility request and the patient’s plan covers it, HIPAA requires the payer to return benefits for the following STCs:

STC

Description

1

Medical Care

33

Chiropractic

47

Hospital

86

Emergency Services

88

Pharmacy

98

Professional (Physician) Visit - Office

AL

Vision (Optometry)

MH

Mental Health

UC

Urgent Care

The payer may include benefits for other STCs as well.

How to use STCs in eligibility requests

If you’re using Stedi’s JSON-based Eligibility APIs, include an STC in the request’s encounter.serviceTypeCodes array:

"encounter": {
  "serviceTypeCodes": ["30"]
}

If you don’t include an STC in the request, Stedi defaults to 30 (Health Benefit Plan Coverage).

The array supports multiple STCs, but payer support varies. Unless you’ve tested a payer specifically, only send one STC per request.

To learn how to test for multi-STC support, see How to avoid eligibility check errors.

How STCs show up in eligibility responses

Most benefit details are in the benefitsInformation object array. Each object in the array includes a serviceTypeCodes field:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "INCLUSIONS SPEECH/PHYSICAL/OCCUPATIONAL THERAPY; APPLIED BEHAVIOR ANALYSIS (ABA)"
        }
      ]
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: DEVELOPMENTAL TESTING, EDUCATIONAL THERAPY"
        }
      ]
    }
  ],
  ...
}

Responses may include entries for STCs you didn't request. And you'll typically see the same STC repeated across multiple benefit entries. That's normal – each entry covers a different aspect of benefits, like coverage status, co-pays, or deductibles.

Payers also use multiple entries to describe different subsets of services within an STC. For example, the MH STC might have one entry for standard therapy and another that notes coverage for other treatments. Descriptions typically appear in entries with code: "1" (Active Coverage) or code: "D" (Benefit Description), but they can appear in other entries as well. Often, coverage notes, inclusions, and exclusions appear in additionalInformation.description.

To get the full picture of benefits for a service, check all entries with the same serviceTypeCodes value.

For more information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

Picking an STC

When sending an eligibility request, use the most specific STC you can. It narrows the response to the benefits you care about and reduces guesswork. For example, if you send a request for STC 33 (chiropractic) instead of STC 30, you’ll get specific benefits related to chiropractic care.

However, some services, like remote therapeutic monitoring (RTM) or speech therapy, don’t map cleanly to well-supported STCs.

In these cases, your best option is to systematically test the STCs that seem most appropriate and compare the responses to see if you get the benefits information you need.

How to test for STC support

  1. Send a baseline eligibility request.
    Submit an eligibility check to the payer using STC 30 (Health Benefit Plan Coverage). This gives you a fallback response to compare against.

  2. Test your specific STC.
    Send a second request to the payer for the same patient with the STC that best matches the benefit type you’re targeting. Use the STCs in the cheat sheet as a starting point.

  3. Compare the specific STC response to the baseline response.
    If responses change based on the STC, the payer likely supports the specific STC.

If responses are identical, the payer may not support the specific STC – or the patient’s plan might not cover that service. When that happens, medical payers are required to return a fallback response using STC 30.

Example: Testing STC support for ABA therapy

Start with a baseline eligibility request using STC 30 (Health Benefit Plan Coverage):

{
  "encounter": {
    "serviceTypeCodes": ["30"]
  }
}

Then try a more specific STC like BD (mental health):

{
  "encounter": {
    "serviceTypeCodes": ["BD"]
  }
}

If the BD response includes benefitsInformation objects with a serviceTypeCodes field value of BD, the payer supports STC BD. Use this response if it includes the benefits information you need.

If the responses are identical, the payer or plan likely doesn't support the BD STC. You continue testing other related STCs, such as MH (Mental Health).

If you've already tried other STCs and no others seem appropriate, use the response for STC 30. If STC 30 doesn't include the benefits information you need, you may need to call the payer or visit the payer portal.

Tip: Automate your STC tests
To speed up testing, script your requests. Loop through candidate STCs and compare the responses against the baseline STC 30 response, using the same patient. Save the benefitsInformation array for each STC and diff them. This helps you spot what changes, if anything, between requests.

The STC cheat sheet

If you’re testing STC support for a service without a clear mapping, use the following table as a starting point. This list isn’t exhaustive and it isn’t payer-specific, but it’s a starting point for what we’ve seen work in production.

Try the STCs in the order shown – from the most specific to more general alternatives.

For a complete list of STCs, see Service Type Codes in the Stedi docs. For help mapping procedure codes to STCs, see How to map procedure codes to STCs.

Type of Care

STCs to Try

ABA Therapy

BD, MH, CF

Acupuncture

64, 1

Chemotherapy

ON, 82, 92

Chemotherapy, IV push

82, 92

Chemotherapy, additional infusion

82, 92

Chronic Care Management (CCM) services

A4, MH, 98, 1

Dermatology

3, 98

Durable Medical Equipment

DM, 11, 12, 18

IV push

92

IV Therapy/Infusion

92, 98

Maternity (professional)

BT, BU, BV, 69

Medical nutrition therapy

98, MH, 1, BZ

Medical nutrition follow-up

98, MH, 1, BZ

Mental health

MH ,96, 98, A4, BD, CF

Neurology

98

Newborn/facility

65, BI

Non-emergency transportation (taxi, wheelchair van, mileage, trip)

56, 57, 58, 59

Occupational Therapy

AD, 98, CF

Physical therapy

PT, AE, CF

Podiatry

93, 98

Primary care

96, 98, A4, A3, 99, A0, A1, A2, 98

Psychiatry

A4, MH

Psychological testing evaluation

A4, MH

Psychotherapy

96, 98, A4, BD, CF

Rehabilitation

A9, AA, AB, AC

Remote Therapeutic Monitoring (RTM) services

A4, 98, MH, 92, DM, 1

Skilled Nursing

AG, AH

Speech Therapy

AF, 98

Substance Abuse/Addiction

AI, AJ, AK, MH

Telehealth

9, 98

Transcranial magnetic stimulation

A4, MH

Jul 18, 2025

Products

You can now use the Retrieve Payer API endpoint to get a single payer record by its Stedi payer ID:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payer/{stediId} \
  --header 'Authorization: <api-key>'

Every payer in the Stedi Payer Network has a Stedi Payer ID: an immutable payer ID you can use to route transactions to the payer in Stedi.

If you already have a payer’s Stedi Payer ID, this new endpoint is the fastest way to fetch their payer metadata. No filtering, no pagination. Just the one record.

The endpoint returns the same payer information as our existing JSON-based Payer API endpoints: payer ID, name, payer ID aliases, transaction support, enrollment requirements, and more. Just for a single payer.

Why we built this

Payer IDs are used to route healthcare transactions to the right payer. If the ID is wrong, the transaction fails.

We recently introduced List Payers and Search Payers API endpoints to let you retrieve payer IDs and other metadata programmatically. This ensures you can always get an accurate, up-to-date payer ID.

Since then, several customers have asked, “Is there a way to get a single payer without a search?”

Until now, you had to either:

  • Use the Payer Search API endpoint, which always returns an array – even for exact matches.
    OR

  • Fetch and paginate the full list of payers (thousands of records), then filter it client-side.

If you already had a Stedi Payer ID from a previous search or a saved mapping, there was no way to get just that payer’s metadata without a search or filter.

Now you can.

How it works

Make a GET request to the /payer/{stediId} endpoint. Pass the Stedi Payer ID as the {stediId} in the path:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payer/HPQRS \
  --header 'Authorization: <api-key>'

You’ll get back a single JSON object that contains:

  • The payer’s name, primary payer ID, and known payer ID aliases

  • Whether the payer supports medical or dental coverage (or both)

  • Supported transaction types

  • Whether transaction enrollment is required for a transaction type

{
  "payer": {
    "stediId": "KRPCH",
    "displayName": "Blue Cross Blue Shield of Michigan",
    "primaryPayerId": "00710",
    "aliases": [
      "00210I",
      "00710",
      "00710D",
      "00710P",
      "1421",
      "2287",
      "2426",
      "710",
      "CBMI1",
      "MIBCSI",
      "MIBCSP",
      "SB710",
      "Z1380"
    ],
    "names": [
      "Blue Cross Blue Shield Michigan Dental",
      "Blue Cross Blue Shield Michigan Institutional",
      "Blue Cross Blue Shield Michigan Professional"
    ],
    "transactionSupport": {
      "eligibilityCheck": "SUPPORTED",
      "claimStatus": "SUPPORTED",
      "claimSubmission": "SUPPORTED",
      "professionalClaimSubmission": "SUPPORTED",
      "institutionalClaimSubmission": "SUPPORTED",
      "claimPayment": "ENROLLMENT_REQUIRED",
      "coordinationOfBenefits": "SUPPORTED",
      "dentalClaimSubmission": "NOT_SUPPORTED",
      "unsolicitedClaimAttachment": "NOT_SUPPORTED"
    },
    "enrollment": {
      "ptanRequired": false,
      "transactionEnrollmentProcesses": {
        "claimPayment": {
          "type": "ONE_CLICK"
        }
      }
    },
    "parentPayerGroupId": "AWOCR",
    "coverageTypes": [
      "medical"
    ]
  }
}

Try it out

The Retrieve Payer API endpoint is free on all paid Stedi plans.

To see how it works, check out the docs or reach out for a free trial.

Jul 17, 2025

Spotlight



Eliana Berger @ Joyful Health

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from Eliana Berger, CEO at Joyful Health. You'll learn what Joyful Health does and how Eliana thinks RCM will change in the next few years.

What does Joyful Health do?

Joyful Health is a specialized revenue recovery service that focuses exclusively on the hardest part of the revenue cycle: following up on denied and unpaid claims. What makes us true experts in this space is that we hire people with payer-specific expertise who know exactly how to navigate each insurance company's systems, understand their unique resolution pathways, and can get things done quickly and accurately.

What makes us different:

  • Performance-based pricing - We only get paid when we successfully recover revenue for you

  • Cost optimization - As your clean claims rate improves, you pay us less, naturally optimizing your cost to collect

  • Zero risk - No upfront costs, no minimum fees, services pay for themselves

  • Seamless integration - We work entirely within your existing systems with no new software required

  • Specialized focus - We handle denials and aged A/R recovery so your team can focus on scaling your core business

For fast-growing digital health companies, this means you can maintain lean operations while ensuring maximum revenue recovery from day one. We typically help practices recover revenue worth 5-10x our fees, making it a no-brainer investment that scales with your growth.

How did you end up working in health tech?

My path into health tech started at home. Growing up, I watched my mom and grandmother run an independent therapy practice, spending countless hours managing claims and constantly worrying about whether payments would come through. That experience stuck with me.

A few years ago, I became curious about what was really keeping independent practices on the brink of survival. So I started volunteering as a free consultant for several practices - helping with everything from ordering office furniture to analyzing their EHR data. What I discovered was eye-opening: practice owners had no idea where their money was.

Their financial data was scattered across multiple systems with no way to see the full picture. This led me to essentially become a fractional CFO for these practices, spending hours pulling together fragmented reports to give them clarity on their revenue. What I found was shocking - many were losing 10-30% of their revenue without even realizing it. The money was there, just trapped in denied claims and unpaid receivables.

That insight became the foundation for Joyful Health. We're building the modern financial operating system for healthcare, starting with revenue recovery. When you can finally see all that fragmented data in one place, it becomes crystal clear how much money is being left on the table - and more importantly, how to systematically recover what practices are rightfully owed.

How does your role intersect with revenue cycle management (RCM)?

As CEO, I'm constantly thinking about RCM from both a strategic and operational lens - but so is our entire team!

Everyone at Joyful, including operations, engineering, and beyond, regularly fights denied claims alongside our RCM specialists. This helps our entire team understand what the workflows actually look like, from interpreting denial codes to navigating payer portals to executing the right resolution actions. As a result, when our engineers are building features, they understand exactly what data points matter and how billers actually work. When our operations team is designing processes, they know the real bottlenecks and pain points. This deep, practical knowledge allows us to build products and services that are truly integrated within the systems and processes practices already have, rather than creating yet another tool that sits on the side.

From a strategic perspective, this operational depth helps me make better decisions about our product roadmap and service delivery. It's not enough to build cool technology - it has to directly solve real problems in the revenue cycle. And because everyone on our team has fought these battles firsthand, we can build solutions that actually work in the messy reality of healthcare billing.

What do you think RCM will look like two years from now?

I think we're heading toward a world where RCM becomes much more specialized and data-driven, with a clear separation between the "science" and "art" of revenue cycle management.

The "science" - rules-based, routine tasks like eligibility verification, claim scrubbing, and payment posting - will increasingly be handled by AI automation, which is already creating huge efficiencies. But the real breakthrough I'm excited about will be when AI starts getting better at the "art" side too: the complex human judgment required for things like denial management and aged A/R recovery. As AI learns from successful resolution patterns and begins to interpret payer behaviors and policies, it will help tackle problems that have traditionally required deep human expertise.

Another bigger shift I'm excited about is the move toward performance-based pricing models across the entire RCM ecosystem. Practices want better alignment between what they pay and the results they get - they want vendors who tie their success directly to their clients' financial outcomes.

Jul 16, 2025

Guide

The revenue cycle is how healthcare providers get paid. When it slows down, the provider’s cash flow dries up, and everything else breaks.

Less cash flow means providers have to make tough choices: not hiring needed staff, delaying investments, or cutting back on services. And that means worse care for everyone.

Despite the stakes, most of the revenue cycle is spent waiting.
… Waiting for eligibility responses.
… Waiting for claims to get accepted.
… Waiting for payments to show up.

A lot of that wait is avoidable.

If you're building RCM tools for healthcare providers, automating parts of the cycle – the critical path – can cut 10-20 days off the typical 30-60 day cycle. Providers – your customers – get paid faster, and you'll win more of them.

This guide walks you through what the revenue cycle is, tells you the key roles, and breaks down each step of the critical path: enrollment, eligibility, and processing claims. It shows how billing platforms can use Stedi to automate this critical path so it runs faster and at scale.

What is the revenue cycle?

The revenue cycle is everything a healthcare provider does to get paid.

In theory, it’s simple: a provider sees a patient and collects any co-pay or coinsurance. The provider records what happened during the visit and sends the details as a claim to the payer. The claim is approved, and the provider gets paid.

The reality is more complicated.

To avoid claim denials, providers need to check the patient’s insurance coverage before a visit. When submitting a claim, they have to use billing codes – picking from tens of thousands of them – to precisely describe the service they provided. Once submitted, payers don’t process their submitted claims right away. It can take days or weeks. A mistake means a rejection – which means starting over, delaying payment even more.

Revenue cycle management (RCM) is how providers stay on top of it all. It can be software, services, internal processes, or a mix of all three. Regardless of the methods, the goals are the same: prevent mistakes, get the provider paid faster, and avoid lost revenue.

The players

The revenue cycle involves several different systems and organizations. Each plays a specific role:

Providers
Short for “healthcare providers.” These are doctors, hospitals, dentists, therapists – anyone who delivers healthcare. They’re the ones trying to get paid.

Payers
Health insurers, including insurance companies like Aetna or Cigna, and government programs like Medicare. They receive claims, decide what to pay, and send back money (or denials) to the provider.

HIPAA and X12
A federal law that protects healthcare data and sets rules for how certain transactions must be conducted. HIPAA requires some healthcare transactions to be exchanged in the X12 EDI format. For example:

270 and 271 refer to official X12 HIPAA transaction types. Stedi’s APIs let you send and receive data for these transactions as JSON. We handle the translation from JSON and X12 (and the reverse) for you. You can also use Stedi to exchange raw X12.

Healthcare clearinghouses
Clearinghouses sit between providers and payers. Their job is to route transactions between the two and ensure that transactions sent to payers are HIPAA-compliant X12.

Most clearinghouses only connect to medical or dental payers. Stedi connects to both. We also connect to some vision and workers' compensation payers. 

For more information on clearinghouses, check out What is a healthcare clearinghouse?.

Billing platforms
Most providers don’t connect directly to clearinghouses or payers. They use billing platforms to manage the work for them. These include EHRs (electronic health record systems), practice management systems (PMS), and revenue cycle management (RCM) systems – often layered on top of each other.

In addition to billing, some of these platforms may help providers manage appointments, documentation, and clinical tasks.

The critical path

To get paid, every provider needs to complete certain steps in the revenue cycle. This “critical path” includes enrollment, eligibility checks, and claims processing.

There are other steps in the revenue cycle, but these are the most important to get right. Mistakes here slow down provider payments and create more work.

The following sections walk through each step in the critical path, covering its purpose, common pitfalls, and how it can be automated using Stedi.

Healthcare revenue cycle: The critical path

Step 0. Enrollment

Before they can exchange transactions with payers, providers need to complete up to three different types of enrollment. Each enrollment is a one-time process, but providers need to repeat enrollment for each payer:

  • Credentialing – Confirms the provider is licensed and qualified.

  • Payer enrollment – Registers the provider with the payer’s health plan(s) and sets their contracted rates – the agreed dollar amounts for specific services. These rates are used later to calculate how much the payer will reimburse for a service in claims.

  • Transaction enrollment – Lets the provider exchange certain types of healthcare transactions with the payer. 

Billing platforms may handle some or all of these enrollments for their providers. Stedi only helps billing platforms with transaction enrollment – but we make it faster, more automated, and easier to manage than other clearinghouses. 

Transaction enrollment
All payers require providers to complete transaction enrollment to receive 835 Electronic Remittance Advice (ERAs). Some also require enrollment for other transactions, like 270/271 eligibility checks or 837 claims.

If you're a billing platform serving many providers, transaction enrollment becomes a bottleneck. Each payer has its own enrollment process with different requirements: some need PDF signatures, others require portal logins. Just tracking the status of hundreds of enrollment submissions can become overwhelming. Many billing platforms end up hiring entire operations teams just to manage the paperwork.

Stedi automates transaction enrollment requests and lets you avoid most of the manual work. You can submit and track enrollment requests using Stedi’s Enrollments API, the Stedi portal, or a bulk CSV file. For the 850+ payers that support one-click transaction enrollment, that’s it – you’re done. For the rest, we offer delegated signature authority, where Stedi signs enrollment forms on your behalf. It’s a one-time step that eliminates 90%+ of your enrollment-related paperwork.

When manual enrollment steps are needed, we do the work for you. You only take action when absolutely needed – and we tell you exactly what to do next. We complete most enrollment requests in 1-2 business days.

Goal

What to use

Submit and track transaction enrollment requests

Enrollments API, Stedi portal, or CSV file

Eliminate 90%+ of enrollment-related paperwork

Delegated signature authority

Step 1. Eligibility check

Before a patient visit, providers need to know three things:

  • Does the patient have active insurance?

  • Does the patient’s plan cover what they’re coming in for?

  • How much will the patient owe at the time of service? What’s the co-pay, coinsurance, or deductible?

An eligibility check checks a patient’s insurance to answer these questions. The checks help prevent claim denials and surprise bills. It’s especially important for specialty care – like therapy – where coverage can vary by service type.

In some cases, especially for scheduled services, providers may also need to give the patient an estimate of their out-of-pocket costs. For certain services, that estimate is required by the No Surprises Act.

To check coverage, you send a 270 eligibility request to the patient’s payer. The payer responds with a 271 eligibility response that includes benefit details: covered services, copays, coinsurance, deductibles, and more.

Real-time and batch eligibility checks
Most providers need to make eligibility checks in real time – during intake or on the phone – right before a visit. Fast, accurate eligibility responses are important. Stedi’s Real-Time Eligibility API typically returns results in 1-3 seconds.

Many providers also want to run weekly or monthly batch refreshes. These refreshes catch coverage between visits, which is helpful for recurring patients or upcoming visits. You can use Stedi’s Batch Eligibility API or a bulk CSV to run 1,000 checks at a time without interfering with your real-time checks.

Insurance discovery and COB checks
Payers only return a patient’s eligibility data if the request matches a single patient. Some payers can match a patient based on their name and date of birth alone, Many require a member ID.

If a patient doesn’t know their member ID or doesn’t know their payer, you can use an insurance discovery check to try to find active coverage using just their demographics, like name and SSN. Results aren’t guaranteed, but it’s a way forward when eligibility checks aren’t possible. If the discovery check returns multiple plans, use a coordination of benefits (COB) check to determine the primary plan.

MBI lookup
Medicare eligibility checks require the patient’s Medicare Beneficiary Identifier (MBI), the Medicare equivalent of a member ID. If a patient doesn’t know their MBI, you can use Stedi’s MBI Lookup feature to get it. If there’s a match, Stedi automatically runs an eligibility check using the MBI to return the patient’s benefits information.

Using Stedi
The following table outlines how billing platforms can run eligibility and related checks using Stedi.

Goal

What to use

Check eligibility in real time

Real-Time Eligibility API or the Stedi portal

Check eligibility in bulk

Batch Eligibility API or upload a bulk CSV

Find active insurance without a member ID or known payer

Insurance Discovery Check API

Get a patient’s Medicare Beneficiary ID (MBI)

Real-Time Eligibility API with MBI lookup

Determine a patient’s primary payer

Use the Coordination of Benefits Check API

Step 2. Charge capture

Once coverage is confirmed, the provider delivers care. During or after the visit, the provider captures what was done using procedure codes – structured billing codes that describe their services. Providers typically enter the codes into their EHR or PMS.

Later, these codes become the core of the claim sent to the payer. The type of code used depends on the service:

  • Current Procedural Terminology (CPT) codes – Used for most medical services. Maintained by the American Medical Association (AMA).

  • Healthcare Common Procedure Coding System (HCPCS) codes – Used for things like medical equipment, ambulance rides, and certain drugs. Includes CPT codes as Level I.

  • Current Dental Terminology (CDT) codes - Used for dental services. Maintained by the American Dental Association (ADA).

Accuracy is important in this step. The captured codes later become part of the provider’s claim. Mistakes can mean a rejected or denied claim.

Stedi doesn’t handle charge capture directly. But many EHR and practice management platforms that integrate with Stedi do. To find them, check out our Platform Partners directory.

Step 3. Claim submission

Once care and charge capture are done, the provider uses their billing platform to submit a claim to the patient’s payer.

Claims must be submitted using an 837 transaction. There are three types:

  • 837P – Professional claims, used for services like doctor visits, outpatient care, and therapy

  • 837D – Dental claims

  • 837I – Institutional claims, used for services like hospital stays and skilled nursing

You can use Stedi’s Claim Submission API or SFTP to submit 837P, 837D, and 837I claims.

275 claim attachments
Some services require additional documentation – like X-rays or itemized bills – to show that the service occurred or was needed. This is common in dental claims, where payers often require attachments for certain procedures.

Providers must send this documentation to the payer as one or more 275 claim attachments. The type of attachments required depends on the service and the payer. Without required attachments, the payer may delay (pend) or deny the claim.

You submit attachments separately from claims, but the request must reference the original claim. Most claim attachments are unsolicited – meaning they’re sent upfront without the payer requesting them.

You can use Stedi’s Claim Attachments API to upload and submit it as an unsolicited 275 claim attachments.

Using Stedi
The following table outlines how billing platforms can submit claims and claim attachments using Stedi.

Goal

What to use

Submit an 837P, 837D, or 837I claim

Claim Submission API or SFTP

Submit an unsolicited 275 claim attachment

Claim Attachments API

Step 4. Claim acknowledgment

Payers don’t process claims in real time. After you submit a claim, you’ll receive one or more asynchronous 277CA claim acknowledgments: 

  • First from Stedi or your primary clearinghouse. For Stedi, this usually arrives within 30 minutes of submitting the claim.

  • From one or more intermediary clearinghouses, if applicable.

  • Finally from the payer. This acknowledgment is the one you usually care about. You typically receive a payer acknowledgment 2-7 days after claims submission, but it can take longer.

Payers send claim acknowledgments to the provider’s – or their designated billing platform’s – clearinghouse. You can use a Stedi webhook or Stedi’s Poll Transactions API endpoint to listen for incoming 277 transactions. When a 277CA arrives, you can use the transaction ID to fetch the claim acknowledgment’s data using Stedi’s Claims acknowledgment (277CA) API.

Claim acceptance and rejection
The payer acknowledgment tells you whether the claim was accepted for processing or rejected:

  • Acceptance  – The claim passed the payer’s initial checks and made it into their system. It doesn’t mean the claim was approved or paid.

  • Rejection – The claim didn’t meet the payer’s formatting or data requirements and wasn’t processed at all. It doesn’t mean the claim was denied.

If the claim is rejected, fix the issue and try again. If it’s accepted, wait for the 835 ERA – the final word on payment or denial.

Claim repairs
The acknowledgment step is where most claim rejections happen. When you submit a claim using the Claim Submission API, Stedi automatically applies various repairs to help your requests meet X12 HIPAA specifications and individual payer requirements. This results in fewer payer rejections.

Prior authorization
Some services require prior authorization, or prior auth, before you can submit a valid claim. It means getting the payer’s approval for a service in advance – usually through a portal, fax, or EHR. Prior auth isn’t handled by Stedi and isn’t covered in this guide.

Using Stedi
The following table outlines how billing platforms can use Stedi to retrieve claim acknowledgments.

Goal

What to use

Get notified of new 277CA claim acknowledgments

Stedi webhook or Poll Transactions API endpoint 

Retrieve 277CA claim acknowledgments after notification

Claims acknowledgment (277CA) API

Step 5. Remittance and claim status

The 835 Electronic Remittance Advice (ERA) transaction is the final word on a claim. It’s the single source of truth for:

  • What was paid and when

  • What was denied and why

  • What the patient owes

  • How to post payments and reconcile accounts

Like a claim acknowledgment, the ERA is sent from the payer to the provider’s – or their billing platform’s – clearinghouse. You can create a Stedi webhook or Stedi’s Poll Transactions API endpoint to listen for incoming 835 transactions. When an 835 ERA arrives, you can use the transaction ID to fetch the ERA’s data using Stedi’s 835 ERA API

Claim approval and denial
Later, after the claim is processed, you may receive an approval or a denial:

  • Approval – The payer agreed to pay for some or all of the claim.

  • Denial – The claim was processed but not paid, usually because the service wasn’t covered or approved by the patient’s plan.

If the claim is approved, you can post the payment and reconcile the patient’s account.

If it’s denied, review the denial reason in the ERA. If the denial was incorrect or preventable, you can correct the issue and resubmit the claim. Otherwise, you can appeal or escalate the denial with the payer or bill the patient. The steps for appealing and escalating claims differ based on the payer’s rules and the patient’s plan.

Real-time claim status checks
If the claim is approved, an ERA typically arrives 7–20 business days after claim submission. If you haven’t received a claim acknowledgment or ERA after 21 days, you can check the claim’s status using a 276/277 real-time claim status check.

A real-time claim status check tells you whether the claim was received by the payer, is in process, or was denied.

You can make a claim status check any time after claim submission, but most billing platforms wait until day 21. Then they monitor the claim using real-time claim status checks until they receive a final status or an ERA. For example, the following table outlines a typical escalation process.

Days since claims submission

Action

1-20

Wait for the 277CA claim acknowledgment or 835 ERA.

21

Run the first 276/277 real-time claim status check.

24

Run a second 276/277 real-time claim status check.

28

Run a third 276/277 real-time claim status check.

30+

Contact Stedi support in your Slack or Teams channel.

Using Stedi
The following table outlines how billing platforms can use Stedi to retrieve ERAs and make check claim statuses.

Goal

What to use

Get notified of new 835 ERAs

Stedi webhook or Poll Transactions API endpoint

Retrieve 835 ERAs after notification

835 ERA API

Check the status of a claim after 21 days or more

Real-Time Claim Status API

Step 6. Revenue recovery

Sometimes, the provider has already delivered care, but the claim gets stuck. This could be because:

  • They didn’t check eligibility or collect the right insurance information, so they can’t submit a claim.

  • They submitted a claim, but it was rejected or denied.

The result is the same: the claim and its revenue are considered lost.

To recover it, run an insurance discovery check to try to find active coverage for the patient. Results aren’t guaranteed, but even a low success rate is acceptable. This is a last-ditch effort to recover lost revenue, and there’s limited downside. If discovery returns multiple plans, run a COB check to determine the primary.

If you can identify the patient’s primary plan, submit a claim using the Claim Submission API or SFTP, and pick up the revenue cycle from there (Step 3).

Using Stedi
The following table outlines how billing platforms can submit claims and claim attachments using Stedi.

Goal

What to use

Find active insurance without a member ID or known payer

Insurance Discovery Check API

Determine a patient’s primary payer

Use the Coordination of Benefits Check API

Submit an 837P, 837D, or 837I claim

Claim Submission API or SFTP

The rest of the revenue cycle

This guide only covers the critical parts of the revenue cycle. The rest of the cycle happens outside the clearinghouse.

After the 835 ERA comes in, the billing platform or another RCM system typically takes over any remaining steps. These can include:

  • Posting payments to the patient’s account

  • Billing the patient for their share of payment

  • Following up on denied or unpaid claims

  • Collecting payment or writing off balances

  • Running reports and reconciling revenue

If you're looking for tools to handle those downstream steps, check out the Stedi Platform Partners directory.

Start automating your workflows today

If you’re building RCM functionality and running into scaling issues, Stedi can help you out. Our APIs let you automate core workflows so you can onboard more providers with less manual work.

If you want to try Stedi out, contact us for a free trial or sign up for a sandbox account. It takes less than 2 minutes. No billing details required.

Jul 14, 2025

Products

You can now use the Stedi Platform Partners directory to find RCM, EHR, and practice management systems that use Stedi.

Many healthcare companies are looking for modern RCM solutions – and can’t or don’t want to build one themselves. The directory helps you find solutions that are powered by Stedi’s API-first clearinghouse rails.

What is the Stedi Platform Partners directory?

The Stedi Platform Partners directory is a public list of platforms that use Stedi’s APIs to power their RCM functionality. The directory is free and publicly accessible. Platforms don’t pay to be listed.

Why we built it

Stedi isn’t always the right fit for healthcare companies that want to benefit from our modern clearinghouse infrastructure.

We’re built for teams with in-house engineers who want to build their own RCM stack using our healthcare clearinghouse APIs. These can be large MSOs or DSOs with custom RCM requirements that aren’t well-served by off-the-shelf solutions, or they can be engineering teams building software platforms that they resell to others in the form of RCM, EHR, or practice management systems.

Many healthcare companies don't have their own developers (for example, individual provider offices) or don't need to build billing workflows themselves (for example, groups with run-of-the-mill RCM requirements). These companies are looking for plug-and-play solutions that can get them up and running quickly with turnkey functionality, and they want to use platforms that aren’t built on top of legacy clearinghouse infrastructure.

The directory allows those companies to find modern platforms that are built on top of Stedi’s modern infrastructure.

Find a partner today

The Stedi Platform Partners directory is now live. If you can’t find a platform that fits your needs – or you want to be listed – contact us and we’ll help you out.

Jul 15, 2025

Spotlight

Laurence Girard @ Fruit Street

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from Laurence Girard, Founder and CEO of Fruit Street Health. You'll learn what Fruit Street does and how Laurence thinks RCM will change in the next few years.

What does Fruit Street do?

Fruit Street delivers the CDC’s diabetes prevention program through live group video conferencing with registered dietitians. The program is designed to help the 1 in 3 Americans with prediabetes avoid developing Type 2 diabetes.

How did you end up working in health tech?

I was planning to go to medical school when I was 18 years old. I was volunteering in my local emergency room while simultaneously taking a nutrition epidemiology course with a Harvard School of Public Health professor. This led me to realize that many of the patients coming into the emergency room had preventable conditions related to their diet and lifestyle, such as Type 2 diabetes.

I also gained my interest in entrepreneurship by going to talks at the Harvard Innovation Lab on the Harvard Business School campus. I thought that maybe instead of going to medical school, I could have a big impact on lifestyle-related diseases and public health through technology and entrepreneurship. I started my company as a summer project at the Harvard Innovation Lab more than a decade ago and have been working on it ever since.

How does your role intersect with revenue cycle management (RCM)?

Fruit Street recently became a Medicare Diabetes Prevention Program Supplier. We use Stedi to run automated eligibility checks and submit claims.

What do you think RCM will look like two years from now?

I think RCM solutions – like those powered by Stedi – will directly and more deeply integrate with other digital health solutions. They'll use AI to check in advance if a service is covered by a health plan so there are fewer claim denials.

Jul 11, 2025

Guide

Your AI voice agent is making 1,000+ calls a day. Payers are limiting how many questions your agent can ask per call. Your infrastructure costs are spiking. Your queues are overloaded and getting worse.

If you're building an AI voice agent for back office tasks that include insurance eligibility – like many of Stedi’s customers – that might sound familiar. This guide is for you.

In this guide, you’ll learn how to restructure your agent’s workflow to make fewer calls and get faster answers at lower costs. You’ll also see how you can use Stedi’s eligibility APIs to get benefits information before – or instead of – triggering a call.

The problem

For providers, AI voice agents fill a real need. Providers need to check eligibility to determine patient coverage and estimate costs. Sometimes, this requires a phone call to the payer.

Before agents, the provider’s staff or BPOs made those calls. They spent hours waiting on hold, pressing buttons, and navigating phone trees. Many of those teams weren’t using real-time eligibility checks at all – just 100% phone calls. Voice agents are now taking that work, and in many cases, winning business from the BPOs they’re replacing.

The problem is twofold:

  • Payers are getting flooded with AI phone calls and are taking defensive measures. Some payers limit the number of questions per call or just hang up when they hear an AI voice.

  • Voice agents still fail sometimes. When they do, the entire call is wasted.

Calling the payer was never meant to be the first step. A real-time eligibility check does the job faster. In most cases, it can provide all the details needed – like coverage status, deductibles, and co-pays – in seconds. Real-time checks should be your first line of defense for eligibility.

Payer calls should be reserved for cases where the eligibility response doesn’t include the benefit details you need, like doula benefits (which don’t have a specific Service Type Code), medical nutrition therapy coverage, or details on prior authorization requirements.

A better workflow for voice agents

If your agent is placing calls without running an eligibility check first, you’re probably making a lot of unnecessary calls.

Here’s how to fix it: Use Stedi’s real-time eligibility and insurance discovery APIs to resolve more cases upstream – before a call ever needs to happen. Even when a call is required, it’s shorter – because you’re not asking for data you already have from the API.

Stedi’s voice agent customers have found that using eligibility checks first drastically reduces the number and duration of phone calls.

This table outlines each step, when to use it, and how long it takes.

Workflow step

When to use

Expected response time

Step 1. Run a real-time eligibility check

As a first step.

1–3s

Step 2. Run an insurance discovery check

The eligibility check fails.

10–30s

Step 3: Place a call (only if needed)

All else fails or special information is needed.

Minutes

The following flowcharts compare the old and new workflows:

Before and After Flowcharts

Step 1. Run a real-time eligibility check

As a first step, use Stedi’s Real-Time Eligibility Check API to get benefits data from the payer. This step alone – running an eligibility check before triggering a call – can greatly reduce your agent’s call volume.

When using the API, include the following in your request:

  • controlNumber – A required identifier for the eligibility check. It doesn’t need to be unique; you can reuse the same value across requests.

  • tradingPartnerServiceId – The payer ID. If you don’t know the payer ID but know the payer name, use the Search Payers API to look it up.

  • Provider information – The provider’s NPI and name.

  • Patient information – First name, last name, date of birth, and member ID.

    • If verifying a dependent, the format depends on the payer:

      • If the dependent has their own member ID: Put their information in the subscriber object. Leave out the dependents array.

      • If they don’t have their own member ID: Put the subscriber’s information in subscriber, and the dependent’s information in the dependents array.

  • Optional but recommended:

    • serviceTypeCodes – Indicates the type of healthcare service provided. We recommend using one Service Type Code (STC) per request unless you’ve tested that the payer accepts multiple STCs. For a complete list of STCs, see Service Type Codes in the Stedi docs.

An example eligibility request body:

{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "AHS",
  "externalPatientId": "UAA111222333",
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19000101",
    "memberId": "123456789"
  },
  "encounter": {
    "serviceTypeCodes": [
      "MH"
    ]
  },
}

Most patient benefits appear in the benefitsInformation array of the eligibility response. An example eligibility response:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network services
    },
    ...
  ],
  ...
}

For information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

If the response includes the information you need, you’re done. No call needed. If you get a response for the patient, but it doesn’t include the benefits information you need, move to Step 3.

If the eligibility check fails because the patient can’t be identified – indicated by an AAA 72 (Invalid/Missing Subscriber/Insured ID) or AAA 75 (Subscriber/Insured Not Found) error – try the tips in our Eligibility troubleshooting docs. If those don’t work, then move to Step 2.

Step 2. Run an insurance discovery check

Use Stedi’s Insurance Discovery API to find a patient’s coverage using just demographics – no payer ID or member ID needed. It’s less reliable and more expensive than an eligibility check, but often cheaper than making a call.

When making the discovery check, submit the following patient’s demographic information along with the provider’s NPI:

  • First name (required)

  • Last name (required)

  • Middle name (optional)

  • Date of birth (required)

  • Full or partial SSN (even the last 4 digits can help)

  • Gender (optional)

  • Current or previous ZIP code (optional but strongly recommended)

Note: Insurance discovery requires transaction enrollment to set up. See the insurance discovery docs.

An example insurance discovery request body:

{
  "provider": {
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "Jane",
    "lastName": "Doe",
    "middleName": "Smith",
    "dateOfBirth": "19800101",
    "ssn": "123456789",
    "gender": "F",
    "address": {
      "address1": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "postalCode": "62701"
    }
  },
  "encounter": {
    ...
  }
}

Stedi enriches the data, searches across commercial payers, and returns active coverage along with subscriber details and benefits.

If available, the benefits information is returned in benefitsInformation objects like those in the eligibility response from Step 1.

If the response includes the information you need, you’re done. If you get a response for the patient, but it doesn’t include the benefits information you need, move to Step 3.

If the insurance discovery check returns no matches, try asking the patient for more information. In these cases, it’s unlikely even a phone call will help. The patient’s information may be incorrect, or they haven’t provided enough information to check their coverage with the payer – call or not.

Step 3: Place a call (only if needed)

By this point, you should have exhausted your other options. We recommend you only call the payer if the eligibility response doesn’t have the information you need. Common examples include:

  • Missing coverage or benefit rules for specific services or procedures, like nutritional counseling, behavioral health, or missing tooth clauses.

  • Checking the provider’s network status.

  • Secondary or tertiary coverage that needs verification. In these cases, you may want to try a coordination of benefits (COB) check before calling the payer.

  • Referral or prior auth requirements aren’t included in the eligibility response.

  • Coverage dates or amounts are missing or don’t make sense.

In these cases, calling the payer is often the best thing to do.

Benefits

Restructuring your agent’s workflow from "call first" to "check first" gives your system real advantages across performance, cost, and control. Here are a few:

  • Fewer calls.
    Voice agents can see a significant reduction in outbound call volume when they use eligibility and insurance discovery checks before making a call.

  • Shorter calls.
    Most payers limit the number of data points that you can ask for per phone call. When a call is still needed, the agent already has the payer, member ID, and basic plan info. You don’t waste time – or calls – asking for information you already have.

  • Faster answers.
    Eligibility and discovery responses come back in seconds. You’re not waiting in a call queue or retrying after a dropped call.

  • Structured data.
    You can get all eligibility and insurance discovery responses as standardized JSON. That makes it easy to parse, store, and use downstream, whether you’re populating a UI or triggering logic.

Get started

You don’t need to rewrite your agent or rework your pipeline to try out Stedi. Most teams start with a simple proof of concept and expand from there.

If you’d like to try it for yourself, reach out to start a free trial. Our team would love to help get your integration rolling.

Jul 10, 2025

Guide

Most developers don’t hate sales. They hate being blocked.

You shouldn’t need to sit through a demo just to run a curl command.

With most dev tools, you can sign up, test things out, and decide for yourself. Stedi is a healthcare clearinghouse, which means we deal with real PHI. That means HIPAA compliance. And that means a BAA before you can send a production request. You can’t just spin up a prod account and start testing.

But we knew devs would want to try things out before committing. That’s why we created sandbox accounts: a free way to test our eligibility API with mock data.

You can sign up for a sandbox and start sending requests in under five minutes. If you’re considering integrating with Stedi, it’s a quick and painless way to try us out.

This guide shows how to set up a sandbox account and send your first mock eligibility check.

What the sandbox supports

The sandbox lets you test real-time 270/271 eligibility checks using mock data.

You can send requests through the JSON API or the Stedi portal. The data is predefined and simulates payers like Aetna, Cigna, UnitedHealthcare, and CMS (Medicare). You can’t use other patient data or select your own payers.

Supported

Not supported

If you need to test these features, request a free trial.

How to get started

Step 1. Create a sandbox account

Go to stedi.com/create-sandbox.

Sign up with your work email. No payment required.

Once in, you’ll land in the Stedi portal.

Step 2. Create a test API key

If you're using Stedi's APIs, you’ll need an API key to send mock requests – even in the sandbox.

  • Click your account name in the top right.

  • Select API Keys.

  • Click Generate new API key.

  • Name the key. For example: sandbox-test.

  • Select Test as the mode.

  • Click Generate.

  • Copy and save the key. You won’t be able to see it again.

If you lose the API key, delete it and create a new one.

Step 3. Send a mock eligibility check

You can send mock requests using the Stedi portal or the API. For the API, use the Real-Time Eligibility Check (270/271) JSON endpoint.

Only approved mock requests will work. For a list, see Eligibility mock requests in the Stedi docs. You must use the exact data provided. Don’t change payers, member IDs, birthdates, or names.

An example mock request:

curl --request POST \
  --url 'https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3' \
  --header 'Authorization: Key {test_api_key}' \
  --header 'Content-Type: application/json' \
  --data '{
    "controlNumber":"112233445",
    "tradingPartnerServiceId": "60054",
    "provider": {
        "organizationName": "Provider Name",
        "npi": "1999999984"
    },
    "subscriber": {
        "firstName": "John",
        "lastName": "Doe",
        "memberId": "AETNA9wcSu"
    },
    "dependents": [
        {
            "firstName": "Jordan",
            "lastName": "Doe",
            "dateOfBirth": "20010714"
        }
    ],
    "encounter": {
        "serviceTypeCodes": ["30"]
    }
}'

If you're trying to make sense of the 271 response, check out How to read a 271 eligibility response in plain English. It explains the key fields, common pitfalls, and how to tell what a patient owes.

Step 4. Explore the results in Eligibility Manager

Every check you run is stored in the Stedi portal’s Eligibility Manager.

In Eligibility Manager, you’ll see:

  • A list of submitted mock checks

  • Full request and response payloads

  • A human-readable benefits view

  • Debugging tools to inspect coverage, errors, and payer behavior

Each check is grouped under a unique Eligibility Search ID, so you can track related requests and retries.

Eligibility Manager

Eligibility Manager is especially useful for debugging failed checks. To test failure scenarios, try the predefined mock error cases, like an invalid member ID or provider NPI.

What’s next

The sandbox is the fastest way to test Stedi’s eligibility API. But it’s only mock data and doesn’t support most features.

If you want to test real patient data or try things like insurance discovery, request a free trial. We can get most users up and running in less than a day.

Jul 7, 2025

Guide

Eligibility checks are intended for ideal circumstances: the patient has their current insurance card handy, or they know their member ID and are certain of the correct payer. For those situations, Stedi offers a Real-Time Eligibility Check API. But that’s not always reality.

In the real world, patients forget their cards, switch plans, or don’t know who covers them at all.

It’s important to get a successful eligibility response from the patient’s current payer. Without one, patients get surprise bills, staff waste time chasing payment, and providers lose money on denied claims.

But payers can only return results when an eligibility check matches a single patient. While certain payers can match on name and date of birth alone, most need a member ID. If the patient doesn’t have it, the check fails, and the provider is stuck.

When you only have partial details, Stedi’s Insurance Discovery API can help.

With an insurance discovery check, you don’t need to know the member ID or payer. Just send the patient’s known demographic details such as name, birthdate, state or ZIP, and SSN, if available. We enrich these details with third-party data sources and search potentially relevant payers for active coverage, then return what we find.

It’s not guaranteed – match rates vary. But when eligibility checks fail, insurance discovery gives you a way forward. This guide shows how to use it and how to get the best results.

When to use insurance discovery

If you know the payer up front, we recommend that you attempt one or more eligibility checks before running an insurance discovery check (if you know the payer name but not the payer ID, you can use our Payer Search API to find it). If you don’t know the payer or if you’re unable to get a successful eligibility response, insurance discovery is the next step. Some common cases include:

  • The payer isn’t known.

  • The member ID is missing or incorrect.

  • Attempted eligibility checks failed with an AAA 75 (Subscriber not found) or other similar error.

  • The patient gave conflicting or incomplete info.

Discovery checks are a great fallback when a standard eligibility check is impossible or unsuccessful. Since a successful discovery check ultimately returns the full details from a successful eligibility check, they are a direct replacement for eligibility checks. The main tradeoffs are:

  • They’re slower, since Stedi performs an average of 13-16 real-time eligibility checks per discovery check.

  • They’re less reliable than sending an eligibility check with full patient details to a known payer, since it isn’t possible for every discovery check to perform searches against every payer.

  • They’re more expensive, since multiple third-party data and eligibility lookups are performed.

Note: Discovery checks don’t support dental payers. If your use case is dental-only, discovery won’t return results – even if the patient is insured. It's best used for medical coverage.

How to set up insurance discovery

To run discovery checks for a provider, you first need to complete transaction enrollment for their NPI with the DISCOVERY payer ID.

This is a one-time step for each provider. You can submit enrollment requests using the Enrollments API, the Stedi portal, or a bulk CSV.

Enrollment is typically approved within 24 hours. When it’s live, you’ll get an email notification. You can also check enrollment status in real time using the API or Stedi portal. After that, you can use the approved NPI in insurance discovery requests.

If you make an insurance discovery check with an unenrolled provider NPI, you’ll get an error response.

What to include in discovery checks

With eligibility checks, too much data can cause a payer to reject a request that otherwise would have been successful . The opposite is true for insurance discovery checks: the more patient data you include, the better your chances of getting a match.

If you only send the patient’s name and date of birth, success rates will be very low. The reason is that ultimately, Stedi and the payers need to be able to resolve to a single member. Unless the name is extremely uncommon, a name + date of birth is likely to match multiple members and result in a rejection. The same is true for sending a common name + date of birth + ZIP code – for example, John Smith with any date of birth in a New York ZIP code will have dozens of matches, and will therefore result in a failure.

For the best results, include at least the following patient info in Insurance Discovery Check API requests:

  • First name (required)

  • Last name (required)

  • Date of birth (required)

  • Full or partial SSN (even the last 4 digits can help)

  • Gender

  • Full address or ZIP code (current or previous)

An example request body with patient info in the subscriber object:

{
  "provider": {
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "John",
    "lastName": "Smith",
    "middleName": "Robert",
    "dateOfBirth": "19800101",
    "ssn": "123456789",
    "gender": "M",
    "address": {
      "address1": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "postalCode": "62701"
    }
  },
  "encounter": {
    "beginningDateOfService": "20240326",
    "endDateOfService": "20240326"
  }
}

How to read discovery responses

Discovery checks usually return a synchronous response within 60-120 seconds. If the result isn’t available by then, you can poll for it using the discoveryId and the Insurance Discovery Check Results endpoint. Most checks don’t require polling.

If the discovery check finds active coverage, you’ll get:

  • Payer name and ID

  • Member ID

  • Group number and plan details

{
  "discoveryId": "12345678-abcd-4321-efgh-987654321abc",
  "status": "COMPLETE",
  "items": [
    {
      "payer": {
        "name": "EXAMPLE INSURANCE CO"
      },
      "subscriber": {
        "memberId": "987654321000",
        "firstName": "John",
        "lastName": "Doe"
      },
      "planInformation": {
        "planNumber": "123456-EXMPL9876",
        "groupNumber": "123456-78",
        "groupDescription": "Individual On-Exchange"
      },
      "benefitsInformation": [
        {
          "code": "1",
          "name": "Active Coverage",
          "serviceTypeCodes": ["30"],
          "serviceTypes": ["Health Benefit Plan Coverage"],
          "planCoverage": "Gold Plan",
          "inPlanNetworkIndicator": "Yes"
        }
      ]
    }
  ]
}

In many cases, the response also includes full benefits data with the same benefitsInformation objects you’d get from a 271 eligibility response. But not always.

If the benefits data is included, you can use it directly. If the data seems incomplete, we recommend running a follow-up 270/271 eligibility check using the returned first name, last name, and member ID to the determined payer, especially if you’re automating downstream logic.

Discovery checks will return multiple payers if multiple coverages are found, but it isn’t guaranteed that they’ll find all of a patient’s plans. If you think the patient may have multiple plans, run a Coordination of Benefits (COB) check after the eligibility check to find other coverage and determine which payer is primary.

How to fix zero matches

A "coveragesFound": 0 result doesn’t always mean the patient is uninsured. It just means the discovery check couldn’t find a match.

{
  "coveragesFound": 0,
  "discoveryId": "0197a79a-ed75-77c3-af58-8ece597ea0be",
  "items": [],
  "meta": {
    "applicationMode": "production",
    "traceId": "1-685c0f14-1b559a954f0bd0127110d161"
  },
  "status": "COMPLETE"
}

Common reasons for no match results include:

  • Recommended fields, like SSN or ZIP code, were missing from the request. Remember that only including name, date of birth, and ZIP code is extremely unlikely to find a single match unless the provided name is extremely uncommon in the provided ZIP code.

  • The patient’s info doesn’t exactly match what the payer has on file. For example, the patient isn’t using their legal name, or their address has changed.

  • The payer doesn’t support real-time eligibility checks, which makes it impossible for Stedi to determine coverage.

  • The patient is covered under a different name, spelling, or demographic variation.

If you think the patient has coverage, try again with corrections or more data. Even small changes like using a partial SSN or legal name can make a difference.

But keep in mind: Even clean, complete input won’t always return a match. Matches aren’t guaranteed.

Since Stedi supports 1,250+ payers for real-time eligibility, it isn’t feasible to check every patient against every payer. Stedi chooses the most probable payers based on the provided demographic details – if the patient has improbable coverage (for example, if the patient has always lived in New York City but has coverage through a small regional payer in the northwest due to their employer’s location), the request is unlikely to find a match.

Limitations

Insurance discovery is a useful tool when used as a fallback. But it has limitations:

  • Hit rates vary. Just sending a name, date of birth, and ZIP code will almost always result in no matches. Including SSN (even last 4), full address, and gender significantly improves results. With strong input data, match rates typically range from 30–70%.

  • It only returns active coverage for the date of service in the request. It can’t return retroactive or future coverage – only what’s active on the date you specify.

  • It doesn’t determine payer primacy. If you get multiple results, use a COB check to figure out which plan is primary.

  • It doesn’t support dental use cases. If your use case is dental-only, discovery won’t return results – even if the patient is insured.

In most cases, you shouldn’t use insurance discovery for your primary workflow. Use it only when eligibility checks fail or aren’t possible.

Get started with insurance discovery

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. But when you can’t get a clean eligibility check, insurance discovery can fill the gap.

If you’re ready to get started, reach out to us for a demo or free trial. We’d love to help you get set up.

Jun 30, 2025

Products

You can now authorize Stedi to sign enrollment forms for you using delegated signature authority, eliminating over 90% of your team’s enrollment paperwork.

Why we built this

Transaction enrollment is the administrative process a provider must complete to exchange certain types of healthcare transactions with a payer. For example, all 835 ERAs require enrollment. Certain payers require enrollment for other transactions, such as 837 claims and 270/271 eligibility checks.

The enrollment process sometimes involves signing a PDF form before submission to a payer. This step can delay enrollments by days or even weeks as signature requests bounce between you and Stedi.

Delegated signature authority solves this by allowing Stedi to sign enrollment forms on your behalf. It removes overhead for your team and can remove days of delay from the enrollment process.

If you manage enrollments for multiple providers, delegated signature authority scales with you. You can onboard more providers faster with less operations work for your team.

How it works

  1. You sign a one-time delegated signature authority agreement with Stedi.

    • If you submit enrollments on behalf of providers, you’ll need to obtain delegated signature authority from your providers during onboarding.

    • Some providers may not allow delegated signing for various reasons, such as their internal policies or legal requirements. In these cases, you can still submit enrollment requests, but the provider must sign the forms directly.

  2. You submit enrollment requests using the Enrollments API, UI, or a bulk CSV upload.

  3. When a payer requires a signature, Stedi checks whether delegated signing is allowed.

    • If allowed, Stedi signs and submits the form.

    • If not allowed, the provider must sign the form directly. Stedi notifies you on the enrollment request and provides instructions to complete the process.

Next steps

Delegated signature authority is available on all paid Stedi plans.

To get started, contact Stedi support in your dedicated Slack or Teams channel.



Jun 30, 2025

Products

You can now see whether a payer supports medical or dental transactions using the Payers API and Stedi Payer Network.

In the API, the new coverageTypes response field helps you filter the list of payers to only those you care about. If you’re using the Search Payers API endpoint, you can also filter by coverage type. Example:

GET /payers/search?query=blue+cross&coverageType=dental

If you’re using the Payer Network, you can also filter by coverage type.

Why we built this

Stedi connects to both dental and medical payers.

Customers building dental applications kept running into the same problem: they couldn’t tell which payers supported dental transactions.

Previously, you could run an eligibility check with STC 35 (General Dental Care) and parse the response – but that was a bit hacky.

We added a coverageTypes field to fix that. It tells you what kind of coverage a payer supports, so you can safely include or exclude them from your workflows.

How it works

Every payer in the network now includes a coverageTypes field in responses from Payers API endpoints – JSON and CSV. For example, in JSON responses:

{
  "items": [
    {
      "displayName": "Blue Cross Blue Shield of North Carolina",
      "primaryPayerId": "BCSNC",
      ...
      "coverageTypes": ["dental"]
      ...
    }
  ]
}

If a payer’s coverageTypes is set to ["medical", "dental"], you can submit supported transaction types for both medical and dental services.

The coverageTypes field applies to any transaction type supported by the payer: eligibility, claims, ERAs, or more.

Try it now

You can filter by coverage type today in both the Payers API and the Payer Network.

Schedule a demo to see it yourself. Or reach out to let us know what else you’d like to see.

Jun 27, 2025

Guide

If you’ve used insurance at a doctor, a healthcare clearinghouse was likely involved.

But most people – even in healthcare – don’t know what a clearinghouse is.

This guide covers what clearinghouses do, who needs one, and why they matter.

What a healthcare clearinghouse does

A clearinghouse helps healthcare providers exchange billing data with payers. Payers include insurance companies, Medicare, and Medicaid.

When your doctor checks your insurance before a visit, that’s a billing transaction – called an eligibility check.

Other common billing transactions include:

  • Claims – a provider asking a payer to pay for their part of a service’s costs

  • Remittances (remits) – a provider receiving payment details or denials from a payer

  • Claim status checks – a provider checking if a claim was received, processed, or delayed

The clearinghouse sits in the middle. It checks the data, keeps it secure, and gets it to the right place.

The jobs of a clearinghouse

The clearinghouse has two main jobs:

  • Connect providers to payers

  • Ensure transactions sent to payers use HIPAA-compliant X12 EDI

HIPAA is a federal law that protects healthcare data and sets rules for how certain transactions must be conducted. It requires that specific billing transactions – like claims and eligibility checks – use the X12 standard of the EDI format.

Without X12, every payer would use a different standard and format. Providers would have to use different formats for different payers. Providers would need custom logic for each one. Billing at scale wouldn’t work.

Connecting providers to payers

In theory, a provider could connect to each payer directly. Some, like large hospital systems, do.

Most don’t. It doesn’t scale.

Even though they all use X12, every payer works differently. Each has its own setup, protocols, and quirks. Connecting to payers takes time and technical skill. Most providers don’t have the staff for it.

That’s where a clearinghouse comes in. They’ve already built payer connections – lots of them – and they keep them running.

But most providers don’t connect directly to a clearinghouse either. Integrating with a clearinghouse still takes engineering work. Most providers don’t have a dev team.

Instead, they use a billing platform that connects to the clearinghouse for them. These platforms can take different shapes:

  • Revenue Cycle Management (RCM) – Software that manages all billing tasks for a provider, including ones that don’t directly involve a payer. That full set of tasks is called RCM.

  • Electronic Health Record (EHR) – Software that stores patient data, like charting, notes, medications, and lab results.

  • Practice Management System (PMS) – Software used by healthcare providers to handle the administrative side of care. In addition to billing, they often help with scheduling and other services.

Providers often layer these platforms on top of each other. For example, a PMS is often used alongside an EHR system.

Handling X12

Clearinghouses don’t just move data between providers and payers. They make sure it’s valid X12.

That process includes:

  • Routing – Sending data to the right payer or provider based on transaction data

  • Translation – Converting common data formats like JSON to X12

  • Validation – Checking for required X12 fields and formatting

  • Delivery – Sending over the right transport protocol

  • Parsing – Turning raw X12 payer responses back into usable data

Some clearinghouses give you raw EDI and expect you to handle it. Others – like Stedi – may also let you use JSON and handle the EDI layer for you.

HIPAA compliance

Healthcare billing data includes protected health information (PHI) – data like names or insurance IDs that can identify patients. Every system that sends, receives, or stores PHI must follow HIPAA rules.

To comply with HIPAA, the clearinghouse must:

  • Encrypt data in transit and at rest

  • Control who can access the data

  • Keep audit logs for every transaction

This matters. It means billing platforms don’t need to build security from scratch. The clearinghouse does it by default.

Why your clearinghouse matters

Billing platforms work with many providers. To scale, they need to write software that automates healthcare transactions. They can't afford the staff – or time – to call payers or use manual payer portals. So they hire developers.

But most legacy clearinghouses weren't built for developers. They have:

  • Poorly documented APIs

  • Cryptic error messages

  • Frequent outages with no updates

  • Slow, unhelpful support

Healthcare transactions are already hard to automate. Most devs don’t know X12. Transactions can fail in strange ways. Error codes don't help. Payers go down without warning. Payer docs don’t match actual responses. And every payer works differently.

When issues hit, you need a clearinghouse that can help. In most cases, they don’t. You submit a ticket, wait days for a reply – then get a boilerplate answer that doesn’t work.

If it’s urgent, you’re on your own. Your team has to scramble to create temporary fixes or call payers themselves.

The right clearinghouse fixes all that. They give you fast, responsive support. Instead of slowing you down, they speed you up and help you scale.

A clearinghouse for developers

If you're building an RCM, EHR, or provider platform, we built Stedi for you. We're an API-first clearinghouse that helps you move fast and scale.

When you hit issues, we give you fast, real-time support from real engineers – not tickets or boilerplate.

Don't take our word for it. See it for yourself. Contact us to set up a demo.

Jun 26, 2025

Spotlight



George Uehling at Ritten

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from George Uehling, Head of Product at Ritten. You'll learn what Ritten does and how George thinks RCM will change in the next few years.

What does Ritten do?

Ritten builds modern Electronic Health Record (EHR) software tailored specifically for Behavioral Health providers.

Its platform is designed to support mental health practices, group therapy clinics, and treatment centers by streamlining documentation, scheduling, billing, and insurance workflows.

Key features include intuitive clinical documentation tools for a wide range of roles – therapists, psychiatrists, counselors, administrators, and technicians – ensuring ease of use across the board.

Ritten also includes integrated Revenue Cycle Management (RCM) to simplify insurance billing, a built-in CRM to streamline client intake, and a strong emphasis on automating repetitive administrative tasks, particularly in note taking and billing.

Ritten delivers an all-in-one solution built to fit the unique workflows of Behavioral Health providers.

How did you end up working in health tech?

With an engineering background, I've always been focused on building useful technology.

I gravitated toward product management as the perfect blend of customer-facing conversations and technology-driven problem solving, giving me the opportunity to deliver new tools that genuinely improve people’s lives.

When the chance came up to work in behavioral health, I jumped on it. Not only did it allow me to keep doing what I loved, but I also got to do it in service of providers who face some of the most complex, emotionally demanding challenges every day.

How does your role intersect with RCM?

Before stepping into my current role as Head of Product, I served as the Product Manager for RItten's RCM solution.

In that role, I spoke directly with dozens of billers and deeply immersed myself in their day-to-day workflows. That experience gave me firsthand insight into the bottlenecks and pain points across the revenue cycle: from coding and claims submission to denials management and payment posting.

Today, that perspective continues to inform how I prioritize features, guide product strategy, and ensure that RCM features and automations are built for billers.

What do you think RCM ops will look like two years from now?

Over the next year or two, each step of RCM will increasingly incorporate AI layered on top of raw clearinghouse data. This shift is already underway. Vendors are introducing AI-powered Verification of Benefits (VOB) tools that make eligibility data easier to query and understand. AI-driven solutions will become more prominent in other areas such as claim scrubbing, interpreting status updates, and remittances.

The next major shift will be the “agentification” of the RCM workflow. AI agents won’t just surface better insights; they’ll begin taking action autonomously, such as contacting payers to appeal denials or updating systems with the latest claim statuses from external systems.

On the payer side, insurance companies are working to modernize the Prior Authorization process through a new electronic submission standard. Although this initiative has seen fits and starts over the years, momentum is building. It would allow providers to submit authorization requests as seamlessly as they do VOBs and claims, ushering in a future where utilization review is significantly faster and more automated.

Jun 24, 2025

Guide

Most payers don’t support procedure codes in 270 eligibility requests.

This guide explains how to work around that using Stedi's Eligibility Check APIs. It covers how to test a payer for procedure code support and common procedure-to-STC mappings.

Why procedure codes don't work for eligibility

A procedure code is a billing code for a specific healthcare service or product – like a dental cleaning or an ambulance ride. It’s what you use to submit claims. It tells the payer what service was performed.

You'd think procedure codes would also work with eligibility requests. Procedure codes are specific. They’re the same codes you use to bill. And they’re supported by the 270 X12 format.

But most payers ignore them in eligibility requests.

Instead, payers expect a Service Type Code (STC) like 30 (General Medical Care) or MH (Mental Health). STCs are broad categories that group related procedures. This makes things simpler for payers. There are thousands of procedure codes. There are fewer than 200 STCs.

If you send a procedure code anyway, most payers just send a fallback response for STC 30 (General Medical) or 35 (General Dental) – or nothing at all.

While common patterns exist, there's no standard way to match procedures to STCs. Each payer has their own mapping, and they don't document how they do it. Even for a single procedure code, the right STC might vary based on the provider type, place of service, or other modifiers.

Common types of procedure codes

There are a few types – or sets – of procedure codes. Major ones include:

  • Current Procedural Terminology (CPT) codes – Used for most medical services. Maintained by the American Medical Association (AMA).

  • Healthcare Common Procedure Coding System (HCPCS) codes – Used for things like medical equipment, ambulance rides, and certain drugs. Includes CPT codes as Level I.

  • Current Dental Terminology (CDT) codes - Used for dental services. Maintained by the American Dental Association (ADA).

How to use a procedure code or STC in a 270 request

You can send either a procedure code or an STC in an eligibility request - not both. If you’re using Stedi’s JSON eligibility API endpoints, you include them in the encounter object:

// Example using a procedure code (CPT 97802)
"encounter": {
  "productOrServiceIDQualifier": "HC",	// CPT/HCPCS codes
  "procedureCode": "97802"			    // CPT code for medical nutrition therapy
}

// Example using an STC
"encounter": {
  "serviceTypeCodes": ["1"] 		    // STC for medical care
}

If using STCs, send one per request. Some payers accept multiple STCs, but test first. See How to avoid eligibility check errors for testing tips.

Note: Technically, you can send both a procedure code and STC using encounter.medicalProcedures and encounter.serviceTypeCodes respectively. However, no payer in our Payer Network other than CMS HETS supports both properties.

Where to find procedure-level info in 271 responses

Even if a payer doesn’t support procedure codes in 270 requests, they might include procedure details in the 271 response.

If you’re using Stedi’s JSON eligibility API endpoints, most benefits information is in the response’s benefitsInformation objects. Here’s what to look for:

The compositeMedicalProcedureIdentifier field

This means the payer tied benefits to a specific procedure:

{
  "code": "B",			                      // Co-pay
  "benefitAmount": "50",		              // $50 co-pay
  "serviceTypeCodes": ["35"],	              // General dental care
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // American Dental Association (ADA)
    "procedureCode": "D0120"     // Periodic Oral Evaluation - established patient
  },
  ...
}

Check additionalInformation.description

Some payers stuff procedure codes into the free-text notes in additionalInformation.description:

{
  "code": "B",
  "serviceTypeCodes": ["35"],	// General dental care
  ...
  "additionalInformation": [
    {
      "description": "Benefit applies to D0150 - Comprehensive oral evaluation"
    }
  ]
}

How to test a payer for procedure code support

There’s no definitive list of which payers support procedure codes in eligibility checks. The only way to find out is to test.

Here’s our recommended approach:

  • Send a 270 request with your procedure code.

  • Send another 270 with the likely STC. See common mappings below.

  • Compare the 271 responses.

Do this for your most common procedures and payers. Create your own mapping to keep track of what works for each payer.

Common mappings to try

These mappings are starting points. Test them with your payers.

For a complete list of STCs, check out Service Type Codes in the Stedi docs.

Medical procedures (CPT/HCPCS)

Procedure

Description

STCs to try

90834, 90837

Psychotherapy

MH, CF, A6, 98

90867

Transcranial magnetic stimulation

MH, A4

96130

Psychological testing evaluation

MH, A4

96375

IV push

92

96493

Chemotherapy, IV push

82, 92

96494

Chemotherapy, additional infusion

82, 92

97802

Medical nutrition therapy

98, MH, 1, BZ

97803

Medical nutrition follow-up

98, MH, 1, BZ

99214

Psychiatry visits

MH, A4

99490, 99439, 99487, 99489, 99491, 99437

Chronic Care Management (CCM) services

1, 30, 98, MH, A4

98975, 98976, 98977, 98980, 98981

Remote Therapeutic Monitoring (RTM) services

1, 30, 92, DM, MH, A4, 98

E1399

Durable medical equipment, miscellaneous

11, 12, 18

A0100, A0130, A0425, T2003

Non-emergency transportation (taxi, wheelchair van, mileage, trip)

56, 57, 58, 59

Dental procedures (CDT)

For CDT codes, industry bodies like the ADA and NDEDIC have published recommended CDT-to-STC mappings. These are useful starting points – but they’re not enforced. Payers can ignore them.

You can find the recommended mappings in:

You can buy those documents or contact Stedi for help with a specific code.In addition to the guides, you can try the mappings below.

Procedure

Description

STCs to try

D4210

Gingivectomy or gingivoplasty

25

D4381

Local delivery of antimicrobial agent

25

D5110

Complete maxillary (upper) denture

39

Get expert help fast

Want help figuring out the right STC for a specific code? Reach out – Stedi’s eligibility experts have seen a lot. We’re happy to help.

Jun 24, 2025

Products

You can now run batch 270/271 eligibility checks by uploading a CSV file in the Stedi portal.

Batch checks refresh patient eligibility between visits. Run them weekly or monthly to catch insurance issues early – before they cause problems.

You can use the new Eligibility check batches page to run batch checks using a bulk CSV. Each file can include up to 1,000 checks. You can upload and run more than one file at a time.

Upload a batch eligibility check CSV in the portal

Before, you could only run batch refreshes using the Batch Eligibility Check API.

Like the API, CSV batch checks run asynchronously. They don’t count against your concurrency limit. And they won’t slow down real-time checks.

You can pull the results of a CSV or API batch check using the Poll Batch Eligibility Checks API. You can also now track the real-time status of every batch check – whether API or CSV – directly on the Eligibility check batches page.

How to run a CSV batch check

  1. Log in to Stedi.

  2. Go to the Eligibility check batches page. You can also select Eligibility > Batch eligibility checks in the Stedi portal’s nav.

  3. Click New CSV batch and give it a name.

  4. Download the template and fill it out. Use one row per check.

  5. Upload your file.

  6. Click Verify file to check for errors. You can fix and re-upload a file as many times as you need.

  7. Click Execute batch to run the checks.

The batch will move to In progress. When all checks are done, it will show Completed. Some checks may fail – that’s normal. You can review and debug them in Stedi’s Eligibility Manager.

All batch checks in one place

The Eligibility check batches page shows all your batch checks – whether you used the API or uploaded a CSV.

Eligibility check batches page in Stedi portal

Click the batch name to view its details. Here, you can see the status of each check – including any errors – as well as the payer, subscriber, and provider.

Batch eligibility check statuses

If the batch was submitted using the portal, it’ll use the name you entered. If the batch was submitted via the API, it’ll use the name value from the request, if provided. If no name is provided, it’ll default to the auto-generated batchId.

If the batch was submitted as a CSV file, you can also download the original CSV input.

You can pull results from any batch using the API – even ones uploaded in the portal. Just use the batchId.

What’s in the CSV

The template covers the most common fields for eligibility checks, including (non-exhaustively):

  • Patient name

  • Date of birth

  • Member ID

  • Provider NPI

  • Payer ID

  • Service Type Codes (STCs)

If you need extra fields, use the API or contact Stedi Support to request them.

Costs

The costs for running a batch eligibility check – manually or using the API – are the same as running the equivalent number of real-time eligibility checks.

For example, if you run a batch with 500 checks, it will cost the same as running 500 real-time eligibility checks.

Try it now

We built CSV uploads for teams who need to move fast – whether you're testing a new workflow or keeping things simple.

If you're ready to go deeper, reach out. We'll help you get set up.

Jun 24, 2025

Products

You can now use the List Payers CSV API to get a full list of Stedi’s supported payers in CSV format:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/csv \
  --header 'Authorization: <api-key>'

The CSV includes the same data as the Stedi Payer Network UI and other JSON-based Payer APIs:

  • Payer IDs

  • Transaction support flags

  • Transaction enrollment requirements, and more.

No setup or feature flag is needed to access the new endpoint. Just use your Stedi API key.

Why we built this

If you’ve worked with a legacy clearinghouse, you’ve probably dealt with CSV payer lists.

Sometimes they show up in your email inbox. Sometimes you have to dig them out of a portal. Either way, they’re static and go stale fast. You end up guessing what’s still valid – and maintaining brittle mappings to keep things running.

That’s risky. Every healthcare transaction depends on using the right payer ID. If the ID is wrong, the transaction fails. At scale, your system fails. And payer IDs change often.

With most clearinghouses, there’s no easy way to know which IDs still work.

That’s why we built a better way.

We already expose our payer lists programmatically through our JSON-based Payer APIs. Now you can get the same list as a CSV – updated in real time, with one row per payer. It’s easy to load into Google Sheets or Excel, feed into your tools, or compare against your current setup.

If you’re migrating to Stedi, this makes it easier. One API call gives you everything you need.

How it works

Make a GET request to the List Payers CSV API endpoint:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/csv \
  --header 'Authorization: <api-key>'

You’ll get a plain-text CSV. The first row contains headers. Each row after that is one payer.

Example of Stedi's CSV payer list

The CSV includes:

  • The payer’s immutable Stedi payer ID

  • Their name, primary payer ID, and known payer ID aliases

  • Supported transaction types

  • Whether transaction enrollment is required for a transaction type

Try it out

The List Payers CSV API is free on all paid Stedi plans.

To see how it works for yourself, reach out to schedule a demo.

Jun 18, 2025

Guide

Payers reject eligibility checks for all kinds of reasons. For failed requests, X12 271 eligibility responses typically include a segment called AAA.

AAA errors tell you what went wrong. Payers return them for things like bad member IDs, missing info, and system outages. They also include tips for what to do next.

Stedi’s Eligibility Check APIs let you get 271 responses as JSON or raw X12 EDI. This guide explains how to find AAA errors in 271 responses, what the most common ones mean, and how to fix them.

How to find and read AAA errors

Every AAA error includes three pieces of information:

  • Code – What went wrong, such as a bad member ID or invalid name.

  • Followup Action – What the payer recommends you do next.

  • Location – The location of the error within the original X12 EDI response.

If you’re reading raw X12, the following table outlines possible loop locations:

Loop

Related part

2100A

Payer

2100B

Provider

2100C

Subscriber

2100D

Dependent

If you’re using JSON responses, you don’t need to decode loops. Instead, AAA errors are nested under the related section of the response:

  • subscriber.aaaErrors[] – The most common spot for AAA errors.

  • dependents[].aaaErrors[] – If checking eligibility for a dependent.

  • provider.aaaErrors[] – Typically indicates an NPI or transaction enrollment issue.

  • payer.aaaErrors[] – Usually indicates a connectivity or access issue with the payer.

All errors at these levels are also returned in the top-level errors array. Example:

{
 "errors": [
    {
      "code": "43",
      "followupAction": "Please Correct and Resubmit",
      "location": "Loop 2100B",
      ...
    }
  ],
  ...
}

Common AAA errors

This section covers the most common AAA errors. Errors are listed in numerical order – not by frequency – for easier lookup.

For complete details, see Payer AAA errors in the Stedi docs.

15 – Required Application Data Missing

The request didn’t include enough info to identify the patient. Or the request is missing a required provider field, like Tax ID.

How to fix it:

  • Double-check the patient’s name, date of birth (DOB), and member ID.

  • Include the provider’s Federal Taxpayer Identification Number (EIN) in the request’s provider.taxID field.

33 – Input Errors

The request is missing payer-required fields or includes invalid data.

Some payers issue each dependent their own member ID and don’t support eligibility requests that include a dependents array. These payers may return this error if the request includes the patient in that array.

How to fix it:

  • Double-check that you’re sending all required fields:

    • Patient first name

    • Patient last name

    • Patient date of birth (DOB)

    • Patient member ID

    • Provider NPI

    • Service Type Code (STC) or procedure codes

  • Make sure all values are in the correct format, especially the patient’s DOB and member ID.

  • If the request includes a dependent patient in the dependents array, try sending their info in the subscriber object instead.

41 – Authorization/Access Restrictions

The provider isn’t authorized to submit eligibility checks to the payer.

How to fix it:

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

42 – Unable to Respond at Current Time

The payer is temporarily unavailable.

How to fix it:

  • Automatically retry using the retry strategy outlined in our docs.

  • If retries fail, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

43 – Invalid/Missing Provider Identification

The NPI you sent isn’t valid for the payer, or the provider isn’t enrolled with the payer for eligibility requests.

How to fix it:

  • Make sure the NPI is correct.

  • Confirm the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • Some payers require transaction enrollment for eligibility requests. If so, ensure the provider is enrolled with the payer.

  • If supported, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

50 – Provider Ineligible for Inquiries

The payer doesn’t allow the provider to submit eligibility checks for the Service Type Code (STC).

How to fix it:

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

  • Confirm that the provider is enrolled with the payer for the Service Type Code (STC) you're using. Some payers only allow certain specialties to check eligibility for specific benefits.

51 – Provider Not on File

The payer doesn’t recognize the provider’s NPI. This usually means the provider isn’t registered with the payer.

How to fix it:

  • Make sure the NPI is correct.

  • Confirm the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

  • Some payers require credentialing before accepting eligibility checks from a provider. The provider must contact the payer to register.

52 – Service Dates Not Within Provider Plan Enrollment

The provider wasn’t enrolled in the patient’s plan with the payer on the date of service.

How to fix it:

  • Confirm the patient was actively enrolled on the specific date of service.

  • Double-check the date of service in the request. Ensure the date(s) are properly formatted as YYYYMMDD.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

57 – Invalid/Missing Date(s) of Service

The date of service (DOS) is missing, incorrectly formatted, far in the future, or outside the payer’s allowed range.

How to fix it:

  • Double-check the date of service in the request. Ensure the date(s) are properly formatted as YYYYMMDD.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

  • If requests still fail, try omitting encounter.dateOfService from the request.

58 – Invalid/Missing Date-of-Birth

The subscriber or dependent’s date of birth is missing or incorrectly formatted. Some payers require it to locate the member.

How to fix it:

  • Include dateOfBirth in the request, formatted as YYYYMMDD.

  • Double-check that the date is accurate and is not a future date or invalid day/month.

  • Some payers require a date of birth (DOB) even when a member ID is present.

62 – Date of Service Not Within Allowable Inquiry Period

The date of service you submitted is outside the payer’s allowed range.

How to fix it:

  • Don’t send dates more than two years in the past. Some payers only support eligibility checks for dates within the last 12 or 24 months.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

63 – Date of Service in Future

The date of service is in the future. The payer doesn’t allow eligibility checks for future dates.

How to fix it:

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

  • Try omitting the date of service from the request.

64 – Invalid/Missing Patient ID

The patient’s ID is missing or doesn’t match what the payer has on file. This usually happens when the payer needs the dependent’s member ID, but the request only includes the subscriber’s.

How to fix it:

  • Check the insurance card. Some plans list separate IDs for dependents. If the patient is a dependent and has their own member ID, treat them as the subscriber and leave out the dependents array.

65 – Invalid/Missing Patient Name

The name of the patient is missing or doesn’t match the payer’s records.

How to fix it:

  • Check the insurance card. Some plans list separate IDs for dependents. If the patient is a dependent and has their own member ID, treat them as the subscriber and leave out the dependents array.

  • Use the full legal name. For example, “Robert” not “Bob.”

  • Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings for compound or hyphenated names. Check if the patient has recently changed names.

67 – Patient Not Found

The payer couldn’t find the patient in their system based on the information you submitted.

How to fix it:

  • Double-check the patient’s name, date of birth (DOB), and member ID.

  • Try sending different combinations of those fields to account for data mismatches – especially if you're missing the member ID.

68 – Duplicate Patient ID Number

The payer found more than one patient record with the ID you submitted. They can’t tell which one you meant.

How to fix it:

  • Include the patient’s name, date of birth (DOB), and member ID to help the payer narrow down the match.

  • In rare cases, this error can occur due to a data issue on the payer side. For example, duplicate records for the same person with the same member ID. If you suspect this, contact Stedi support.

71 – Patient DOB Does Not Match That for the Patient on the Database

The date of birth (DOB) in your request doesn’t match what the payer has on file for the patient.

How to fix it:

  • Double-check the patient’s dateOfBirth. Use the YYYYMMDD format.

  • Confirm the patient’s DOB from a reliable source, like the insurance card or other identification.

72 – Invalid/Missing Subscriber/Insured ID

The member ID doesn’t match the payer’s requirements.

How to fix it:

  • Use the exact ID on the subscriber’s insurance card.

  • If no card is available, run an insurance discovery check using demographic data, like name and date of birth (DOB).

73 – Invalid/Missing Subscriber/Insured Name

The subscriber’s name doesn’t match the payer’s records.

How to fix it:

  • Use the full legal name. For example, “Robert” not “Bob.”

  • Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings for compound or hyphenated names. Check if the patient has recently changed names.

74 – Invalid/Missing Subscriber/Insured Gender Code

The patient’s gender code is missing, incorrect, or not formatted the way the payer expects.

How to fix it:

  • Double-check that the gender matches what’s on file with the payer.

  • Try omitting gender to see if the payer defaults correctly.

75 – Subscriber/Insured Not Found

The payer couldn’t match the subscriber’s details to anyone in their system. This is the most common error.

How to fix it:

  • Verify the member ID matches the patient’s insurance card. Include any prefix or suffix on the patient’s ID.

  • Double-check the patient’s name and date of birth (DOB).

  • Use the full legal name. For example, “Robert” not “Bob.” Avoid nicknames, abbreviations, or special characters. Try different name orderings. Check if the patient has recently changed names.

  • If the info is correct, confirm the request is going to the right payer.

76 – Duplicate Subscriber/Insured ID Number

The payer found more than one member with the subscriber ID you sent. They can’t determine which one to return.

How to fix it:

  • Include the patient’s name, date of birth (DOB), and member ID in the request.

78 – Subscriber/Insured Not in Group/Plan identified

The payer found the member, but they aren’t enrolled in the group or plan you specified.

How to fix it:

  • If possible, include the groupNumber or planNumber in the request. Make sure it matches what’s on the member’s insurance card.

  • Try omitting the groupNumber. Many payers can still return eligibility without it.

79 – Invalid Participant Identification

If the response has a 200 HTTP status, this usually means there’s a connectivity issue with the payer. If the response has a 400 HTTP status, it means the payer ID is invalid or the payer doesn’t support eligibility checks.

How to fix it:

  • If the error comes back with a 200 HTTP status, automatically retry using the retry strategy outlined in our docs.

  • If you get a 400 status, don’t retry. Confirm the payer ID and that the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • If all requests continue failing, contact Stedi support.

80 – No Response Received - Transaction Terminated

The payer didn’t return any eligibility data. The transaction timed out or failed midstream.

How to fix it:

  • Automatically retry using the retry strategy outlined in our docs.

  • If retries fail, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

97 – Invalid or Missing Provider Address

The address submitted for the provider is missing or doesn’t match what the payer has on file.

How to fix it:

  • Double-check the provider’s address in informationReceiverName.address. Check for formatting issues, like missing ZIP or street line, or common mismatches (e.g. “St.” vs. “Street”).

Retryable AAA errors

Only AAA errors 42, 79, and 80 are retryable. These indicate temporary payer issues like downtime or throttling. All other AAA errors require fixing before retrying.

AAA 79 errors are only retryable if it comes back with a 200 HTTP status. If you get a 400 status, it usually means the payer ID is invalid or not configured. Don’t retry in these cases.

The right retry strategy depends on your use case:

Retry strategy for real-time eligibility checks
If you’re using the real-time endpoint and need a fast response – like checking in a patient – we recommend:

  • Wait 15 seconds before the first retry.

  • Retry every 15 seconds for up to 2 minutes.

  • Don’t send the same NPI to the payer more than once every 15 seconds.

  • If requests still fail, stop retrying and contact Stedi support.

If requests still fail, stop retrying and contact Stedi support.

Retry strategy for batch refreshes
If you’re running eligibility refreshes between appointments, use the batch endpoint. For this endpoint, Stedi automatically retries AAA 42, eligible 79, and 80 errors in the background for up to 8 hours.

If you’re using the real-time endpoint and can tolerate longer delays:

  • Wait 1 minute before the first retry.

  • Then exponentially back off – up to 30 minutes between retries

  • Continue retrying for up to 8 hours.

For full guidance, see our retry strategy docs.

How to handle non-retryable AAA errors

If a 271 response includes any AAA errors, treat it as a failure – even if it includes benefits data. Payers sometimes return benefits alongside AAA errors. In JSON responses, you can check the top-level errors array to quickly detect AAA errors.

If the error isn’t retryable, use Stedi’s Eligibility Manager to debug and try the request again.

How to mock AAA errors

In Stedi’s test mode, you can use mock eligibility checks to simulate common AAA errors and test your application’s error-handling logic. See Eligibility mock requests for more details.

Fast, expert eligibility help

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. When errors do happen, you get help fast. Our average support response time is under 8 minutes.

Want to see how good support can be? Get in touch.

Jun 17, 2025

Guide

Insurance verification is important for dental care. Before the provider can get paid, they need to know what a patient’s plan covers. The patient needs to know too – so they’re not surprised by a bill later.

That’s where Stedi comes in. We make it easy to check dental insurance in real time. Stedi's Eligibility Check APIs let you work with JSON or raw X12 EDI. You can check coverage with thousands of payers, including major dental insurers like Delta Dental, DentaQuest, and Guardian.

This post answers the most common questions we hear from developers using Stedi to check dental eligibility.

What STCs should I use for dental?

A Service Type Code (STC) tells the payer what kind of benefits you're checking. For general dental coverage, use STC 35 (Dental Care) in the eligibility request:

"encounter": {
  "serviceTypeCodes": ["35"]
}

If you leave it out, Stedi defaults to 30 (Health Benefit Plan Coverage). This may return incomplete or irrelevant data. Many payers only return dental benefits for STC 35.

Other common dental STCs include:

  • 4 - Diagnostic X-Ray

  • 5 - Diagnostic Lab

  • 23 - Diagnostic Dental

  • 24 - Periodontics

  • 25 - Restorative

  • 26 - Endodontics

  • 27 - Maxillofacial Prosthetics

  • 28 - Adjunctive Dental Services

  • 36 - Dental Crowns

  • 37 - Dental Accident

  • 38 - Orthodontics

  • 39 - Prosthodontics

  • 40 - Oral Surgery

  • 41 - Routine (Preventive) Dental

Most payers only support one STC per request. Don’t send multiple STCs unless you’ve tested that it works. For testing tips, see our How to avoid eligibility check errors blog.

Which payers support dental eligibility checks?

Use the coverageTypes field in Payers API responses:

{
  "items": [
    {
      "displayName": "Blue Cross Blue Shield of North Carolina",
      "primaryPayerId": "BCSNC",
      ...
      "coverageTypes": ["dental"]
      ...
    }
  ]
}

If you’re using the Search Payers API endpoint, you can also filter by coverage type.

You can also filter for coverage type in the Stedi Payer Network.

The coverageTypes field applies to any transaction type supported by the payer: eligibility, claims, ERAs, or more.

Can I use CDT codes in eligibility checks?

Yes – but support depends on the payer.

Current Dental Terminology (CDT) codes are procedure codes used in dental billing. For example, D0120 is a routine exam.

You can send a CDT code in your request using the productOrServiceIDQualifier and procedureCode fields. But many payers will return the same data you’d get from STC 35. Those results often include CDT code-level benefits.

To test it, send one request with the CDT code and one with STC 35 to the same payer. Then compare what you get back.

What’s included in dental eligibility responses?

This depends on the payer, but most responses include the following.

Basic coverage
These fields confirm whether the member has dental coverage and when it starts and ends:

  • Coverage status (active/inactive): benefitsInformation.code ("1" = Active, "6" = Inactive).

    Plan start and end dates: planInformation.planBeginDate and planInformation.planEndDate.

Patient responsibility
These fields tell you what the patient might owe:

  • Co-insurance and deductible: benefitsInformation.code and benefitsInformation.benefitPercent or benefitsInformation.benefitAmount.

    Coverage levels for common categories, such as diagnostic or preventative: benefitsInformation.serviceTypeCode (35 for basic or 41 for preventive).

Lifetime maximums
Many dental plans include lifetime maximums. These often show up as two entries with the same benefitsInformation.code.

For example, one for the total lifetime maximum:

{
  "code": "F",
  "serviceTypeCodes": ["38"],
  "benefitAmount": 2000,		// $2,000 amount
  "timeQualifierCode": "32"		// Lifetime maximum
}

One for the remaining amount:

{
  "code": "F",
  "serviceTypeCodes": ["38"],
  "benefitAmount": 1200,		// $1,200 amount
  "timeQualifierCode": "33"		// Lifetime remaining
}

CDT-level detail
Many payers return benefits tied to specific CDT codes, using compositeMedicalProcedureIdentifier:

{
  "code": "A",  				              // Co-insurance
  "insuranceTypeCode": "GP",  		          // Group policy
  "benefitPercent": "0",  			          // Patient owes 0% co-insurance for procedure
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // CDT code qualifier
    "procedureCode": "D0372"  		          // CDT code for the procedure
  },
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "202420722"  // Most recent date this procedure was used
  }
}

Cigna is a known edge case. It puts CDT info in additionalInformation.description as free text.

Age limitations
Age limitations use one of the following quantityQualifierCode values:

  • S8 - Age, Minimum

  • S7 - Age, Maximum

The benefitQuantity is the minimum or maximum age allowed.

{
  // Age limit: patient must be at least 18 years old
  "quantityQualifierCode": "S8",	// Age (minimum)
  "benefitQuantity": "18"

}

Frequency limitations
 Frequency limitations typically use one of the following quantityQualifierCode values:

  • P6 - Number of Services or Procedures

  • VS - Visits

The timePeriodQualifierCode defines the time window (such as 7 for per year). The benefitQuantity sets the frequency limit.

{
  // Frequency limit: 2 services per calendar year
  "quantityQualifierCode": "VS",  			// Visits
  "benefitQuantity": "2",
  "timePeriodQualifierCode": "7",  			// Annual
  "numOfPeriods": "1",
}

History
Many payers include the last time a procedure was done. This shows up in benefitsDateInformation.latestVisitOrConsultation.

Example at the STC level:

{
  // STC-level entry
  "code": "A",                                // Co-insurance
  "insuranceTypeCode": "GP",                  // Group policy
  "benefitPercent": "80",                     // 80% covered
  "serviceTypeCodes": ["41"],                 // Routine (Preventive) Dental
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "20240301"   // Last preventive service date
  }
}

At the CDT level:

{
  // CDT code-level entry
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // CDT code qualifier
    "procedureCode": "D0150"                  // Comprehensive oral evaluation
  },
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "20240404"   // Last time this procedure was used
  }
}

Free-text details
Some payers add notes as free text in additionalInformation.description, like:

  • Frequency limits shared between CDT codes.

  • Waiting periods.

  • Restrictions, such as the missing tooth clause.

For more tips on reading eligibility responses, see our How to read a 271 eligibility response in plain English blog.

How “real time” are Stedi’s real-time eligibility checks?

Most responses come back in 3-4 seconds.

But it depends on the payer. Some take longer – up to 60 seconds.

To handle slow responses, Stedi keeps the request open for up to 2 minutes. During that time, we retry the request in the background if needed.

Can I run dental eligibility checks in batches?

Yes. Use the Batch Eligibility Check API to send up to 1,000 checks at once. This works well if you want to refresh coverage data before appointments.

Batch checks are asynchronous. They don’t count toward your real-time concurrency limit. But the response can take longer – sometimes up to 8 hours.

Got more questions?

Contact us to talk to a dental eligibility expert at Stedi.

Jun 16, 2025

Products

When the status of a transaction enrollment request changes, Stedi now sends you an automated email.

No setup is needed. These email notifications replace our previous manual notification process.

How it works

You can submit a transaction enrollment request via API, UI, or bulk CSV import. When you submit a request, you must provide an email address that Stedi can reach out to with updates or questions. If you’re a vendor submitting on behalf of a provider, you typically provide your own email address.

Stedi monitors the status of each transaction enrollment. When a status changes – say, from PROVISIONING to LIVE – we send you an email. The only time we don’t send an email is when the status changes from DRAFT to SUBMITTED.

Status update emails are sent once per hour and batched per email address:

  • If one enrollment updates, we send a single-entry email.

  • If multiple enrollments update, we send a summary email listing up to 100 changes.

  • If more than 100 updates occur for the same email address in an hour, we send multiple summary emails.

We never include PHI in these emails.

If an enrollment requires action on your part, we’ll continue to reach out to you via Slack or email with next steps.

How it looks

Each single-entry email includes:

  • Enrollment status

  • Payer name

  • Transaction type, such 835 ERA or 270/271 eligibility check

  • Whether a note was added

  • Timestamp

If the status is REJECTED, we'll include the reason and how to fix it in an added note. If you have questions, you can reach out to us on Slack.

Single entry enrollment status email

Summary emails that cover multiple enrollments include:

  • Enrollment statuses

  • Counts for each status

  • Timestamp

Multiple entry email

Clicking Open enrollments opens the Enrollments page in the UI. The page is filtered to the specific list of enrollment requests from the email.

Other ways to track enrollments

Email notifications are one way to stay updated on transaction enrollment requests. You can also:

Enrollment – done for you

Transaction enrollment is one of the biggest bottlenecks in healthcare billing.Tt can stop you from scaling RCM operations.

At Stedi, we handle the hard parts for you: status tracking, payer follow-up, and notifications. This lets you onboard more providers faster – and keep building.

Contact us to set up a demo or POC.

Jun 13, 2025

Company

CAQH CORE has officially certified Stedi for real-time eligibility checks.

As a result of HIPAA, payers, providers, and other parties are required to use the X12 270/271 EDI format for eligibility checks. This format is commonly known as X12 HIPAA.

But X12 HIPAA only covers part of the picture: the transaction schema. It leaves a lot of room for interpretation when it comes to topics like response content, error handling, and system availability. This opens the door to inconsistencies and reliability issues that make it harder for systems to talk to each other.

CAQH CORE certification exists to fix that.

What is CAQH CORE?

The Council for Affordable Quality Healthcare (CAQH) is a non-profit group backed by major health insurers and provider groups. In 2005, it created the Committee on Operating Rules for Information Exchange (CORE) to improve how healthcare systems exchange data. The U.S. Department of Health and Human Services (HHS) designated CAQH as the official rule authoring entity for HIPAA administrative transactions.

CORE builds on the HIPAA-mandated X12 standard. It further defines how to create, respond to, and transmit the X12 transactions. The CORE Operating Rules define stricter, more specific standards for how eligibility data should be exchanged, including:

  • Required data content in the 271 eligibility response, so data is complete and consistent.

  • Support for the CAQH CORE SOAP+WSDL and HTTP MIME Multipart protocols, as defined in the CORE Connectivity Rule vC2, to ensure secure and interoperable data exchange.

  • Standardized error messages to simplify troubleshooting.

  • Uptime, availability, and performance benchmarks.

These rules make system behavior more predictable and interoperable across the industry.

What Stedi’s certification covers

This certification validates our implementation as an Information Requestor for real-time eligibility checks. In practice, that means:

  • Our handling of 270/271 transactions follows the CORE spec.

  • Our use of the CAQH CORE SOAP protocol.

  • Our 271 responses are consistently structured, complete, and reliable.

  • We’ve passed an independent certification test confirming our compliance.

Why certification matters

If you’re building around eligibility, Stedi’s CORE certification saves you time and guesswork. You know exactly how our system behaves – and that it matches industry standards for speed, structure, and reliability.

Verify insurance with Stedi

CORE certification is one of the only public signals that a clearinghouse’s systems work as expected. Ours does.

Contact us to book a demo and start a POC.

Jun 12, 2025

Guide

Payer IDs are the routing numbers of healthcare. Every transaction – including eligibility checks and claim submissions – depends on the right one. If the ID is wrong, the transaction fails.

But finding the right payer ID is hard. Payers go by different names. Their IDs change. Most clearinghouses still send out monthly CSVs, which go stale fast. Developers end up maintaining brittle mappings that break every time the list updates.

The Search Payers API fixes that. It lets you search the Stedi Payer Network for payers by name, payer ID, or payer ID alias. You get accurate, up-to-date results in JSON.

You can use it anywhere you need to look up a payer: intake forms, billing tools, internal dashboards, and more.

We use the API in our own Payer Network UI. This post walks through a simplified version of that implementation, using TypeScript and Next.js. You don’t need to be an expert to follow along. If you know basic Node.js and JavaScript, you’ll be fine.

For full details on the Search Payers API, check out the Search Payers API docs.

How it works

We implement search as an API route in Next.js. The front end sends a request when a user types a payer name or applies filters. This route receives search requests from the front end, sends them to the API, and returns cleaned-up payer results in JSON.

Here’s a simplified version of our real implementation. It shows how to handle the request, construct filters, and format the response. It’s not exactly our production code, but it’s close. You can use it as a starting point for integrating the Search Payers API into your own application.

import { NextApiRequest, NextApiResponse } from "next";
import fetch from "node-fetch";

// Stedi API key, pulled from environment variables
const STEDI_API_KEY = process.env["STEDI_API_KEY"]!;

// The Search Payers API endpoint
const STEDI_SEARCH_URL =
  "https://healthcare.us.stedi.com/2024-04-01/payers/search";

// The handler for the POST /api/search route
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // Only allow POST requests
  if (req.method !== "POST") {
    return res.status(405).json({ message: "Method not allowed" });
  }

  // Extract values from the request body
  const {
    query = "*",
    pageSize = 100,
    pageToken,
    eligibilityCheck,
    claimStatus,
    claimPayment,
    professionalClaimSubmission,
    institutionalClaimSubmission,
    dentalClaimSubmission,
    coordinationOfBenefits,
    unsolicitedClaimAttachment,
  } = req.body as SearchPayersInput;

  // Build query string parameters
  const params = new URLSearchParams({
    query,                            // the payer name, payer ID, or payer ID alias
    pageSize: pageSize.toString(),    // how many results to return
    ...(pageToken && { pageToken }),  // pagination token, if provided

    // transaction support filters
    ...(eligibilityCheck && { eligibilityCheck }),
    ...(claimStatus && { claimStatus }),
    ...(claimPayment && { claimPayment }),
    ...(professionalClaimSubmission && { professionalClaimSubmission }),
    ...(institutionalClaimSubmission && { institutionalClaimSubmission }),
    ...(dentalClaimSubmission && { dentalClaimSubmission }),
    ...(coordinationOfBenefits && { coordinationOfBenefits }),
    ...(unsolicitedClaimAttachment && { unsolicitedClaimAttachment }),
  });

  try {
    // Send GET request to the Search Payers API
    const response = await fetch(`${STEDI_SEARCH_URL}?${params.toString()}`, {
      headers: {
        Authorization: STEDI_API_KEY,
      },
    });

    const data = await response.json() as SearchPayersOutput;
    const payers = data.items?.map((item) => item.payer) ?? [];
    const totalPayers = data.stats?.total ?? 0;

    // Return the formatted results and metadata
    res.status(200).json({
      payers,
      totalPayers,
      nextPageToken: data.nextPageToken,
    });
  } catch (err) {
    // Log and return a server error if the request fails
    console.error("Search error", err);
    res.status(500).json({ message: "Error performing search" });
  }
}

We've left out the interfaces and enums to save space.

Search by payer name, payer ID, or alias

You can pass in a payer name, payer ID, or payer ID alias. The API supports fuzzy matching on names and aliases, so even partial or slightly misspelled inputs work. For example: AETNA, ATENA, and 60054 all return Aetna. Results are ranked by how closely they match the input.

The query parameter is required by the API. In our UI, it’s optional. If the user doesn’t provide a value, we default to "*". This returns all payers, optionally filtered by transaction support. It’s useful for showing initial results or populating a custom dropdown.

Here’s how the query gets extracted and passed:

  // Extract values from the request body
  const {
    query = "*",
    pageSize = 100,
    pageToken,
    eligibilityCheck,
    claimStatus,
    claimPayment,
    professionalClaimSubmission,
    institutionalClaimSubmission,
    dentalClaimSubmission,
    coordinationOfBenefits,
    unsolicitedClaimAttachment,
  } = req.body as SearchPayersInput;

Filter by transaction type

The Search Payers API supports filters for specific transaction types. You can filter by any combination of the following:

  • eligibilityCheck (270/271)

  • claimStatus (276/277)

  • professionalClaimSubmission (837P)

  • dentalClaimSubmission (837D)

  • institutionalClaimSubmission (837I)

  • claimPayment (835 ERA)

  • coordinationOfBenefits (270/271 COB check)

Each filter accepts one of the following values:

  • SUPPORTED – The payer supports the transaction type.

  • ENROLLMENT_REQUIRED – The payer supports the transaction type but requires transaction enrollment.

  • EITHER – Includes both SUPPORTED and ENROLLMENT_REQUIRED.

  • NOT_SUPPORTED – The payer doesn’t support the transaction type.

Here’s how the filters are added to the query string:

const params = new URLSearchParams({
  query,
  pageSize: pageSize.toString(),
  ...(eligibilityCheck && { eligibilityCheck }),
  ...(claimStatus && { claimStatus }),
  ...(claimPayment && { claimPayment }),
  ...(professionalClaimSubmission && { professionalClaimSubmission }),
  ...(institutionalClaimSubmission && { institutionalClaimSubmission }),
  ...(dentalClaimSubmission && { dentalClaimSubmission }),
  ...(coordinationOfBenefits && { coordinationOfBenefits }),
  ...(unsolicitedClaimAttachment && { unsolicitedClaimAttachment }),
});

This setup lets you filter for exactly the payers your app can work with and ignore everything else.

Pagination

The Search Payers API returns a maximum of 100 results per request. If there are more results, the response includes a nextPageToken.

You can pass that token in your next request to fetch the next page. For example:

const response = await fetch(
  `${STEDI_SEARCH_URL}?query=blue+cross&pageToken=${nextPageToken}`,
  { headers: { Authorization: STEDI_API_KEY } }
);

This approach avoids offset-based pagination. You don’t need to track indices or compute limits. Just pass the token forward.

In your handler, you can return the nextPageToken like this:

res.status(200).json({
  payers,
  totalPayers,
  nextPageToken: data.nextPageToken,
});

If nextPageToken is undefined, you’ve reached the end of the results.

Format results for the UI

The API response includes detailed records for each matching payer. Each result includes:

  • stediId – A unique, immutable payer ID you can use in transactions with Stedi

  • displayName – The payer’s name

  • primaryPayerId –The most commonly used payer ID

  • aliases – Other payer IDs or names associated with this payer

  • transactionSupport – Which transaction types the payer supports

  • enrollment – Whether transaction enrollment is required, and how it works

Here’s how you might extract the data you need:

const payers = data.items?.map((item) => item.payer) ?? [];

This keeps the front end simple. If you need more data – like payer ID aliases, enrollment type, or COB support – you can include it as needed.

Tips

Here are a few tips to help get you started with your own implementation of the Search Payers API.

Multiple filters use AND logic.
If you pass multiple transaction filters, the API only returns payers that match all of them.

The Search Payers API is designed for routing, not patients.
The payer names and aliases are optimized for clearinghouse routing – not for display. If you need a patient-facing dropdown, build your own list and map to the Stedi payer ID.

Try it yourself

The Search Payers API is available on all paid Stedi plans.

To get started, contact us. We’ll help you set up a proof of concept and walk you through the integration.





Jun 9, 2025

Guide

Stedi’s Eligibility Check APIs let you get 271 eligibility responses as JSON. That makes them easier to use in code – not easier to understand.

The 271 format is standard. The data inside isn’t. Most fields are optional, and payers use them in different ways. Two payers might return different info for the same patient or put the same info in different places. Luckily, there are consistent ways to extract the data you need.

This guide shows you how to read Stedi’s 271 responses in plain English. You’ll learn how to check if a patient has coverage, what benefits they have, and what they’ll owe. It also includes common mistakes and troubleshooting tips.

This article covers the highlights. For complete details, see Determine patient benefits in our docs or contact us.

Where to find most benefits info

Most of a patient’s benefit info is in the 271 response’s benefitsInformation array.

Each object in the array answers a different question about benefit coverage: Is it active? What’s the co-pay? What's the remaining deductible?

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network services
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["30"],         // General medical
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["30"],
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    }
  ],
  ...
}

Each benefitsInformation object includes a few key fields. Most of them contain codes:

  • code: What the benefit is, like "1" (Active Coverage), "B" (Co-pay), or "C" for (Deductible). Although it's one field, there are two classes of codes: 1-8 for coverage status and A-Y for benefit categories. For more details, see Benefit type codes in the docs.

  • serviceTypeCodes: What kind of care the benefit applies to, like "30" (General Medical) or "88" (Pharmacy). See Service Type Codes in the docs.

    Some Service Type Codes (STCs) are broader categories that include other STCs. For example, "MH" (Mental Health) may include "A4" (Psychiatric), "A6" (Psychotherapy), and more. But this varies by payer.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include its STC.

  • timeQualifierCode: What the benefit amount represents – often the time period it applies to, like "23" (Calendar Year). Sometimes, this indicates whether the amount is a total or remaining portion, like "29" (Remaining Amount). For the full list, see Time Qualifier Codes in the docs.

    Use this field to understand how to interpret the dollar amount. For example, whether it’s the total annual deductible or the remaining balance of a maximum.

  • inPlanNetworkIndicatorCode: Whether the benefit applies to in-network or out-of-network care – not whether the provider is in-network. Possible values are "Y" (In-network), "N" (Out-of-network), "W" (Both), and "U" (Unknown). For more details, see In Plan Network Indicator in the docs.

  • additionalInformation.description: Free-text notes from the payer. These often override structured fields. Payers often include important info here that doesn’t fit elsewhere.

Most of these fields have human-readable versions, like codeName for code. Use those for display, not logic. Always use the related code field in your code.

Unless otherwise indicated, the fields referenced in the rest of this guide are in benefitsInformation objects.

Check active coverage

To check if a patient has active coverage, look for two things:

  • A benefitsInformation object with code = "1"

  • A date range that includes the date of service

Start with the code. In the following example, the patient has coverage for general medical care.

{
  "code": "1",                      // Active coverage
  "serviceTypeCodes": ["30"]        // General medical
}

Note: Some payers use code: "D" (Benefit Description) entries to list coverage exclusions or limitations. Check these alongside code: "1" entries for a complete picture of benefits coverage.

Next, check the coverage dates. If there’s a benefitsDateInformation field in the same object, use that:

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "benefitsDateInformation": {
    "service": "20241216-20250114", // Coverage window for this benefit
    "periodStart": "20981216"       // Optional start date (duplicate of above)
  }
}

The benefitsDateInformation dates apply specifically to the benefit in the object. They override the top-level plan dates, so they take precedence.

If that’s missing, use the top-level planDateInformation field:

{
  "planDateInformation": {
    "planBegin": "20250101",       // Plan start date
    "planEnd": "20251231"          // Plan end date
  },
  ...
  "benefitsInformation": [
    {
      "code": "1",                 // Active coverage
      "serviceTypeCodes": ["30"]   // General medical
    }
  ]
}

planDateInformation contains the coverage dates for the patient’s plan.

If the date of service isn’t in the date range, coverage is not active, even if code = "1".

Get patient responsibility

Patient responsibility is what the patient has to pay, usually before or at the time of service. This includes co-pays, co-insurance, or deductibles.

Each cost type uses a different code, and the amount is either a dollar (benefitAmount) or a percent (benefitPercent).

code

What it means

Field to read

A

Co-insurance

benefitPercent (Percentage)

B

Co-payment

benefitAmount (Dollar amount)

C

Deductible

benefitAmount (Dollar amount)

F

Limitations (Maximums)

benefitAmount (Max covered)

J

Cost Containment

benefitAmount (Dollar amount)

G

Out-of-pocket max

benefitAmount (Dollar limit)

Y

Spend down (Medicaid)

benefitAmount (Amount to quality)

Use inPlanNetworkIndicatorCode to see if the cost applies in-network ("Y") or out-of-network ("N"). If both in- and out-of-network costs exist, you’ll see two benefitsInformation objects with the same code, one for each.

Example: $20 co-pay for in-network mental health

{
  "code": "B",                        // Co-payment
  "serviceTypeCodes": ["MH"],         // Mental health
  "benefitAmount": "20",              // $20 per visit
  "inPlanNetworkIndicatorCode": "Y"   // Applies only to in-network services
}

Example: $1,000 annual deductible with $500 left

{
  "code": "C",                      // Deductible
  "serviceTypeCodes": ["30"],       // General medical
  "timeQualifierCode": "23",        // Calendar year total
  "benefitAmount": "1000",          // $1,000 total
  "inPlanNetworkIndicatorCode": "Y"
},
{
  "code": "C",
  "serviceTypeCodes": ["30"],
  "timeQualifierCode": "29",        // Remaining
  "benefitAmount": "500",           // $500 left to meet deductible
  "inPlanNetworkIndicatorCode": "Y"
}

Check prior authorization requirements

Some services need prior authorization, also called preauthorization. That means the payer must approve the service before they’ll cover it.

Check authOrCertIndicator:

authOrCertIndicator

What it means

Y

Prior auth required

N

Not required

U

Unknown

If authOrCertIndicator is missing, it means prior auth isn’t required or the payer didn’t return that info. In practice, most payers set this field to "Y" if prior auth is required for at least some services.

Also check additionalInformation.description. Payers often add notes about prior authorization there.

{
  "additionalInformation": [
    {
      "description": "Preauthorization required for all imaging services performed out-of-network."
    }
  ]
}

If the free text says prior auth (may also be called “preauthorization”) is needed, trust it – even if authOrCertIndicator says otherwise.

Check if benefits apply to in-network providers

The field inPlanNetworkIndicatorCode only tells you whether a benefit applies to in-network care. It doesn’t tell you if the provider is in-network. Example:

{
  "code": "B",                        // Co-payment
  "serviceTypeCodes": ["88"],         // Pharmacy
  "benefitAmount": "10",
  "inPlanNetworkIndicatorCode": "Y"   // Co-pay applies to in-network services
}

This means: If the provider is in-network and the co-pay is $10. It doesn’t say whether the provider actually is in-network.

To check if a provider is in-network:

You can’t tell if a provider is in-network just from the 271. Your best option is to call the payer or provider directly. Some payers may offer FHIR APIs you can use.

Some payers include network status for the provider as free text in additionalInformation.description. However, it’s not standardized and may not be reliable. It's best to confirm via phone. Example:

{
  "description": "Provider is out-of-network for member."
}

Check for a Medicare Advantage plan

A 271 response won’t always say “Medicare Advantage” directly – but you can often infer it.

From a commercial payer:

It’s likely a Medicare Advantage plan if either of the following are true:

  • insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B).

  • A hicNumber is populated in benefitsAdditionalInformation or planInformation. This is the patient’s Medicare Beneficiary Identifier (MBI).

Example: Medicare Advantage indicators

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "insuranceTypeCode": "MA",
  "benefitsAdditionalInformation": {
    "hicNumber": "123456789A"
  }
}

From a CMS response:

Look for code = "U" and serviceTypeCodes = ["30"]. Then check for a message in benefitsInformation.additionalInformation.description that includes MA Bill Option Code: in the free text:

{
  "additionalInformation": [
    {
      "description": "MA Bill Option Code: B"
    }
  ]
}

The bill option code tells you how claims are handled. If you see B, C, or 2, it’s likely a Medicare Advantage plan.

Bill option code

What it means

A, 1

 

Claims go to Medicare

B, 2

Medicare Advantage plan handles some claims

C

Medicare Advantage plan handles all claims

Benefit overrides and free-text messages

Not everything is in a structured field. Some of the most important rules only show up in additionalInformation.description as free text.

This free text can include:

  • Prior auth or referral rules

  • Network status hints

  • Legal notices (like NSA or BBPA)

  • Plan limitations or quirks

This field contains overrides. If it contradicts a structured value, like authOrCertIndicator or code, trust the text.

We recommend you surface this text to end users or flag it for review. Ignoring it means missing critical info.

Example: Prior auth rule not shown in authOrCertIndicator:

{
  "code": "B",
  "serviceTypeCodes": ["MH"],
  "benefitAmount": "20",
  "inPlanNetworkIndicatorCode": "Y",
  "additionalInformation": [
    {
      "description": "Preauthorization required for mental health visits after 6 sessions."
    }
  ]
}

Example: Coverage excluded even though code = "1":

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "additionalInformation": [
    {
      "description": "Coverage excluded due to missing referral."
    }
  ]
}

Common errors and troubleshooting

Eligibility responses aren’t always clean. Payers sometimes return conflicts or errors.

Here’s how to handle common problems:

No code = "1" for active coverage

That doesn’t necessarily mean coverage is inactive. Check for:

  • code = "6" (Inactive)

  • code = "V" (Cannot Process)

  • code = "U" (Contact Following Entity for Eligibility or Benefit Information)

Some payers send code = "V" or code = "U" first but still include code = "1" later. If you see a valid code = "1", use it.

Top-level errors

If the response includes a populated top-level errors array, the whole response is invalid. Even if it includes benefitsInformation. Use Stedi’s Eligibility Manager to debug and try the request again.

Unclear results? Retry with STC 30 (Medical) or 25 (Dental)

If the response is confusing, resend the eligibility check using STC 30 for medical or STC 35 for dental.

These STCS are the most widely supported and usually give the clearest data.

Fast, expert support for eligibility

Stedi’s Eligibility Check APIs let you build fast, reliable eligibility checks. Even with the best tools, you’ll sometimes hit errors or unclear responses.

When that happens, we can help – fast. Our average support time is under 8 minutes.

Want to see how good support can be? Get in touch.

Jun 5, 2025

Products

You can now submit transaction enrollments to select payers in a single step. No PDFs, no portals, no hassle.

Just submit an enrollment request using Stedi’s Enrollments API, UI, or a bulk CSV import. We do the rest.

One-click enrollment is available for 850+ payers. Check the Stedi Payer Network or Payer APIs to see which payers are supported.

What is transaction enrollment?

For certain transactions and payers, providers must first submit an enrollment with a payer before they can exchange transactions with them.

All payers require enrollment for 835 Electronic Remittance Advice (ERA) transactions. Some require it for other transactions too, like 270/271 eligibility checks.

Each payer has its own requirements and steps. You usually need to include the provider’s NPI, tax ID, and contact information. Some payers also ask you to sign a PDF or log into a portal to complete follow-up steps.

When Stedi is able to collect all of the required information up front in a single form, we classify the enrollment as one-click.

How to find payers with one-click enrollment

You can check whether a payer needs enrollment for a certain transaction type using the Stedi Payer Network or the Payer APIs.

In the Stedi Payer Network UI, one-click enrollment support is indicated in the Payer pane:

One-click enrollment indicator in the Payer pane

Or the Payer page:

One-click enrollment indicator on the Payer Detail page

In the Payer APIs, support is indicated in the enrollment.transactionEnrollmentProcesses.{transactionType}.type field with a value of ONE_CLICK. For example, for 835 ERA (claimPayment) enrollments:

  {
    "stediId": "ABHDB",
    "displayName": "Community Health Plan of Washington",
    ...
    "enrollment": {
      "ptanRequired": false,
      "transactionEnrollmentProcesses": {
        "claimPayment": {
          "type": "ONE_CLICK"   // Supports one-click enrollment for 835 ERAs
        }
      }
    }
  }

For payers that require enrollment, we show the required steps in the Transaction Enrollments Hub and we work with you until the enrollment is complete.

We handle enrollment for you

Most clearinghouses make you figure out enrollments. We do it for you. You send enrollment requests using our API, UI, or a bulk CSV import. Then we:

  • We normalize the request to better match the payer’s requirements.

  • Send the request to the payer and coordinate with them.

  • Let you know if anything is needed from your side.

You can track status using the Enrollments API or UI. We also send updates by Slack/Team and email. Once the enrollment has been approved, the provider can start exchanging the enrolled transaction type with the payer.

Tired of slow enrollments?

Transaction enrollments often slow teams down. We’ve built systems that avoid the usual delays.

If enrollments are a bottleneck, we can help. Contact us to see how it works.

Jun 4, 2025

Products

Stedi now has a direct connection to Zelis, a multi-payer provider platform.

Many payers use Zelis as their primary way of delivering 835 Electronic Remittance Advice (ERA) files through clearinghouses to providers.

To receive ERAs from Zelis, providers must set up an account in the Zelis portal. This involves selecting a clearinghouse from a prepopulated list. Previously, providers using Stedi had to select an intermediary clearinghouse in order to receive ERAs.

With Stedi’s new direct Zelis connection, you can now choose Stedi from the list of integrated clearinghouses. Once set up, you’ll automatically receive ERAs from all Zelis-connected payers directly through Stedi.

For an example, see the ERA transaction enrollment steps for United Healthcare Dental.

New Zelis enrollments

To submit new enrollment requests, use the Enrollments API, UI, or a bulk CSV import. For Zelis-connected payers, we’ll instruct you to select Stedi as the clearinghouse in Zelis when needed.

Existing Zelis enrollments

If you previously submitted a Stedi enrollment for a Zelis-connected payer, you can log into Zelis and select Stedi as the clearinghouse to transition to Stedi’s direct Zelis connection without any interruption or downtime.

Fix enrollment delays with Stedi

Other clearinghouses make transaction enrollment slow and manual. Stedi handles enrollment for you. Our processes remove the usual mistakes and delays.

If transaction enrollment is slowing you down, we can help. Contact us to see how it works.

Jun 2, 2025

Guide

Stedi’s Eligibility Check APIs let you get Medicare 271 eligibility responses as JSON. But your system – or one downstream – might need to display that JSON data in Common Working File (CWF) fields. Many providers still expect a CWF-style layout.

This guide shows how to map Stedi’s JSON 271 eligibility responses to CWF fields. It also covers what the CWF was, how Medicare eligibility checks work today, and why the CWF format still persists.

What Is the Common Working File (CWF)?

The Centers for Medicare & Medicaid Services (CMS) built the CWF in the 1980s to centrally manage Medicare eligibility. It was the source of truth for who was covered, when, and under which Medicare part.

The system produced fixed-format text files – also called “CWFs” – for mainframe terminals and printed reports. Each file had a set layout, with fields like member ID, coverage type, and benefit dates. For example:

Example of a CWF-like layout with fixed fields

How Medicare eligibility checks work today

CMS replaced the CWF in 2019 with the HIPAA Eligibility Transaction System (HETS). HETS returns standard 271 eligibility responses, the same as commercial insurers. Medicare 271s include a lot of Medicare-specific info, including:

  • Medicare Part A and Part B entitlements and dates

  • Part C (Medicare Advantage) and Part D (Prescription Drug) plan info

  • Deductibles, copays, and benefit limits

  • Remaining Skilled Nursing Facility (SNF) days

  • ESRD transplant or dialysis dates

  • Smoking cessation visits and therapy caps

  • Qualified Medicare Beneficiary (QMB), secondary payers, Medicaid crossover, and other coordination of benefits information.

In Stedi’s JSON 271 eligibility responses, that data lives under benefitsInformation. Each object describes a specific coverage, limit, or service type. For example:

{
  ...
  "subscriber": {
    "memberId": "123456789",
    "firstName": "JANE",
    "lastName": "DOE",
    ...
  },
  ...
  "benefitsInformation": [
    {
      "code": "B",
      "name": "Co-Payment",
      "serviceTypeCodes": ["30"],
      "serviceTypes": ["Health Benefit Plan Coverage"],
      "insuranceTypeCode": "MA",
      "insuranceType": "Medicare Part A",
      "timeQualifierCode": "7",
      "timeQualifier": "Day",
      "benefitAmount": "408",
      "inPlanNetworkIndicatorCode": "W",
      "inPlanNetworkIndicator": "Not Applicable",
      "benefitsDateInformation": {
        "admission": "20241231",
        "admissions": [
          {
            "startDate": "20240101",
            "endDate": "20241231"
          }
        ]
      },
      "benefitsServiceDelivery": [
        {
          "unitForMeasurementCode": "Days",
          "timePeriodQualifierCode": "30",
          "timePeriodQualifier": "Exceeded",
          "numOfPeriods": "60",
          "unitForMeasurementQualifierCode": "DA",
          "unitForMeasurementQualifier": "Days"
        },
        {
          "unitForMeasurementCode": "Days",
          "timePeriodQualifierCode": "31",
          "timePeriodQualifier": "Not Exceeded",
          "numOfPeriods": "90",
          "unitForMeasurementQualifierCode": "DA",
          "unitForMeasurementQualifier": "Days"
        },
        {
          "timePeriodQualifierCode": "26",
          "timePeriodQualifier": "Episode",
          "numOfPeriods": "1"
        }
      ]
    }
  ]
}

How to use this guide

Stedi’s JSON 271 eligibility response is easier to use in modern applications. But if your customers need the old CWF layout, you’ll need to map each field from JSON.

This guide isn’t an official spec. It’s a practical reference. Each section shows a CWF-style layout, a table of field mappings, and notes on when to use each field. For details on our JSON 271 eligibility responses, see our API documentation.

Provider information

Provider information in a CWF-like format

CWF field

JSON 271 eligibility response property

Organization Name

provider.providerOrgName

NPI ID

provider.npi

Patient Demographics

The following table includes patient demographics from the SUBMITTED TO PAYER and RETURNED BY PAYER sections of the sample CWF.

Patient demographics section in an example CWF

CWF field

JSON 271 eligibility response property

When to use

SUBMITTED TO PAYER



First Name

subscriber.firstName



Last Name

subscriber.lastName



Member ID (MBI)

subscriber.memberId



D.O.B.

subscriber.dateOfBirth



Eligibility Date(From)

planDateInformation.eligibility (first date)

planDateInformation.eligibility can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Eligibility Date(To)

planDateInformation.eligibility (second date, if present)

planDateInformation.eligibility can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Service Types(s)

benefitsInformation.serviceTypeCodes



benefitsInformation.compositeMedicalProcedureIdentifier.procedureCode



benefitsInformation.compositeMedicalProcedureIdentifier.procedureModifiers



RETURNED TO PAYER



First Name

subscriber.firstName



Middle Name

subscriber.middleName



Last Name

subscriber.lastName



Suffix

subscriber.suffix



Member ID (MBI)

subscriber.memberId



D.O.B.

subscriber.dateOfBirth



Gender

subscriber.gender



Address Line 1

subscriber.address.address1



Address Line 2

subscriber.address.address2



City

subscriber.address.city



State

subscriber.address.state



Zip Code

subscriber.address.postalCode



Benefit Information

In the JSON 271 eligibility response, benefitsInformation objects contain most of the benefits information.

The benefitsInformation.insuranceTypeCode property indicates the type of insurance policy within a program, such as Medicare. A code of MA indicates Medicare Part A. A code of MB indicates Medicare Part B. For a complete list of insurance type codes, see Insurance Type Codes in our docs.

The benefitsInformation.serviceTypeCodes property identifies the type of healthcare services the benefits information relates to. A service type code (STC) of 30 relates to general benefits information. For a complete list of STCs, see Service Type Codes in our docs.

Benefit Information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.plan (first date)



All of the following must be true:

  • benefitsInformation.code = 1 (Active Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.plan (second date, if present)

All of the following must be true:

  • benefitsInformation.code = 1 (Active Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Ineligible Start

benefitsInformation.benefitsDateInformation.plan (first date)



All of the following must be true:

  • benefitsInformation.code = 6 (Inactive)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Ineligible End

benefitsInformation.benefitsDateInformation.plan (second date, if present)

All of the following must be true:

  • benefitsInformation.code = 6 (Inactive)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Date of Death

planDateInformation.dateOfDeath



Lifetime Psychiatric Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = K (Reserve)

  • benefitsInformation.serviceTypeCodes = A7 (Psychiatric - Inpatient)

  • benefitsInformation.timeQualifierCode = 32 (Lifetime)

  • benefitsInformation.quantityQualifierCode = DY (Days)

Lifetime Reserve Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = K (Reserve)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) 

  • benefitsInformation.timeQualifierCode = 32 (Lifetime)

  • benefitsInformation.quantityQualifierCode = DY (Days)

Smoking Cessation Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = 67 (Smoking Cessation)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B) 

  • benefitsInformation.timeQualifierCode = 22 (Service Year)

  • benefitsInformation.quantityQualifierCode = VS (Visits)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 29 (Remaining)

Initial Cessation Date

benefitsInformation.benefitsDateInformation.plan

This information is only available if an initial counseling visit was used in the past 12 months.

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = 67 (Smoking Cessation)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 22 (Service Year)

  • benefitsInformation.quantityQualifierCode = VS (Visits)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 29 (Remaining)

ESRD Dialysis Date

benefitsInformation.benefitsDateInformation.discharge

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

ESRD Transplant Date

benefitsInformation.benefitsDateInformation.service

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

ESRD Coverage Period

benefitsInformation.benefitsDateInformation.plan

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

Plan Benefits

Plan Benefits in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Medicare Part A

Type

Use Base when all of the following are true:

  • benefitsInformation.timeQualifierCode = 7 (Day)

  • benefitsInformation.benefitsDateInformation.admission contains a full year date range, such as 20980101-20991231.



Use Spell when all of the following are true:

  • benefitsInformation.timeQualifierCode = 7 (Day)

  • benefitsInformation.benefitsDateInformation.admission contains a partial year date range, such as 20980506-20980508.



First Bill

benefitsInformation.benefitsDateInformation.admissions.startDate



Last Bill

benefitsInformation.benefitsDateInformation.admissions.endDate



Hospital Days Full

For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode31 (Not Exceeded).



For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode29 (Remaining).



Hospital Days Colns

For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when:

  • benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode30 (Exceeded).



For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when:

  • benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode29 (Remaining).



Hospital Days Base

benefitsInformation.benefitAmount



SNF Days Full

benefitsInformation.BenefitsServiceDelivery.numOfPeriods

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 31 (Not Exceeded)

SNF Days Colns

benefitsInformation.BenefitsServiceDelivery.numOfPeriods

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 30 (Exceeded)

SNF Days Base

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.timeQualifierCode = 7 (Day)

Inpatient Deductible

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = C (Deductible)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

Medicare Part B

Deductible Remaining

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = C (Deductible)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Physical Therapy

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AE (Physical Medicine) 

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Occupational Therapy

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AD (Occupational Therapy)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Blood Pints Part A/B

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = E (Exclusions)

  • benefitsInformation.serviceTypeCodes = 10 (Blood Charges)

  • benefitsInformation.quantityQualifierCode = DB (Deductible Blood Units)

  • benefitsInformation.benefitQuantity = Blood Units Excluded

  • benefitsInformation.benefitsServiceDelivery.quantityQualifierCode = FL (Units)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifier = Blood Units Remaining

  • benefitsInformation.benefitsDateInformation.plan = A date or date range in the current calendar year

Medicare Part A Stays

Type

Use Hospital when:

  • benefitsInformation.serviceTypeCodes30 (Health Benefit Plan Coverage)



Use Hospital Stay when:

  • benefitsInformation.serviceTypeCodes48 (Hospital - Inpatient)



Use SNF Stay when:

  • benefitsInformation.serviceTypeCodesAH (Skilled Nursing Care - Room and Board)



The following must be true:

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

Start Date

benefitsInformation.benefitsDateInformation.plan (first date)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

End Date

benefitsInformation.benefitsDateInformation.plan (second date, if present)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Billing NPI

benefitsInformation.benefitsRelatedEntities.entityIdentificationValue

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentification is FA (Facility Identification)

Qualified Medicare Beneficiary (QMB) Status

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.insuranceTypeCode = QM (Qualified Medicare Beneficiary)

Qualified Medicare Beneficiary (QMB) Status information in a CWF example

CWF field

JSON 271 eligibility response property

When to use

Period From

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Period Through

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

QMB Plan

benefitsInformation.planCoverage



Medicare Secondary Payor

Medicare Secondary Payor info in an example CWF file

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = 14, 15, 47, or WC

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Policy Number

benefitsInformation.benefitsAdditionalInformation.insurancePolicyNumber



Insurer

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Type

benefitsInformation.insuranceType



Medicare Advantage

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = U (Contact Following Entity for Eligibility or Benefit Information)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage) OR both 30 (Health Benefit Plan Coverage) AND CQ (Case Management)

  • benefitsInformation.insuranceTypeCode = HM (HMO), HN (HMO - Medicare Risk), IN (Indemnity), PR (PPO), or PS (POS)

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Primary Payer

Medicare Advantage information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsInformation.benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsInformation.benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Plan Code

benefitsInformation.benefitsAdditionalInformation.planNumber



Payer Name

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Plan Name

benefitsInformation.benefitsRelatedEntities.entityName



Website

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (URL)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Uniform Resource Locator (URL)



In most cases, CMS only provides just the payer’s domain name, such as examplepayer.com, not a complete URL.

Phone Number

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (Telephone)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Phone Number

Message(s)

benefitsInformation.additionalInformation.description



Part D

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.serviceTypeCodes = 88 (Pharmacy)

Medicare Part D info in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.benefit (second date, if present)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Plan Code

benefitsInformation.benefitsAdditionalInformation.planNumber



Payer Name

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Plan Name

benefitsInformation.benefitsRelatedEntities.entityName



Website

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (URL)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Uniform Resource Locator (URL)



In most cases, CMS only provides just the payer’s domain name, such as examplepayer.com, not a complete URL.

Phone Number

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (Telephone)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Phone Number

Therapy Caps

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AD (Occupational Therapy) or AE (Physical Medicine)

Therapy Caps information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Period Begin

benefitsInformation.benefitsDateInformation.benefit

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Period End

benefitsInformation.benefitsDateInformation.benefit

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

PT/ST Applied

benefitsInformation.additionalInformation.description



OT Applied

benefitsInformation.additionalInformation.description



Hospice

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = X (Health Care Facility)

  • benefitsInformation.serviceTypeCodes = 45 (Hospice)

Hospice info in a CWF

CWF field

JSON 271 eligibility response property

When to use

Benefit Period

No direct mapping. Calculated by ordering the episodes by date for the calendar year.



Benefit Period Start Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Benefit Period End Date

benefitsInformation.benefitsDateInformation.benefit (second date, if present)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Provider

benefitsInformation.benefitsRelatedEntities.entityIdentificationValue

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Provider

Provider Name

benefitsInformation.benefitsRelatedEntities.entityName

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Provider

Hospice Elections

Election Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Election Receipt Date

benefitsInformation.benefitsDateInformation.added



Election Revocation Date

benefitsInformation.benefitsDateInformation.benefitEnd



Election Revocation Code

benefitsInformation.additionalInformation.description



Election NPI

benefitsRelatedEntities.entityIdentificationValue



Home Health Certification

Home Health Certification information in an example CWF

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = X (Health Care Facility)

  • benefitsInformation.compositeMedicalProcedureIdentifier.procedureCode = G0180

CWF field

JSON 271 eligibility response property

Certification HCPCS Code

compositeMedicalProcedureIdentifier.procedureCode

Process Date

benefitsDateInformation.periodStart

Rehabilitation Services

Rehabilitation Services information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Pulmonary Remaining (G0424) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BF (Pulmonary Rehabilitation)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

  • additionalInformation.description = Technical

Pulmonary Remaining (G0424) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BF (Pulmonary Rehabilitation)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

  • additionalInformation.description = Professional

Cardiac Applied (93797, 93798) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Technical

Cardiac Applied (93797, 93798) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Professional

Intensive Cardiac Applied (G0422, G0423) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Intensive Cardiac Rehabilitation - Technical

Intensive Cardiac Applied (G0422, G0423) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Intensive Cardiac Rehabilitation - Professional

May 29, 2025

Guide

Insurance verification is the first step in the revenue cycle – and the first place it can break.

When an eligibility check fails or returns bad data, everything downstream falls apart. Claims get denied. Patients get surprise bills. Providers wait to get paid. Staff waste hours on the phone verifying coverage or fixing claims.

Bad eligibility checks set providers up to fail.

But there’s good news: If you’re using Stedi’s Eligibility Check APIs, most eligibility check errors are avoidable. This guide shows how to prevent them – and how to use Stedi to build a faster, more reliable eligibility workflow.

Only send the required patient data

Payers return AAA errors when they can’t match an eligibility request to a subscriber. It may seem counterintuitive, but this often happens because the request includes too much patient data.

Payers require that each check match a single patient. Extra data increases the risk of mismatches. Even a small typo in a non-required field can cause a failed match.

For the best results, only send:

  • Member ID

  • First name

  • Last name

  • Date of birth

If you’re verifying the subscriber, put this info in the subscriber object.

{
  ...
  "subscriber": {
    "memberId": "123456789",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19000101"
  }
}

If you’re verifying a dependent, the format depends on the payer. If the dependent has their own member ID, try this:

  • Put the dependent’s info in the subscriber object.

  • Leave out the dependents array.

If the dependent doesn’t have their own member ID:

  • Put the subscriber’s info in the subscriber object.

  • Put the dependent’s info in an object in the dependents array.

Don’t send SSNs, addresses, or phone numbers in eligibility checks. They often cause mismatches. For medical checks, if you don’t know the insurer, or the payer requires a member ID and you don’t have it, start with an insurance discovery check.

If your eligibility checks still fail, try a name variation. For example, “Nicholas” might work where “Nick” doesn’t. Use Stedi’s Eligibility Manager to test variations and retry requests directly from the UI.

When you get a successful response from the payer, update your records with the returned member ID, name, and date of birth. This improves your chances of a successful response on the next eligibility check, whether it’s for a future visit or a batch refresh.

Use insurance discovery when patient data is missing

If you don’t know the insurer or the payer requires a member ID and you don’t have it, start with an insurance discovery check.

Discovery checks use demographic data, like name and date of birth, to search payers for active coverage. If they find a match, they’ll return a response with the payer and member ID.

While helpful, discovery checks aren’t guaranteed to match. Match rates vary. They’re also slower than eligibility checks. But when you can’t send a clean eligibility check, discovery is your best fallback.

Discovery checks might not return all of a patient’s active insurance plans. If the patient could have multiple payers, follow up with a coordination of benefits (COB) check after the eligibility check. A COB check can find other coverage and figure out the primary payer.

Check eligibility before checking COB

COB checks are more sensitive than eligibility checks. Even small mismatches, like using a nickname instead of a legal name, can cause them to fail.

To reduce errors, run an eligibility check first. Then use the member ID from the eligibility response in your COB request. You’ll get cleaner results and fewer failures.

If you don’t have a member ID, start with an insurance discovery check first. Then follow up with an eligibility check and COB check – in that order.

Keep coverage data fresh with batch refreshes

Insurance changes often. Patients switch plans. Cached coverage data goes stale fast.

Always re-check eligibility before a visit. Between visits, use Stedi’s Batch Eligibility Check API to run weekly or monthly refreshes. Batch checks return full coverage breakdowns, just like real-time checks. They can catch insurance issues before they cause problems.

Batch eligibility checks are asynchronous. They don’t count toward your Stedi account’s concurrency limit. You can run thousands of batch eligibility checks and still send real-time checks at the same time.

Include an MBI for Medicare eligibility checks

Every Medicare eligibility check requires a Medicare Beneficiary Identifier (MBI) for the patient. If it’s missing or wrong, the check will fail.

If the patient doesn’t know their MBI, run an MBI lookup using the same Real-Time Eligibility Check API. You’ll need to:

  1. Set the tradingPartnerServiceId to MBILU (MBI Lookup).

  2. Include the following in the request:

    • subscriber.firstName

    • subscriber.lastName

    • subscriber.dateOfBirth

    • subscriber.ssn

For example:

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "MBILU",
  "externalPatientId": "UAA111222333",
  "encounter": {
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1999999984"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "ssn": "123456789"
  }
}'

Stedi will return the patient’s full coverage details and their MBI. You can then use the MBI as the member ID for eligibility checks.

MBI lookups require setup with Stedi and incur additional costs. Contact us for details.

Test each STC one at a time

Service Type Codes (STCs) tell the payer what kind of benefits info you’re requesting. You can include them in the encounter.serviceTypeCodes array:

"encounter": {
  "serviceTypeCodes": ["30"]
}

You can send multiple STCs in one request, but support varies by payer:

  • Some only respond to the first STC.

  • Some ignore your STCs and always return a default response for STC 30 (Health Benefit Plan Coverage).

  • Some don’t support multiple STCs in a single request.

To figure out what works, test each payer individually:

  1. Send a request with just STC 30 for general medical benefits or 35 for general dental benefits.

  2. Send one with a specific STC you care about, like 88 for pharmacy benefits.

  3. Try a request with multiple STCs.

Compare the responses. If they change based on the STC or the number of STCs, the payer likely supports them. If not, they may be ignoring or only partially supporting STCs.

Success depends on the payer’s implementation. Partial support and fallback behavior are common.

Avoid timeouts with request hedging

Payer responses can be slow. Payers can take up to 60 seconds to respond to eligibility requests. To handle this, Stedi keeps real-time eligibility requests open for up to 120 seconds. Internally, Stedi may retry a request to the payer multiple times during that window.

Don’t cancel and retry your own requests during this window. It can create duplicates, increase your concurrency usage, and further slow things down.

If a check feels stuck, use request hedging instead. Wait 30 seconds, then send a second request without canceling the first. Use whichever response returns first. It’s a simple way to avoid timeouts.

Debug errors with Eligibility Manager

Sometimes, eligibility errors are unavoidable. Payers go down, data is missing, or coverage has changed. Stedi’s Eligibility Manager shows exactly why a check failed so you can fix it instead of guessing.

Screenshot of the Eligibility Manager

Each check is grouped under a unique Eligibility Search ID. Retries stay in the same thread, giving you a full audit trail.

Use Eligibility Manager to:

  • Filter by error code, payer, or status. For example, you can find all checks that failed with a specific AAA code (like 75 for Subscriber Not Found). Or see issues by a specific payer.

  • View raw request and response data.

  • Edit and retry failed checks directly from the UI.

  • Compare retries to see what changed between failures and successes.

If you’re running eligibility at scale, this tool can save you hours of guessing and debugging.

Screenshot of the Eligibility Manager debugger

Eligibility workflows that don’t break

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. When errors do happen, you get help fast. Our average support response time is under 8 minutes.

Want to see how it works? Contact us to set up a proof of concept.

May 28, 2025

Products

To get paid, healthcare providers submit an 837 claim to their patient’s insurer. The payer processes the claim and sends back an 835 Electronic Remittance Advice (ERA). That ERA tells you what got paid, what got denied, and why.

Today, most providers and payers submit claims and receive ERAs this way – all electronically. But not every payer sends ERAs. Some still mail EOBs – explanations of benefits – on paper.

An EOB contains the same information as an ERA… just on dead trees. Providers with digital workflows have to build a separate process to open mail, scan or read documents, and manually key in payment data. It’s full of delays, errors, and extra costs.

Turn every EOB into an 835 with Anatomy

We want to make remittance paper optional. To do that, Stedi has partnered with Anatomy, a modern healthcare lockbox and document conversion service, to help you convert paper EOBs into standard 835s. Providers and billing companies can redirect paper EOBs to a PO Box managed by Anatomy – or upload PDFs directly using Anatomy’s UI. Anatomy converts each document into a standard 835.

Anatomy then securely sends the 835s to Stedi on your behalf. In Stedi, you can enroll to receive 835s from Anatomy just like you would if they were a payer. Once enrolled, you’ll get your ERAs as usual – using the 835 ERA Report API or the from-stedi directory of your Stedi SFTP connection. You can even set up webhooks to get notified when new ERAs are available.

If you're already using Stedi, you likely already have this set up. You just need to contract with Anatomy and then enroll with Anatomy in Stedi. The best part? We do all the enrollment work for you as part of our streamlined process.

Enroll today

Anatomy is now listed as a supported payer in the Stedi Payer Network. If you have a contract with Anatomy and are already using Stedi, it takes just minutes to add Anatomy and start receiving ERA transactions.

If you’re new to Stedi, making remits painless is just one part of what we do. When you sign up, you’ll get access to 3,393+ payers, modern APIs, dev-friendly docs, and legendary support. We promise it’ll be the best clearinghouse experience you’ve ever had.

To get started, contact us or book a demo.

May 16, 2025

Products

You can now use the Search Payers API to programmatically search for payers in the Stedi Payer Network. We’ve also updated the Payer Network UI to use the new API. You now get consistent search results across the UI and API.

Screen capture of a search for "Blue Cross" in the Stedi Payer Network UI

Why payer IDs matter

When you send a healthcare transaction, such as a dental claim (837D) or eligibility check (270/271), you need a payer ID. The payer ID tells your clearinghouse where to send the transaction. If the ID is wrong, the transaction might fail or be rejected.

That sounds simple. It’s not.

Primary payer IDs often change. They can vary between clearinghouses, sometimes even between transaction types. Most clearinghouses send out their IDs in CSV payer lists that are updated once a month at best. These CSVs can grow stale quickly. Worse, they often have duplicate names, typos, and other errors.

For developers building healthcare billing applications, CSV-based payer lists create a recurring pain. Every month, you need to update payer name-to-ID mappings or lookup tables. You end up writing logic to normalize names, match payer ID aliases, and handle edge cases – just to get the right payer ID.

So we built something better.

We created the Stedi Payer Network and Payers API to provide accurate, up-to-date data on thousands of medical and dental payers. You can get the right payer ID without digging through CSVs.

Now, with the Search Payers API, it’s faster to find the right payer and build tools that scale. For example, you can use the API to create an application that lets patients search for and select their insurance provider in a patient intake form.

Find payer IDs with the Search Payers API

The Search Payers API does one thing well: find the payer you're looking for.

You can search by the payer’s name, alias, or payer ID. The search supports fuzzy matching, so it returns close matches even if the provided payer name isn’t exact.

Stedi weights results based on text match relevance and additional factors, such as payer size, market share, and transaction volume, to present the most likely matches first.

You can further filter the results by supported transaction types, like dental claims (837D) or eligibility checks (270/271).

For example, the following request searches for the “Blue Cross” payer name and filters for payers that support eligibility checks and real-time claim status.

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/search?query=Blue%20Cross&eligibilityCheck=SUPPORTED&claimStatus=SUPPORTED \
  --header 'Authorization: <api-key>'

The response returns a list of matching payers. Each result includes:

  • The payer’s immutable Stedi Payer ID

  • Their name, primary payer ID, and known aliases

  • Supported transaction types

  • Whether transaction enrollment is required

  • A score indicating how relevant the payer is to the search query.

{
 "items": [
   {
     "payer": {
       "stediId": "QDTRP",
       "displayName": "Blue Cross Blue Shield of Texas",
       "primaryPayerId": "G84980",
       "aliases": [
         "1406",
         "84980",
         "CB900",
         "G84980",
         "SB900",
         "TXBCBS",
         "TXBLS"
       ],
       "names": [
         "Blue Cross Blue Shield Texas Medicaid STAR CHIP",
         "Blue Cross Blue Shield Texas Medicaid STAR Children's Health Insurance Program",
         "Blue Cross Blue Shield of Texas",
         "Bryan Independent School",
         "Federal Employee Program Texas (FEP)",
         "Health Maintenance Organization Blue",
         "Health Maintenance Organization Blue Texas",
         "Healthcare Benefits",
         "Rio Grande",
         "Walmart (BlueCard Carriers)"
       ],
       "transactionSupport": {
         "eligibilityCheck": "SUPPORTED",
         "claimStatus": "SUPPORTED",
         "claimPayment": "ENROLLMENT_REQUIRED",
         "dentalClaimSubmission": "SUPPORTED",
         "professionalClaimSubmission": "SUPPORTED",
         "institutionalClaimSubmission": "SUPPORTED",
         "coordinationOfBenefits": "SUPPORTED",
         "unsolicitedClaimAttachment": "NOT_SUPPORTED"
       }
     },
     "score": 14.517873
   },
   ...
 ],
 ...
}

Get started with Stedi

At Stedi, we’re working to eliminate the toil in healthcare transactions. Programmatic access to accurate payer data is just one part.

The Search Payers API is free on all paid Stedi plans. Try it for yourself: Schedule a demo today.

May 2, 2025

Products

You can now include claim attachments in API-based 837P professional and 837D dental claim submissions.

When you need a claim attachment

Some payers require attachments to approve claims for specific services. Claim attachments show a service occurred or was needed. They can include X-rays, treatment plans, or itemized bills.

The type of attachment needed depends on the payer and the service. Without these attachments, the payer may delay (pend) or deny the claim.

How to submit a claim attachment

Follow these steps:

  1. Check payer support.
    While uncommon, some payers may not accept claim attachments or may require transaction enrollment first. Check the Payers API or Stedi Payer Network for support.

  2. Create an upload URL.
    Use the Create Claim Attachment JSON API to generate a pre-signed uploadUrl and an attachmentId. Specify the contentType in the request. Supported file types include application/pdf, image/tiff, and image/jpg.

    Example request:

    curl --request POST \
      --url https://claims.us.stedi.com/2025-03-07/claim-attachments/file \
      --header 'Authorization: <api-key>' \
      --header 'Content-Type: application/json' \
      --data '{
      "contentType": "application/pdf"
    }'


    Example response:

    {
      "attachmentId": "d3b3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e",
      "uploadUrl": "https://s3.amazonaws.com/bucket/key"
    }



  3. Upload your attachment.
    Upload your file to the uploadUrl.

    Example:

    curl --request PUT \
      --url "<your-uploadUrl>" \
      --header "Content-Type: application/pdf" \
      --upload-file /path/to/file.pdf



  4. Submit the claim.
    Submit the claim using the Professional Claims (837P) JSON API or Dental Claims (837D) JSON API. Include the attachmentId in the payload’s claimInformation.claimSupplementalInformation.reportInformations[].attachmentId. In the same reportInformations object, include:

    • An attachmentReportTypeCode. This code identifies the type of report or document you plan to submit as an attachment. See Attachment Report Type Codes for a full list of codes.

    • An attachmentTransmissionCode of EL (Electronically Only). This property indicates the attachment will be sent in a separate, electronic 275 transaction.

      Example:

      curl --request POST \
        --url https://healthcare.us.stedi.com/2024-04-01/dental-claims/submission \
        --header 'Authorization: <api-key>' \
        --header 'Content-Type: application/json' \
        --data '{
         ...
         "claimInformation": {
            "claimSupplementalInformation": {
              "reportInformations": [
                {
                  "attachmentReportTypeCode": "RB",
                  "attachmentTransmissionCode": "EL
                  "attachmentId": "<your-attachment-id>"
                }
              ]
            }
          },
          ...
        }'

Get started

Claim attachments are available for all paid Stedi accounts.

If you’re not a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Apr 29, 2025

Guide

Virtual healthcare visits are now common, but verifying patient eligibility for them isn't always straightforward.

Telehealth benefits vary by payer and plan, and it can be a challenge to accurately retrieve coverage details. Without accurate eligibility checks, providers risk billing issues, denied claims, and upset patients.

Stedi’s Real-Time Eligibility Check API gives you reliable, programmatic access to eligibility data as JSON. But access isn’t enough. To get the right eligibility information, it’s just as important to use the right service type code (STC) for the payer.

Why the STC matters

A service type code (STC) tells the payer exactly what benefit information you want. For example, STC 98 indicates a "Professional (Physician) Visit – Office."

The problem is that individual payers may return coverage details for virtual visits differently. Some payers treat virtual visits as office visits, some as separate telemedicine benefits, and others as general medical services.

As a result, different payers require different STCs for eligibility checks. For example, UnitedHealthcare (UHC) maps “VIRTUAL VISITS/TELEMEDICINE” to STC 9. Other payers use STC 98 or STC 30. Payers may use other STCs, such as MH or CF, for virtual mental health visits.

If you use the wrong STC in an eligibility check, the response may omit benefits or return only partial coverage information. This can lead to denied claims or billing surprises for the patient after care is delivered.

Find the virtual visit STC for each payer

To find the right STC for a payer, you’ll need to try multiple STCs, one at a time.

To check virtual visit STCs using the Real-Time Eligibility Check API:

  1. Send an eligibility check request.
    In the request, use one of the following STCs at a time, in order, for each payer. Inspect each response before moving to the next STC.

    98 – Professional (Physician) Visit – Office

    9 – Other Medical

    30 – Health Benefit Plan Coverage

    An example Real-Time Eligibility Check request using STC 98:

    curl --request POST \
      --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
      --header 'Authorization: <api-key>' \
      --header 'Content-Type: application/json' \
      --data '{
      "controlNumber": "123456789",
      "tradingPartnerServiceId": "AHS",
      "externalPatientId": "UAA111222333",
      "encounter": {
        "serviceTypeCodes": [
          "98"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1999999984"
      },
      "subscriber": {
        "dateOfBirth": "19000101",
        "firstName": "Jane",
        "lastName": "Doe",
        "memberId": "123456789"
      }
    }'



  2. Check the response for a matching code or phrase.
    Check the response’s benefitsInformation objects for the following eligibilityAdditionalInformationList.industryCode value:

    02 (Telehealth Provided Other than in Patient’s Home)

    10 (Telehealth Provided in Patient’s Home)

    The eligibilityAdditionalInformationList.industryCode is a CMS Place of Service code, which indicates the location where healthcare was provided. The 02 and 10 values are used for telehealth services.

    If the eligibilityAdditionalInformationList.industryCode isn’t present, check the benefitsInformation objects for an additionalInformation.description property that contains a phrase like:

    • "VIRTUAL VISITS"

    • "TELEMEDICINE"

    • "E-VISIT"



  3. Stop at the first matching STC.
    Use the first matching STC for any future virtual visit eligibility checks with the payer.

    If you don’t find a match after checking all three STCs, fall back to interpreting the response based on STC 98.

Repeat the process for each required payer.

Interpret the eligibility response

After finding the right STC, use the Real-Time Eligibility Check API’s response to extract any needed eligibility information, such as:

  • Whether the patient is eligible for a virtual visit

  • Whether the patient will owe anything for the visit

  • Whether the patient has a limited number of virtual visits

Most eligibility details are in the response’s benefitsInformation object array. Look for benefitsInformation objects containing the STC used by the payer for virtual visits. Then use the following guidelines to interpret the API response.

Patient eligibility

If benefitsInformation.name contains "Active Coverage", the patient is eligible for a virtual visit.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "name": "Active Coverage",
      ...
    }
  ],
  ...
}

Patient responsibility

Some plans require patients to pay a portion of the cost of care, such as a co-pay or deductible. This amount is called the patient responsibility.

You can use the benefitsInformation objects with benefitsInformation.code values A, B, C, F, G, and Y to determine the patient’s financial responsibility for a given STC. For a detailed guide on determining patient responsibility, see Patient costs in the Stedi docs.

Visit limits

Some payers and plans limit the number of covered visits, including virtual visits, per year. In many cases, these limits aren’t hard caps. Patients may be able to get additional benefits with approval, called prior authorization, from their payer.

If the benefitsInformation object’s code is F, the benefitsInformation object includes details about limitations like a numeric visit cap, time period, or other restrictions. However, not all payers return limitations consistently or in the same way.

{
  ...
  "benefitsInformation": [
    {
      "code": "F",
      "name": "Limitations",
      "additionalInformation": {
        "description": "20 visits per calendar year"
      },
      ...
    }
  ],
  ...
}

Most common pattern

The most common pattern is to return values in the following benefitsInformation object properties. For example:

  • benefitsInformation.timeQualifierCode: "23" (Calendar year)

  • benefitsInformation.quantityQualifierCode: "VS" (Visits)

  • benefitsInformation.benefitQuantity: “<number>” (Number of allowed visits)

{
  ...
  "timeQualifierCode": "23",
  "timeQualifier": "Calendar Year",
  "quantityQualifierCode": "VS",
  "quantityQualifier": "Visits",
  "benefitQuantity": "20"
  ...
}

Benefits service delivery

Some payers may include visit limits in the benefitsInformation object’s benefitsServiceDelivery object array instead. For example:

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode: "23" (Calendar year)

  • benefitsInformation.benefitsServiceDelivery.quantityQualifierCode: "VS" (Visits)

  • benefitsInformation.benefitsServiceDelivery.quantity: "<number>" (Number of allowed visits)

{
  ...
  "benefitsServiceDelivery": [
    {
      "timePeriodQualifierCode": "23",
      "timePeriodQualifier": "Calendar Year",
      "quantityQualifierCode": "VS",
      "quantityQualifier": "Visits",
      "quantity": "20"
    },
    ...
  ],
  ...
}

Process eligibility checks with Stedi today

Eligibility checks don’t just confirm coverage. They remove uncertainty for patients and providers. With the Stedi Real-Time Eligibility Check and Batch Eligibility Check APIs, you can automate eligibility checks within minutes.

To start testing eligibility checks today, create a free sandbox. Or contact us to speak with our team and book a demo.

Apr 30, 2025

Guide

We’ve noticed that in the healthcare world, Postman seems to be the most popular local client for working with APIs. While we think Postman is a great general-purpose tool for testing APIs, we don’t use it internally and don’t recommend that customers use it for processing PHI (Protected Health Information).

As there seems to be an awareness gap in the healthcare industry about Postman’s shortcomings when it comes to processing PHI, we wanted to publish a post outlining the issues.

The root of the problem is that Postman stores request history – including full request payloads – on its cloud servers, and you can’t turn this feature off without impractical workarounds that we’ve rarely seen used in practice. Many users are unaware that request payloads containing PHI are being synced to Postman’s servers, and if their company does not have a BAA in place with Postman, they may be unintentionally falling short of HIPAA requirements.

Why Postman isn’t safe for requests containing PHI

Postman’s core flaw for HIPAA compliance is its sync feature. Sync makes your Postman data available across devices and Postman’s web client. This lets you reuse prior API requests and share them with others. But if you're sending PHI, you’re leaking sensitive patient data to Postman, a third party, without knowing it.

Sync works by uploading your Postman data, including API request history, to Postman’s cloud servers. There’s no opt-in; syncs occur automatically while you’re logged in to Postman. You can’t stop syncing without logging out, which cuts off basic features like OpenAPI imports.

Despite this, many companies that offer APIs in the healthcare ecosystem – including healthcare clearinghouses – recommend Postman for API testing (Postman itself even highlights these APIs in their curated Healthcare APIs directory, and may be unaware of the necessary caveats).

Postman’s workarounds are impractical

There are multiple GitHub issues and community posts that raise concerns about Postman and HIPAA compliance. Postman’s own docs state:

Some organizations have security guidelines that prevent team members from syncing data to the Postman cloud.

Postman offers two workarounds:

  • A lightweight API client: Essentially just using Postman while logged out. However, if you log back in, syncing starts again.

  • Postman Vault: Secrets that you can reference as variables in requests. Vault secrets aren’t synced to the cloud. However, using variables for every request payload would be tedious.

Neither of these solutions scale – data leakage is one login or one bad request away.

Alternative API clients

Ultimately, proper API client usage is your responsibility. You should do your own research to determine HIPAA requirements – you can use any tool in a non-HIPAA-compliant way. At a minimum, if you are going to test APIs that handle PHI from your local machine, use an API client that defaults to local-only storage.

The following open-source API clients use an offline-first approach, which sidesteps the fundamental Postman problem. Each client also supports OpenAPI imports, which you can use to import the Stedi OpenAPI specs. With that said, you should have your security and compliance teams review any tool carefully, especially because applications evolve – there was a time that Postman was local-only, too.

  • Bruno (repo): A local-only API client built to avoid cloud syncing. Bruno has several interfaces, including a desktop app, CLI, and VS Code extension.

  • Pororoca (repo): A desktop-only API client with no cloud sync, built for secure local testing. Poroca’s data policy states that no data is synced to remote servers.

  • Yaak (repo): A simple, fast desktop API client. Yaak supports several import formats, including Postman collections, OpenAPI, and Insomnia.

Secure defaults matter

Postman is a great tool for general APIs. But healthcare isn't general software. When you’re handling PHI, invisible cloud storage is a failure, not a feature. Secure defaults, like local-only storage, prevent developers from accidentally exposing sensitive data. Of course, even if your requests never leave the machine, every laptop that handles PHI should still be locked down with best practices like full-disk encryption, strong authentication, automatic screen-lock, and remote-wipe.

Security is job zero at Stedi. We build every system and design every API with secure defaults in mind. If you want a healthcare clearinghouse that’s serious about security and developer experience, start with Stedi. Reach out or create a free sandbox today.

Disclaimer: Product features, security controls, and regulations change over time. Your organization must perform its own HIPAA risk analysis, implement appropriate administrative, technical, and physical safeguards, and verify that every vendor and workflow meets current legal and policy requirements.

Apr 10, 2025

Products

Without accurate insurance details, providers can’t run an eligibility check to verify patient benefits. Without a successful eligibility check, patients are often told they’ll need to pay out of pocket, causing them to cancel or never schedule services at all. Uncertainty about coverage status can also cause billing surprises, denied claims, and delayed payments to providers down the line – especially in urgent care scenarios when patients can’t communicate insurance details before receiving care. 

Unfortunately, patients often have trouble providing accurate information about their insurance. They make mistakes on online or in-person intake forms, come to appointments without their insurance cards, or forget to mention that their coverage changed since the last visit.

With Stedi’s Insurance Discovery, you can use an API or user-friendly form to find a patient’s active coverage in minutes with only their demographic data, like name, date of birth, and address. Insurance discovery checks replace standard eligibility checks when you don’t know the payer or the full patient details because they return benefits information for each active health plan found.

Improve eligibility and claims processing workflows

Insurance Discovery augments your existing eligibility workflow to help you reliably verify benefits, even when the patient can’t provide accurate insurance information. We recommend using Insurance Discovery for:

  • Eligibility troubleshooting. When eligibility checks fail with a Subscriber/Insured Not Found error, run an insurance discovery check instead of manually following up with the patient.

  • Walk-in or urgent care visits. Run an insurance discovery check when patients show up without their insurance card.

  • Virtual care and telehealth appointments. Simplify intake by letting patients schedule without insurance details. Then, run an insurance discovery check to verify their coverage before the visit.

Insurance Discovery can also help optimize claims processing. First, you can run insurance discovery checks to retroactively find active coverage for denied claims. Then, you can use the results to run a coordination of benefits (COB) check to identify any overlapping coverage and determine which payer is responsible for paying claims (primacy).

Find active coverage in minutes without knowing the payer

Here’s how Insurance Discovery works:

  1. Enroll one or more provider NPIs with Stedi for Insurance Discovery. You can submit a request and get approval in less than two minutes.

  2. Once the enrollment is live, you can either submit requests programmatically through our Insurance Discovery API or manually through the Create insurance discovery check form. At a minimum, you must provide the patient’s first name, last name, and date of birth (DOB), but we strongly recommend providing additional details like the patient's Social Security Number (SSN), gender, or full address to maximize the chances of finding matching coverage. You’ll also include information like the provider’s NPI and the service dates, similar to a standard eligibility check.

  3. Stedi determines if the patient has active coverage with one or more payers. This process involves demographic lookups to enrich partial patient details, comparisons across third-party data sources to determine member IDs, and submitting real-time eligibility checks to payers to detect coverage.

  4. Stedi returns an array of potential active coverages, along with subscriber details and benefits information. 

You should always review the results to ensure the returned subscriber information for each active health plan matches the patient's demographic information. Once you confirm matching coverage, you can use the benefits information to determine the patient’s eligibility for services.

The following example shows an insurance discovery request for a fictional patient named Jane Doe.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/insurance-discovery/check/v1 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "provider": {
    "organizationName": "THE DOCTORS OFFICE",
    "npi": "1234567891"
  },
  "encounter": {
    "beginningDateOfService": "20250326",
    "endDateOfService": "20250328"

  },
  "controlNumber": "123456789",
  "subscriber": {
    "dateOfBirth": "20010925",
    "firstName": "Jane",
    "lastName": "Doe",
    "address": {
      "address1": "1 MAIN ST",
      "address2": "UNIT 1",
      "city": "ANYTOWN",
      "state": "MO",
      "postalCode": "12341"
    }
  }
}'

In the following example response, Stedi found one instance of potential matching coverage for Jane Doe. The information is available in the items array. 

  • The payer is Aetna.

  • The patient in the request, Jane, is a dependent on the Aetna plan because her demographic information appears in the dependent object in the response.

  • The confidence.level is marked as REVIEW_NEEDED, because the dependent’s last name is slightly different from the patient’s last name in the insurance discovery request. However, all of the other demographic details in the dependent object – first name, date of birth, address – match the patient from the request. The two-part last name, Smith Doe, appears to be the complete version of the last name in the request, Doe. Based on this information, we can confirm that this is active coverage for the patient. 

  • The benefitsInformation object (truncated to keep this post concise) contains the patient’s benefits details. For example, the patient has active medical coverage under their health plan for the service dates in the request. Visit Determine patient benefits to learn more about interpreting the benefits information in the insurance discovery check response.

{ 
  "coveragesFound": 1,
  "discoveryId": "e856b480-0b41-11f0-aee6-fc0434004bca",
  "items": [
    {
      "provider": {
        "providerName": "THE DOCTORS OFFICE",
        "entityType": "Non-Person Entity",
        "npi": "1234567891"
      },
      "subscriber": {
        "memberId": "J9606211996",
        "firstName": "JOHN",
        "lastName": "DOE",
        "groupNumber": "012345607890008",
        "groupDescription": "SAMPLE HEALTH GROUP",
        "insuredIndicator": "Y"
      },
      "dependent": {
        "firstName": "JANE",
        "lastName": "SMITH DOE",
        "gender": "F",
        "dateOfBirth": "20010925",
        "planNumber": "0123654",
        "relationToSubscriber": "Child",
        "relationToSubscriberCode": "19",
        "address": {
          "address1": "1 MAIN ST",
          "address2": "UNIT 1",
          "city": "ANYTOWN",
          "state": "MO",
          "postalCode": "12341"
        }
      },
      "payer": {
        "entityIdentifier": "Payer",
        "entityType": "Non-Person Entity",
        "lastName": "Aetna",
        "name": "Aetna",
        "payorIdentification": "100003"
      },
      "planInformation": {
        "planNumber": "0123654"
      },
      "planDateInformation": {
        "planBegin": "2025-01-01",
        "eligibilityBegin": "2025-01-01",
        "service": "2025-03-27"
      },
      "benefitsInformation": [
        {
           "code": "1",
           "name": "Active Coverage",
           "coverageLevelCode": "FAM",
           "coverageLevel": "Family",
           "serviceTypeCodes": [
               "30"
           ],
           "serviceTypes": [
               "Health Benefit Plan Coverage"
           ],
           "insuranceTypeCode": "PS",
           "insuranceType": "Point of Service (POS)",
           "planCoverage": "Aetna Choice POS II",
           "inPlanNetworkIndicatorCode": "W",
           "inPlanNetworkIndicator": "Not Applicable"
        },
	 .... truncated to preserve space
        {
          "code": "W",
          "name": "Other Source of Data",
          "benefitsRelatedEntities": [
            {
              "entityIdentifier": "Payer",
              "entityType": "Non-Person Entity",
              "entityName": "AETNA",
              "address": {
                "address1": "PO BOX 981106",
                "city": "EL PASO",
                "state": "TX",
                "postalCode": "79998"
              }
            }
          ]
        }
      ],
      "confidence": {
        "level": "REVIEW_NEEDED",
        "reason": "This record was identified as a low confidence match due to a last name mismatch."
      }

    }
  ],
  "meta": {
    "applicationMode": "production",
    "traceId": "1-67e5a730-75011daa6caebf3c6595bf7c"
  },
  "status": "COMPLETE"
}

Visit our Insurance Discovery docs for complete details and API references.

Try Insurance Discovery today

Contact our team for pricing and to learn more about how Stedi can automate and streamline your eligibility and claims processing workflows.

Mar 19, 2025

Products

Last year, we introduced Transaction Enrollment, a streamlined way to submit enrollment requests for transaction types like claim remittances (ERAs) and eligibility checks through either our Enrollments API or user-friendly interface. Once you submit a request, Stedi manages the entire process for you, including submitting the enrollment to the payer, following up as needed, and giving clear guidance for any additional steps that might be required. 

To make transaction enrollment even more convenient, we’re excited to introduce CSV imports, a guided workflow that allows you to submit enrollment requests in bulk through Stedi’s UI. With CSV imports, operations teams can efficiently submit hundreds of enrollment requests in seconds without any development effort. 

Bulk import enrollments in seconds

Stedi guides you through the bulk CSV import process step-by-step: 

  1. Go to the Bulk imports page and click New bulk import.

  2. Download the CSV template with the required fields. The upload page contains detailed formatting instructions. 

  3. Complete and upload the CSV file containing enrollment information—one row equals one enrollment request for a specific transaction to a payer. 

  4. Stedi validates the file and provides clear error messages describing any issues. You can fix the errors and re-upload the file as many times as needed.

Once you execute the import, Stedi automatically creates enrollment requests. When the import is complete, you can download a report that shows the status of each row in the CSV file to ensure all your enrollment requests were submitted successfully. 

You can also track the status of each enrollment request through the Enrollments page.

Clear updates and guidance from Stedi’s enrollment experts

Our network and enrollment operations team knows the nuances of each payer’s enrollment requirements and maintains a public repository of payers that require additional steps through our Transaction Enrollments Hub. When an enrollment is submitted, the Stedi team is notified immediately and kicks off the enrollment process within the same business day.

If payers have additional requirements as part of their standard enrollment process, Stedi contacts you with clear, actionable steps to move the process forward. In addition to updating your enrollment request with action items required for your team, we’ll also reach out in your dedicated Slack channel with resources and to answer any follow-up questions.

Check out the Transaction Enrollment docs for complete details about each stage of the enrollment process. You can also search Stedi’s Payer Network to determine which payers require enrollment for the transaction types you need to process.

Get started with Stedi today

Contact us to learn more about how Stedi can automate and optimize eligibility and claims processing workflows for your business.

Mar 4, 2025

Products

We're excited to introduce sandbox accounts—a free, no-commitment way to explore integrating with Stedi. In a sandbox, you can simulate real-world transactions, test API integrations, and validate processing workflows at your own pace without using real patient data or committing to a paid plan.

You can create a sandbox account in under two minutes without talking to our team or entering any payment information. When you’re ready to start sending production data, you can contact customer support to seamlessly upgrade your account.

Simulate realistic transactions without PHI/PII

In a sandbox account, you can submit mock real-time eligibility checks for popular payers, and Stedi sends back a realistic benefits response so you know what kinds of data to expect in production. Mock transactions are always free for testing purposes and won’t incur any charges from Stedi.

You can send mock requests for a variety of well-known payers, including:

  • Aetna

  • Cigna

  • UnitedHealthcare

  • National Centers for Medicare & Medicaid Services (CMS)

  • Many more - Visit Eligibility mock requests for a complete list

Mock claims aren’t yet supported in Sandbox accounts. Contact us if you’d like to learn more about claims processing with Stedi.

Explore how Stedi can streamline your workflow

A sandbox account allows you to explore how Stedi’s UI tools can help you manage, track, and troubleshoot your eligibility check pipeline.

For example, after you submit a mock eligibility check, you can review all of the request and response details in Eligibility Manager. This includes:

  • Historical logs with filters for status, Payer ID, date, and error codes.

  • Raw API requests and responses.​

  • User-friendly benefits views highlighting patient coverage details, co-pays, and deductibles.​

  • Debugging tools to troubleshoot and resolve issues in transactions.​

Create a sandbox account today

Create a sandbox account to start testing Stedi at your own pace with no fees or commitments. You can also contact our team to learn more about how Stedi can help automate your eligibility checks and claims processing workflows.

Mar 3, 2025

Products

Stedi’s APIs allow you to submit claims programmatically and get notified through webhooks when payers return claim statuses and remittances. These APIs are a great fit for developers who prefer an API integration, especially those who don’t want to deal with the complexities of EDI.

However, many existing claims processing workflows use SFTP to submit claims and fetch payer responses. That’s why we’re excited to announce that fully managed SFTP-based claims submission is now available for all Stedi payers that accept claims. With SFTP connectivity, developers and revenue cycle teams can submit claims through Stedi and fetch claim status and remittances without rebuilding their existing architecture.

Submit claims through fully managed SFTP

We recommend using SFTP-based claims submission when you have an existing system that generates and ingests X12 EDI files via SFTP (if your system sends X12 EDI files via API, we support that too). With SFTP connectivity, you can send X12 EDI claims through Stedi and retrieve X12 EDI 277 Claim Acknowledgments and 835 Electronic Remittance Advice (ERAs) without an API integration.

Here’s how SFTP claims processing works:

  1. Create both test and production SFTP users on your account's Healthcare page. 

  2. Connect to Stedi's server and drop compliant X12 EDI claims into the to-stedi directory. 

  3. Stedi automatically validates the claim data. If there are no errors, Stedi routes production claims to payers and test claims to our test clearinghouse. 

  4. Stedi places claim responses – X12 277s and ERAs – into the from-stedi directory. 

  5. Retrieve these files from Stedi’s SFTP server at your preferred cadence.  

You can also configure Stedi webhooks to send claim processing events to your API endpoint. This allows you to monitor for processing issues, confirm successful claim submissions, and get notified when new payer responses are available.

Visit the Submit claims through SFTP documentation for complete details.

Monitor and debug your claims pipeline in the Stedi UI

You can review all of the claims and claim responses flowing through your SFTP connection on the Files and Transactions pages in Stedi. Each transaction page highlights the key information about the claim, such as the insured’s name, member ID, and line items. Stedi also displays a linked list of related transactions, so you can easily move between the original claim and any responses.

These UI tools allow you to track your entire claim pipeline, review claim submissions, quickly diagnose errors, and more.

Start processing claims with Stedi today

With SFTP claims processing, you can send claims and retrieve payer responses through Stedi in minutes. 

Contact us to learn more about how Stedi can help automate and streamline claims processing for your business.

Feb 25, 2025

Products

A Medicare Beneficiary Identifier (MBI) is a unique, randomly-generated identifier assigned to individuals enrolled in Medicare. It’s required in every eligibility check submitted to the Centers for Medicare and Medicaid Services (Payer ID: CMS), which is also known as the HIPAA Eligibility Transaction System (HETS).

Providers need to run eligibility and benefits verification checks to CMS to verify active coverage, calculate patient costs and responsibility, and route claims to the right state Medicare payer.

Many providers are unable to identify or locate a patient’s MBI due to problems with manual patient intake processes, paper forms, incomplete data from referrals, and patients simply not having their Medicare card on hand. These issues prevent providers from verifying patient eligibility with CMS, delaying patients from receiving critical care and creating unnecessary stress for patients, providers, and billing teams. That’s why we’re excited to introduce MBI lookups for CMS eligibility checks.

You can now use Stedi’s eligibility check APIs to retrieve benefits information from CMS with a patient’s Social Security Number (SSN) instead of their MBI. Stedi looks up the patient’s MBI, submits a compliant eligibility check to CMS, and returns a complete benefits response that includes the patient’s MBI for future reference.

Retrieve complete benefits information with the patient’s SSN

You can perform MBI lookups using Stedi’s Real-Time Eligibility Check and Batch Eligibility Check APIs. To perform an MBI lookup:

  1. Construct an eligibility check request that includes the patient’s first name, last name, date of birth, and Social Security Number (SSN).

  2. Set the tradingPartnerServiceId to MBILU, which is the payer ID for the MBI lookup to CMS.

  3. Stedi uses the patient’s demographic data and SSN to perform an MBI lookup. If there is a match, Stedi submits an eligibility check to CMS.

  4. Stedi returns a complete benefits response from CMS that includes the patient’s coverage status and their MBI in the subscriber object for future reference.

The following sample request uses Stedi’s Real-Time Eligibility Check API to perform an MBI lookup for a patient named Jane Doe.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "MBILU",
  "externalPatientId": "UAA111222333",
  "encounter": {
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1234567890"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "ssn": "123456789"
  }
}'

The following snippet shows part of a CMS benefits response returned from an MBI lookup. Stedi places the patient’s MBI in the subscriber.memberId property, so in this example, the patient’s MBI is 1AA2CC3DD45.

{
   "meta": {
       "senderId": "STEDI",
       "submitterId": "117151744",
       "applicationMode": "production",
       "traceId": "11112222333344445555666677",
       "outboundTraceId": "11112222333344445555666677"
   },
   "controlNumber": "112233445",
   "reassociationKey": "112233445",
   "tradingPartnerServiceId": "CMS",
   "provider": {
       "providerName": "ACME HEALTH SERVICES",
       "entityIdentifier": "Provider",
       "entityType": "Non-Person Entity",
       "npi": "1234567890"
   },
   "subscriber": {
       "memberId": "1AA2CC3DD45",
       "firstName": "JANE",
       "lastName": "DOE",
       "middleName": "A",
       "gender": "F",
       "entityIdentifier": "Insured or Subscriber",
       "entityType": "Person",
       "dateOfBirth": "19000101",
       "address": {
           "address1": "1234 FIRST ST",
           "city": "NEW YORK",
           "state": "WV",
           "postalCode": "123451111"
       }
   },
   "payer": {
       "entityIdentifier": "Payer",
       "entityType": "Non-Person Entity",
       "name": "CMS",
       "payorIdentification": "CMS"
   },
   // truncated; visit the docs for a full CMS benefits response example
}

Visit the MBI lookup docs for a full example response with complete benefits information.

Monitor your entire eligibility pipeline in the Stedi UI

Once you submit an MBI lookup, you can review its complete details in the Stedi UI. This includes the full request and response payload as well as clear status messages designed to help you quickly resolve eligibility issues.  

You can retry failed checks in real time through Stedi’s user-friendly eligibility form and use the Debug view to systematically troubleshoot failed checks until you receive a successful response from the payer.

Start processing eligibility checks with Stedi today

MBI lookup makes it easier for providers to access critical benefits information for Medicare beneficiaries.

Contact us to learn more about how Stedi Clearinghouse can help you streamline eligibility checks for CMS and thousands of other payers.

Feb 5, 2025

Products

When a patient has active coverage with multiple health plans, providers need to know which plan is primarily responsible for paying claims. The process of figuring out which payers to bill and in what order is called coordination of benefits (COB), and it’s one of the leading causes of claim denials and delayed payments in healthcare.

Providers often don’t know patients have overlapping coverage until after a claim is rejected or denied with a message that doesn’t contain any information about where to resubmit the claim: 

[PATTERN 28937] REJECT - THIS PATIENT HAS PRIMARY INSURANCE COVERAGE WITH ANOTHER CARRIER WITH EFFECTIVE DATE 1/01/2025. PLEASE RESUBMIT AS ELECTRONIC SECONDARY ONCE ADJUDICATED BY THE PRIMARY PAYOR.

Back at square one, providers must contact patients to ask about additional coverage and then confirm the new plan’s primacy for payment (usually by calling the payer) before they can resubmit. This tedious process delays payments to providers for months and creates stressful billing surprises for patients.

To help streamline claims processing, we’re excited to announce that you can now perform COB checks through our developer-friendly Coordination of Benefits Check API or user-friendly online form. COB checks help you proactively identify additional coverage for patients and confidently submit claims to the right payer the first time. 

Reduce claim denials with coordination of benefits checks

The best practice is to run COB checks for all new patients who have coverage through one of Stedi’s supported COB payers. In seconds, you can determine if the patient has coverage with other supported COB payers, whether there is coverage overlap, and which plan is responsible for paying for services (primacy).

Here’s how COB checks work: 

  1. You submit a COB check through Stedi’s COB form or COB check API with information for one of the patient's known health plans. The information required is similar to a standard eligibility check – first name, last name, DOB, and either member ID or SSN – and you should first run a real-time eligibility check to ensure that the member’s details are accurate.

  2. Stedi searches a database of eligibility data from regional and national plans. This database has 245+ million patient coverage records from 45+ health plans, ASOs, TPAs, and others, including participation from the vast majority of national commercial health plans. Data is updated at least weekly to ensure accuracy.

  3. Stedi synchronously returns summary information about each of the patient’s active health plans and whether there is coverage overlap. When there is coverage overlap, Stedi returns the responsibility sequence number for each payer (such as primary or secondary), if that can be determined.

Once you receive the results, you should send real-time eligibility checks to each additional payer to verify coverage and view the full details of the patient’s plan before submitting claims.

Real-time COB information in developer-friendly JSON

The following example COB response shows information for a dependent covered by multiple health plans through their parents’ policies. The COB check was submitted to Aetna with a service type code of 30 (Health Benefit Plan Coverage) and a service date of 2024-11-27.

The response indicates the following:

  • Active coverage: The patient has active coverage with Aetna for medical care, pharmacy, and vision services. This is indicated by the three objects in the benefitsInformation array with the code set to 1, which indicates Active Coverage.

  • Coverage overlap: The patient has overlapping coverage for medical care services between two health plans. This is indicated by the benefitsInformation object with its code set to R.

  • Primacy: The other health plan is Cigna, listed in benefitsInformation.benefitsRelatedEntities. Cigna is the primary payer for medical care services.

  • COB instance: There is a COB instance for medical care services on the date of service provided in the request. This is indicated in the coordinationOfBenefits object.

Based on this response, you must send claims first to Cigna as the primary payer for medical care services. Once Cigna adjudicates the claim, you can send another claim, if necessary, to Aetna as the secondary payer (subject to specific payer claims processing rules).

Before sending any claim(s) to Cigna, you should first send a separate eligibility check to Aetna to verify coverage status and confirm the full details of Aetna’s coverage.

{
  "benefitsInformation": [
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "1"
      ],
      "serviceTypes": [
        "Medical Care"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "88"
      ],
      "serviceTypes": [
        "Pharmacy"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "AL"
      ],
      "serviceTypes": [
        "Vision (Optometry)"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "R",
      "name": "Other or Additional Payor",
      "serviceTypeCodes": [
        "1"
      ],
      "serviceTypes": [
        "Medical Care"
      ],
      "benefitsDateInformation": {
        "coordinationOfBenefits": "2024-07-01"
      },
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Primary Payer",
          "entityName": "CIGNA",
          "entityIdentification": "PI",
          "entityIdentificationValue": "1006"
        },
        {
          "entityIdentifier": "Insured or Subscriber",
          "entityFirstName": "JOHN",
          "entityMiddleName": "X",
          "entityIdentification": "MI",
          "entityIdentificationValue": "00000000000",
          "entityLastName": "DOE"
        }
      ],
      "subscriber": {
        "dateOfBirth": "2002-12-31"
      }
    }
  ],
  "coordinationOfBenefits": {
    "classification": "CobInstanceExistsPrimacyDetermined",
    "instanceExists": true,
    "primacyDetermined": true,
    "coverageOverlap": true,
    "benefitOverlap": true
  },
  "dependent": {
    "firstName": "JORDAN",
    "lastName": "DOE",
    "gender": "M",
    "dateOfBirth": "2012-12-31",
    "relationToSubscriber": "Child",
    "relationToSubscriberCode": "19",
    "address": {
      "address1": "1 MAIN ST.",
      "city": "NEW YORK",
      "state": "NY",
      "postalCode": "10000"
    }
  },
  "errors": [],
  "meta": {
    "applicationMode": "production",
    "traceId": "01JDQFT4W3KTWZNTADEZ55BFFX",
    "outboundTraceId": "01JDQFT4W3KTWZNTADEZ55BFFX"
  },
  "payer": {
    "name": "Aetna",
    "identification": "AETNA-USH"
  },
  "provider": {
    "providerName": "ACME Health Services",
    "entityType": "Non-Person Entity",
    "npi": "1999999984"
  },
  "subscriber": {
    "memberId": "W000000000",
    "firstName": "JOHN",
    "lastName": "DOE",
    "address": {
      "address1": "1 MAIN ST.",
      "city": "NEW YORK",
      "state": "NY",
      "postalCode": "10000"

Check out our coordination of benefits docs for a complete API reference, test requests and responses, and more.

Get started with coordination of benefits checks today

Add coordination of benefits checks to your claims processing workflow to increase claim acceptance rates, reduce patient billing surprises, and help providers get paid faster. 

Contact us to learn more and speak to the team.

Jan 16, 2025

Products

We’re excited to announce that our Dental Claims API is now Generally Available. 

Dental claims allow dental providers to bill insurers for services, such as preventive cleanings, fillings, crowns, bridges, and other restorative procedures. Our new Dental Claims API allows you to automate this process and streamline the claims lifecycle for dental providers and payers.

You can use the new API to submit 837D Dental Claims to hundreds of payers in Stedi’s developer-friendly JSON format or raw X12 EDI format. Once submitted, you can programmatically check the claim status in real time and retrieve 277 claim acknowledgments and 835 electronic remittance advice (ERA) from payers, all through the Stedi clearinghouse.

Submit dental claims to hundreds of payers

Submit requests to one of Stedi’s dental claims endpoints:

  • Dental Claims: Submit your claim in user-friendly JSON. Stedi translates your request to the X12 EDI 837D format.

  • Dental Claims Raw X12: Submit your claim in X12 EDI format. This is ideal if you have an existing system that generates X12 EDI files and you want to send them through Stedi’s API.

Stedi first validates your request against the 837D specification to ensure compliance, reducing payer rejections later on. Then, Stedi constructs and sends a valid X12 EDI 837D claim to the payer. Finally, Stedi returns a response containing information about the claim you submitted and whether the submission was successful.

You can also send test claims to Stedi’s QA clearinghouse by setting the usageIndicator to T, as shown in the example below for the JSON endpoint. This helps you quickly test and debug your end-to-end claims processing pipeline.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/dental-claims/submission \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "usageIndicator": "T",
  "tradingPartnerServiceId": "1748",
  "tradingPartnerName": "Tricare West AZ",
  "subscriber": {
    "paymentResponsibilityLevelCode": "P",
    "memberId": "123412345",
    "firstName": "John",
    "lastName": "Doe",
    "groupNumber": "1234567890",
    "gender": "F",
    "address": {
      "address1": "1234 Some St",
      "city": "Buckeye",
      "state": "AZ",
      "postalCode": "85326"
    },
    "dateOfBirth": "20180615"
  },
  "submitter": {
    "organizationName": "ABA Inc",
    "submitterIdentification": "123456789",
    "contactInformation": {
      "phoneNumber": "3131234567",
      "name": "BILLING DEPARTMENT"
    }
  },
  "rendering": {
    "npi": "1234567893",
    "taxonomyCode": "106S00000X",
    "providerType": "RenderingProvider",
    "lastName": "Doe",
    "firstName": "Jane"
  },
  "receiver": {
    "organizationName": "Tricare West AZ"
  },
  "payerAddress": {
    "address1": "PO Box 7000",
    "city": "Camden",
    "state": "SC",
    "postalCode": "29000"
  },
  "claimInformation": {
    "signatureIndicator": "Y",
    "toothStatus": [
      {
        "toothNumber": "3",
        "toothStatusCode": "E"
      }
    ],
    "serviceLines": [
      {
        "serviceDate": "20230428",
        "renderingProvider": {
          "npi": "1234567893",
          "taxonomyCode": "122300000X",
          "providerType": "RenderingProvider",
          "lastName": "Doe",
          "firstName": "Jane"
        },
        "providerControlNumber": "a0UDo000000dd2dMAA",
        "dentalService": {
          "procedureCode": "D7140",
          "lineItemChargeAmount": "832.00",
          "placeOfServiceCode": "12",
          "oralCavityDesignationCode": [
            "1",
            "2"
          ],
          "prosthesisCrownOrInlayCode": "I",
          "procedureCount": 2,
          "compositeDiagnosisCodePointers": {
            "diagnosisCodePointers": [
              "1"
            ]
          }
        },
        "teethInformation": [
          {
            "toothCode": "3",
            "toothSurface": [
              "M",
              "O"
            ]
          }
        ]
      }
    ],
    "serviceFacilityLocation": {
      "phoneNumber": "3131234567",
      "organizationName": "ABA Inc",
      "npi": "1234567893",
      "address": {
        "address1": "ABA Inc 123 Some St",
        "city": "Denver",
        "state": "CO",
        "postalCode": "802383100"
      }
    },
    "releaseInformationCode": "Y",
    "planParticipationCode": "A",
    "placeOfServiceCode": "12",
    "patientControlNumber": "0000112233",
    "healthCareCodeInformation": [
      {
        "diagnosisTypeCode": "ABK",
        "diagnosisCode": "K081"
      }
    ],
    "claimSupplementalInformation": {
      "priorAuthorizationNumber": "20231010012345678"
    },
    "claimFrequencyCode": "1",
    "claimFilingCode": "FI",
    "claimChargeAmount": "832.00",
    "benefitsAssignmentCertificationIndicator": "Y"
  },
  "billing": {
    "taxonomyCode": "106S00000X",
    "providerType": "BillingProvider",
    "organizationName": "ABA Inc",
    "npi": "1999999992",
    "employerId": "123456789",
    "contactInformation": {
      "phoneNumber": "3134893157",
      "name": "ABA Inc"
    },
    "address": {
      "address1": "ABA Inc 123 Some St",
      "city": "Denver",
      "state": "CO",
      "postalCode": "802383000"
    }
  }
}'

Retrieve 277 acknowledgments and 835 ERAs programmatically

After you submit a dental claim, you may receive asynchronous 277CA and 835 ERA responses from the payer. The 277CA indicates whether the claim was accepted or rejected and (if relevant) the reasons for rejection. The 835 ERA, also known as a claim remittance, contains details about payments for specific services and explanations for any adjustments or denials.

You can either poll Stedi for processed 277CAs and 835 ERAs or set up webhooks that automatically send events for processed responses to your endpoint. Then, you can use the following APIs to retrieve them from Stedi in JSON format:

Track claims and responses in Stedi

You can view a list of every claim you submit through Stedi clearinghouse and all associated responses on the Transactions page in Stedi. Click any transaction to review its details, including the full request and response payload.

Start processing claims with Stedi

With the Dental Claims API, you can start automating your claim submission process today. Contact us to learn more and speak to the team.

Dec 17, 2024

Products

All payers require providers to complete an enrollment process before they can start receiving claim remittances (ERAs). Though less common, certain payers also require enrollment before allowing providers to submit transactions like claims and eligibility checks. 

The typical enrollment process is highly manual and notoriously painful. There are no APIs or uniform standards, so every payer has different requirements. Forms, online portals, PDFs, wet signatures, and even fax machines can all be part of the process, causing significant frustration and slowing down the overall revenue cycle. After the enrollment is submitted, it’s a black box; days or weeks go by with no status updates, forcing providers to wait to submit claims, check patient eligibility, and receive information about payments. These issues compound for revenue cycle management companies that do hundreds or thousands of enrollments on behalf of many providers.

That’s why Stedi designed a new enrollment experience from the ground up. Through Stedi's user-friendly interface or modern API, developers and operations teams can now submit enrollments for specific transaction types in a streamlined flow – either one at a time or in large batches.

Once you submit an enrollment request, Stedi manages the entire process for you, including submitting the enrollment to the payer, following up as needed, and giving clear guidance for any additional steps that might be required. You can monitor the enrollment status throughout the process using the API or our searchable Enrollments dashboard.

Manage transaction enrollments through Stedi’s user-friendly app or APIs

You can submit and monitor all of your organization’s transaction enrollments from the Enrollments page in Stedi. For each enrollment request, Stedi displays the provider, status, payer, and transaction type. You can click any enrollment request to view its complete details, including any notes from Stedi regarding the next steps.

You can also manage the entire transaction enrollment process programmatically through Stedi’s Transaction Enrollment API. This approach is especially useful when you need to perform bulk enrollments for many providers at once, or when you want to embed enrollment capabilities into your own application. The following example request enrolls a provider for claim payments (835 ERAs) with UnitedHealthcare (Payer ID: 87726). 

curl --request POST \
  --url https://enrollments.us.stedi.com/2024-09-01/enrollments \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "transactions": {
    "claimPayment": {
      "enroll": true
    }
  },
  "primaryContact": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "test@example.com",
    "phone": "555-123-34354",
    "streetAddress1": "123 Some Str.",
    "city": "A City",
    "state": "MD",
    "zipCode": "20814"
  },
  "userEmail": "test@example.com",
  "payer": {
    "id": "87726"
  },
  "provider": {
    "id": "db6665c5-7b97-4af9-8c68-a00a336c2998"
  }
}'

Streamlined processing

Transaction enrollment requests you submit manually and programmatically follow the same streamlined process:

  1. Add provider: You add a new provider record with the information required for enrollment, including the provider's name, tax ID, NPI, and contact information.

  2. Submit transaction enrollment requests: You submit requests to enroll the provider with required payers, one for each transaction type. For example, you’d submit three separate requests to enroll a provider for 837P (professional) claims, 270 real-time eligibility checks, and 835 ERAs (claim payments). You can save requests in DRAFT status until you're ready to submit them to Stedi. Once you submit an enrollment request, its status is set to SUBMITTED, and it enters Stedi’s queue for processing.

  3. Provisioning: Stedi begins the enrollment process with the payer and sets the enrollment request status to PROVISIONING. Our team leaves clear instructions about what to do next, if required, and provides hands-on help as needed with additional steps.

  4. Enrollment is Live: Once the enrollment is approved, the enrollment request status is set to LIVE, and the provider can start exchanging the enrolled transactions with the payer.

Clear status updates and expert guidance

Our customer success team knows the nuances of each payer’s enrollment requirements and breaks them down into clear, actionable steps. In addition to updating your enrollment request with any action items required for your team, we also reach out to you in your dedicated Slack channel with resources and to answer any follow-up questions. 

The following example shows an enrollment request for Medicaid California Medi-Cal that requires additional steps for the provider. In Step 1, customer support notes that they dropped the required PDF in Slack for easy access.

You can also retrieve this information through the GET Enrollment endpoint. Notes from Stedi about the next steps are available in the reason property. 

{
  "createdAt": "2024-12-11T22:39:16.772Z",
  "id": "0193b7e0-6526-72a2-90a3-4e05dce4775a",
  "payer": {
    "id": "100065"
  },
  "primaryContact": {
    "organizationName": "Test Healthcare Org",
    "firstName": "",
    "lastName": "",
    "email": "test@testemail.com",
    "phone": "4112334567",
    "streetAddress1": "123 Some Street",
    "streetAddress2": "",
    "city": "Pittsburgh",
    "zipCode": "12345",
    "state": "PA"
  },
  "provider": {
    "name": "Test Healthcare Provider",
    "id": "0193b7db-528d-7723-9943-959b955ba103"
  },
  "reason": "The following steps are required by Medi-Cal to complete this enrollment.\n- **Step 1:** You (or your provider who requires this enrollment) must log into the DHCS portal and complete the steps found on page 2 of the PDF sent in your Slack channel on 12/11/2024. This step tells Medi-Cal to \"affiliate\" the provider with Availity (the intermediary clearinghouse we go through for our Medi-Cal connection). Let Stedi know in Slack once you have done this.\n- **Step 2:** Stedi performs the enrollment process. In the following days, you will receive an email from Medi-Cal stating that the affiliation process is complete.\n- **Step 3:** You (or your provider who requires the enrollment) must log back into the DHCS portal and complete the steps found on pages 3-5 of the PDF. This will complete the enrollment process.",
  "status": "SUBMITTED",
  "transactions": {
    "professionalClaimSubmission": {
      "enroll": true
    }
  },
  "updatedAt": "2024-12-17T17:31:42.012Z",
  "userEmail": "demos@stedi.com"
}

Get started with Stedi today

Streamlined transaction enrollments are just one of the ways Stedi accelerates payer integration and offers a modern, developer-friendly alternative to traditional clearinghouses. You can search Stedi’s Payer Network to determine which payers require enrollment for the transaction types you need to process. 

Contact us to speak to the team and learn how Stedi can help you automate and simplify your eligibility and claims workflows.

Dec 5, 2024

Products

Many providers need to perform batch eligibility checks for patients monthly or before upcoming appointments. These refresh or background checks have historically required significant development effort.

First, you need complex logic to avoid payer concurrency constraints; payers can throttle requests or even block the NPI when providers send too many checks at once. Then, you need a robust way to detect payer outages and manage retries. Finally, you need to monitor your clearinghouse concurrency ‘budget’ to ensure you have enough throughput left for time-sensitive real-time checks. 

Stedi’s new Batch Eligibility Check API handles all of this complexity for you. You can submit millions of eligibility checks – up to 500 per batch request – for Stedi to process asynchronously. Stedi manages throughput and retries automatically using industry best practices. Even better, asynchronous checks don’t count toward your Stedi account’s concurrency limits, so you can stage an unlimited number of batch checks while continuing to send real-time checks in parallel.

Under the hood, Stedi’s Batch Eligibility Check API uses the same real-time EDI rails as our normal Eligibility Check API, which means that our batch eligibility check jobs aren’t subject to the delays commonly found in other batch eligibility offerings.

Automate batch eligibility checks with two simple endpoints

You can automate asynchronous batch checks to thousands of payers by calling Stedi’s Batch Eligibility Check endpoint with a JSON payload. You may want to submit batch requests for:

  • All patients at the beginning of every month to identify those who have lost or changed coverage.

  • Patients with upcoming appointments to identify those who no longer have active coverage. 

  • New claims before they are submitted, to ensure they are routed to the correct payer.

Once you submit a batch, Stedi translates the JSON into compliant X12 270 payloads and delivers them to the designated payers as quickly as possible using Stedi’s master payer connections. You can then use the Poll Batch Eligibility Checks endpoint to retrieve the results for completed checks. Stedi automatically handles payer downtime and retries requests as needed.

The following example request demonstrates how to submit a batch of eligibility checks, where each item in the array represents an individual check. The request format is the same as our Real-Time Eligibility Check endpoint, with the addition of the submitterTransactionIdentifier property, a unique identifier you can use to match each check to the payer’s response.

curl --request POST \
  --url https://manager.us.stedi.com/2024-09-01/eligibility-manager/batch-eligibility \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "items": [
    {
      "submitterTransactionIdentifier": "ABC123456789",
      "controlNumber": "000022222",
      "tradingPartnerServiceId": "AHS",
      "encounter": {
        "serviceTypeCodes": [
          "MH"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1234567891"
      },
      "subscriber": {
        "dateOfBirth": "19000101",
        "firstName": "Jane",
        "lastName": "Doe",
        "memberId": "1234567890"
      }
    },
    {
      "submitterTransactionIdentifier": "DEF123456799",
      "controlNumber": "333344444",
      "tradingPartnerServiceId": "AHS",
      "encounter": {
        "serviceTypeCodes": [
          "78"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1234567891"
      },
      "subscriber": {
        "dateOfBirth": "19001021",
        "firstName": "John",
        "lastName": "Doe",
        "memberId": "1234567892"
      }
    }
  ]
}'

Minimize payer throttling through industry best practices

Stedi processes batch eligibility checks as quickly as possible while implementing best practices to avoid payer throttling, such as:

  • Observing NPI throughput limits in real time and adjusting the rate of requests accordingly.

  • Interleaving requests to multiple payers in a round-robin style to avoid hitting a payer with the same provider NPI too many times consecutively. For example, instead of sending all requests to payer #1 and then all requests to payer #2, Stedi will send one request to payer #1, then one request to payer #2, and then go back to payer #1.

  • Using backoff algorithms and other strategies when retrying requests to the same payer.

We continually incorporate our learnings about individual payer requirements and behavior into the system to ensure the most efficient batch processing time. 

Detect payer downtime and automatically retry

Stedi supports a growing list of payers for eligibility checks and continually monitors payer uptime. 

When Stedi detects a payer outage, our APIs reroute requests through alternate direct-to-payer or peer clearinghouse connections if possible. When requests fail due to payer processing issues, Stedi implements a retry strategy based on best practices and each payer’s connectivity requirements.

Start processing eligibility checks with Stedi

With Stedi’s Real-Time Eligibility Check and Batch Eligibility Check APIs, you can start automating eligibility checks for your organization today. 

Contact us to learn more and speak to the team.

Nov 20, 2024

Products

We’re excited to announce that our Institutional Claims API is now Generally Available. 

Institutional claims are how hospitals, nursing homes, and other healthcare facilities bill insurers for services, such as inpatient care, diagnostics, and therapies. Our new Institutional Claims API allows you to automate this process and streamline the claims lifecycle for healthcare providers.

You can use the new API to submit 837I Institutional Claims to thousands of payers using Stedi’s developer-friendly JSON format. Once submitted, you can programmatically check the claim status in real time and retrieve 277 claim acknowledgments and 835 electronic remittance advice (ERA) from payers through the Stedi clearinghouse. 

Submit institutional claims to thousands of payers

Call the Institutional Claims endpoint with a JSON payload. Stedi first validates your request against the 837I specification to ensure it’s compliant, reducing payer rejections down the line. Then, Stedi translates your request to the X12 837 EDI format and sends it to the payer. Finally, Stedi returns a response containing information about the claim you submitted and whether the submission was successful.

You can also send test claims to Stedi’s QA clearinghouse by setting the usageIndicator to T, as shown in the following example. This helps you quickly test and debug your end-to-end claims processing pipeline.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/submission \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "usageIndicator": "T",
  "tradingPartnerName": "UnitedHealthcare",
  "tradingPartnerServiceId": "87726",
  "controlNumber": "123456789",
  "submitter": {
    "organizationName": "Test Facility",
    "contactInformation": {
      "name": "Test Facility",
      "phoneNumber": "2225551234"
    },
    "taxId": "123456789"
  },
  "receiver": {
    "organizationName": "UnitedHealthcare"
  },
  "subscriber": {
    "memberId": "98765",
    "paymentResponsibilityLevelCode": "P",
    "firstName": "JANE",
    "lastName": "DOE",
    "groupNumber": "67890"
  },
  "claimInformation": {
    "claimFilingCode": "ZZ",
    "patientControlNumber": "00001111222233334444",
    "claimChargeAmount": "500.00",
    "placeOfServiceCode": "11",
    "claimFrequencyCode": "0",
    "planParticipationCode": "C",
    "benefitsAssignmentCertificationIndicator": "Y",
    "releaseInformationCode": "Y",
    "principalDiagnosis": {
      "qualifierCode": "ABK",
      "principalDiagnosisCode": "R45851"
    },
    "serviceLines": [
      {
        "assignedNumber": "0",
        "serviceDate": "20241015",
        "serviceDateEnd": "20241015",
        "lineItemControlNumber": "111222333",
        "institutionalService": {
          "serviceLineRevenueCode": "90",
          "lineItemChargeAmount": "500.00",
          "measurementUnit": "UN",
          "serviceUnitCount": "1",
          "procedureIdentifier": "HC",
          "procedureCode": "H0001"
        }
      }
    ],
    "claimCodeInformation": {
      "admissionTypeCode": "3",
      "admissionSourceCode": "9",
      "patientStatusCode": "30"
    },
    "claimDateInformation": {
      "admissionDateAndHour": "202409091000",
      "statementBeginDate": "20241015",
      "statementEndDate": "20241015"
    }
  },
  "providers": [
    {
      "providerType": "BillingProvider",
      "npi": "0123456789",
      "employerId": "123456789",
      "organizationName": "Test Facility",
      "address": {
        "address1": "123 Mulberry Street",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "111135272"
      },
      "contactInformation": {
        "name": "Test Facility",
        "phoneNumber": "2065551234"
      }
    },
    {
      "providerType": "AttendingProvider",
      "npi": "1234567890",
      "firstName": "Doctor",
      "lastName": "Provider",
      "contactInformation": {
        "name": "name"
      }
    }
  ]
}'

Retrieve 277 acknowledgments and 835 ERAs programmatically

After you submit an institutional claim, you may receive asynchronous 277CA and 835 ERA responses from the payer. The 277CA indicates whether the claim was accepted or rejected and (if relevant) the reasons for rejection. The 835 ERA, also known as a claim remittance, contains details about payments for specific services and explanations for any adjustments or denials.

You can either poll Stedi for processed 277CAs and 835 ERAs or set up webhooks that automatically send events for processed responses to your endpoint. Then, you can use the following APIs to retrieve them from Stedi in JSON format:

Track claims and responses in the Stedi app

You can view a list of every claim you submit through the Stedi clearinghouse and all associated responses on the Transactions page of the Stedi app. Click any transaction to review its details, including the full request and response payload.

Start processing claims with Stedi

With the Institutional Claims API, you can start automating your claim submission process today. Contact us to learn more and speak to the team.

Sep 24, 2024

Products

Every clearinghouse maintains a list of supported payers, supported transaction types, and other key integration details. The problem? This vital information is typically provided in a CSV file over email and updated monthly at best. Worse, updated payer lists often contain breaking changes, duplicate payer names, and typos that cause failed transactions and endless code rewrites. 

Instead of a static payer list, we built the world’s most developer-friendly payer database: the Stedi Payer Network, which is uniquely designed to help you build more efficient and reliable integrations with thousands of medical and dental payers. It’s just one of the ways Stedi's clearinghouse gives modern healthcare companies the developer experience, security, and reliability they need to build world-class products for providers and patients.

Stable, unified payer records

Every EHR system and clearinghouse uses payer IDs to route transactions to payers. It can be hard to know which ID to use for requests because payer IDs vary between clearinghouses, and some clearinghouses assign separate IDs to different transactions with the same payer. Once you determine the right ID, frequent payer list updates require tedious remapping in your codebase.

The Stedi Payer Network eliminates this problem by mapping all of a payer’s commonly used names and IDs (aliases) to a single payer record, and Stedi automatically uses the required ID for the payer on the backend. For example, searching our interactive network page by any of Cigna’s aliases, such as 1841, CIGNA, and GWSTHC returns the same result, even though the most common Payer ID for Cigna is 62308.

This approach means Stedi likely already supports all of the common payer IDs you use today, making it easy to migrate to Stedi from other clearinghouses. And if you need a new alias for any existing payer, just let us know and we’ll add it to the network the same day.

Programmatic access to real-time updates

With the List Payers API, developers can retrieve Stedi’s complete, production payer list in seconds for use in any system or application. For example, you can:

  • Embed Stedi’s payer list within a patient intake form, allowing patients to choose from a list of supported payers.

  • Build simpler EHR integrations using the payer IDs in the provider’s EHR.

  • Create nightly or real-time syncs between your internal payer list and Stedi’s payer list.

  • Migrate or reroute transactions to Stedi’s clearinghouse without dynamically changing payer IDs.

The following Blue Cross Blue Shield of North Carolina response example shows that real-time eligibility checks, real-time claim status requests, and professional claims are supported for this payer. The response also indicates that payer enrollment is required for 835 ERAs (claim remittances).

{
  "stediId": "UPICO",
  "displayName": "Blue Cross Blue Shield of North Carolina",
  "primaryPayerId": "BCSNC",
  "aliases": [
    "1411",
    "560894904",
    "61473",
    "7472",
    "7814",
    "BCBS-NC",
    "BCSNC",
    "NCBCBS",
    "NCPNHP"
   ],
   "names": [
     "Blue Cross Blue Shield North Carolina"
   ],
   "transactionSupport": {
     "eligibilityCheck": "SUPPORTED",
     "claimStatus": "SUPPORTED",
     "claimSubmission": "SUPPORTED",
     "claimPayment": "ENROLLMENT_REQUIRED"
   }
}

Broad, reliable connectivity

Stedi already supports thousands of medical and dental payers, and we add more every week. Here are the number of unique payers supported for specific transaction sets:

  • Real-time eligibility checks - 1,100+ unique payers 

  • Claim submissions - 2,700+ unique payers 

  • Real-time claim status requests - 300+ unique payers 

  • 835 ERAs (claim remittances) - 1,800+ unique payers

On the backend, Stedi has multiple, redundant routes to payers through a combination of direct payer integrations and connectivity through intermediary clearinghouses. Whenever possible, Stedi automatically routes requests and responses to the most reliable connection, increasing uptime and reliability across the network.

You get redundancy and reliability with no additional effort or cost when you integrate with Stedi –  we manage all of the payer routing logic seamlessly behind the scenes. We add new payers daily, so feel free to request new payers or new transaction types for an existing payer.

Get started with Stedi

Stedi’s programmatically accessible payer network is one of the many ways our clearinghouse accelerates payer integration.

"The speed at which we got up and running in production with Stedi was remarkable. From the moment we began integration, it was clear that Stedi was designed with user ease and efficiency in mind.”

- Chris Parker, Chief Technology Officer PQT Health

Contact us to speak to the team and learn how Stedi can help you automate and simplify your eligibility and claims workflows. And if you want to see what percentage of your payer list we support, we’d be happy to do the comparison for you.

Sep 3, 2024

Products

Today, we’re introducing two new features to streamline your claims-processing workflow.

  • Manual claim submission to speed up integration testing, simplify troubleshooting, or handle out-of-band claim submissions. 

  • Auto-generated CMS-1500 Claim Form PDFs for record-keeping, mailing or faxing claims to payers, and more. 

We also revamped the Stedi app to make it easier to track claims from submission through remittance.

Submit claims manually in the Stedi app

We adapted the National Uniform Claim Committee (NUCC) 1500 Claim Form into a user-friendly digital form you can use to submit professional claims to thousands of payers

Our form validates key information, including provider NPIs and diagnosis codes, to reduce errors and payer rejections. You can also review a live preview of the auto-generated JSON payload for the Professional Claims API to understand how the form relates to the request structure.

Get auto-generated CMS-1500 Claim Form PDFs

Even if you submit the majority of claims programmatically, you may still need to create traditional claim forms for internal record-keeping, mailing and faxing claims to payers, reviewing claim information in a familiar format, or troubleshooting a complicated submission. You may also want to make claim form PDFs available for download within an EHR or RCM system.

To make these tasks easier, Stedi now automatically generates a complete CMS-1500 Claim Form PDF for every submitted claim. You can download these PDFs from the app or retrieve them through the CMS-1500 Claim Form PDF API

Monitor your entire claims processing pipeline

You can review complete details about every professional claim (837), claim acknowledgment (277), and claim payment (835 ERA) in the Stedi app. On the Transactions page, you can filter and sort by transaction type, usage (production or test), processed date, and business identifiers to quickly find specific claims and responses.  

On each transaction’s details page, Stedi now automatically puts key information at the top for easy access, such as subscriber information and the claim value. You can also instantly download the auto-generated CMS-1500 Claim Form PDF, review all related transactions, and review the full API request and response payloads.

Start processing claims today

With Stedi’s API-first clearinghouse, you can automate business flows like claims processing and eligibility checks using APIs that support thousands of payers

Contact us to learn more and speak to the team.

Jun 25, 2024

Guide

Your claim was denied—another patient forgot to update their insurance details after switching jobs. Now, you need to submit a new claim and wait an extra month for reimbursement, putting a strain on your business.

To prevent these kinds of delays, many healthcare customers use our Eligibility Check API to perform monthly refreshes. This is the common practice of scheduling eligibility checks for all or a subset of patients so you can proactively contact them when they lose or change coverage.

This post explains how you can optimize monthly eligibility refresh checks and avoid common pitfalls like payer throttling.

How to structure refresh checks

We recommend the following to get the most useful data from payers:

  • Dates of service: Only include the current month. Patients can gain or lose eligibility in the middle of a month, but eligibility usually starts and ends on month boundaries.

  • Service type codes: Dental providers should send a single service type code of 35 (Dental Care), and medical providers should send 30 (Health Benefit Plan Coverage), which returns eligibility for all coverage types included in the subscriber’s plan. Some payers may not support other values. Don’t include multiple service type codes because many payers will either return an error or ignore additional service type codes entirely.

Here’s an example of a refresh check for our Eligibility Check API:

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "Cigna",
  "encounter": {
    "beginningDateOfService": "20240601",
    "endDateOfService": "20240630",
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1234567891"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "gender": "F",
    "memberId": "123456789"
  }
}'

When to schedule refresh checks

Wait until at least 3:00 AM local time on the first day of the month to begin monthly eligibility refresh checks. This approach helps you avoid delays from payer system updates and time zone differences. It also avoids the large volume of requests that occur around midnight, when many organizations schedule their refresh checks to begin.

You should also perform refresh checks outside your business hours. You don’t want to run into issues with payer throttling or your API’s concurrency limits while processing time-sensitive eligibility requests from customers.

Avoid payer throttling

Payers may throttle high volumes of requests for the same provider at once because they expect to receive requests at a “human scale”, such as a member entering insurance information into a website or presenting their insurance card at a doctor’s office. 

When payers are throttling you, they typically send back payer.aaaErrors.code = “42”, which indicates that the payer is having issues. (Our docs have a complete list of payer error codes for eligibility checks and possible resolution steps that you can use for debugging.)

To avoid throttling during monthly refreshes, we recommend:

  • Only send requests with the same provider NPI ID 1-2 times every 15 seconds. 

  • Use an exponential backoff algorithm to wait progressively longer periods before sending another request to the same payer. This approach differs from “live” eligibility checks where we recommend immediate retries because a patient might be waiting on the results.

  • Wait a few hours to retry if the first day of the month falls on a weekend and all requests to a particular payer are failing. Some payers have scheduled downtime, usually on weekends.

  • Interleave requests to multiple payers in a round-robin style. For example, instead of sending all requests to payer #1 and then all requests to payer #2, send one request to payer #1, then one request to payer #2, and then go back to payer #1.

Minimize waste

Don’t perform more checks than you need to. We recommend: 

  • Periodically purge or archive records for inactive patients. It’s a waste to perform eligibility checks on patients who have died or who haven’t scheduled an encounter for several years.

  • Remove or deactivate patients that are no longer eligible. The payer indicates ineligibility by setting benefitsInformation.code = “6” (Inactive) in the response.

When to follow up 

Flag responses that have benefitsInformation.code = “5”, which stands for Active - Pending investigation, or a benefitsDateInformation.premiumPaidToDateEnd before the current date. Some payers may still show active coverage while the subscriber is behind on premium payments. 

You may want to conduct additional checks on these patients, as they are at a higher risk of losing coverage soon.

Start processing eligibility checks today

Monthly eligibility refresh checks can help improve the care experience for both patients and providers. With Stedi’s API-first clearinghouse, you can automate business flows like eligibility checks and claims processing using APIs that support thousands of payers

Contact us to learn more and speak to the team.

May 6, 2024

Products

Modern companies want to integrate using modern tools. Until now, there hasn’t been a modern, developer-friendly clearinghouse to serve the growing healthcare technology ecosystem. 

That’s why we built Stedi – the only API-first healthcare clearinghouse that provides companies with the developer experience, security, and reliability they’ve come to expect.

Developer-friendly APIs

With Stedi, developers can automate business flows like eligibility checks and claims processing using APIs that support thousands of payers. Here is a list of the APIs available today, with more in the works: 

These developer-friendly APIs enable growing health tech companies, digital healthcare providers, and mature, technology-forward enterprises to build critical services that enable providers to determine what services are covered, create cost estimates, and automate revenue cycle management. 

Under the hood, Stedi transforms JSON API requests into HIPAA-compliant X12 EDI and sends those transactions to payers. Stedi then automatically processes payer responses – benefits information, claim statuses, and electronic remittance advice transactions (ERAs) – and returns them in an approachable JSON format. 

Each API has built-in validation and repair – commonly referred to as “edits” – to help reduce payer rejections that can cause delays and hours of manual labor to fix. We continue to expand our capabilities to address common issues, including data validation, formatting, code usage, payer-specific requirements, and more. 

Security-first, multi-region architecture with redundant connectivity

Security and reliability have always been job zero at Stedi.  

Our clearinghouse is built 100% on AWS infrastructure, with multi-region ‘active-active’ APIs and a growing set of payers covered by either redundant clearinghouse connectivity or direct-to-payer integrations. When possible, Stedi will dynamically route traffic to the most reliable connection, eliminating single points of failure.

Beyond maintaining SOC 2 Type II compliance and HIPAA eligibility, Stedi healthcare accounts are secured by mandatory multi-factor Authentication (MFA) and have role-based access control (RBAC) powered by Amazon Verified Permissions

You can learn more about our approach to security and compliance at trust.stedi.com.

Lightning-fast onboarding and support

“We submitted claims three days after signing, and the support is excellent in quality and timeliness of response. Stedi's team regularly resolves our engineers’ issues within minutes. This used to take months or just never happen at all with our previous vendor.” 

- Craig Micon, Head of Product at Pair Team

Our customer success engineers provide hands-on support to get you into production as quickly as possible. Within an hour of signing up, we provide you with a dedicated Slack channel, a Stedi account, and an API key so you can immediately start processing transactions. After you’re in production, we will keep the Slack channel open for communication between you and our support, engineering, and product teams.

We also prioritize comprehensive documentation, including step-by-step guides for claims and eligibility, reference implementations, and helpful API error messages. You can also download our public OpenAPI spec and access a user-friendly version of every X12 HIPAA transaction from our online reference site. 

Better UX 

The Stedi web application supports real-time manual eligibility checks that help developers test new payer integrations and help operations teams easily understand coverage for a specific member. You can search and filter responses for specific benefits (like copays and deductibles), coverage types (individual vs. family), coverage dates, and more.

Get started with Stedi

Stedi makes healthcare transactions more secure, efficient, and reliable for developers across the health tech industry. Contact us to learn more and speak to the team.

Apr 9, 2024

Engineering

It’s not AWS.

There’s no way it’s AWS.

It was AWS.



We use AWS IAM extensively throughout our codebase. Last year, we extended our use of IAM to build and enforce role-based access control (RBAC) for our customers using AWS Security Token Service (STS), an IAM service you can use to provide temporary access to AWS resources. Along the way, we discovered a vulnerability in STS that caused role trust policy statements to be evaluated incorrectly. 

Yes, you read that right – during the development process, we found an edge case that would have allowed certain users to gain unauthorized access to their AWS accounts.

We caught it before rolling out our RBAC product, and AWS has since corrected the issue and notified all affected users, so you don’t need to hit the panic button. However, we wanted to share how we discovered this vulnerability, our disclosure process with AWS, and what we learned from the experience.

How Stedi uses IAM and STS

To understand how we found the bug, you need to know a bit about Stedi’s architecture.

Behind the scenes, we assign a dedicated AWS account per tenant – that is, each customer account in the Stedi platform is attached to its own separate AWS account that contains Stedi resources, such as transactions and trading partner configurations. Our customers usually aren’t even aware that the underlying AWS resources exist or that they have a dedicated AWS account assigned to them, but using a dedicated AWS account as the tenancy boundary helps ensure data isolation (which is important for regulated industries like healthcare) and also eliminates potential noisy neighbor problems (which is important for high-volume customers).

When a customer takes an action in their account, it triggers a call to a resource using a Stedi API, or by calling the underlying AWS resource directly. One example is filtering processed transactions on the Stedi dashboard – when a customer applies a filter, the browser makes a direct request to an AWS database that contains the customer’s transaction data. This approach significantly reduces the code we need to write and maintain (since we don’t need to rebuild existing AWS APIs) and allows us to focus on shipping features and fixes faster.

To facilitate these requests, Stedi uses AWS STS to provide temporary access to AWS IAM policies, allowing the user’s browser session to access their corresponding AWS account. Specifically, we use the STS AssumeRoleWithWebIdentity operation, which allows federated users to temporarily assume an IAM role in their AWS account with a specific set of permissions.

IAM tags

Our IAM role trust policies use tags to control who can view and interact with resources. 

A tag is a custom attribute label (a key:value pair) you can add to an AWS resource. There are three tag types you can use to control access in IAM policies

  • Request: A tag added to a resource during an operation. You can use the aws:RequestTag/key-name condition key to specify what tags can be added, changed, or removed from an IAM user or role. 

  • Resource: An existing tag on an AWS resource, such as a tag describing a resource’s environment (“environment: production”). You can use the aws:ResourceTag/key-name condition key to specify which tag key-value pair must be attached to the resource to perform an operation.

  • Principal tag: A tag on a user or role performing an operation. You can use the aws:PrincipalTag/key-name condition key to specify what tags must be attached to the user or role before the operation is allowed.

Assuming roles

Here’s how we set up RBAC for Stedi accounts. 

We give Stedi users a JSON Web Token (JWT) containing the following AWS-specific principal tags:

"https://aws.amazon.com/tags": {
    "principal_tags": {
      "StediAccountId": [
        "39b2f40d-dc59-4j0c-a5e9-37df5d1e6417"
      ],
      "MemberRole": [
        "stedi:readonly"
      ]
    }
  }

The user can assume a role (specifically, they’re granted a time-bound role session) in their assigned AWS account if the following conditions are true:

  1. The token is issued by the referenced federation service and has the appropriate audience set.

  2. The role has a trust relationship granting access to the specified StediAccountId.

  3. The role has a trust relationship granting access to the specified MemberRole

The following snippet from our role trust policy evaluates these requirements in the Condition object. For example, we check whether the StediAccountId tag in the JWT token is equal to the MappedStediAccountId tag on the AWS account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
      "Principal": {
        "Federated": {
          "Ref": "ProdOidcProvider"
        }
      },
      "Condition": {
        "StringEquals": {
          "tokens.saas.stedi.com/v1:aud": "tenants",
          "aws:RequestTag/StediAccountId": "${iam:ResourceTag/MappedStediAccountId}",
          "aws:RequestTag/MemberRole": "${iam:ResourceTag/MemberRole}"
        }
      }
    }
  ]
}

Caption: If the IAM role's resource tag for MappedStediAccountId and MemberRole matches the StediAccountId and MemberRole request tag (the JWT token principal tag), the user can access this role. Otherwise, role access is denied.

When assuming a role from a JWT token (or with SAML), STS reads the token claims under the principal_tags object and adds them to the role session as principal tags. 

However, during the AssumeRoleWithWebIdentity operation (within the policy logic), you must reference the principal tags from the JWT token as request tags because the IAM principal isn’t the one making the request, instead the tags are being added to a resource. Existing tags on the role are referenced as resource tags because they are tags on the subject of the operation.

These naming conventions are a bit confusing – more on that later.

Discovering the vulnerability

We set up our role trust policy based on this AWS tutorial, using JWT tokens instead of SAML. Another difference from the tutorial is that our policy uses variables to reference tags instead of hardcoding the values into the condition statements.

For example, "${aws:RequestTag/StediAccountId}": "${iam:ResourceTag/MappedStediAccountId}" instead of "${aws:RequestTag/StediAccountId}": 39b2f40d-dc59-4j0c-a5e9-37df5d1e6417".

During development, we began testing to determine whether our fine-grained access controls were working as expected. They were not. 

Finding the bug

Again and again, our tests gained access to roles above their designated authorization level.

We scoured the documentation to find the source of the error. The different tag types, IAM statement templating, and different (aws vs. iam) prefixes caused extra confusion, and we kept thinking we weren’t reading the instructions correctly. We attempted to use the IAM policy simulator but found it lacked support for evaluating role trust policies.

Eventually, we resorted to systematically experimenting with dozens of configuration changes. For every update, we had to wait minutes for our changes to propagate due to the eventual consistency of IAM. Four team members worked for several hours until we finally made a surprising discovery – the tag variable names affected whether trust policy conditions were evaluated correctly.

If the request tag referenced a principal tag called MemberRole in the JWT token, and the IAM role referenced a resource tag with the same variable name, the condition was always evaluated as true, regardless of whether the tag's values actually matched. This is how test users with stedi:readonly permissions in Stedi gained unauthorized admin access to their AWS accounts. 

Changing one of the tag variable names appeared to fix the issue. For example, the snippet below changes the resource tag variable name to MemberRole2. The policy only functioned properly when the variable names for the request and resource tags were different. 

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
      "Principal": {
        "Federated": {
          "Ref": "ProdOidcProvider"
        }
      },
      "Condition": {
        "StringEquals": {
          "tokens.saas.stedi.com/v1:aud": "tenants",
          "aws:RequestTag/StediAccountId": "${iam:ResourceTag/MappedStediAccountId}",
          "aws:RequestTag/MemberRole": "${iam:ResourceTag/MemberRole2}"
        }
      }
    }
  ]
}

Caption: Initial IAM vulnerability workaround – ensuring request tag and resource tag names did not match.

Alerting AWS

We used the documentation to construct a model of the role assumption process and contacted AWS Support and AWS Security on June 20, 2023 with our findings. We also contacted Chris Munns, Tech Lead for the AWS Startups team, who engaged directly with AWS Security and escalated the issue internally.

AWS was initially skeptical that the problem was with STS/IAM, which is understandable – we were too. They first suggested that we used the wrong prefixes in our condition statements (aws vs. iam), but we confirmed the issue occurred with both prefixes. Then, they suggested that the tag types in our condition statements were incorrect. After some back and forth, we ruled that out as well, once again noting that the tag naming conventions for the AssumeRoleWithWebIdentity operation are confusing.

In the following days, we investigated the issue further and found we could trigger the bug with STS AssumeRole calls, meaning the vulnerability was not limited to assuming roles with web identity or SAML. We also found that hard-coding one of the tag values in the policy statement did not expose the vulnerability. Only role trust policies that used a variable substitution for both the request tag and the resource tag in the policy statement resulted in the policy evaluating incorrectly.

We implemented a workaround (changing one of the variable names), confirmed our tests passed, and kept building.

Resolution

On July 6th, we received an email from AWS stating that their engineering team had reproduced the bug and was working on a fix. On October 30th, STS AssumeRole operations for all new IAM roles used an updated tag handling implementation, which provided the corrected tag input values into the logic to fix the role evaluation issue. This same change was then deployed for existing roles on January 9, 2024. AWS typically rolls out changes in this manner to avoid unexpectedly breaking customer workflows.

AWS also discovered the issue was not limited to role trust policies, which are just resource policies for IAM roles (as a resource) – it also extended to statements within IAM boundary policies and SCP policies that contained the same pattern of STS role assumption with tag-based conditions.

AWS notified customers with existing problematic roles, SCP trust policies, and boundary policies that had usage in the past 30 days. They also displayed a list of affected resources in each customer’s AWS Health Dashboard.

Timeline

  • 2023-06-20 - Role access issue discovered, AWS alerted

  • 2023-06-21 - Minimal reproduction steps provided using STS assume role, AWS acknowledges report and the issue is picked up by an engineer

  • 2023-07-06 - AWS acknowledges issue and determines root cause

  • 2023-10-30 - STS tag handling implementation updated for new IAM roles

  • 2024-01-09 - STS tag handling implementation updated for IAM roles for customers impacted in a 30-day window

What we learned

After we implemented our workaround, we conducted a retrospective. Here are our key takeaways: 

Even the most established software has bugs. 

This might seem obvious, but we think it’s an important reminder. We spent a lot of time second-guessing ourselves when discovering and diagnosing this bug. We were well aware of IAM’s provable security via automated reasoning, and the documentation is so comprehensive (and intimidating at times) that we were sure it had to be our fault. Of course, you should do your due diligence before reporting issues, but no system is infallible. Sometimes, it is AWS.

Glossaries and indexes are underrated. 

Defining service-specific terminology in a single location can be game-changing for users onboarding to a new product and can dramatically speed up the debugging process. 

We struggled to understand the difference between global condition keys with the  “aws:” namespace and service-specific keys with the  “iam:” namespace. We were further confused by how these keys can overlap; the “iam:ResourceTag” and “aws:ResourceTag” resolve to the same value. Finally, it was hard to keep track of the lifecycle from a jwt principal tag becoming a request tag before finally being a resource tag. 

The AWS documentation provides all this information, but we lacked the proper vocabulary to search for it. A comprehensive glossary would have saved us significant time and effort. We’re now adding one to the Stedi docs to better serve our own users.

We need better tools for testing IAM policies. 

The IAM policy simulator does not support role trust policy evaluation. Proving the security of a system to grant federated identities access to IAM roles continues to rely on both positive and negative end-to-end tests with long test cycles. Developing more mature tooling would massively improve the developer experience, and we hope AWS will consider investing in this area moving forward. 



Thank you to all the Stedi team members who contributed to uncovering this issue and the AWS team for working with us to find a solution.

Apr 4, 2024

Engineering

Editor’s note: Every company has a way of working. I wrote this document several years ago when we were still figuring out our way of working – “how we do things here” – and were spending a lot of time aligning on decisions of all sizes. We talked about it a lot at the time, but we rarely have to reference it now – as Alfred North Whitehead said, "Civilization advances by extending the number of important operations which we can perform without thinking of them."

The most difficult part of hiring for any company is finding people who are philosophically aligned with a certain way of working. We are hiring across multiple engineering, product, and design roles right now, so we wanted to post this publicly to give a sense of what it’s like to work here. If this resonates with you, we would love to hear from you. – Zack Kanter

——

This document captures the cornerstones of Stedi’s engineering culture. As a result of following the framework, product development may sometimes come to a screeching halt. The work itself may be tedious and frustrating. Progress may take many multiples of the time it would take using other methods.

This impact is short term, immaterial, and ultimately irrelevant. The framework aligns with our long-term ambitions; nothing is more important than keeping a high bar across these dimensions.

That's the difference between principles and preferences: principles are what you do regardless of the cost – everything else is just a preference.

Key tenets

  • Minimize long-term operational toil.

  • Eliminate waste.

Practices

The following practices represent the bare minimum expectation of all teams:

  • Security is job zero.

    • There is nothing more important than securing our systems.

  • Prioritize on-call.

    • Implement thorough, pragmatic alerting and alarming.

    • The baseline for alerts and alarms is expected to be zero.

    • Prioritize work to mitigate the causes of pages above all else except for security.

  • Define all infrastructure as code.

    • Define all production infrastructure as code, from build and deployment pipelines to operational dashboards.

    • Automate dependency updates and deployments to production.

    • Keep all dependencies up to date across all non-archived repositories.

    • Dependencies can be updated individually or in batches, in real-time or subject to some delay (for security or convenience), but the process must be automated.

  • Investigate service metrics, build times, and infrastructure costs.

    • Review service metrics, build times, and infrastructure costs (both per-request and total spend).

    • The goal is not to strictly minimize these metrics via unbounded investment that has diminishing returns, but rather to surface flaws in configuration or implementation that are causing unnecessary waste.

  • Pull pain forward.

    • When a service’s future is assured (that is, when it reaches or will soon reach GA), pull forward technical problems that get harder over time (e.g., backwards-incompatible changes; major architectural shortcomings).

Practices for practices

‘Best effort’ is ineffective – if ‘best effort’ were all it took, everything would be in place already!

Instead, implement mechanisms for continuous improvement – generally: preventative controls and/or detective controls wherever possible, with a root cause analysis process for any exceptions.

Component selection

In the fight against entropy, we use the following prioritization framework for component types:

  1. Managed, ‘serverless’ primitives.

    • Use managed, serverless (defined as ‘usage-based pricing with little to no capacity planning required’) primitives wherever possible, even at the cost of functionality.

    • Within those managed service options, use the highest-order primitive that fits a given use case.

  2. Open source libraries and open standards.

    • Where a suitable managed service cannot be found, use open source libraries and adopt open standards wherever possible.

  3. Our own code and formats.

    • When all other options have been exhausted, build it ourselves.

Selection framework for component sources

  • Optimize for a cohesive set of components rather than selecting best-in-breed options, even when more compelling or popular alternatives exist.

  • When choosing a set of components, invest in continuously-compounding ecosystems built by high-velocity organizations who are philosophically aligned with Stedi. Current ecosystems include AWS (infrastructure) and GitHub (source control, build, and deployment).

  • Introduce new ecosystems at a clear boundary (e.g., GitHub for source control, build, and deployment of code; AWS for running the code), rather than in the middle of a system. For example, we would not use a database hosted at PlanetScale in an otherwise all-AWS backend.

Refactors

We often identify more suitable ways of building something (that is, more suitable according to the framework listed above) after we’ve begun, or after we’ve finished. For example, we might start with writing our own code, only to later realize that what we’ve written is similar to some open source library instead.

When the refactor:

  • will get easier over time, we’ll wait until it gets easier.

  • will get harder over time, we'll do it without delay.

Generally, the framework when considering a refactor is to ask: if we were starting again today knowing everything we know now, would we build it this new way that we’re considering? If yes, will it get easier or harder over time? If you’d build it the new way and it will get harder over time, do it now.

That said, large-scale lateral migrations (lifting-and-shifting that results in a functionally-equivalent customer experience) are extremely costly. We try to avoid these when possible.

Communication

Discussing tradeoffs

Like all approaches to building software, Stedi’s approach comes with many tradeoffs – including, but certainly not limited to:

  • Managed services, open source libraries, and open standards often have steep learning curves and poor documentation; they are often inflexible and lacking functionality.

  • Managed services are harder to test, slower to iterate against, and harder to diagnose; they are expensive and have unpredictable roadmaps.

  • Maintaining infrastructure as code is tedious and painful.

  • Automated dependencies updates are distracting and error-prone.

These same tradeoffs will show up again and again; enumerating them at each juncture is distracting and demoralizing. Instead, focus discussions on mitigation. For example:

  • “Given that this managed service will be hard to unit test. Let’s come up with a plan for how we can ship with confidence.”

  • “Since the cloud deployment feedback loop is slower than local development, we should invest in tooling to speed this up.”

  • “The documentation in this AWS library is sparse and outdated, so let’s make sure we contribute back early and often before we lose the context.”

  • “This AWS service doesn’t support this feature we’ll want down the line, so let’s schedule a meeting with the AWS PM to see if it’s on their roadmap before building it ourselves.”

Discussing roadblocks

Technology evolves rapidly. All features and functionality we use today did not exist at one point; features and functionality that don’t exist today might exist tomorrow. Most importantly, features and functionality you think don’t exist today might already exist.

When hitting a roadblock or an apparent dead end – for example, when it appears that a certain managed service or library isn’t able to do something – draw a distinction between when you’ve definitively determined that something is not supported, vs. when you’ve exhausted ideas that you’ve come up with but have not definitively proven it isn’t possible.

In other words: ‘Absence of evidence’ is not ‘evidence of absence.’ False determinations of impossibilities are extremely costly to us, particularly because the false determination in one corner of Stedi spreads to all of Stedi.

Something is definitive when you can provide a link to source documentation confirming that it isn’t possible.

  • Acceptable: “A Lambda can’t run for more than 15 minutes – see documentation: Function timeout – 900 seconds (15 minutes).”

  • Unacceptable: “X-ray doesn’t support cross-account traces [no citation].”

When you have tried a number of things but haven’t been able to make it work, that is not definitive.

  • Acceptable: “I haven’t been able to lock down this AWS resource via a tenant-scoped role. Here’s what I’ve tried…”

  • Unacceptable: “Tenant scoped IAM access won’t work for this.”

If you have tried a number of things and haven’t been able to make something work, post somewhere: “I haven’t been able to make this work, and I’m running out of ideas/time. Here’s what I’ve tried…” The fastest way to lose credibility here is to falsely and authoritatively proclaim that something isn’t possible without having done the work to back it up.

On the flip side, if you see something pronounced as impossible without the supporting documentation, ask for the documentation. If you see this happening and fail to ask for the work to back it up, you have lowered our bar for technical rigor.

Written communication

Our standard of “Write important things down” doesn’t mean “record actions already taken.”

The most important function of writing is as a tool for thinking. It follows that writing should almost always precede action, particularly in software development.

Paul Graham explained this nicely in an essay that I’ve pulled passages from below:

“Writing about something, even something you know well, usually shows you that you didn't know it as well as you thought. Putting ideas into words is a severe test.

Once you publish something, the convention is that whatever you wrote was what you thought before you wrote it. These were your ideas, and now you've expressed them. But you know this isn't true.

It's not just having to commit your ideas to specific words that makes writing so exacting. The real test is reading what you've written. You have to pretend to be a neutral reader who knows nothing of what's in your head, only what you wrote.

There may exist people whose thoughts are so perfectly formed that they just flow straight into words. But I've never known anyone who could do this, and if I met someone who said they could, it would seem evidence of their limitations rather than their ability. Indeed, this is a trope in movies: the guy who claims to have a plan for doing some difficult thing, and who when questioned further, taps his head and says "It's all up here." Everyone watching the movie knows what that means. At best the plan is vague and incomplete. Very likely there's some undiscovered flaw that invalidates it completely.

In precisely defined domains it's possible to form complete ideas in your head. People can play chess in their heads, for example. And mathematicians can do some amount of math in their heads, though they don't seem to feel sure of a proof over a certain length till they write it down. But this only seems possible with ideas you can express in a formal language.

The reason I've spent so long establishing this rather obvious point is that it leads to another that many people will find shocking. If writing down your ideas always makes them more precise and more complete, then no one who hasn't written about a topic has fully formed ideas about it. And someone who never writes has no fully formed ideas about anything nontrivial.

It feels to them as if they do, especially if they're not in the habit of critically examining their own thinking. Ideas can feel complete. It's only when you try to put them into words that you discover they're not. So if you never subject your ideas to that test, you'll not only never have fully formed ideas, but also never realize it.

Putting ideas into words is certainly no guarantee that they'll be right. Far from it. But though it's not a sufficient condition, it is a necessary one.”

Writing a doc is not a perfunctory gesture, and asking someone for a doc on something is not a punishment or a mechanism for control. Writing a doc is a way of:

  • surfacing requirements and assumptions, and

  • driving clarity of reasoning stemming from those requirements and assumptions.

Without this step, our software has little hope of delivering the results we want over the long term.

Note that a doc doesn’t necessarily have to take the form of prose – in some cases, the right format for a doc could be a proposed API spec with bullet point lists of constraints, principles, or requirements. The goal of a doc is to reify your thinking and to share it with others.

As a final thought, not everyone has to write docs here. Some people just want to execute, and there is plenty of room for that, too – but if you just want to execute, you’ll be executing on the plan, architecture, or implementation described in someone else’s doc. Our domain is too complex, and our ambitions are too large to build software willy-nilly.

Mar 3, 2024

Products

The prolonged Change Healthcare outage due to a cyberattack has left thousands of healthcare providers unable to submit claims, eligibility checks, and other critical transactions to payers. The outage is expected to continue for weeks. Companies have been scrambling to implement solutions to get back online but are faced with large development efforts to switch to bespoke API formats offered by other clearinghouses.

Stedi's drop-in replacement for Change Healthcare's Professional Claims V3 and Eligibility V3 APIs allow customers who have been using Change to directly switch with minimal development effort. Customers can use the same Change API JSON or X12 request format with Stedi’s APIs, and Stedi will automatically submit transactions to payers and other clearinghouses as necessary. Our APIs are Generally Available today and we are able to get customers live on a same-day turnaround.

Our primary goal is to help providers submit claims and eligibility checks as quickly as possible. We’ve created a streamlined contracting process along with a standardized price list and the ability to match volume pricing previously provided by Change. We’ve set up a dedicated email address for inquiries – change@stedi.com – and can establish a dedicated Slack channel to start working with engineering teams in under an hour from first contact.

Stedi’s Claims and Eligibility APIs

Our APIs are designed as drop-in replacements for Change Healthcare’s Professional Claims V3 and Eligibility V3 APIs. The APIs:

  • Accept Change Healthcare's JSON request format.

  • Translate it to X12 837P for claims and X12 270 for eligibility.

  • Submit the transactions to payers and clearinghouses using SFTP and real-time EDI.

  • Return the Claims and Eligibility API response in Change Healthcare's JSON format.

  • Have full support for returning 277/835 files as JSON-formatted webhooks to endpoints of your choosing (to replace Change’s Claims Responses and Reports V2 API functionality), as well as 999s and other transaction types.

Visit our API documentation for details on API endpoints available as well as example request and response payloads.

Security-first architecture

Stedi is SOC 2 Type II and HIPAA compliant. We view these certifications as a minimum floor and have built security into our platform from the ground up:

  • Stedi’s Healthcare APIs are built 100% on AWS infrastructure with no other external vendor dependencies.

  • Customer resources are stored in dedicated single-tenant AWS accounts for strict tenancy isolation, with one customer per AWS account.

  • Stedi HIPAA accounts are secured by mandatory multi-factor Authentication (MFA) and have role-based access control (RBAC) powered by Amazon Verified Permissions.

You can see more in our Trust Center.

Get in touch

To get into our priority queue for an immediate response, visit stedi.com/healthcare to submit a contact request or email us at change@stedi.com. We can start working with you immediately to get back online.

Jan 15, 2024

Products

Today, we’re excited to showcase the Stedi EDI platform, a modern, developer-friendly EDI system that includes everything you need to build end-to-end integrations with your trading partners.

No-code EDI

The Stedi EDI platform includes all of the turn-key functionality you need to send and receive EDI. Within minutes, you can configure a new trading partnership, import EDI specifications from Stedi’s extensive Network of pre-built partner integrations, enable SFTP/FTPS/AS2 connectivity, ingest EDI files, and post JSON transactions to any API endpoint – all without writing a single line of code.

Once configured, Stedi turns EDI into just another API integration. As your trading partner sends you EDI, Stedi outputs JSON webhooks that you can ingest into your system. For outbound transactions, a single API endpoint enables you to generate and deliver fully-formed EDI files back to your trading partner.

All of Stedi’s functionality is available instantly via self-service, but you don’t need to do it all yourself. We can help you configure a new trading partner integration end-to-end on a free 30-minute demo call – and then support you all the way to production and beyond.

World-class support

Over the past 6 years of working with customers via Slack, Zoom, Teams, email, text, phone, forums, and more, we have built a lightning-fast, exceptionally knowledgeable support team. Every day, they work with customers to solve onboarding and operational problems ranging from troubleshooting EDI errors and meeting with key trading partners to helping extend Stedi for custom integration needs.

This world-class premium support is included at no additional cost.

Get started with Stedi

The Stedi EDI platform provides everything you need to build end-to-end EDI integrations in days. There's no faster way to get started with EDI.

To get started, book a demo with our team.

Nov 30, 2023

EDI

So, you’ve been tasked with “figuring out EDI.” Maybe you’re in the supply chain or logistics world, or maybe you’re building a product in the healthcare or insurance space – chances are that you’re reading this because one of your large customers, vendors, or partners said that if you want to move the relationship forward, you have to do something called EDI: Electronic Data Interchange.

Maybe they sent you a sample file that looks like it came off a dot matrix printer in 1985, or maybe they sent you a PDF “mapping guide” that doesn’t seem to make much sense.

Regardless of what you’re starting with, this post will give you, a modern software developer, a practical crash course in EDI. It will also explain how to build an EDI integration on Stedi that transforms EDI to and from JSON.

  • The two most common questions about EDI

  • Overview of an EDI integration

  • Step 1: Add your trading partner to Stedi

  • Step 2: Transform JSON from Stedi to your API shape

  • Step 3: Configure a webhook to send transactions to your API

The two most common questions about EDI

This section provides some context. If you want to just start building, skip to Add your trading parter to Stedi.

Raw EDI next to a page from an EDI mapping guide

What is EDI?

You may have heard a lot of jargon or acronyms when researching EDI – things like AS2, VANs, or MFT. Put simply, EDI is a way to get data from one business system into another.

Your customer, vendor, or partner wants to exchange data with you, and they need you to work with a very specific file format. The process of exchanging that specific file format is called EDI. The file itself is called an EDI file or EDI document. Each EDI file can contain one or more transaction types - called transaction sets – each with a unique numeric code and name to identify it. In the EDI world, your customer, vendor, or partner is called a trading partner. This post will use all these terms going forward.

The subject of EDI typically comes up because a company like yours wants to achieve some business goal with your trading partner. Those business goals vary widely, but they all require either sending data to a trading partner, receiving data from a trading partner, or both.

Here are a few common examples. EDI spans many industries, so if you don’t see your use case listed, it’s not because it isn’t supported – it’s just because we can’t list out every possible flow here.

Logistics

  • 204 Request pickup of a shipment (load tender)

  • 990 Accept or reject the shipment (response to a load tender)

  • 214 Shipment status update

  • 210 Invoicing details for a shipment

Warehousing

  • 940 Request shipping from a warehouse

  • 945 Communicate fulfillment to a seller

  • 944 Indicate receipt of a stock transfer shipment

  • 943 Stock transfer shipment notification

Retail

  • 850 Send a purchase order to a retailer

  • 855 Send a purchase order acknowledgment back to a customer

  • 856 Send a ship notice to a retailer

  • 810 Send an invoice

Healthcare

  • 834 Send benefits enrollments to an insurance provider

  • 277 Indicate the status of a healthcare claim

  • 276 Request the status of a healthcare claim

  • 278 Communicate patient health information

  • 835 Make a payment and/or send an Explanation of Benefits (EOB) remittance advice

If it’s just data exchange, can’t we use an API instead?

The short answer is no.

Exactly why your trading partner wants to exchange files instead of integrating with an API is a much longer story (and is out of the scope of this blog post), but EDI has been around since the 1960s and even hypermodern companies like Amazon use it as their primary method for trading partner integration. If you plan to wait around for your trading partner to roll out an API, you’ll likely be waiting a long time.

Overview of an EDI integration

To achieve some business goal, your trading partner wants to either send EDI files to you, receive EDI files from you, or both. But what is it that you are trying to accomplish? In all likelihood, you’re trying to get data into or out of your system.

For the purposes of simplifying this post, we’re going to assume two things: first, that your business system has an API, and second, that the API can accept JSON as a format. We’re also going to focus only on receiving EDI files from your trading partner – getting data into your system – because otherwise this post would get too long.

When you build an EDI integration on Stedi, the end-to-end flow for an inbound file (from your trading partner) will look something like this:

End-to-end inbound EDI integration on the Stedi platform.
  1. Receive an EDI file through a configured connection (SFTP/FTPS or AS2).

  2. Translate the file to JSON according to your partner’s EDI requirements.

  3. Use a Destination webhook to automatically send the JSON to your API. The webhook may be configured to use a Stedi mapping to transform the JSON to a custom shape before sending. You can also just receive raw JSON from Stedi and use other methods to transform it into the shape you need for your API (more on that in step 2).

Step 1: Add your trading partner to Stedi

Adding a trading partner configuration to the Stedi platform takes about 10-15 minutes and doesn’t require any code.

End-to-end inbound EDI integration on the Stedi platform.

Here’s a video of adding a trading partner that demonstrates each of the following steps in detail.

  • Create a partnership: A partnership defines the EDI relationship between you and your trading partner. Within the partnership, you’ll define a connection to exchange files, specify the EDI transactions you plan to send and receive, and configure other settings, such as whether to send automatic acknowledgments.

  • Configure a connection: Set up a connection to exchange files with your trading partner. Stedi supports SFTP/FTPS, remote SFTP/FTPS, and AS2.

  • Add transaction settings: Create a transaction setting for each EDI transaction you plan to send or receive. For example, an inbound transaction setting to receive 850 Purchase Orders and an outbound transaction setting to send 855 Purchase Order Acknowledgments. This process involves specifying a Stedi guide for the transaction type. Stedi guides are a machine-readable format for trading partner EDI specifications, and Stedi uses them to validate data according to your partner’s requirements.

Stedi automatically translates the EDI files your trading partner sends over the connection into JSON. The next step is to map the JSON fields in Stedi’s output to the JSON shape you need for your API.

Step 2: Transform JSON from Stedi to your API shape

There are three ways you can transform Stedi transaction data (JSON) into a custom format: Stedi Mappings, writing custom code, and using an iPaaS platform. This post demonstrates the Stedi Mappings method, and you can check out our docs for more details about the alternatives.

Guide JSON: Stedi’s JSON EDI format

Stedi converts EDI files into a human-readable JSON format called Guide JSON.

The following EDI example is is an Amazon 850 Purchase Order that follows Amazon’s specific requirements for constructing a Purchase Order transaction (each company has their own requirements for each transaction type). It’s formatted according to the X12 Standard, which is the most popular standard in North America.

You can play around with it by opening the example file in our Inspector tool. On the right, Stedi shows what each data element in the EDI transaction means. Open in Inspector →

ISA*00*          *00*          *ZZ*AMAZON         *ZZ*VENDOR         *221110*0201*U*00401*000012911*0*P*>~
GS*PO*AMAZON*VENDOR*20221110*0201*12911*X*004010~
ST*850*0001~
BEG*00*NE*T82Z63Y5**20221110~
REF*CR*AMZN_VENDORCODE~
FOB*CC~
CSH*N~
DTM*064*20221203~
DTM*063*20221209~
N1*ST**92*RNO1~
PO1*1*8*EA*39*PE*UP*028877454078~
PO1*2*6*EA*40*PE*UP*028877454077~
CTT*2*14~
SE*12*0001~
GE*1*12911~
IEA*1*000012911

The following example shows the Guide JSON shape for the translated Amazon 850 Purchase Order.

{
  "heading": {
    "transaction_set_header_ST": {
      "transaction_set_identifier_code_01": "850",
      "transaction_set_control_number_02": 1
    },
    "beginning_segment_for_purchase_order_BEG": {
      "transaction_set_purpose_code_01": "00",
      "purchase_order_type_code_02": "NE",
      "purchase_order_number_03": "T82Z63Y5",
      "date_05": "2022-11-10"
    },
    "reference_identification_REF_customer_reference": [
      {
        "reference_identification_qualifier_01": "CR",
        "reference_identification_02": "AMZN_VENDORCODE"
      }
    ],
    "fob_related_instructions_FOB": [
      {
        "shipment_method_of_payment_01": "CC"
      }
    ],
    "sales_requirements_CSH": [
      {
        "sales_requirement_code_01": "N"
      }
    ],
    "date_time_reference_DTM_do_not_deliver_before": [
      {
        "date_time_qualifier_01": "064",
        "date_02": "2022-12-03"
      }
    ],
    "date_time_reference_DTM_do_not_deliver_after": [
      {
        "date_time_qualifier_01": "063",
        "date_02": "2022-12-09"
      }
    ],
    "name_N1_loop": [
      {
        "name_N1": {
          "entity_identifier_code_01": "ST",
          "identification_code_qualifier_03": "92",
          "identification_code_04": "RNO1"
        }
      }
    ]
  },
  "detail": {
    "baseline_item_data_PO1_loop": [
      {
        "baseline_item_data_PO1": {
          "assigned_identification_01": "1",
          "quantity_ordered_02": 8,
          "unit_or_basis_for_measurement_code_03": "EA",
          "unit_price_04": 39,
          "basis_of_unit_price_code_05": "PE",
          "product_service_id_qualifier_06": "UP",
          "product_service_id_07": "028877454078"
        }
      },
      {
        "baseline_item_data_PO1": {
          "assigned_identification_01": "2",
          "quantity_ordered_02": 6,
          "unit_or_basis_for_measurement_code_03": "EA",
          "unit_price_04": 40,
          "basis_of_unit_price_code_05": "PE",
          "product_service_id_qualifier_06": "UP",
          "product_service_id_07": "028877454077"
        }
      }
    ]
  },
  "summary": {
    "transaction_totals_CTT_loop": [
      {
        "transaction_totals_CTT": {
          "number_of_line_items_01": 2,
          "hash_total_02": 14
        }
      }
    ],
    "transaction_set_trailer_SE": {
      "number_of_included_segments_01": 12,
      "transaction_set_control_number_02": 1
    }
  }
}

Let’s compare a smaller snippet of the Guide JSON to the original EDI file.

Stedi turns this EDI line:

BEG*00*NE*T82Z63Y5**20221110

into the following object in Guide JSON:

"beginning_segment_for_purchase_order_BEG": {
"transaction_set_purpose_code_01": "00",
"purchase_order_type_code_02": "NE",
"purchase_order_number_03": "T82Z63Y5",
"date_05": "2022-11-10"
},

Notice how Guide JSON makes it easier to understand each data element in the transaction.

Create a Stedi Mapping

Now that you understand Guide JSON, you can map fields from Stedi transactions into a custom shape for your API. If you plan to do this outside of Stedi, you can skip to step 3.

The following example shows a JSON payload for a very simple Orders API endpoint. This happens to be an API for creating e-commerce orders, but it could just as easily be an API for anything from railroad waybills to health insurance eligibility checks.

The Orders API:

{
  "records": [
    {
      "fields": {
        "purchaseOrderNumber": "PO102388",
        "orderDate": "223-11-24",
        "lineItems": [
          {
            "sku": 123,
            "quantity": 2,
            "unitPrice": 10.0
          },
          {
            "sku": 456,
            "quantity": 2,
            "unitPrice": 12.0
          }
        ]
      }
    }
  ]
}

Assuming that each one of these fields is mandatory, you can extract this data out of the EDI files - in this case, 850 Purchase Orders - by creating a Stedi mapping.

Stedi Mappings is a powerful JSON-to-JSON transformation engine. To transform the Guide JSON from an 850 Purchase Order into the shape for the Orders API, you would create the following Stedi mapping. Note that you don’t need to map every field from the original transaction, only the fields you need for your API.

While is a very simple one-to-one mapping, Stedi Mappings supports the full power of the JSONata language, allowing you to combine fields, split text, and more. Mappings also supports lookup tables that you can use to replace fields from the original transaction with a list of static values (for example, automatically replacing a country code like USA with its full name United States).

For detailed instructions and more examples, check out the Mappings documentation.

Step 3: Configure a webhook to send transactions to your API

Once you create your mapping, you can attach it to a Destination webook to automatically transform JSON transactions from Stedi into a custom shape before automatically sending them to your API.

You can set up an event binding for transaction.processed.v2 events that triggers the webhook every time Stedi successfully processes a 850 Purchase Order.

End-to-end inbound EDI integration on the Stedi platform.

Get started on Stedi

Now you know how to take an EDI file, translate it into Guide JSON, transform it into your target API shape, and automatically send it to your API. Check out the following resources as you keep building:

  • EDI 101 for a deeper dive into EDI standards and format.

  • Stedi Network for free access to EDI guides for hundreds of popular trading partners that you can use to configure integrations.

To get started building EDI integrations on Stedi, book a demo with our team.

Nov 14, 2023

EDI

Large EDI files are common across many industries, including healthcare (such as 834 Benefit Enrollments), logistics (210 Motor Carrier Freight Details and Invoices), and retail (846 Inventory Advice). Unlike most other EDI solutions, Stedi has virtually no file size limitations and can process EDI files that are gigabytes in size.

However, large files that have been translated into JSON still pose significant challenges for downstream applications. The translation ratio of an EDI file to JSON is typically 1:10, which means that a large EDI file can easily produce a multi-gigabyte payload that is difficult for downstream applications to receive and ingest.

To solve this problem, we’re excited to introduce Fragments, a new feature in our Large File Processing module. Fragments allow you to automatically split processed transactions into smaller chunks for easier downstream ingestion.

Use fragments to split large transactions

Large files are often the result of transactions containing many repeated loops or segments. A healthcare provider may send an 834 Healthcare Benefit Enrollment file containing tens of thousands of individual members – each one in the INS segment – or a brand may send an 846 Inventory Inquiry/Advice file containing millions of SKUs – each one in the LIN segment.

With Fragments, you can use repeated segments like these to split the transaction. For example, when you enable fragments on the INS loop in an 834, Stedi emits batches of INS loops instead of a single giant JSON payload.

For each fragment batch, Stedi emits a fragment.processed.v2 event, which you can use to automatically send Destination webhooks to your API. Fragment processed events contain details about the original transaction as well as the actual fragment payload itself.

An 834 Benefit Enrollment split into fragments

Configure fragments

You can configure fragments for any transaction in minutes without writing any code. Before you begin, create a partnership in Stedi between you and your trading partner.

Set up the Stedi guide

First, you need to enable fragments in the Stedi guide for the transaction.

Stedi guides are a machine-readable format for the trading partner EDI specifications you typically receive as PDF or CSV files. The Stedi Network has pre-built guides for hundreds of popular partners that you can import into your account and use in your integrations for free.

You can enable fragments on one repeated segment within each guide. To enable fragments, open the guide in your Stedi account, click the segment, toggle Set as fragment to ON, and click Publish changes.

Enable fragments in a Stedi guide

Create a transaction setting

Next, you need to create an inbound transaction setting. Transaction settings define the EDI transactions you plan to exchange with your trading partner and the guide Stedi should use to validate data.

When creating the transaction setting, choose the guide you previously configured with fragments and then toggle Enable fragments to ON.

Create an inbound transaction setting with fragment-enabled guide

Send fragments to your API

Finally, you can automatically deliver fragments to your API. To do this, add an event binding for fragment.processed.v2 events to a Destination webhook.

Set up a destination webhook for fragment events

You can also use the Get Fragment API to manually retrieve fragments for processed transactions as needed.

  curl --request GET \
    --url https://core.us.stedi.com/2023-08-01/transactions/{transactionId}/fragments/{fragmentIndex} \
    --header 'Authorization: Key {STEDI_API_KEY}'

Get started on Stedi

Stedi allows you to seamlessly process large EDI files of virtually any size. You can also use fragments to efficiently manage large transactions and reduce the development work required to integrate with new trading partners. Check out the Fragments documentation for complete details.

To get started building EDI integrations on Stedi, book a demo with our team.

Jul 12, 2023

Products

We recently launched Stedi Core, an event-driven EDI system that does most of the heavy lifting for EDI integrations. Core can validate, parse, and generate EDI for any trading partner and provides complete visibility into your real-time transaction data.

After configuring Core, you can use Stedi Functions to create an end-to-end flow between Core and your internal systems and business applications. Functions can react to Core events to run custom code. You can transform the data shape, call out to external applications and APIs, or extend Stedi to fulfill any requirement.

We want to showcase two features you can use to automatically invoke functions in your EDI integration: event bindings and function schedules.

Event bindings

Core emits events for every file, functional group, and transaction set it processes successfully. Core also emits events for processing failures. Visit the Core events documentation for details.

You can configure event bindings on a function so the function is automatically invoked in response to specific Core events.

Create event binding in Functions UI

Example: Send inbound purchase orders to an ERP system

The following function has an event binding that listens to transaction.processed events for inbound 850 Purchase Order transactions from a specific trading partner. When Core successfully processes an 850 transaction, it fires an event. The event binding invokes this function when the filter criteria are met.

The function uses a Stedi mapping to transform the translated JSON output from Core to the JSON shape required by the ERP system. It then calls the ERP system’s API and sends the transformed payload to create the purchase order within the system.

const buckets = bucketsClient();
const mappings = mappingsClient();

export const handler = async (event) => {
  const transactionObject = await buckets.getObject({
    bucketName: event.detail.output.bucketName,
    key: event.detail.output.key,
  });
  const content = JSON.parse(await transactionObject.body?.transformToString());

  const erpJson = await mappings.mapDocument({
    id: "<your_mapping_id>",
    content,
    validationMode: "strict",
  });

  await fetch(process.env.ERP_CREATE_PURCHASE_ORDER, {
    method: "POST",
    body: JSON.stringify(erpJson),
    headers: {
      "Authorization": `Key ${process.env.ERP_API_KEY}`,
      "Content-Type": "application/json",
    },
  });
};

You can find additional example code on GitHub.

Example: Publish processing failures to Slack

Stedi Core emits file.failed events when it cannot process an inbound file or when it cannot deliver a generated outbound file to a partner. For example, Core emits a file.failed event when your partner sends an invalid EDI file.

The following function has an event binding that listens to file.failed events and posts a Slack webhook to notify a support team to take action. You can use this approach to integrate with your own alerting system.

export const handler = async (event) => {
  const lines = [
    `ISA IDs: receiver: ${event.interchange.receiverId},
       sender: ${event.interchange.senderId}`,
    `File ID: ${event.fileId}`,
    `Direction: ${event.direction}`,
    ...event.errors.map((err) => `Error: ${err}`),
  ];

  await fetch(process.env["SLACK_URL"], {
    method: "POST",
    body: JSON.stringify({
      blocks: [
        {
          type: "header",
          text: {
            type: "plain_text",
            text: "⚠️ Stedi Core File Processing Failed",
            emoji: true,
          },
        },
        ...lines.map((line) => ({
          type: "section",
          text: {
            type: "mrkdwn",
            text: line,
          },
        })),
      ],
    }),
  });
};

You can find full template code on GitHub.

Custom schedules

You can quickly set up a basic schedule in the Functions UI to run your function periodically at a set frequency (every X minutes, hours, or days). You can also define advanced schedules with custom expressions to set the frequency of execution, such as specific dates and times in a week.

Create schedule in the Functions UI

Example: Poll for inventory data

The following function polls to check inventory levels for product SKUs. When products run low on inventory, the function automatically generates an 850 purchase order based on current stock and preferred vendor information.

Stedi Core uses Stedi guides to generate EDI according to partner-specific requirements. Each guide’s JSON schema represents the shape of the EDI transaction set that the guide defines. This example uses a Stedi mapping to transform the JSON inventory payload into the required schema for 850 purchase orders.

const mappings = mappingsClient();
const core = coreClient();

export const handler = async () => {
  const lowInventoryItems = await fetch(process.env.ERP_LOW_INVENTORY_URL, {
    method: "GET",
    headers: {
      Authorization: `Key ${process.env.ERP_API_KEY}`,
    },
  });

  // Use Stedi mapping to transform JSON schema
  const lowInventoryOrdersPromises = (await lowInventoryItems.json()).map(async (item) => {
    const orderJson = await mappings.mapDocument({
      id: "<your_mapping_id>",
      content: item,
      validationMode: "strict",
    });

    return core.generateEdi({
      partnershipId: orderJson.partnershipId,
      transactionGroups: [
        {
          transactionSettingsId: "004010-850",
          transactions: orderJson.transactions,
        },
      ],
    });
  });

  await Promise.all(lowInventoryOrdersPromises);
};

Complete your integration with Stedi Functions

Functions allow you to extend Stedi’s event-driven architecture to fulfill any requirement and build end-to-end EDI integrations tailored to your business.

Book a demo with our onboarding team, and we'll help you set up Stedi Core for your first trading partner and create the functions you need to complete your integration.

Jun 20, 2023

Products

EDI formats are designed to use as little space as possible, but most companies still eventually need to process files that are tens or hundreds of megabytes. Unfortunately, large EDI files are a notorious pain point for EDI systems, as most cannot handle payloads over a few dozen megabytes and sometimes take hours or days to work through larger payloads.

We are excited to announce that Stedi Core now supports validating and parsing EDI files up to 500MB without pre-processing or custom development required.

Large file support in Stedi Core

Stedi Core is an EDI integration hub that translates inbound EDI to JSON and generates outbound EDI from JSON—for any of the 300+ transaction sets across every X12 version release.

With the latest enhancements, Core can reliably ingest files up to 500MB and individual transaction sets up to 130MB, with processing times ranging from seconds to minutes.

If your integration requires parsing EDI files or transaction sets beyond Core’s current upper limits, Stedi can automatically split files and process them in manageable pieces. With this approach, you can use Core to process transaction sets and files of virtually any size.

Is there a size limit for EDI files?

No. Neither the X12 nor EDIFACT standards explicitly specify an upper limit for file size.

Even though there are no explicit limits, your EDI system should list upper bounds for the file size and individual transaction set size it can handle successfully. However, it can be hard to predict whether files approaching the upper limit will produce failures.

This is because the upper limit of what an EDI system can successfully process often depends not on the file size but on the size and complexity of the largest EDI transaction set within the file. Sometimes files are 500MB, but no single transaction is larger than 1MB. In other cases, a 60MB file contains a single transaction set that's 50MB. Depending on how the system is designed, it may have no problem parsing the former but a lot of trouble parsing the latter.

Large EDI file examples

There are many scenarios in which it’s important to have an EDI solution that can handle large files.

  • Many transaction sets within a single file: Your trading partner may send batches of business data instead of generating a new EDI file for every transaction set. For example, a carrier may batch multiple 210 Motor Carrier Freight Details and Invoice transactions into a single file at regular intervals.

  • Very large transaction sets: Some transaction sets can contain many repeated segments or loops. For example, a healthcare provider could send an 837 HealthCare Claim that contains multiple, separate insurance claims, or a brand may send an 846 Inventory Inquiry/Advice file containing millions of SKUs.

  • A lot of data in one or more elements: For example, a single 275 Patient Information transaction containing x-ray images could produce a very large file.

Try Stedi Core

Stedi Core seamlessly processes large EDI files and allows you to search, inspect, and debug all real-time transaction data.

Book a demo, and we’ll help you set up Stedi Core for your first trading partner. Stedi has a generous free tier for evaluation and transparent pricing with no hidden fees.

Jun 6, 2023

Products

Over the past 18 months, Stedi launched eight key building blocks for developing EDI systems.

We designed these developer-focused products to address the major flaws in existing EDI solutions: a lack of control, extensibility, or both. While our building blocks solved these problems, stitching them together into an end-to-end EDI integration still required substantial development effort—until today.

We are excited to introduce the new, integrated Stedi platform. Stedi is a turn-key, event-driven EDI system that allows you to configure EDI integrations in minutes without being an EDI or development expert.

Stedi: The hub for EDI integrations

Stedi is bi-directional and can translate inbound EDI to JSON and generate outbound EDI from JSON—for any of the 300+ transaction sets across every X12 version release. With Stedi, you can integrate with any trading partner and maintain complete visibility into your transactions.

The Stedi platform allows you to:

  • Manage relationships between you and your trading partners.

  • Configure secure file exchange, including SFTP, FTPS, FTP, AS2, and HTTP.

  • Use machine-readable Stedi Guides to validate and generate EDI according to partner requirements.

  • Monitor, filter, and inspect real-time data for all inbound and outbound transactions.

  • Troubleshoot and retry errors to unblock your pipeline without opening a support case.

Configure an EDI integration in minutes

For each trading partner, you'll create partnerships to define the transaction types you plan to exchange and how Stedi should process each transaction. You'll also configure a connection protocol to securely exchange EDI files. Stedi supports file exchange via SFTP, FTPS, FTP, AS2, and HTTP. Finally, you'll set up Destination webhooks to automatically send processed transaction data and events from Stedi to your business system.

Once you've completed this configuration, you can start exchanging EDI files with your partner.

  • Stedi automatically validates and translates files your partner sends over the connection.

  • With a single API call, you can generate and send fully-formed EDI files, complete with autogenerated control numbers, to your partners through the connection.

A diagram of an end-to-end EDI integration on Stedi

Own your integrations and your data

Stedi gives you complete control over your EDI integrations. You can build, test, and modify every integration quickly and on your timeline. Our onboarding team is always here to help, but we'll never block you.

Stedi allows you to search, inspect, and debug real-time transaction data. You can view both individual transactions and entire files as well as filter by transaction type, use case (production or test), sender or receiver, specific business identifiers (such as PO numbers), and more.

To help with testing and debugging, Stedi shows detailed error messages and resolution tips based on the X12 EDI specification. After you fix an issue, you can retry files anytime to unblock your pipeline.

Inspecting errors

Get started

Stedi handles all the hardest parts of building an EDI integration, allowing you to onboard new trading partners faster and focus development efforts on the functionality unique to your business.

"Stedi has made me look like a superstar to my clients and trading partners.

EDI setup and trading partner testing have become seamless, and the full visibility into file executions and transaction flows makes Stedi the new industry standard for organizations of all sizes. I have worked in the EDI space since 2012, and I can say unequivocally that Stedi is the future!"

– Paul Tittel - Founder, Surpass Solutions Inc.

Book a demo with our onboarding team, and we’ll help you set up Stedi for your first trading partner, or check out the docs to learn more.

Feb 23, 2023

Engineering

There are many different ways to provision AWS services, and we use several of them to address different use cases at Stedi. We set out to benchmark the performance of each option – direct APIs, Cloud Control, CloudFormation, and Service Catalog.

When compared to direct service APIs, we found that:

  • Cloud Control introduced an additional ~5 seconds of deployment latency

  • CloudFormation introduced an additional ~13 seconds of deployment latency

  • Service Catalog introduced an additional ~33 seconds of deployment latency.

This additional latency can make day-to-day operations quite painful.

How we provision resources at Stedi

Each AWS service has its own APIs for CRUD of various resources, but since AWS services are built by many different teams, the ergonomics of these APIs vary greatly – as an example, you would use the Lambda CreateFunction API to create a function vs the EC2 RunInstances API to create an EC2 instance.

To make it easier for developers to work with these disparate APIs in a uniform fashion, AWS launched the Cloud Control API, which exposes five normalized verbs (CreateResource, GetResource, UpdateResource, DeleteResource, ListResources) to manage the lifecycle of various services. Cloud Control provides a convenient way of working with many different AWS services in the same way.

That said, we rarely use the ‘native’ service APIs or Cloud Control APIs directly. Instead, we typically define resources using CDK, which synthesizes AWS CloudFormation templates that are then deployed by the CloudFormation service.

Over the past year, we’ve also begun to use AWS Service Catalog for certain use cases. Service Catalog allows us to define a set of CloudFormation templates in a single AWS account, which are then shared with many other AWS accounts for deployment on-demand. Service Catalog handles complexity such as versioning and governance, and we’ve been thrilled with the higher-order functionality it provides.

Expectations

We expect to pay a performance penalty as we move ‘up the stack’ of value delivery – it would be unreasonable to expect a value-add layer to offer identical performance as the underlying abstractions. Cloud Control offers added value (in the form of normalization) over direct APIs; CloudFormation offers added value over direct APIs or Cloud Control (in the form of state management and dependency resolution); Service Catalog offers added value over CloudFormation (in the form of versioning, governance, and more).

Any performance hit can be broken into two categories: essential latency and incidental latency. Essential latency is the latency required to deliver the functionality, and incidental latency is the latency introduced as a result of a chosen implementation. The theoretical minimum performance hit, then, is equal to the essential latency, and the actual performance hit is equal to the essential latency plus the incidental latency.

It requires substantial investment to achieve something approaching essential latency, and such an investment isn’t sensible in anything but the most latency-sensitive use cases. But as an AWS customer, it’s reasonable to expect that the actual latency of AWS’s various layers of abstraction is within some margin that is difficult to perceive in the normal course of development work – in other words, we expect the unnecessary latency to be largely unnoticeable.

Reality

To test the relative performance of each provisioning method, we ran a series of performance benchmarks for managing Lambda Functions and SQS Queues. Here is a summary of the P50 (median) results:

  • Cloud Control was 744% (~5 seconds) and 1,259% (500 ms) slower than Lambda and SQS direct APIs, respectively.

  • CloudFormation was 1,736% (~13 seconds) and 21,076% (8 seconds) slower than Lambda and SQS direct APIs, respectively.

  • Service Catalog was 4,339% and 86,771% (~33 seconds, in both cases) slower than Lambda and SQS direct APIs, respectively.

The full results are below.

We experimented with Service Catalog to determine what is causing its staggeringly poor performance. According to CloudTrail logs, Service Catalog is triggering the underlying CloudFormation stack create/update/delete, and then sleeping for 30 seconds before polling every 30 seconds until it’s finished. In practice, this means that Service Catalog can never take less than 30 seconds to complete an operation, and if the CloudFormation stack isn’t finished within 30 seconds, then Service Catalog can’t finish in under a minute.

Conclusion

Our hope is that AWS tracks provisioning latency for each of these options internally and takes steps towards improving them – ideally, each provisioning method only introduces the minimum latecy overhead necessary to provide its corresponding functionality.

Full results

Lambda

|                 | Absolute |        |        |        | Delta |       |       |      |
|-Service---------|-P10------|-P50----|-P90----|-P99----|-P10---|-P50---|-P90---|-P99--|
| Lambda          | 464      | 744    | 2,301  | 5,310  |       |       |       |      |
| Cloud Control   | 6,098    | 6,278  | 7,206  | 12,971 | 1214% | 744%  | 213%  | 144% |
| CloudFormation  | 13,054   | 13,654 | 14,591 | 15,906 | 2713% | 1736% | 534%  | 200% |
| Service Catalog | 32,797   | 33,013 | 33,389 | 34,049 | 6967% | 4339% | 1351% | 541

Methodology:

  • Change an existing function's code via different services, which involves first calling UpdateFunctionCode then polling GetFunction.

  • In the case of CloudFormation and Service Catalog, the new code value was passed in as a parameter rather than changing the template.

  • The "Wait" timings represent how long it took the resource to stabilize. This was determined by polling the applicable service operation every 50 milliseconds.

SQS

|                 | Absolute |          |        |        | Delta   |         |         |         |
|-Service---------|-P10------|-P50------|-P90----|-P99----|-P10-----|-P50-----|-P90-----|-P99-----|
| SQS             | 34       | 38       | 45     | 51     |         |         |         |         |
| Cloud Control   | 444      | 516      | 669    | 1,023  | 1,205%  | 1,259%  | 1,382%  | 1,904%  |
| CloudFormation  | 7,417    | 8,047    | 8,766  | 11,398 | 21,714% | 21,076% | 19,337% | 22,239% |
| Service Catalog | 32,785   | 33,011   | 33,320 | 33,659 | 96,327% | 86,771% | 73,780% | 65,873

Methodology:

  • Change an existing queue's visibility timeout attribute via different services, which involves calling SetQueueAttributes.

  • In the case of CloudFormation and Service Catalog, the new visibility timeout value was passed in as a parameter rather than changing the template.

  • The "Wait" timings represent how long it took the resource to stabilize. This was determined by polling the applicable service operation every 50 milliseconds.

Feb 21, 2023

Products

EDI is more prevalent in healthcare than in any other industry, yet EDI specifications for healthcare are some of the most challenging and opaque standards to understand.

The format, commonly known as X12 HIPAA, is captured in a series of PDFs that are up to 700 pages long. These PDFs aren't available to the general public and they don't provide machine-readable schemas for parsing, generating, or validating files. These challenges have made working directly with X12 HIPAA transactions out of reach for all but the largest companies - until now.

We are excited to announce the availability of Stedi's X12 HIPAA guides, a free catalog of X12 HIPAA specifications that make it easier to understand, test, and translate healthcare EDI.

EDI in Healthcare

In 1996, the Healthcare Insurance Portability and Accountability Act (HIPAA) required that the United States Department of Health and Human Services (HHS) establish national standards for electronic transactions so that health information could be transmitted electronically. Essentially, HIPAA mandated the use of EDI.

HHS ultimately adopted the X12 standard, which was already in use throughout retail, supply chain, and other industries. While the X12 standard was extremely comprehensive, it was also extremely flexible, so HHS created a much narrower, opinionated subset of X12: X12 HIPAA.

With few exceptions, all HIPAA covered entities must support electronic transactions that conform to the X12 HIPAA format. Covered entities include health plans, healthcare clearinghouses, and healthcare providers who accept payment from health plans.

User-friendly, machine-readable X12 HIPAA specifications

The EDI Guide Catalog now has Stedi guides for every X12 HIPAA transaction set. Stedi guides are interactive, machine-readable EDI specifications that let you instantly validate EDI test files.

Validate and debug EDI files

Each guide's EDI Inspector automatically identifies errors in EDI test files, including missing or wrong codes, incorrect formatting, and invalid segments. EDI Inspector runs completely in your browser and doesn't send EDI payloads to Stedi, so you can debug production data without data privacy concerns.

View accurate samples

Each guide contains sample transactions that demonstrate valid usage patterns. You can add samples to EDI Inspector, edit them, and download the result to share with collaborators and trading partners.

Parse and generate compliant EDI

You can import any X12 HIPAA guide directly into your Stedi account and customize it to fit your use case. If you need to integrate X12 HIPAA parsing and generation into your workflows, you can use Stedi Core to programmatically validate against any X12 HIPAA guides and generate X12 EDI that conforms to the specifications.

Build a healthcare EDI integration

We're incredibly excited to make X12 HIPAA specifications accessible to businesses of any size so they can easily build healthcare integrations.

"Stedi’s X12 HIPAA guides are a great way to skip the traditional back-and-forth of PDF guides and sample files. We have been able to cut implementation fees with some partners by five-figure amounts because we can generate valid data so quickly. Our partners also appreciate that we can handle integrations ourselves."

– Russell Pekala, Co-Founder of Yuzu Health

If you're new to EDI, check out our EDI Essentials documentation. Our experts explain the basics of the EDI format and provide answers to common X12 HIPAA questions.

Book a call with our technical team to learn how you can build an EDI integration using Stedi.

Jan 26, 2023

Engineering

Stedi’s cloud EDI platform handles sensitive customer data, from business transactions like Purchase Orders and Invoices to healthcare data like Benefits Enrollments and Prior Authorizations. While we maintain a SOC 2 Type II compliance certification and our products have been certified as HIPAA-eligible by an external audit, we view these industry standards as minimum baselines and are constantly evaluating ways to elevate our security posture.

One key area of focus for us is evaluating and restricting our own access to customer data. Last summer, we set out to prove that our least-privilege access policies work as intended by applying automated reasoning. Unlike traditional software testing or penetration testing often used in security audits, this method uses mathematical proof that the security properties we intend to protect are always upheld.

We are thrilled to present the results of the work in a paper written by software engineering intern Hye Woong Jeon, a mathematics student at the University of Chicago and Susa Ventures Fellow, who designed and implemented this approach.

Overview of the paper

Stedi’s cloud infrastructure runs on Amazon Web Services (AWS). We use AWS’s Identity Access Management (IAM) Policies at every level of our stack to enable least-privilege access. We grant our engineers and software processes only the minimum necessary permissions required to perform their tasks. Using IAM, we define the specific actions that can be taken on specific resources under specific conditions.

By carefully separating the resources that Stedi needs to make our systems function from customer-owned resources, such as stored data, we can craft access policies that allow us to operate the platform securely while having no access to customer data. IAM independently enforces these access policies – its mechanisms cannot be circumvented. However, this raises a question: how do we know that we have crafted the access control policies correctly? And further, how can we provably demonstrate this not just to ourselves, but also to our customers?

When customers access Stedi’s systems via our API or web UI, they perform actions under their own identity using a specific IAM role that gives them full access to their data – for example, to EDI files stored in Stedi Buckets. When our engineers operate Stedi systems, or our automated processes perform tasks such as deployments, they use a different set of IAM roles. This enables us to manage configuration, deploy software, and access the operational logs and metrics necessary to run our services with the high level of availability that our customers expect.

With a clear separation of roles built on top of a security-first, battle-tested system like IAM, the key concern becomes ensuring that the underlying IAM policies for those roles are written as intended – in other words, ensuring that the role assigned to Stedi employees does not inadvertently include an IAM policy that can potentially allow access to customer data. While we carefully peer-review all software configuration changes with particular attention paid to security policies, we wanted to achieve a higher level of certainty. We wanted to prove definitively to ourselves that we hadn’t made an error, and put in place a mechanism that provides ongoing assurance that our access controls match our intent.

Concrete and complete proof is difficult to come by – especially for the sort of complex systems that Stedi builds – but, given a well-enough defined problem, automated reasoning makes it possible.

Our proof used a Satisfiability Modulo Theory (SMT) solver, a class of logic statement solvers concerned with identifying if some mathematical formula is satisfiable (i.e. is there some combination of values that will make some formula true). Examples of satisfiability questions are not difficult to come by – e.g. if there are twenty apples and oranges, and four more oranges than apples, then how many apples/oranges are there? In this example, twelve oranges and eight apples satisfies the formula.

The problem of access control can also be formulated as a satisfiability problem. For any Stedi-affiliated account X, can X access customer data? A more rigorous characterization of this problem requires understanding the different components that make up the formula of “accessing customer data.”

AWS encodes IAM policies using a specialized JSON grammar, which breaks IAM into several core components: Actions, Principals, Effects, and Resources. In brief, an IAM policy consists of an Effect, which describes if a Principal is allowed to perform an Action on a Resource. These core components hence act as the apples and oranges of our elementary example: under the various combinations of Actions, Principals, Effects, and Resources ascribed to Stedi account X, does there exist some combination that allows X to access a customer account?

Under the well-defined structure of IAM policies, it is relatively straightforward to encode access control for use in the SMT solver. Once each grammar component has been translated into its appropriate logical formulation, encoded policies can be tested against various template policies to check if a policy is allowed to (or prohibited from) accessing customer data.

Our implementation sorted roles into three categories: allowed, prohibited, and inconclusive. We ensured that items in the allowed and prohibited categories were appropriately categorized and invested time in making changes to IAM policies in the inconclusive category to get further assurance that our policies functioned as intended. Overall, we found that using the proof techniques gave us an even higher level of confidence that our security controls work as intended, isolating our own systems from the data our customers have entrusted to us.

More details about the tools and methods we used to conduct this research and our full conclusions are available in the paper.

Jan 25, 2023

Products

At Stedi, we have always prioritized the security and privacy of our customers and their data. That is why we are thrilled to announce that Stedi has received SOC 2 Type II compliance certification and our products have been certified as HIPAA eligible by an external audit.

SOC 2 Type II compliance is a widely recognized standard for data security and privacy in the technology industry. It involves a rigorous evaluation process that assesses a company's systems, policies, procedures, and controls related to data security, availability, and confidentiality. The certification is an ongoing process, and we will maintain and improve our security posture to meet or exceed industry standards.

We understand that HIPAA eligibility is important for our customers in the healthcare industry as it ensures their sensitive patient data and protected health information (PHI) will be safe and kept confidential when using Stedi products. This is critical when you are exchanging health information such as claims, eligibility, and enrollment information with your partners.

We are committed to providing our customers and their trading partners with the peace of mind that their data is always handled safely and securely. These certifications are just one validation of the high bar we set for security and operational rigor, and one of the many ways we’re fortifying our products to handle customer data safely and securely.

Visit our trust page for more information on our security and compliance policies, or contact us with any compliance questions related to your business case.

Book a demo to learn how you can run a modern EDI system on Stedi.

Nov 29, 2022

Products

This post mentions Stedi’s EDI Translate API. Converting EDI into JSON remains a key Stedi offering, however EDI Translate has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

They say that “no two businesses are the same,” and these differences are shown in the wide variation between EDI implementations.

Every EDI implementation is unique in some way – you’ll rarely find a company that doesn’t need some sort of customization. The problem with typical off-the-shelf EDI software is that customers quickly hit limitations and then wait months or years for new features to address their specific needs or unlock valuable new use cases. And when platforms do offer customization capabilities, they often require deep EDI expertise in order to implement.

Introducing Stedi Functions

Stedi Functions is a powerful way for customers to extend Stedi’s functionality to meet any unique requirement. Functions enables you to run your own purpose-built code on Stedi’s platform. You can use Functions to build end-to-end workflows using Stedi’s APIs – Guides, EDI Translate, Mappings, Stash, Buckets, and SFTP – or build custom functionality using open-source libraries, external web APIs, and even custom business logic to address virtually any requirement.

Functions can serve as an orchestration layer to link various steps in an EDI workflow. For example, you can build a function to pick up an EDI file from a designated location (e.g. an SFTP folder), perform validation and translate the EDI to JSON, transform the JSON to an internal schema, post this data to a custom backend API, and send an acknowledgment to the trading partner – all in one seamless flow.

Functions are ready to execute as soon as they are created, without the hassle of provisioning or managing infrastructure. You can invoke a function directly using the API or SDK, or trigger it through an event (e.g. a new EDI file uploaded to a Bucket via SFTP).

Features

  • Built for extensibility: Include functionality from open-source libraries, external web APIs, or custom business logic to handle virtually any business use case.

  • Seamless orchestration: Stitch together various functional steps with tailored sequence and logic to build an EDI transaction flow.

  • Event-driven execution: Trigger functions in response to events from Buckets (such as file uploads via SFTP). You can also invoke functions directly via API or SDK.

  • Automated scaling: Functions run on high-availability infrastructure that automatically scales up with the number of requests.

  • Automated deployment: Your code is ready to execute within seconds. No need to provision or manage infrastructure to deploy your code.

Functions currently supports JavaScript or any other language that compiles to JavaScript (such as TypeScript).

Using Stedi Functions

You can create, update, and invoke functions using the web UI, or programmatically with the Functions API and Functions SDK. See Functions tutorial for a walkthrough of each approach.

The web UI allows you to experiment, build and test your functions. Below is a high-level overview of creating and executing a function using the web UI.

Creating a function: In the web UI, you can create a function by clicking Create function, adding code to the editor, and clicking Save.

Your code must include a handler function with two parameters: event parameter that lets you pass JSON data into the function, and context parameter that contains information about the environment. You can include any logic, including external API calls, and return any value from the function. You cannot include libraries or additional packages using web UI, for that you will have to use the SDK or API.

Invoking a function: You can invoke your function by clicking Execute. You can also execute functions in the web UI that you created using the API. When the function completes execution, you can view the results, which include the return value and the logs.

Functions UI

Using Stedi function for an outbound EDI flow

Now that we have seen the basics, let’s look at a real-world solution built using Stedi Functions. In this example, a Stedi function is used to connect various Stedi offerings to generate an EDI document and then send it to a trading partner.

The function is called with a JSON payload representing the source data for the EDI transaction. The function performs several steps, as illustrated in the diagram below:

Outbound EDI diagram
  1. Accepts a JSON payload (source) for the transaction.

  2. Calls Stash to generate a control number for the EDI document.

  3. Passes the JSON payload to Mappings using a predefined mapping that maps the internal schema of the payload to a target JSON Schema of a guide specific to the trading partner.

  4. Combines the output from step 3, the envelope information (with control number from step 2), and guide reference before calling the EDI Translate API.

  5. The EDI Translate API validates that the input conforms to the guide schema, and generates the X12 EDI document.

  6. The function saves the EDI output as a file in a bucket.

  7. A trading partner can retrieve the EDI from Buckets using SFTP.

Stedi Functions is now Generally Available

Stedi Functions is the connecting tissue that lets you orchestrate various steps in an end-to-end EDI workflow. Stedi Functions also gives you unlimited extensibility to bring in functionality from third-party APIs, open-source libraries, or custom business logic to build an EDI implementation for your needs. There is no need for deployment or provisioning infrastructure to run a function. With the API access, you can integrate a Stedi function into your own tech stack.

Try it out for yourself – build a Function using our web UI. We have a generous free tier, so there is no need to worry about costs when you are experimenting.

Start building today.

Nov 3, 2022

Products

Every EDI relationship begins with an implementation guide. These EDI implementation guides have been shared in the same format for decades – they’re typically PDFs with dozens or hundreds of pages of details to sift through, and a successful EDI implementation requires getting the details just right.

After reviewing and implementing a partner’s guide, companies have no way of validating their EDI files locally. Their only option to test is to send test files and hope that they’re correct, wasting time and effort on both sides. The fixing of one error reveals another, dragging the partner onboarding process on for weeks. Throw in the fact that many PDF implementation guides and sample files have errors within them, you begin to understand why EDI gets a bad reputation.

To solve these problems, we’ve built Public Guides – a radically new and improved way for businesses to share EDI specifications and speed up the trading partner onboarding process. A public guide is a live, interactive web page that is accessible by anyone with a link. Links can be shared directly with trading partners or included in an EDI portal – they provide the same information as a traditional PDF guide, but allow users to instantly validate and troubleshoot EDI files right in the browser. An intuitive UI and helpful error messages lead them to quickly build EDI files that conform to the guide’s specifications, reducing the testing process from weeks to hours – and eliminating dozens of back-and-forth emails.

Stedi guides can be built out in minutes by anyone using a no-code visual interface, regardless of technical ability. Existing PDFs can be quickly replicated into a Stedi guide, or new guides can be built from scratch. Once built, a guide can be made public instantly.

Features

  • Interactive documentation for trading partners to easily navigate specifications with contextual references. No more scrolling through dozens of pages in a PDF.

  • Embedded EDI Inspector to help your partners instantly validate EDI payloads against your requirements without sending you any traffic.

  • Built-in support for all X12 transaction sets and releases, allowing you to configure X12-conformant specifications without custom development.

  • Customizable specifications, so you can make the X12 standard work for you and not be constrained by strict interpretation.

  • No coding experience required to build a public guide using the visual interface.

  • Brand your guide with a business logo and name, a custom URL slug, and a link to your own website.

Public Guides is now generally available

Try it for yourself by creating a Stedi guide using the guide builder interface. Creating guides and sharing them within your organization is entirely free. Public Guides has no setup fees or long-term subscriptions, and are a flat $250 per month per public guide.

Start building today.

Using Public Guides

Creating a guide: You can create a Stedi guide based on a PDF guide from a trading partner, or from scratch.

Using the guide builder UI, you can create a new guide starting with the X12 release (e.g. 5010) and transaction set (e.g. 810 Invoice). You’ll then select the necessary segments and elements and, if necessary, customize each of them based on your requirements.

Guides UI

Customizing the branding: To customize the appearance of your guide, you can click on “published guide settings” on the guides listing page.

Making your guide public: Once your changes are published, you can make your guide public by choosing the “Actions → Make public” option on the guide. You can view the guide by clicking on “Actions → View public guide”. You can also make this guide private at any time. You can also see a preview of your guide page before making it public.

Sharing the guide: Once a guide is made public, you can view the guide using “Actions → View public guide” option on the guide builder. You can use the "Share" option on this page to get the link to be shared with your partners or to embed into your EDI resources page.

Accessing the public guide (partners): To access a public guide, partners just need to click on the link you send them. The guide will be available to them on a web browser.

You or a trading partner can also export the guide as a PDF. The PDF still retains all the references so partners can look up information on the segments and elements in the transaction set that they don’t fully understand from the guide. However, you’ll miss the ability to validate EDI files.

guide preview

Validating EDI files (partners): Once a partner is on the public guide page, they can use EDI Inspector to validate an EDI file instantly. To use Inspector, one can click the EDI Inspector link and paste their EDI file contents into the designated area. The validation and error messages will be specific to the underlying guide.

EDI Inspector in Guides

Oct 26, 2022

Products

Developers building EDI integrations often find that receiving EDI documents is relatively straightforward compared to the difficulty of sending EDI documents. What makes these use cases so different?

Imagine you’ve switched cell phone providers and you receive your first bill. Though you’ve never seen an invoice from this cell phone provider before, you can quickly ‘parse’ out the relevant details – the total, the due date, your address, how many units of data you’ve consumed, and so on.

But what if you had to create an invoice in this exact format – from scratch? How would you know which fields were required, what order to put them in, and details like whether to use the full name of states or just the abbreviation?

What’s needed is a template, and a way to validate an input against it – which is precisely what we’ve launched with EDI Translate, a new API for creating and validating EDI files that conform to precise trading partner specifications. EDI Translate accepts user-defined JSON payloads and translates them into valid EDI files, and vice versa.

EDI Translate works in conjunction with recently-launched Stedi Guides, which enables users to quickly and accurately define EDI specifications using a no-code visual interface. Once a guide has been defined, users can use the EDI Translate API to create outbound EDI files that conform to the guide. Users can also use EDI Translate API to validate that inbound EDI files meet the defined specification, as well as parse these EDI files into JSON for further processing.

Stedi Guides uses the popular, open-source JSON Schema format, giving developers a familiar way to ensure that their JSON payloads have all the information required to create a valid EDI file.

Features of EDI Translate

  • Read and write any X12 EDI transaction.

  • Validate EDI documents automatically against trading partner specifications.

  • Seamlessly integrate with your own tech stack – or build your end-to-end EDI workflow on Stedi.

How EDI Translate works

EDI Translate makes EDI integrations a lot simpler, taking the EDI format out of the equation and enabling you to work with JSON. Behind the scenes, the nuances of EDI are handled for you to ensure every transaction meets your or your trading partners’ requirements.

For outbound EDI: Once you map your internal payload to the JSON Schema generated by your Stedi guide, you can use EDI Translate to create an EDI document that conforms to the guide’s specifications.

For inbound EDI: When you receive an EDI file from a trading partner, you can use EDI Translate to convert it to JSON. You can use this output JSON directly, or map the output to your own data schema for additional processing (e.g. posting data to an ERP system).

Using EDI Translate

Generating EDI: The following example generates EDI using a custom JSON payload. For a detailed walkthrough, see the demo for writing EDI.

You need three pieces of data to generate EDI:

  1. A Stedi guideId that refers to the trading partner specification. For more information, see Creating a Stedi Guide.

  2. A JSON payload that conforms to the JSON Schema of your guide. If you need help creating this payload, you can use Stedi Mappings to map your internal format to the guide’s JSON Schema.

  3. The X12 envelope data, including interchange control header and functional group headers. Here is an example:

const envelope = {
  interchangeHeader: {
    senderQualifier: "ZZ",
    senderId,
    receiverQualifier: "14",
    receiverId,
    date: format(documentDate, "yyyy-MM-dd"),
    time: format(documentDate, "HH:mm"),
    controlNumber,
    usageIndicatorCode,
  },
  groupHeader: {
    functionalIdentifierCode,
    applicationSenderCode: "WRITEDEMO",
    applicationReceiverCode: "072271711TMS",
    date: format(documentDate, "yyyy-MM-dd"),
    time: format(documentDate, "HH:mm:ss"),
    controlNumber,
  },
};

Once you have these three inputs, you can call the EDI Translate API.

// Translate the Guide schema-based JSON to X12 EDI
const translation = await translateJsonToEdi(mapResult.content, guideId, envelope);

The output of the API call is an EDI payload that you can send to your trading partner using Stedi SFTP or another file transfer mechanism. The sample output below is for the X12-5010-850 transaction set.

{
    "output": "ISA*00*          *00*          *ZZ*AMERCHANT      *14*ANOTHERMERCH   *220915*0218*U*00501*000000001*0*T*>~GS*OW*WRITEDEMO*072271711TMS*20220915*021828*000000001*X*005010~ST*850*000000001~BEG*00*DS*365465413**20220830~REF*CO*ACME-4567~REF*ZZ*Thank you for your business~PER*OC*Marvin Acme*TE*973-555-1212*EM*marvin@acme.com~TD5****ZZ*FHD~N1*ST*Wile E Coyote*92*123~N3*111 Canyon Court~N4*Phoenix*AZ*85001*US~PO1*item-1*0008*EA*400**VC*VND1234567*SK*ACM/8900-400~PID*F****400 pound anvil~PO1*item-2*0004*EA*125**VC*VND000111222*SK*ACM/1100-001~PID*F****Detonator~CTT*2~AMT*TT*3700~SE*16*000000001~GE*1*000000001~IEA*1*000000001~"
}

Parsing EDI: To parse an EDI file, you only need the raw EDI payload (file contents) and the relevant Stedi guideId.

{
    "input": "ISA*00*          *00*          *ZZ*ANOTHERMERCH   *14*AMERCHANT      *220914*2022*U*00501*000001746*0*T*>~\nGS*PR*072271711TMS*READDEMO*20220914*202222*000001746*X*005010~\nST*855*0001~\nBAK*00*AD*365465413*20220914*****20220913~\nREF*CO*ACME-4567~\nN1*SE*Marvin Acme*92*DROPSHIP CUSTOMER~\nN3*123 Main Street~\nN4*Fairfield*NJ*07004*US~\nN1*ST*Wile E Coyote*92*DROPSHIP CUSTOMER~\nN3*111 Canyon Court~\nN4*Phoenix*AZ*85001*US~\nPO1*item-1*8*EA*400**VC*VND1234567*SK*ACM/8900-400~\nPID*F****400 pound anvil~\nACK*IA*8*EA~\nPO1*item-2*4*EA*125**VC*VND000111222*SK*ACM/1100-001~\nPID*F****Detonator~\nACK*IA*4*EA~\nCTT*2~\nSE*17*0001~\nGE*1*000001746~\nIEA*1*000001746~",
    "guideId": "01GEJBYTQCHWK59PKANTKKGJXM"
}

EDI Translate API is now generally available

EDI Translate is the core of any modern EDI system. Backed by Stedi Guides, EDI Translate enables you to convert data between JSON and EDI while conforming to your trading partners’ specifications. Use EDI Translate regardless of whether you are building an EDI system from scratch or simplifying your existing architecture. You can build your end-to-end flow entirely on Stedi, or integrate EDI Translate into your existing tech stack.

Try it for yourself – start by creating a Stedi guide using the guide builder UI. Use the guide with EDI Translate to parse and generate EDI files. We have a generous free tier for API calls, so there is no need to worry about costs when you are experimenting. Creating guides and sharing them within your organization is entirely free.

Start building today.

Did you know? You can build an end-to-end EDI integration on Stedi. Capture EDI specifications from your trading partner in a Stedi Guide, convert data between JSON and EDI using EDI Translate, transform data between schemas using Stedi Mappings, handle transmission of EDI files with your trading partners using Stedi SFTP (backed by Stedi Buckets), and orchestrate API calls and business logic with Stedi Functions. Use Stash as a reliable key-value store to keep track of transactions, reference lookup tables, and more.

Oct 20, 2022

Products

This post mentions Stedi’s EDI Translate API. Converting EDI into JSON remains a key Stedi offering, however EDI Translate has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

The first step in any EDI relationship is agreeing on a set of EDI specifications. These requirements are typically shared as a PDF document, which is helpful for human reference but does not enable automated translation or validation of EDI documents.

Stedi Guides is a new product that enables users to quickly and accurately create specifications using a visual interface. These specifications can be used to programmatically parse and generate EDI documents via API using a modern JSON Schema format, or to validate EDI manually in a web browser against interactive documentation.

Features of Stedi Guides

  • Machine-readable JSON Schema artifacts to read, write, and validate EDI documents.

  • Built-in support for all X12 transaction sets and releases, allowing you to configure X12-conformant specifications without custom development.

  • Customizable specifications, so you can make the standard work for you and not be constrained by strict X12 interpretation.

  • No coding experience required to build a Stedi guide using the visual interface.

Guides UI

How Stedi Guides can help your business

If you are reading this, you probably encountered one of these situations:

  • You’ve received EDI requirements from a trading partner in a PDF document, and are wondering about how best to incorporate those into your development workflow.

  • You meticulously built and sent an EDI file to your trading partner, only to be told days or weeks later that the file has errors.

  • You receive EDI files from a partner and want to validate and parse them according to your specifications.

Stedi Guides is built to address these use cases, and more.

The fundamental problem with EDI is that while conforming to EDI specifications is critical to every EDI implementation, PDFs are a poor format for communicating a schema. Trading partners are left trying to visually validate an EDI file against a PDF and attempting to capture the nuances of a complex guide in custom validation logic. To complicate matters, EDI specifications almost always differ from one trading partner to another, making it unmanageable to keep track of differences while generating accurate EDI files for every partner.

Stedi Guides solves these problems by giving you a visual interface for building robust artifacts – guides – to encapsulate EDI specifications, while making them available for programmatic access.

Stedi Guides is natively integrated with the EDI Translate API, which allows you to generate outbound EDI that conforms to a specified guide, and to validate and parse inbound EDI against a guide defined by you or your trading partner.

Using Stedi Guides

In the browser

Creating and publishing a guide: You can create a Stedi guide based on a PDF guide from a trading partner, or from scratch.

Using the guide builder UI, you can create a new guide starting with the X12 release (e.g. 5010) and transaction set (e.g. 810 Invoice). You’ll then select the necessary segments and elements and, if necessary, customize each of them based on your requirements.

guide builder UI

You can publish the guide privately within your organization for others to access it as an interactive documentation on a web page.

guide preview

Validating EDI files in the browser: Once you’ve built a guide, you can use the Inspector view to validate an EDI file instantly, without having to reach out to your trading partner.

To use Inspector, click the EDI Inspector link within the guide builder, or on the published guide page. The validation and error messages will be specific to the underlying guide.

EDI Inspector in Guides

Via API

To generate EDI: To use a Stedi guide to generate EDI, you can call the EDI Translate API with the guide reference (guideId) and a JSON object, and the API will return a conforming EDI file – or a list of validation errors encountered during translation. See the demo for writing EDI for a detailed walkthrough.

// Translate the Guide schema-based JSON to X12 EDI
const translation = await translateJsonToEdi(mapResult.content, guideId, envelope);

To parse EDI: Similarly, you can call EDI Translate API to parse or validate EDI by passing the EDI file and the guideId, and the API will return a translated JSON representation of your EDI file.

const translation = await translateEdiToJson(ediDocument, guideId);

Stedi Guides is now Generally Available

Stedi Guides is the foundation of every modern EDI system. Stedi Guides allows you to build integrations that are reliable, easy to troubleshoot, and scalable as you onboard with new trading partners and transaction sets. The no-code interface makes it easy for anyone to create and maintain EDI specifications, regardless of their technical ability or EDI knowledge. In addition to reading and writing EDI programmatically, Stedi Guides renders human-readable documentation in the browser, allowing for real-time validation of EDI files using Inspector.

Try it for yourself by creating a Stedi guide using the guide builder interface. Creating guides and sharing them within your organization is entirely free. Validate EDI files for free using the Inspector – or use the EDI Translate API to parse and generate EDI files programmatically. The EDI Translate API also has a generous free tier, so there is no need to worry about costs when experimenting.

For more details, check out our user guide.

Aug 2, 2022

Products

Stedi Buckets is worth getting excited about, or at least so I’m told. We launched it together with Stedi SFTP and I can’t help but feel that SFTP stole the show that day. Receive documents from customers without hassle? That is solving a business problem. But storing those documents? Sure, it’s necessary, but there’s not a lot for me to do with documents that just sit there. Unless you give me programmatic access. Then I can do with them whatever I want and that gets my coder’s heart pumping.

Setting a challenge

Stedi SFTP and Stedi Buckets are related and I can access both of them using the SDK. I need a starting point though, so I’ll set myself a challenge. I want to generate a report that tells me how many files each of my customers has sent me. I also want to know how many of those files contain EDI. What I’m looking for is something like this.

user: Paper Wrap - files: 10 - EDI: 8
user: Blank Billing - files: 4 - EDI: 2
user: Blank Shipping - files: 11 - EDI: 7
user: Blank Management - files: 17 - EDI: 9

Let me stop pretending that I’m making this up as I go: I already finished the challenge. It turned out easier than I thought. There, I spoiled the ending, so you might as well stop reading. Actually, that’s not a bad idea. If you feel you can code this yourself, then open the Stedi SFTP SDK reference and the Stedi Buckets SDK reference and go for it.

  • If you want a little more help, continue reading Programming with the SDK.

  • If you want a lot more help, jump to Walkthrough.

Programming with the SDK

Assuming that installing Node.js is something you’ve done already, you can get started with the SDK by running the following commands in your project directory.

npm install @stedi/sdk-client-buckets
npm install @stedi/sdk-client-sftp

Since I’ve already finished the challenge, I know exactly which operations I’m going to need.

  • List all SFTP users.

  • List all objects in a bucket.

  • Download the contents of an object.

List all SFTP users

const sftp = require("@stedi/sdk-client-sftp");

async function main() {
  const sftpClient = new sftp.Sftp({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const listUsersResult = await sftpClient.listUsers({});
  const users = listUsersResult.items;
  console.info(users);
}

main();

List all objects in a bucket

const buckets = require("@stedi/sdk-client-buckets");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const listObjectsResult = await bucketsClient.listObjects({
    bucketName: "YOUR BUCKET NAME HERE",
  });
  const objects = listObjectsResult.items || []; // items is undefined if the bucket doesn’t contain objects
  console.info(objects);
}

main();

Download the contents of an object

const buckets = require("@stedi/sdk-client-buckets");
const consumers = require("stream/consumers");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const getObjectResult = await bucketsClient.getObject({
    bucketName: "YOUR BUCKET NAME HERE",
    key: "YOUR OBJECT KEY HERE",
  });
  const contents = await consumers.text(getObjectResult.body);
  console.info(contents);
}

main();

Paging

The list-operations only return the first page of results. Let’s not settle for that.

const buckets = require("@stedi/sdk-client-buckets");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  let objects = [];
  let pageToken = undefined;
  do {
    const listObjectsResult = await bucketsClient.listObjects({
      bucketName: "YOUR BUCKET NAME HERE",
      pageSize: 5,
      pageToken: pageToken,
    });
    objects = objects.concat(listObjectsResult.items || []);

    pageToken = listObjectsResult.nextPageToken;
  } while (pageToken !== undefined);

  console.info(objects);
}

main();

Walkthrough

The devil is in the details. The code above shows you what you need to program with the SDK, but it doesn’t explain anything. Allow me to rectify.

From time to time, I’ll do something on the command line. To follow along, you need to run a Linux shell, the Mac terminal, or Windows Subsystem for Linux.

Installing Node.js

  1. Install Node.js.

The Stedi SDK is written for JavaScript. It also works with languages that compile to JavaScript—like TypeScript, CoffeeScript, and Haxe—but it requires a JavaScript environment. In practice, that means Node.js.

How you get up and running with Node.js depends on your operating system. I’m not going to cover all the options here.

Creating a project

  1. Create a folder for your project.

mkdir stedi-challenge
cd stedi-challenge
  1. Create a file called main.js.

touch main.js
  1. Open the file in your preferred code editor.

  2. Paste the following code into the file.

console.info("Hello, world!");
  1. Run the code.

node main.js

This should output the following:

Hello, world

Creating a Stedi-account

  1. Go to the sign-up page and create a free Stedi-account.

If you already have Stedi-account, you can use that one. If you’re already using Stedi SFTP, then your results might look a little different, but it will all still work.

Create an API-key

  1. Sign in to the Stedi Dashboard.

  2. Open the menu on the top left.

Dashboard menu
  1. Under Account, click API Keys.

API Keys
  1. Click on Generate API key.

Generate API key
  1. Enter a description for the API key. If you have multiple API keys in your account, you can use the description to tell them apart. Other than that, it doesn’t matter what you fill in. I usually just type my name.

  2. Click on Generate.

Generate
  1. Copy the API key and store it somewhere safe.

  2. Click Close.

You need the API key to work with the SDK. It tells the SDK which account to connect to. It’s important you keep the API key safe, because anyone who has your API key can run code that access your account.

Test the Stedi Buckets SDK

  1. Install the Stedi Buckets SDK.

npm install @stedi/sdk-client-buckets
  1. Open main.js.

  2. Remove all code.

  3. Import the Stedi Buckets package.

const buckets = require("@stedi/sdk-client-buckets");
  1. Create a Stedi Buckets client.

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. List all buckets in your account.

async function main() {
  const listBucketsResult = await bucketsClient.listBuckets({});
  const buckets = listBucketsResult.items;
  console.info(buckets);
}

main();
  1. On the command line, set the environment variable STEDI_API_KEY to your API key.

export STEDI_API_KEY=YOUR.API.KEY.HERE
  1. Run the code.

node main.js

This should output the following:

[]

The client allows you to send commands to Stedi Buckets. You must pass it a region, although we only support us right now. You also give it your API key, so it knows which account to connect to. You could paste your API key directly into the code, but then everyone who has access to your code, can see your API key and you should keep it safe.

Instead, this code reads the API key from an environment variable that you set on the command line. This way, the API key is only available to you. If someone else wants to run the code, they need to have their own API key and set it on their command line.

To get a list of all buckets, you call listBuckets(). The functions in the SDK expect all their parameters wrapped in an object. listBuckets() doesn’t have any required parameters, but it still expects an object. That’s why you have to pass in {}.

listBuckets() is an async function, so you have to await it, but you can only use await inside another async function. That’s why the code is wrapped inside the function main().

Since you don’t have any buckets in your account yet, the output is an empty array.

Test the Stedi SFTP SDK

  1. Install the Stedi SFTP SDK.

npm install @stedi/sdk-client-sftp
  1. Remove all code from main.js.

  2. Import the Stedi SFTP package.

const sftp = require("@stedi/sdk-client-sftp");
  1. Create a Stedi SFTP client.

const sftpClient = new sftp.Sftp({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. List all SFTP users in your account.

async function main() {
  const listUsersResult = await sftpClient.listUsers({});
  const users = listUsersResult.items;
  console.info(users);
}

main();
  1. Run the code.

node main.js

This should output the following:

[]

As you can see, this works just like the Stedi Buckets SDK. The same notes apply.

Create an SFTP user

  1. Open the Stedi Dashboard.

  2. Open the menu on the top left.

  3. Under Products, click on SFTP.

SFTP
  1. Click on Create User.

  2. Fill in the form with the values you see below.

Create SFTP User: Paper Wrap
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

This creates a user that has access to the directory /paper-wrap on the SFTP server.

Paper Wrap is a store that sells edible gift wrapping. They care greatly about preventing paper waste, so they do all their business electronically.

The SFTP server is ready for use, even though you didn’t create a bucket for it. Stedi does this for you automatically.

Find the bucket name

  1. Add the following code at the end of main().

if (users.length == 0) {
  console.info("No users.");
  return;
}

const bucketName = users[0].bucketName;
console.info(bucketName);
  1. Run the code.

node main.js

This should output the following, although your bucket name and username will be different.

[
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Paper Wrap',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/paper-wrap',
    lastConnectedDate: undefined,
    username: 'P7EGRE9H'
  }
]
e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp

It’s convenient that Stedi creates the SFTP bucket for you, but you need the name of the bucket if you want to access it from code. You could get the name from the dashboard, but you didn’t get into programming to copy and paste things from the UI.

When you retrieve information about a user, it includes the bucket name in a field conveniently called bucketName. All SFTP users use the same bucket, so you can get the bucket name from the first user and use it throughout.

Create more SFTP users

  1. In the Stedi Dashboard, click on Create User.

  2. Fill in the form with the values you see below.

Create SFTP User: Blank Billing
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

  4. Click on Create User.

  5. Fill in the form with the values you see below.

Create SFTP User: Blank Shipping
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

  4. Click on Create User.

  5. Fill in the form with the values you see below.

Create SFTP User: Blank Management
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

Blank is a manufacterer of invisible ink. They went fully digital after the court ruled that their paper contracts weren’t legally binding.

Blank has separate departments for billing and shipping and both departments get their own user with their own directory. This way, Shipping can’t accidentally put their ship notices among the invoices. The general manager likes to keep an eye on everything, so he has a user that can see documents from both Shipping and Billing, but can’t access Paper Wrap’s documents.

Create test files

  1. Create a file called not_edi.txt.

  2. Add the following content to not_edi.txt.

This is not an EDI file
  1. Create a file called edi.txt.

  2. Add the following content to edi.txt.

ISA*00*          *00*          *ZZ*PAPER WRAP     *ZZ*BLANK          *210901*1234*U*00801*000000001*0*T*>~
GS*PO*SENDERGS*007326879*20210901*1234*1*X*008020~
ST*850*000000001~
BEG*24*SP*PO-00001**20210901~
PO1**5000*04*0.0075*PE*GE*Lemon Juice Gift Card~
SE*1*000000001~
GE*1*1~
IEA*1*000000001

If the script is going to count files, you should give it some files to count. The contents don’t really matter, other than that some files should contain EDI. You’ll upload copies of these two files to the SFTP server.

A word about SFTP clients

I’m about to show you how to upload files to the SFTP server and I’m going to do it from the command line. I think that’s convenient since I’m doing a lot of things on the command line already, but it’s not the only way. If you prefer dragging and dropping your files, you can install an FTP client like FileZilla. You won’t be able to follow the instructions in the next paragraph step by step, but it shouldn’t be too hard to adapt them. Here are some pointers.

  • You can find the username and host on the Stedi Dashboard.

  • Host and SFTP endpoint are the same thing.

  • If you need to specify a port, use 22.

  • Call the files on the SFTP server whatever you like; it doesn’t matter to the code.

  • The exact number of files you upload doesn’t matter; just gives every user a couple of files.

Upload test files

  1. On the command line, make sure you’re in the directory that contains the files edi.txt and not-edi.txt.

  2. Connect to the SFTP server with the connection string from the Paper Wrap user. Your connection string will look a little different than the one in example.

sftp 9UE5Z386@data.sftp.us.stedi.com
  1. Enter the password for the Paper Wrap user.

  2. Upload a couple of files.

put edi.txt coconut-christmas
put edi.txt toffee-birthday
put edi.txt blueberry-ribbon
put edi.txt cinnamon-surprise
put edi.txt salty-sixteen
put edi.txt beefy-graduation
put edi.txt cashew-coupon
put edi.txt bittersweet-valentine
put not-edi.txt whats-that-smell
put not-edi.txt dont-ship-the-fish
  1. Disconnect from the SFTP server.

bye
  1. Connect to the SFTP server with the connection string from the Blank Billing user.

  2. Enter the password for the Blank Billing user.

  3. Upload a couple of files.

put edi.txt pay-me
put edi.txt seriously-pay-me
put not-edi.txt i-know-where-you-live
put not-edi.txt thank-you
  1. Disconnect from the SFTP server

  2. Connect to the SFTP server with the connection string from the Blank Shipping user.

  3. Enter the password for the Blank Shipping user.

  4. Upload a couple of files.

put edi.txt too-heavy
put edi.txt is-this-empty
put edi.txt poorly-wrapped
put edi.txt other-side-down
put edi.txt handle-with-gloves
put edi.txt negative-shelf-space
put edi.txt destination-undisclosed
put not-edi.txt empty-labels
put not-edi.txt boxes-and-bows
put not-edi.txt be-transparent
put not-edi.txt how-to-drop-without-breaking
  1. Disconnect from the SFTP server.

  2. Connect to the SFTP server with the connection string from the Blank Management user.

  3. Enter the password for the Blank Management user.

  4. Upload a couple of files.

put not-edi.txt strategic-strategizing
put not-edi.txt eat-my-spam

List all files

  1. Create a Stedi Buckets client. Add the following code at the top of main.js.

const buckets = require("@stedi/sdk-client-buckets");

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. Get a list of files from the SFTP server. Add the following code to the end of main().

const listObjectsResult = await bucketsClient.listObjects({
  bucketName: bucketName,
});
const objects = listObjectsResult.items || [];
console.info(objects);
  1. Run the code.

node main.js

This should output the following:

[
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Management',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank',
    lastConnectedDate: '2022-07-28',
    username: '8X0HLJJW'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Shipping',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank/shipping',
    lastConnectedDate: '2022-07-28',
    username: '4UBJ9H9C'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Paper Wrap',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/paper-wrap',
    lastConnectedDate: '2022-07-28',
    username: 'P7EGRE9H'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Billing',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank/billing',
    lastConnectedDate: '2022-07-28',
    username: '23SO1YKM'
  }
]
e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp
[
  {
    key: 'blank/billing/i-know-where-you-live',
    updatedAt: '2022-07-28T15:21:23.000Z',
    size: 24
  },
  {
    key: 'blank/billing/pay-me',
    updatedAt: '2022-07-28T15:21:22.000Z',
    size: 295
  },
  {
    key: 'blank/billing/seriously-pay-me',
    updatedAt: '2022-07-28T15:21:22.000Z',
    size: 295
  },
  {
    key: 'blank/billing/thank-you',
    updatedAt: '2022-07-28T15:21:24.000Z',
    size: 24
  },
  {
    key: 'blank/eat-my-spam',
    updatedAt: '2022-07-28T15:25:22.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/be-transparent',
    updatedAt: '2022-07-28T15:22:26.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/boxes-and-bows',
    updatedAt: '2022-07-28T15:22:25.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/destination-undisclosed',
    updatedAt: '2022-07-28T15:22:24.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/empty-labels',
    updatedAt: '2022-07-28T15:22:24.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/handle-with-gloves',
    updatedAt: '2022-07-28T15:22:23.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/how-to-drop-without-breaking',
    updatedAt: '2022-07-28T15:22:26.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/is-this-empty',
    updatedAt: '2022-07-28T15:22:21.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/negative-shelf-space',
    updatedAt: '2022-07-28T15:22:23.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/other-side-down',
    updatedAt: '2022-07-28T15:22:22.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/poorly-wrapped',
    updatedAt: '2022-07-28T15:22:21.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/too-heavy',
    updatedAt: '2022-07-28T15:22:20.000Z',
    size: 295
  },
  {
    key: 'blank/strategic-strategizing',
    updatedAt: '2022-07-28T15:25:21.000Z',
    size: 24
  },
  {
    key: 'paper-wrap/beefy-graduation',
    updatedAt: '2022-07-28T15:20:11.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/bittersweet-valentine',
    updatedAt: '2022-07-28T15:20:12.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/blueberry-ribbon',
    updatedAt: '2022-07-28T15:20:09.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/cashew-coupon',
    updatedAt: '2022-07-28T15:20:12.000Z',
    size: 295
  }
]

You can’t get files per user, because Stedi Buckets doesn’t know anything about users. You also can’t get files per directory, because technically Stedi Buckets doesn’t have directories. That’s a topic for another time, though.

You can list all the files. The result from listObjects() contains an array called items with information on each file. If there are no files on the SFTP server, items will be undefined. For our code, it’s more convenient to have an empty array if there are no files, hence the expression listObjectsResult.items || [].

If you take a close look at the output—and you’ve been following this walkthrough to the letter—you’ll discover that it doesn’t contain every single file. listObjects() only return the first 25. To get the rest as well, you’ll need paging.

Paging through files

  1. Page through the files on the SFTP server. Replace the code from the previous paragraph with the following.

let objects = [];
let pageToken = undefined;
do {
  const listObjectsResult = await bucketsClient.listObjects({
    bucketName: bucketName,
    pageToken: pageToken,
  });
  objects = objects.concat(listObjectsResult.items || []);

  pageToken = listObjectsResult.nextPageToken;
} while (pageToken !== undefined);

console.info(objects.length);
  1. Run the code.

node main.js

This should output the following:

27

When there are more results to fetch, listObjects() will add the field nextPageToken to its result. If you then call listObjects() again, passing in the page token, you will get the next page of results. The first time you call listObjects(), you won’t have a page token yet, so you can pass in undefined to get the first page.

You can use concat() to put the files of each page into a single array.

Paging through users

  1. Replace the line let pageToken = undefined; with the following.

pageToken = undefined;
  1. Page through all SFTP users. Replace the code that lists users, at the beginning of main(), with the following.

let users = [];
let pageToken = undefined;
do {
  const listUsersResult = await sftpClient.listUsers({
    pageToken: pageToken,
  });
  users = users.concat(listUsersResult.items);

  pageToken = listUsersResult.nextPageToken;
} while (pageToken !== undefined);

There are only four users, so for this challenge, paging through the users won’t make a difference, but it makes the scripts more robust. It works just like paging through files.

Counting files

  1. For every user, loop through all files and count the ones that are in their home directory. Add the following code to the end of main().

for (let user of users) {
  const homeDirectory = user.homeDirectory.substring(1);

  let fileCount = 0;
  for (let object of objects) {
    if (object.key.startsWith(homeDirectory)) {
      fileCount++;
    }
  }

  console.info(`user: ${user.description} - files: ${fileCount}`);
}
  1. Run the code.

node main.js

This should output the following:

user: Blank Management - files: 17
user: Blank Shipping - files: 11
user: Paper Wrap - files: 10
user: Blank Billing - files: 4

Every file has a field called key, which contains the full path, for example blank/billing/thank-you. If the start of the key is the same as the user’s home directory, then the user owns the file. The only problem is that the home directory has a / at the start and the key doesn’t. user.homeDirectory.substring(1) strips the / from the home directory.

Detecting EDI

  1. Import the stream consumer package. Add the following code at the top of main.js.

const consumers = require("stream/consumers");
  1. Download each file from the SFTP server. Add the following code right below fileCount++.

const getObjectResult = await bucketsClient.getObject({
  bucketName: bucketName,
  key: object.key,
});
const contents = await consumers.text(getObjectResult.body);
  1. Add a counter for EDI files. Add the following code right below let fileCount = 0.

let ediCount = 0;
  1. Determine if the file contents is EDI. Add the following right below the previous code.

if (contents.startsWith("ISA")) {
  ediCount++;
}
  1. Output the result. Replace the last line that calls console.info() with the following code.

console.info(`user: ${user.description} - files: ${fileCount} - EDI: ${ediCount}`);

This should output the following:

user: Blank Management - files: 17 - EDI: 9
user: Blank Shipping - files: 11 - EDI: 7
user: Paper Wrap - files: 10 - EDI: 8
user: Blank Billing - files: 4 - EDI: 2

When you call getObject() the result includes a stream that allows you to download the contents of the file. The easiest way to do this, is using consumer.text() from Node.js’s stream consumer package.

To detect if a document is EDI, you can check if it starts with the letters ISA. It’s easy to do and I expect it covers at least 99% of cases, so I call that good enough.

Challenge completed

const buckets = require("@stedi/sdk-client-buckets");
const sftp = require("@stedi/sdk-client-sftp");
const consumers = require("stream/consumers");

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});

const sftpClient = new sftp.Sftp({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});

async function main() {
  let users = [];
  let pageToken = undefined;
  do {
    const listUsersResult = await sftpClient.listUsers({
      pageToken: pageToken,
    });
    users = users.concat(listUsersResult.items);

    pageToken = listUsersResult.nextPageToken;
  } while (pageToken !== undefined);

  if (users.length == 0) {
    console.info("No users.");
    return;
  }

  const bucketName = users[0].bucketName;
  console.info(bucketName);

  let objects = [];
  pageToken = undefined;
  do {
    const listObjectsResult = await bucketsClient.listObjects({
      bucketName: bucketName,
      pageToken: pageToken,
    });
    objects = objects.concat(listObjectsResult.items || []);

    pageToken = listObjectsResult.nextPageToken;
  } while (pageToken !== undefined);

  console.info(objects.length);

  for (let user of users) {
    const homeDirectory = user.homeDirectory.substring(1);

    let fileCount = 0;
    let ediCount = 0;
    for (let object of objects) {
      if (object.key.startsWith(homeDirectory)) {
        fileCount++;

        const getObjectResult = await bucketsClient.getObject({
          bucketName: bucketName,
          key: object.key,
        });
        const contents = await consumers.text(getObjectResult.body);
        if (contents.startsWith("ISA")) {
          ediCount++;
        }
      }
    }

    console.info(`user: ${user.description} - files: ${fileCount} - EDI: ${ediCount}`);
  }
}

main();

Jul 12, 2022

Products

Building an EDI system or B2B integration requires a secure, scalable way to exchange files with trading partners. With Stedi SFTP, developers can provision users and begin transferring files in seconds. Files received via Stedi SFTP are immediately available in Stedi Buckets - a simple, reliable data store – for further processing. With a usage-based pricing model and no servers to manage, builders can easily offer SFTP connectivity to their trading partners as part of a new or existing B2B workflow without incurring fixed costs or operational overhead.

Starting today, both Stedi SFTP and Buckets are now Generally Available.

Features

  • Provision SFTP users via the Stedi dashboard or the API

  • Securely manage credentials

  • Scale to an unlimited number of trading partners

  • Send, receive, and store an unlimited number of files

Where Stedi SFTP fits in

Stedi SFTP works for any workflow requiring file transfer between trading partners. If you’re building an EDI integration, you could use Stedi SFTP to:

  • Receive EDI files from your trading partner via SFTP, and retrieve those files using the Buckets SDK

  • Using the Buckets SDK, send EDI files for your trading partner to pick up via SFTP.

Stedi SFTP is fully integrated with Stedi Buckets. When you upload files programmatically via the Buckets SDK, those files are available to your trading partner via their SFTP credentials. And each time your trading partner uploads files via SFTP, those files are available via the Buckets SDK, too. Check out the Buckets documentation for more details.

Using Stedi SFTP

The first step is to create an SFTP user, which you can do via the SFTP UI or via the API. Once you have the generated credentials, you can connect to the SFTP endpoint using your favorite SFTP client:

sftp WL9F11A9@data.sftp.us.stedi.com

WL9F11A9@data.sftp.us.stedi.com's password:
Connected to data.sftp.us.stedi.com.
sftp

After you've uploaded a file via your SFTP client, you can retrieve the file via Stedi's Buckets SDK using the following code:

// Stedi SDK example for GetObject from your local machine

import { BucketsClient, GetObjectCommand } from "@stedi/sdk-client-buckets";
import consumers from 'stream/consumers';

// Enter your Stedi API key here
const apiKey = "<your-stedi-api-key>"; // Change this to your Stedi API key

async function main() {

 // create a new BucketsClient to the Stedi US region
 const stediclient = new BucketsClient({
  region: "us",
  apiKey: apiKey
 });

 // Prepare a GetObject command
 const getObject = new GetObjectCommand(
  {
   bucketName: "your-stedi-bucket" // Change this to your existing Bucket name
   key: "document.txt" // Change this to an existing object name
  }
 );

 // Send the request to GetObject request to Stedi
 const getObjectOutput = await stediclient.send(getObject);

 // Pretty print the object output
 console.log(await consumers.text(getObjectOutput.body));

}

// Run the main function
main();

In this example, we've printed the file as a string, but you could also write it to a file on your disk.

Stedi SFTP pricing

Stedi SFTP is billed based on the number of files and the amount of data uploaded and downloaded. There are no minimum fees, no monthly commitments, and no upfront costs to use Stedi SFTP.

SFTP is backed by Stedi Buckets for file storage and retrieval, and related storage and data transfer charges will be billed separately.

Stedi SFTP and Stedi Buckets are now Generally Available

Stedi SFTP offers a hassle-free way to provision SFTP access as part of your EDI solution or B2B integration, without any operational overhead or minimum cost. It gives developers the ability to configure SFTP access for their trading partners via the UI or API. Developers can programmatically access the same files using the Stedi Buckets SDK.

Get started today.

Jun 24, 2022

EDI

A colleague told me about transaction set variants and now I want to get some more experience with them myself. In a nutshell, the problem is that one transaction set can represent different kinds of documents. For example, he showed me an implementation guide that specified that the 204 Motor Carrier Load Tender could be used to create, cancel, modify, or confirm a load tender. That feels like four different documents to me, but they are all represented by the same transaction set. So, how do you deal with that in practice?

My goal isn’t to come up with a solution, but to understand the problem. I’ll take an implementation guide that describes some transaction set variants and then I’ll try to come up with a strategy to deal with them. I don’t want to build a full implementation; I want to get a feel of what it takes to handle this in practice.

I’m going to look at the implementation guide for the 850 Purchase Order from Amazon, because I happen to have a copy. This means that today, I’m a wholesaler! Amazon sends me a purchase order, now what.

Understanding the variants

I don’t sell directly to the consumer; I leave that to Amazon, so they need my product in their warehouse. That’s why they send me an 850 Purchase Order. How do I know that I have to deal with transaction set variants? The implementation guide from Amazon doesn’t contain any preamble. It goes straight into the default information.

The start of the implementation guide, with the heading '850 Purchase Order' followed by a generic description of the transaction set.

That generic description of the transaction set doesn’t do me any good. Fortunately, I know to keep an eye out for transaction set variants, thanks to my colleague. The first segment of the transaction set (I’m not counting the envelopes) seems to contain what I’m looking for.

The details of the BEG segment as described by the implementation guide, with the BEG02 element highlighted. The element lists four codes: CN, NE, NP, and RO.

There seem to be 4 variants. Although, as you can see above, the guide mentions that there are 69 total codes. I assume that means that the standard defines 69 codes, but Amazon only uses 4 of them. Surely, Amazon doesn’t mean to say that they use 65 other codes as well, but they’re not going to tell you any more about them. Let me check that assumption with a look at the standard transaction set. Yes, that one has 69 codes. Good.

The details of the BEG segment as described by the standard. A highlight shows that the standard contains 69 codes for the BEG02 element.

So, what variants do we have? New Order I understand: that’s a new order. I feel so smart right now! Rush Order is an order that needs to be rushed, but I’m not sure how this is supposed to be treated differently than a new order. Maybe it isn’t different in terms of EDI messaging and it’s just a notification to the business: do whatever you need to do to rush this. Perhaps alarm bells should go of to let everyone know to drop whatever it is they’re doing and take care of this order. I have no idea what a consigned order is, so I should ask a domain expert, but I’m going to ask Google instead.

If I understand correctly, with a normal order, Amazon buys a product from me, puts it in their warehouse, and can then do whatever they want with it, although presumably they’ll try to sell it. With a consigned order, Amazon puts the product in their warehouse, but it still belongs to me until Amazon sells it. At that point, Amazon handles the transaction with the customer and they forward me the money, minus a transaction fee they keep for their work. If the product doesn’t sell, that’s my loss and Amazon can ship it back to me to free up some shelfspace.

Let’s see if I can get this confirmed or corrected by someone more knowledgeable than the Internet. Zack confirmed it.

That leaves New Product Introduction. Does that mean that the first time Amazon orders that product from you, it will have a separate code? Why would that be the case? Again, I’m lacking domain knowledge. For now, I’ll assume you can handle it like New Order.

Handling the variants

Now that I have an idea of the variants, I need to decide how to deal with them. That’s not a technical issue; it depends on the business needs. Since this exercise takes place in a fictitious context, there’s no one I can ask questions about the business. No matter, I might not be able to get answers, but I still should be able to figure out the questions.

The first one is whether we use consignment or not. It seems to me that we either do all our orders with consignment or none of them. That implies that we receive either NE-orders (New Order) or CN-orders (Consigned Order). That’s an understanding we need to come to with Amazon before we start processing their EDI messages. Other than that, I don’t expect a difference in the way the orders need to be routed. In both cases, the warehouse needs to fulfill the order and Finance needs to process it. How Finance processes each order will be different, but that’s beyond my concern.

A rush order may be a simple boolean flag in the system, or it may have to kick off its own, separate flow, depending on how the business operates. Does the business need me to do something special in this case? If there’s a separate flow, can we kick that off based on the simple boolean flag? That seems reasonable to me and it means that we can map the message regardless of the impact a rush order has on the business. I might need to route it somewhere else if there’s a special department handling rush orders, though.

The considerations for a new product introduction are similar to those for rush orders. If there’s a special flow, can we kick it off based on a boolean flag? Because that would make mapping simple. If we need to inform specific people of a new product introduction, we can do that based on the flag as well.

A minimal mapping

Even without the answers, I can make some reasonable assumptions about how the system should work and create a mapping based on that. I’m not going to map the entire 850, because that’s not what I’m trying to figure out here. I’m interested in handling the different variants and for that, I only need to map the BEG-segment. I can get away with that, because all three variants—NE, NP, and RO—are basically the same, except for a flag or two. Yes, I’m assuming non-consigned orders, but even that doesn’t make a big difference. For handling consigned orders, just swap out NE for CN and keep the flags. Okay, let’s open Mappings and start mapping.

The starting page for Mappings.

I’ll come up with my own custom target JSON based on the implementation guide. Yes, this is cheating, because typically you have a defined JSON shape you need to map to, but the principle is the same.

An example of the target JSON, with four fields: purchaseOrderNumber, orderDate, isRushOrder, isProductIntroduction.

I decided to go with two flags: one for rush order, one for product introduction. Another option is to create a field type that can be normal, rush, or introduction. Doesn’t matter too much; I just like the flags.

The source JSON is just a BEG-segment. That’s small enough that I can type the EDI by hand. However, in order to do the mapping, I need to convert it to JSON, because that’s what Mappings uses. I’ll use Inspector to translate my handcrafted segment to JEDI, which is Stedi’s JSON representation of EDI.

A screenshot of the Inspector parsing a BEG segment.

Inspector doesn’t like that single segment very much. I guess I should tell it which transaction set it’s dealing with.

Hey, I just noticed that the Amazon implementation guide provides sample data for each individual segment, so I didn’t have to type it by hand. That’s marvelous.

An example of a BEG segment, as provided by the Amazon implementation guide.

I add a transaction set identifier code. That’s still not enough to make Inspector happy, but it’s enough to keep it satisfied and it does its best to produce some JEDI. It looks fine for my purposes, so kudos to Inspector.

A screenshot of the Inspector parsing a BEG segment wrapped in a transaction set envelope.

The purchase order is trivial to map, because it’s a one-to-one copy of a specific field.

The mapping expression for the field purchaseOrderNumber.

The date isn’t much harder. It just requires a minimal bit of conversion, because I like dashes in my date.

The mapping expression for the field orderDate.

Will the flags be more challenging?

The mapping expression for the field isRushOrder.

That’s not quite right. The code in the sample JEDI isn’t NE, but new_order_NE. JEDI makes the values more readable, but now I have to figure out what the JEDIfied versions are of RO and NP. Fortunately, Inspector can help me here.

A screenshot of the Inspector showing the JEDI values for RO and NP.

Now that I know that, the mapping expressions for the flags will be simple.

The corrected mapping expression for the field isRushOrder.The mapping expression for the field isProductIntroduction.

That’s it?

The whole exercise was less of a hassle than I expected to be honest. Granted, I made things easy on myself by assuming only a single transaction set in the EDI message and by mapping only a single segment, but why would I want to make things hard for myself?

That’s not the real reason it was easier than expected, though. The real reason is that the variants of the Amazon 850 purchase order are all pretty much the same. I was able to handle them with nothing more than two flags. Most transaction sets that have variants will be more difficult to handle. If you’re disappointed by the lack of suffering I had to go through this time, then you can take comfort in the fact that the next transaction set with variants is bound to cause me more of a headache.

Jun 22, 2022

EDI

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

This document is the tl;dr of EDI. It won’t contain everything. It may not answer all your questions. It will only apply to X12. But it will save you a ton of time if you’re trying to decipher your first EDI document.

For this document, I’ll be using an 856 Ship Notice as an example. If you’re wondering what the heck an 856 Ship Notice is, you’re in the right place. Anyway, here’s the example.

ISA*00*          *00*          *ZZ*MYORG          *ZZ*Interchange Rec*200831*2324*U*00501*100030537*0*P*>~
GS*SH*MYORG*Interchange Rec*20200831*2324*200030537*X*005010~
ST*856*300089443~
BSN*00*P1982123*20200831*2324*0001~
DTM*011*20200831*2324~
HL*1**S~
TD1*CTN*1****G*2*LB~
TD5**2*FDE*U********3D*3D*3D~
DTM*011*20200831~
N1*ST*SOO KIM*92*DROPSHIP CUSTOMER~
N3*26218 QUENTIN TURNPIKE~
N4*REANNAFORT*MS*51135~
HL*2*1*O~
PRF*1881225***20200831~
HL*3*2*P~
MAN*CP*037178019302492~
HL*4*3*I~
LIN*1*VC*11216.32*SK*RGR-11216.32~
SN1*1*1*EA~
PID*F****ALL TERRAIN ENTRY GUARD KIT 2020 JEEP WRANGLER J~
CTT*4~
SE*20*300089443~
GE*1*200030537~
IEA*1*100030537

What is EDI?

EDI a structured way for businesses to send documents back and forth. It can be:

  • a purchase order (“I want to buy this!”),

  • an invoice (“Pay me for this!”),

  • a ship notice (“I sent you this!”),

  • or any of 300+ other transaction types.

Think of it as a JSON Schema to cover all possible business transactions and details, but before JSON, or XML, or the World Wide Web were invented.

You might hear people talk about X12 EDI. This is just a specific format of EDI. Another common one is EDIFACT. We’ll focus on X12 EDI here.

You might also hear of 5010 or 4010 . These are specific versions (formally known as releases) of EDI. Think of a versioned API, which might have small but breaking changes as you increase versions. EDI is an old standard and it required some changes over the years.

Structure

Segments, elements, and separators

When you first look at the 856 Ship Notice, you’ll see the following:

ISA*00*          *00*          *ZZ*MYORG          *ZZ*Interchange Rec*200831*2324*U*00501*100030537*0*P*>~
GS*SH*MYORG*Interchange Rec*20200831*2324*200030537*X*005010~
ST*856*300089443~
BSN*00*P1982123*20200831*2324*0001~
DTM*011*20200831*2324~

This feels overwhelming. Don’t let it be.

Notice that the example of this particular EDI document is split into 5 lines. (The full document has 24 lines, as you can see at the start of the article.) Each line is called a segment. Each segment is broken into multiple elements.

Let’s start by breaking down what’s happening on line 1.

  1. It starts with ISA. This is the segment name. We’ll often refer to the segment by its name. “Ohh, they’re missing an ABC segment”, or “That’s an invalid value in the XYZ segment”. This is the ISA segment.

  2. There are a bunch of letters and numbers separated by *. An asterisk serves as a separator between elements (technically, the fourth character—right after ISA—is the element separator, but it’s commonly a *). Elements are often referred to by their numerical index within a particular segment. For example, notice the 200831 near the end of the ISA segment. That is called ISA09 as it is the ninth element in the ISA segment. When you look at the definition of the segment, you’ll see that it refers to the Interchange Date in YYMMDD format. Thus, 200831 is August 31, 2020.

In the file we’ve looked at so far, each segment is on a different line. Sometimes you’ll get a file that’s all on one line. EDI documents will generally use ~ or a newline \n to distinguish between segments. (It’s technically the 105th byte in the ISA segment.) When reviewing a document, I find it much easier to split it into new lines based on the segment delimiter.

Common segments

Now that we know the basics of segments, elements, and separators, let’s look at some common segments. Specifically, we’re going to look at ISA, GS, and ST.

ISA

We looked at the ISA segment in the previous section. This will always be the first line in an EDI document. It’s called an interchange and contains metadata that applies to the document as a whole. This includes the document’s date and time, a control number (similar to an auto-incrementing primary key), whether it’s a test document, and more.

Typically, there’s only one ISA segment in a document, but some organizations combine multiple EDI documents into one.

GS

The GS segment will always be next. It’s the functional group and it includes additional metadata about the transactions within the group. The most important value is the last one (GS08) which indicates the X12 version that is used by the document. In the example above, the value of this element is 005010. That indicates it’s using the 5010 version.

The X12 spec allows multiple functional groups in a single document.

ST

The ST segment is for the actual transaction being sent. This is the star of the show and the reason we’re all here. It could be an invoice, a purchase order, or any other of the many transaction types.

Each functional group can have multiple transactions.

ST01 identifies the transaction type for the given transaction. In the example above, it says 856, which maps to a ship notice.

Other common transaction types are:

Closing segments

Finally, for each of these key sections, there is a closing segment (formally a trailer segment) to indicate when they end. These closing segments include a checksum and a control number.

The code fragment above shows the last three lines of our ship notice example. The SE segment indicates the end of the ST block. SE01 refers to the number of segments within the ST block. The value 20 indicates there were 20 segments in that transaction. SE02 is a control number that matches the control number from the ST segment.

Likewise, the GE segment indicates the end of the functional group, which started with GS. GE01 states how many transactions were included within the functional group, and GE02 is a control number for the group. Same thing with IEA which closes the ISA block.

Implementation guides

The X12 spec is expansive, and most companies don’t need all of it. Accordingly, companies will limit what they support. The terms may be dictated and standardized by the trading partner with the most market power, or it may be by mutual agreement between the two trading partners.

As a result, you may get an implementation guide indicating what is supported by a your trading partner. We’ll take a look at an example for a mapping guide for an 856 Ship Notice. There are two kinds of pages here: the overview page, and the segment detail page.

Overview

The overview page shows all the segments that are allowed in this particular transaction.

The Req column shows whether a segment is mandatory (M) or optional (O).

For this transaction, only two segments are optional.

The Max Use column shows the maximum number of times a segment can be repeated.

For example, the DTM segment can be used up to 10 times, which means a ship notice can have 10 dates associated with it.

A loop is a collection of segments that can be repeated, in this case up to 200 times.

A loop can also contain other loops.

Note that repeats are multiplicative. The top-level loop can be repeated 200,000 times and the inner loop 200 times, so the N1, N3, and N4 segment can be included 200,000 × 200 = 40,000,000 times.

Another look at the example document shows multiple iterations of the HL loop.

Details

The overview usually doesn’t take up more than a page or two. The rest of the implementation guide is made up of segment detail pages. Here’s one for the PRF segment.

The details page describes each element within the segment.

For example, the PRF01 segment is mandatory and can have at most 22 characters.

The PRF06 segment is optional and can have at most 30 characters.

At times, an element will accept a small number of codes that map to specific values, similar to an enum. You can see an example of that on the details page for DTM.

Transforming EDI

So far, we’ve looked at a raw EDI document and you may have noticed that the format is hard to use with your favorite programming language. It would take a lot of string parsing, array indexing, and other tedium. Stedi offers an EDI parser, converter, and validator called EDI Core.

We can take our EDI example and give it to EDI Core. EDI Core will return a JSON representation of the EDI that we call JEDI.

{
  "interchanges": [
    {
      "interchange_control_header_ISA": {
        "authorization_information_qualifier_01": "no_authorization_information_present_no_meaningful_information_in_i02_00",
        "authorization_information_02": "",
        "security_information_qualifier_03": "no_security_information_present_no_meaningful_information_in_i04_00",
        "security_information_04": "",
        "interchange_id_qualifier_05": "mutually_defined_ZZ",
        "interchange_sender_id_06": "MYORG",
        "interchange_id_qualifier_07": "mutually_defined_ZZ",
        "interchange_receiver_id_08": "Interchange Rec",
        "interchange_date_09": "200831",
        "interchange_time_10": "2324",
        "repetition_separator_11": "U",
        "interchange_control_version_number_12": "00501",
        "interchange_control_number_13": "100030537",
        "acknowledgment_requested_14": "no_interchange_acknowledgment_requested_0",
        "interchange_usage_indicator_15": "production_data_P",
        "component_element_separator_16": ">"
      },
      "groups": [
        {
          "functional_group_header_GS": {
            "functional_identifier_code_01": "ship_notice_manifest_856_SH",
            "application_senders_code_02": "MYORG",
            "application_receivers_code_03": "Interchange Rec",
            "date_04": "20200831",
            "time_05": "2324",
            "group_control_number_06": "200030537",
            "responsible_agency_code_07": "accredited_standards_committee_x12_X",
            "version_release_industry_identifier_code_08": "005010"
          },
          "transaction_sets": [
            {
              "set": "856",
              "heading": {
                "transaction_set_header_ST": {
                  "transaction_set_identifier_code_01": "856",
                  "transaction_set_control_number_02": "300089443"
                },
                "beginning_segment_for_ship_notice_BSN": {
                  "transaction_set_purpose_code_01": "original_00",
                  "shipment_identification_02": "P1982123",
                  "date_03": "20200831",
                  "time_04": "2324",
                  "hierarchical_structure_code_05": "shipment_order_packaging_item_0001"
                },
                "date_time_reference_DTM": [
                  {
                    "date_time_qualifier_01": "shipped_011",
                    "date_02": "20200831",
                    "time_03": "2324"
                  }
                ]
              },
              "detail": {
                "hierarchical_level_HL_loop": [
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "1",
                      "hierarchical_level_code_03": "shipment_S"
                    },
                    "carrier_details_quantity_and_weight_TD1": [
                      {
                        "packaging_code_01": "CTN",
                        "lading_quantity_02": "1",
                        "weight_qualifier_06": "gross_weight_G",
                        "weight_07": "2",
                        "unit_or_basis_for_measurement_code_08": "pound_LB"
                      }
                    ],
                    "carrier_details_routing_sequence_transit_time_TD5": [
                      {
                        "identification_code_qualifier_02": "standard_carrier_alpha_code_scac_2",
                        "identification_code_03": "FDE",
                        "transportation_method_type_code_04": "private_parcel_service_U",
                        "service_level_code_12": "three_day_service_3D",
                        "service_level_code_13": "three_day_service_3D",
                        "service_level_code_14": "three_day_service_3D"
                      }
                    ],
                    "date_time_reference_DTM": [
                      {
                        "date_time_qualifier_01": "shipped_011",
                        "date_02": "20200831"
                      }
                    ],
                    "party_identification_N1_loop": [
                      {
                        "party_identification_N1": {
                          "entity_identifier_code_01": "ship_to_ST",
                          "name_02": "SOO KIM",
                          "identification_code_qualifier_03": "assigned_by_buyer_or_buyers_agent_92",
                          "identification_code_04": "DROPSHIP CUSTOMER"
                        },
                        "party_location_N3": [
                          {
                            "address_information_01": "26218 QUENTIN TURNPIKE"
                          }
                        ],
                        "geographic_location_N4": {
                          "city_name_01": "REANNAFORT",
                          "state_or_province_code_02": "MS",
                          "postal_code_03": "51135"
                        }
                      }
                    ]
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "2",
                      "hierarchical_parent_id_number_02": "1",
                      "hierarchical_level_code_03": "order_O"
                    },
                    "purchase_order_reference_PRF": {
                      "purchase_order_number_01": "1881225",
                      "date_04": "20200831"
                    }
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "3",
                      "hierarchical_parent_id_number_02": "2",
                      "hierarchical_level_code_03": "pack_P"
                    },
                    "marks_and_numbers_information_MAN": [
                      {
                        "marks_and_numbers_qualifier_01": "carrier_assigned_package_id_number_CP",
                        "marks_and_numbers_02": "037178019302492"
                      }
                    ]
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "4",
                      "hierarchical_parent_id_number_02": "3",
                      "hierarchical_level_code_03": "item_I"
                    },
                    "item_identification_LIN": {
                      "assigned_identification_01": "1",
                      "product_service_id_qualifier_02": "vendors_sellers_catalog_number_VC",
                      "product_service_id_03": "11216.32",
                      "product_service_id_qualifier_04": "stock_keeping_unit_sku_SK",
                      "product_service_id_05": "RGR-11216.32"
                    },
                    "item_detail_shipment_SN1": {
                      "assigned_identification_01": "1",
                      "number_of_units_shipped_02": "1",
                      "unit_or_basis_for_measurement_code_03": "each_EA"
                    },
                    "product_item_description_PID": [
                      {
                        "item_description_type_01": "free_form_F",
                        "description_05": "ALL TERRAIN ENTRY GUARD KIT 2020 JEEP WRANGLER J"
                      }
                    ]
                  }
                ]
              },
              "summary": {
                "transaction_totals_CTT": {
                  "number_of_line_items_01": "4"
                },
                "transaction_set_trailer_SE": {
                  "number_of_included_segments_01": "20",
                  "transaction_set_control_number_02": "300089443"
                }
              }
            }
          ],
          "functional_group_trailer_GE": {
            "number_of_transaction_sets_included_01": "1",
            "group_control_number_02": "200030537"
          },
          "release": "005010"
        }
      ],
      "interchange_control_trailer_IEA": {
        "number_of_included_functional_groups_01": "1",
        "interchange_control_number_02": "100030537"
      },
      "delimiters": {
        "element": "*",
        "segment": "~",
        "sub_element": ">"
      }
    }
  ],
  "__version": "jedi@2.0"
}

This is more verbose, but it’s much easier to deal with from code. You can see the EDI and the JSON side-by-side by using the Inspector. It’ll show an explanation of each segment, too.

Getting started

This is certainly not all you need to know, but it’s enough to get you started. Don’t forget to check out our blog for more information on working with EDI.

Jun 13, 2022

EDI

I don’t blame EDI for failures in logistics any more than I blame kittens for messing up my floor. I only got Sam and Toby a day ago, but they’re the cutest balls of fuzz ever to grow legs and they jumped straight into my heart. Still, as I’m staring at the puddle on my floor, I’m convinced that there's a design flaw in their digestive system. I don’t remember their breakfast being this chunky, or this sticky. Sputter gunk kittens are a new experience for me, so I call a friend and she recommends a steam cleaner.

There’s a store that sells steam cleaners about a thirty-minute drive from here, but that means I’d have to leave the kittens alone and I don’t want to do that. After a short search, I find an online store with same-day delivery, called Howard’s House of Cleanliness—How Clean, for short. They sell steam cleaners they do not own. That’s not a scam; it’s dropshipping and it’s a marvel of online logistics.

Traditionally, How Clean would order a whole bunch of steam cleaners from Vaporware and keep them in their own warehouse. Then, if I order one, How Clean would ship it to me.

  1. How Clean sends an order to Vaporware.

  1. Vaporware hands of a bunch of steam cleaners to Truckload.

  1. Truckload ships a bunch of steam cleaners to How Clean.

  1. How Clean stores the steam cleaners in their own warehouse.

  1. I order a steam cleaner from How Clean.

  1. How Clean hands off one steam cleaner to Last Mile.

  1. Last Mile drives the steam cleaner to me.

  1. I receive my steam cleaner.



Dropshipping

In the case of dropshipping, the steam cleaners don’t need to go to How Clean’s warehouse.

  1. I order a steam cleaner from How Clean.

  1. How Clean forwards the order to Vaporware.

  1. Vaporware hands over one steam cleaner to Last Mile.

  1. Last Mile drives the steam cleaner to me.

  1. I receive my steam cleaner.

This is convenient for How Clean, because they don’t have to order a bunch of steam cleaners up front in the hope of selling them. It’s convenient for Vaporware, because they don’t have to open an online shop and deal with customers. And customers don’t even notice, unless something goes wrong.

Toby is finding out that gravity doesn’t always work in his favor. A monstera is a good house plant for decoration, but not so much for climbing. It’s certainly not strong enough to carry his full weight.

My floor is now decorated with one cat, one plant, and a lot of dirt, so I check my email. Toby gets up, a little confused. He knows where the plant came from, of course, but the dirt is new to him. I hit refresh; nothing. He decides to do some digging and discovers that underneath all the dirt is more dirt. Refresh, refresh, refresh; nothing, nothing, nothing. Sam joins in. To her, the unpotted plant is more of an artwork than a research project and the carpet is her canvas. I need a drink. The steam cleaner should’ve shipped by now, but apparently How Clean can’t be bothered to send me a confirmation. It’s time to give them a call. Of course, now I’m on hold.

Somewhere, the logistics failed. There’s always quite a bit of communication going on between different parties. That doesn't happen by someone picking up the phone; it all happens electronically. The documents that are sent back and forth adhere to a specific format. EDI is that format. There are different documents involved and they each have a number and a name. For example, the document that How Clean sends to Vaporware is called an 850 Purchase Order.

  1. How Clean sends an 850 Purchase Order to Vaporware to place an order.

  1. Vaporware sends a 216 Motor Carrier Shipment Pick-up Notification to Last Mile to request delivery.

  1. After Last Mile picks up the package, Vaporware sends an 856 Ship Notice to How Clean to let them know the order has shipped.

That’s how it should work.

Sam has found a spot near the window in the warm sunlight and starts dancing with her own shadow. Toby looks at her from across the room and is content observing the merriment, but that won’t do for Sam. She wants him to join in the fun. She runs over to him and on the way, she knocks over my drink.

“Good afternoon. Howard’s House of Cleanliness. This is Mira speaking. How may I help you?”
“Do you have kittens?”
“No sir, we don’t sell pets.”
“You sell steam cleaners, though, don’t you? I need my steam cleaner.”
Mira pulls up my record.
“That one hasn’t shipped yet.”
“It’s supposed to arrive today. Will it arrive today? I need it today.”
“Let me check. Oh, that’s odd: no delivery date. I don’t know yet. Perhaps if you ask again later.”
“How much later?”
“If you call me tomorrow, I should have an answer for you.”
“Well, tomorrow I don’t really need your help to figure out if it arrived today.”
“Oh no, sir, you’re right.”
Mira laughs. I don’t know why. I decide to make myself another drink, but then I think better off it.
“You know what, just cancel my order. I’ll get it somewhere else.”
“I’m sorry, but I can’t seem to cancel it. Maybe because it hasn’t shipped yet.”
“Wouldn’t it be more difficult to cancel it after it has shipped?”
Mira promises to keep an eye on my order and cancel it when she can.
“You’ll get your refund, sir, don’t worry.”

I don’t want a refund; I want my steam cleaner. I grab a roll of paper towels and try to get rid of the puddle. Sam grabs the end of the roll with her mouth, gives it a tug, then offers it to me. Someone messed up here and it isn’t my kittens.

How Clean’s website works, otherwise Mira wouldn’t have been able to pull up my details. Maybe they didn’t place the order with Vaporware. Or maybe the order picker at Vaporware got lost in their own warehouse. I don’t know, but someone didn’t send the document they were supposed to.

  1. The order I placed with How Clean arrived just fine, because Mira can see it in the system.

  1. It’s possible the 850 Purchase Order from How Clean to Vaporware didn’t arrive. It would explain why How Clean didn’t receive a delivery date from Vaporware; Vaporware doesn’t know there’s anything to deliver.

  1. It’s possible the 216 Motor Carrier Shipment Pick-up Notification from Vaporware to Last Mile didn’t arrive. It would explain why the order hasn’t shipped yet; Last Mile doesn’t know there’s something for them to ship.

  1. It’s possible the 856 Ship Notice from Vaporware to How Clean didn’t arrive. It would explain why Mira can’t see the delivery date; Vaporware didn’t give one to How Clean.

They’ll probably blame EDI; that’s what they always do. That’s wrong, though. EDI tells you what a document should look like; it’s not responsible for making sure the document arrives.

The paper towels have turned into papier mache. Enough! I put on my coat and grab my car keys. The kittens can manage for an hour, because whatever the state of my floor and my plant and my drink, Sam and Toby are just fine. Then I notice the clock and I realize: the store is closed. There’s nothing left for me to do.

I still want to know what went wrong. Maybe I can call Vaporware. No phone number, but I do have their address now. I know where that is; that’s not an industrial area. Judging from Street View, Vaporware is a pretty small business. That would explain— Is that the doorbell?

I call How Clean.
“It’s here,” I say.
“Let me check,” Mira says.
“There’s no need to check; it’s here.”
“No, it isn’t.”
“It isn’t?”
“No sir, I’m looking right at my computer screen and it clearly says it isn’t.”
“And I’m looking right at my steam cleaner and it clearly is. It’s here!”
“Well, it’s not supposed to be.”
“You and I disagree on that one.”
“Are you sure it’s the steam cleaner you ordered from us?”
“It’s not like I ordered steam cleaners from five different— It doesn’t matter. Could you please just cancel the cancellation, so that you get properly paid?”
“I can’t find the button for canceling a cancellation.”
“…”
“But I’ll figure it out.”
“Thank you.”
“Before you go, sir, one more thing. Seeing that this kind of concludes your order and all that, I wanted to ask you a question.”
“Go ahead.”
“Are you satisfied with your new steam cleaner?”

All of this could’ve been prevented. I have my steam cleaner, which means Last Mile picked it up from Vaporware, which means that Vaporware sent it to me, which means How Clean did put in the order. In other words, the 850 Purchase Order from How Clean arrived at Vaporware just fine, and Last Mile did receive the 216 Motor Carrier Shipment Pick-up Notification from Vaporware, so the problem must be that the 856 Ship Notice never got back to How Clean. That’s why How Clean thinks the steam cleaner hasn’t shipped yet.

Showing the ordering process with the 856 as the point of failure.

Since Vaporware is a small vendor, they don’t have the resources to properly automate this process. They could hire a third party to do the automation for them, but chances are they’re perfectly happy filling out the occassional 856 Ship Notice manually and sending it when they get around to it. That may not be fast enough for my taste, but it makes good business sense to Vaporware. It also makes Mira’s job harder.

How Clean does have another option, though. Since they are a large retailer, dealing with many more shipments than just the ones from Vaporware, they could strike a deal with Last Mile. Every time Last Mile is asked to deliver a package to one of How Clean’s customers, they can send a message to How Clean. And yes, EDI has a template for that message as well. It’s called a 214 Transportation Carrier Shipment Status Message.

The ordering process with Last Mile sending a 214 to How Clean.

How Clean can use this message from Last Mile as a backup. It’s bound to be more reliable than the 856 Ship Notice from Vaporware, because a large carrier like Last Mile does have this process automated. As soon as a courier picks up the steam cleaner from Vaporware, they will scan its bar code and the system will generate and send the 214 Transportation Carrier Shipment Status Message. Even if Vaporware is tardy with their 856 Ship Notice, How Clean can let me know with confidence that the order has shipped and Mira will never have to deal with me.

That was yesterday. Today, I’m watching how Toby discovers that a water bowl isn't just for drinking. At the same time, Sam indulges her creativity with some paw painting. It’s fine. There’s no mess they can make that my steam cleaner can’t handle. I sit down on the couch with my drink and then the doorbell rings.

My best guess is that Mira didn’t figure out how to undo the cancellation and, to be on the safe side, she put in another order for me. EDI can’t fix a broken system, nor can it prevent human error. If you’ve enriched your life with kittens, or puppies, or gerbils and you now find yourself in the market for a top-of-the-line steam cleaner, don’t order one from Howard’s House of Cleanliness. Just drop me a line; I have a spare.

Editor: Kasia Fojucik. Illustrator: Katherine Meng.

Jun 7, 2022

Guide

Big takeaway: If you use Stedi’s JSON APIs, you can safely ignore X12 control numbers. If a top-level controlNumber is required, use a dummy value.

Control numbers in X12 can be confusing. There are three different types, and they have weird formatting rules.

Luckily, if you use Stedi’s JSON APIs, you don’t need to deal with X12 control numbers at all. Even if you pass X12 directly to Stedi, you only need to follow a few validation rules. This guide covers:

  • What control numbers are

  • The different types of control numbers

  • How to use them with Stedi

What are control numbers?

Every X12 file contains three nested containers called envelopes:

  • Interchange – The outer envelope containing the entire EDI file. The file may contain one – and only one – interchange.

  • Functional group – A functional group of related transactions inside that envelope. An interchange can contain many groups.

  • Transaction set – An individual transaction in a group. A group can contain many transactions.

Each envelope has its own control number. The control number acts as an ID for that specific container.

Why have different control numbers?

Each control number serves a different purpose.

Interchange control numbers confirm whether an X12 file was received.
For example, Stedi sends an X12 file with interchange control number 000000001 to a payer. The payer can send back an acknowledgment for 000000001. Stedi knows that the file wasn’t lost.

Group control numbers help with batching and routing.
If the receiver indicates there was an error with group 234, the sender can just resend the transactions in that group. They don’t have to resend the entire file.

In some use cases, group control numbers are also used for internal routing. The receiver can split the X12 file by group and route each group’s transactions to a different system or department in their organization.

A transaction set control number identifies a specific transaction.
If there’s only an error with transaction 123456789, the sender can just resend that transaction – not the entire group or file.

Control numbers in Stedi’s JSON APIs

If you’re using Stedi’s JSON APIs, you can ignore X12 control numbers.
Our JSON APIs handle any needed X12 translation for you.

Don’t use X12 control numbers for tracking.
Don't use the controlNumber in API responses for transaction tracking. These values won't match what you pass in requests and aren't guaranteed to be unique to the transaction.

If you need to track claims, use the patient control number instead. See our How to track claims blog.

Control numbers in Stedi X12

Where control numbers are located
In an X12 file, each control number appears twice in an X12 message: once in the envelope's opening header and once in its closing trailer. The control number in the header must match the control number in its respective trailer.

The following table outlines each control number’s location and requirements.

Control number

Header location

Trailer location

Data type

Length

Interchange

ISA-13

IEA-02

Numeric (integer)

Exactly 9 digits. Can include leading zeroes.


Functional group

GS-06

GE-02

Numeric (integer)

1-9 digits.

Transaction set

ST-02

SE-02

Numeric (integer)

4-9 digits. Only include leading zeroes to meet length requirements.

Don’t worry about uniqueness.
If you pass X12 directly to Stedi using our X12 APIs or SFTP, you only need to ensure the ISA/IEA, GS/GE, and ST/SE envelopes are valid X12.

Stedi creates its own ISA /IEA and GS/GE envelopes before sending them to the payer. This means you can use any valid transaction set control number in the ST/SE envelope. The transaction set control number only has to be unique within its groups.

Get started

You don’t have to learn EDI or wrestle with control numbers to process healthcare transactions. Stedi’s modern JSON APIs can handle it for you.

Sign up for free and request a trial to try them out. It takes less than two minutes and gives you instant access.

May 31, 2022

Engineering

At Stedi, we are fans of everything related to AWS and serverless. By standing on the shoulders of giants, the engineering teams can quickly iterate on their ideas by not having to reinvent the wheel. Serverless-first development allows product backends to scale elastically with low maintenance effort and costs.

One of the serverless products in the AWS catalog is Amazon DynamoDB – an excellent NoSQL storage solution that works wonderfully for most of our needs. Designed with scale, availability, and speed in mind, it stands at the center of our application architectures.

While a pleasure to work with, Amazon DynamoDB also has limitations that engineers must consider in their applications. The following is a story of how one of the product teams dealt with the 400 KB Amazon DynamoDB item size limit and the challenge of ensuring data integrity across different AWS regions while leveraging Amazon DynamoDB global tables.

Background

I'm part of a team that keeps all its application data in Amazon DynamoDB. One of the items in the database is a JSON blob, which holds the data for our core entity. It has three large payload fields for customer input and one much smaller field containing various metadata. A simplified control flow diagram of creating/updating the entity follows.

The user sends the JSON blob to the application API. The API is fronted with Amazon API Gateway and backed by the AWS Lambda function. The logic within the AWS Lambda function validates and applies business logic to the data and then saves or updates the JSON blob as a single Amazon DynamoDB item.

The product matures

As the product matured, more and more customers began to rely on our application. We have observed that the user payload started to grow during the application lifecycle.

Aware of the 400 KB limitation of a single Amazon DynamoDB item, we started thinking about alternatives in how we could store the entity differently. One of such was splitting the single entity into multiple sub-items, following the logical separation of data inside it.

At the same time, prompted by one of the AWS region outages that rendered the service unavailable, we decided to re-architect the application to increase the system availability and prevent the application from going down due to AWS region outages.

Therefore, we deferred splitting the entity to the last responsible moment, pivoting our work towards system availability, exercising one of the Stedi core standards – "bringing the pain forward" and focusing on operational excellence.

We have opted for an active-active architecture backed by DynamoDB global tables to improve the service's availability. The following depicts the new API architecture in a simplified form.

Amazon Route 53 routes the user request to the endpoint that responds the fastest, while Amazon DynamoDB global tables take care of data replication between different AWS regions.

The tipping point

After some time, customers expected the application's API to allow for bigger and bigger payloads. We got a strong signal that the 400 KB limit imposed by the underlying architecture no longer fits the product requirements – that was the last responsible moment I alluded to earlier.

Considering prior exploratory work, we identified two viable solutions that would enable us to expand the amount of data the entity consists of, allowing the service to accept bigger payloads.

The first approach would be to switch from Amazon DynamoDB to Amazon S3 as the storage layer solution. Changing to Amazon S3 would allow the entity, in theory, to grow to the size of 5 TB (which we will never reach).

The second approach would be to keep using Amazon DynamoDB and take advantage of the ability to split the entity into multiple sub-items. The sub-items would be concatenated back into a singular entity object upon retrieval.

After careful consideration, we decided to keep using Amazon DynamoDB as the storage solution and opted to split the entity into multiple Amazon DynamoDB sub-items. Sticking with the same storage layer allowed us to re-use most of our existing code while giving us much-needed leeway in terms of the entity size. Because we have four sub-items, the entity could grow up to 1.6 MB.

After splitting up the core entity into four sub-items, each of them had the following structure.

{
  pk: "CustomerID",
  sk: "EntityID#SubItemA",
  data: ...
}

The challenges with global replication and eventual consistency

The diagrams above do not take data replication to secondary AWS regions into account. The data replication between regions is eventually consistent and takes some time – usually a couple of seconds. Due to these circumstances, one might run into cases where all four sub-items are only partially replicated to another region. Reading such data without performing additional reconciliation might lead to data integrity issues.

The following sequence diagram depicts the problem of potential data integrity issues in the read-after-write scenario.

Below the orange box, you can see four replications of the entity's sub-items. Below the red box, you can see a query to retrieve the entity. The query operation took place before all sub-items had the chance to replicate. Consequently, the fetched data consists of two sub-items from the new version and two ones from the old version. Merging those four sub-items leads to an inconsistent state and is not what the customer expects. Here's a code example that shows how this query would work:

import { DocumentClient } from "aws-sdk/clients/dynamodb";
const ddb = new DocumentClient();
const NUMBER_OF_SUB_ITEMS = 4;

const subItems = (
  await ddb
    .query({
      TableName: "MyTableName",
      KeyConditionExpression: "pk = :pk and begins_with(sk, :sk_prefix)",
      ExpressionAttributeValues: {
        ":pk": "CustomerID",
        ":sk": "EntityID#",
      },
      Limit: NUMBER_OF_SUB_ITEMS,
    })
    .promise()
).Items;

const entity = merge(subItems);

The application's API should not allow any data integrity issues to happen. The logic that powers fetching and stitching the entity sub-items must reconcile the data to ensure consistency.

The solution

The solution we came up with is a versioning scheme where we append the same version attribute for each entity sub-item. Attaching a sortable identifier to each sub-item, in our case – a ULID, allows for data reconciliation when fetching the data. We like ULIDs because we can sort them in chronological order. In fact, we don't even sort them ourselves. When we build a sort key that contains the ULID, DynamoDB will sort the items for us. That's very convenient in the context of data reconciliation.

Below, you can see how we added the version to the sort key.

{
  pk: "CustomerID",
  sk: "EntityID#Version1#SubItemA",
  data: ...
}

The version changes every time the API consumer creates or modifies the entity. Each sub-item has the same version in the context of a given API request. The version attribute is entirely internal to the application and is not exposed to the end-users.

By appending the version to the sub-items' Amazon DynamoDB sort key, we ensure that the API will always return either the newest or the previous entity version to the API consumer.

The following is a sequence diagram that uses the version attribute of the entity sub-item to perform data reconciliation upon data retrieval.

Similar to the previous diagram, we again see that two sub-items are replicated before we try to read the entity. Instead of getting the four sub-items, we now query at least the two latest versions by utilizing the version in the sort key. Here's a code example that shows how this query would work:

import { DocumentClient } from "aws-sdk/clients/dynamodb";
const ddb = new DocumentClient();
const NUMBER_OF_SUB_ITEMS = 4;

const subItems = (
  await ddb
    .query({
      TableName: "MyTableName",
      KeyConditionExpression: "pk = :pk and begins_with(sk, :sk_prefix)",
      ExpressionAttributeValues: {
        ":pk": "CustomerID",
        ":sk": "EntityID#",
      },
      // This line changed: We now load two versions if possible.
      Limit: 2 * NUMBER_OF_SUB_ITEMS,
    })
    .promise()
).Items;

const latestSubItems = filterForHighestReplicatedVersion(subItems);
const entity = merge(latestSubItems);

Instead of returning possibly inconsistent data to the customer hitting the API in the AWS region B, the API ensures that the entity consists of sub-items with the same version.

We do it by fetching at minimum two latest versions of the entity sub-items, sorted by the version identifier. Then we merge only those sub-items that are of the same version, favoring the newest version available. The API behaves in an eventually consistent manner in the worst-case scenario, which was acceptable for the product team.

As for the delete operation – there is no need for data reconciliation. When the user requests the entity's removal, we delete all of the entity sub-items from the database. Every time we insert a new group of sub-items into the database, we asynchronously delete the previous group, leveraging the Amazon DynamoDB Streams. This ensures that we do not accumulate a considerable backlog of previous sub-item versions, keeping the delete operation fast.

Engineering tradeoffs

There is a saying that software engineering is all about making the right tradeoffs. The solution described above is not an exception to this rule. Let us consider tradeoffs related to costs next.

The logic behind querying the data must fetch more items than necessary. It is not viable to only query for the last version of sub-items as some of them might still be replicating and might not be available. These factors increase the retrieval costs – see the Amazon DynamoDB read and write requests pricing.

Another axis of cost consideration for Amazon DynamoDB is the storage cost. Storing multiple versions of the entity sub-items increases the amount of data stored in the database. To optimize storage costs, one might look into using the Amazon DynamoDB Time to Live or removing the records via logic triggered by DynamoDB Streams. The product team chose the latter to keep storage costs low.

Conclusion

DynamoDB Global Tables are eventually consistent, with a replication latency measured in seconds. With increased data requirements, splitting entities into multiple sub-items becomes an option but must be implemented carefully.

This article shows how my team used custom logic to version entities and merge sub-items into the entity that our customers expect. It would be nice if DynamoDB Global Tables provided global consistency. Still, until then, I hope this article helps you understand the problems and a possible solution to implement yourself.

May 24, 2022

Company

Below is an edited version of our internal annual letter.

Our business is the business of business systems – specifically, we offer a catalog of tools that developers can use to build integration systems for themselves and their customers.

I started Stedi, though, not just to solve the problem of business integrations – nor just to build a big business or a successful business – but to build a world-class business.

A world-class business is a business that doesn't just win a market – it defines a category, dominates a field. Its name becomes synonymous with its industry. Its hallmark characteristic is not some single milestone or achievement, but enduring success over a very long period of time.

Types of businesses

When the topic of world-class businesses comes up, someone inevitably mentions Amazon. Amazon bills itself as a customer-obsessed company. It seems sensible that customer obsession is a path to building a world-class business; customers are the lifeblood of a business – without customers, a business will cease to exist.

The problem with customer obsession is that optimizing for delivering value to customers can cause a company to make structural choices that are detrimental to the company's long term operational efficiency. A company that is inefficient becomes painful to work for; when a company becomes painful to work for, the best people leave – and when the best people leave, a company becomes disadvantaged in its pursuit of delivering value to customers. Paradoxically, customer obsession as a company's paramount operating principle can lead to a declining experience for the customer over time.

The alternative to a customer-obsessed company is a company that is operations-obsessed. The operations-obsessed company optimizes for efficient operations – it works to continuously eliminate toil.

What exactly is toil? Toil has many definitions – a popular one is from Google's SRE book:

“The kind of work tied to running a production service that tends to be manual, repetitive, automatable, tactical, devoid of enduring value, and that scales linearly as a service grows.”

If "fear is the mind killer," toil is the soul-killer. Eliminating toil allows people to focus on the inherent complexity of the difficult, interesting problems at hand, rather than the incidental complexity caused by choices made along the way. Of course, most companies are neither customer- nor operations-obsessed, and, without an explicit optimization function, drift around aimlessly with middling results.

To be sure, a customer-obsessed company can also prioritize operational excellence, just as an operations-obsessed company can also – must also – prioritize delivering exceptional customer value. The difference between the two comes at the margins: when push comes to shove, what takes priority? In other words: will the company incur toil in order to further its success with customers? Or will it leave upside – that is, revenue or profit – on the table in order to further its long-term operational advantage?

Amazon is the former: a customer-obsessed company that also prioritizes operational excellence. Said in a slightly different way, Amazon is an operationally-excellent company subject to the constraint of customer demands. It is operations-obsessed up until the point that operational concerns conflict with customer demands, at which point Amazon – as a customer-obsessed company – runs the risk of prioritizing the latter. This is one reason why on-call at Amazon can get notoriously bad: when push comes to shove, operational concerns can lose out.[1]

Costco and ALDI, on the other hand, are examples of the latter: operations-obsessed companies that also excel at delivering customer value. They are customer-focused companies subject to the constraint of minimal operational overhead. When customer demands conflict with minimizing operational overhead, these companies sacrifice customer experience. This is why, as a Costco customer looking for paper towel, you have to walk through a harshly-lit warehouse with a giant shopping cart and pick a 48-pack off of a pallet – certainly not the most customer-friendly experience in the world, but in exchange for it, you get to purchase exceptional quality items at 14% over cost.

Stedi is definitively in this latter category: we are an operations-obsessed company. When push comes to shove, we are willing to leave revenue or profit on the table in order to reach ever-lower levels of operational toil.

I made this choice early on for two reasons. First, because I believe that over a long enough time horizon, an operations-obsessed company will lead to better outcomes for our customers. And second, because, well, I think it's just an enormously rewarding way to work. Eliminating toil brings me tremendous, tremendous satisfaction. If you feel the same way, you've come to the right place.

Thermodynamics and toil

Most of us have heard the first law of thermodynamics, though likely aren't familiar with it by name – it's also called the law of conservation of energy, and it states that energy can neither be created nor destroyed.

But the lesser-known second law is, in my opinion, a lot more interesting and applicable to our daily lives. The second law of thermodynamics states that entropy - the measure of disorder in a closed system – is always increasing.

What exactly does this mean? Eric Beinhocker sums it up nicely in The Origin of Wealth:

“The universe as a whole is drifting from a state of order to a state of disorder[…]over time, all order, structure, and pattern in the universe breaks down, decays, and dissipates. Cars rust, buildings crumble, mountains erode, apples rot, and cream poured into coffee dissipates until it is evenly mixed.”

The law of entropy applies to any thermodynamic system – that is, "any defined set of space, matter, energy, or information that we care to draw a box around and study." But while the total entropy – the disorder – of the universe at large is always increasing, the entropy of a given system can ebb and flow. "[A] system can use the energy and matter flowing through it to temporarily fight entropy and create order, structure, and patterns for a time[...]in [a system], there is a never-ending battle between energy-powered order creation and entropy-driven order destruction."

Stedi itself is a thermodynamic system. It imports energy in the form of capital (either from investors or from customers), which it uses to pay humans – that is, all of us – to create order and structure in the form of software that our customers can use, along with the various business systems required to support that customer-facing software.

You and I are thermodynamic systems, too – we use some of that capital to buy food, and we use the energy stored in that food to repair that damage that entropy has done since our last meal and to power the energy-hungry brain that works to build Stedi. From a thermodynamic standpoint, the food we eat becomes the intellectual assets – code, pixels, docs, emails, and more – that makes Stedi, Stedi. I think that's pretty remarkable (it's also another great reason why Stedi pays for lunch).

Anyways, some portion of our energy (your and my time, and Stedi's capital) is spent on building new order and structure, and some portion of it is dedicated to fighting the decay brought on by entropy.

Our word for this latter energy expenditure – energy expended just to stay in the same place – is toil. Our goal as a company is to spend as little energy on toil as possible, allowing us to allocate the maximum amount of energy possible to building new order and structure – that is, new value for our customers.

The practice of mitigating toil has a quality of what economists call increasing returns or agglomeration effects – the more toil we mitigate, the more attractive it is to work at Stedi, and the more attractive it is to work at Stedi, the more great people we can hire to build new order and structure and mitigate more toil, which starts the process over again.

Another way of thinking about this is that if we continue to mitigate toil wherever we find it, we'll find ourselves with ever-larger amounts of time to work on building new order and structure for our customers. That's the basic reason why an operations-obsessed business model wins out over a long enough time horizon: it's designed to minimize the surface area upon which entropy can do its work.

Ways to mitigate toil deserves its own memo, but there are two basic tactics that I wanted to touch on briefly: automation and elimination.

If you think about entropy as automatic decay, then one tactic is automatic [mitigation of] toil. One example of this is Dependabot. Dependabot doesn’t eliminate toil – rather, it employs a machine to fight automatic decay with automatic repair. Automatic repair is better than manual repair, but it’s not as good as eliminating it altogether; automation inevitably fails in one way or another, and if we’re going to disappear to a different dimension, there isn’t going to be anyone there to give our automatic decay-mitigation systems a kick when they get stuck.

Toil can be eliminated (from our thermodynamic system, at least) by drawing the system boundary a bit differently. When we use an external service instead of an external library, we’re moving the code outside of our system – thereby outsourcing the entropy-fighting toil to some third party. Not our entropy, not our problem.

As a general rule of thumb, when trying to reduce our surface area in the fight against entropy, we want to use the highest-order (that is, the lowest amount of entropy) system that satisfies our use case. A well-supported open source library is preferable to a library of our own (because the code entropy is someone else’s problem), and a managed service is preferable to open source (because fighting the code entropy and the operational entropy are someone else’s problem).

When we choose to whom and to what we outsource our fight against entropy, there are a number of questions to ask ourselves. Is the entity in question – be it a library or a service or a company – winning, losing, or standing still in the fight against entropy? Pick a GitHub library at random, and the overwhelming odds are that there hasn’t been a commit made in recent history – the library is rotting. Amongst more popular libraries, the picture is better, with brisk roadmaps and regular housekeeping.

When it comes to managed services, though, it’s quite rare to find a company that's winning – the vast majority of Software-as-a-Service companies are either treading water or falling behind. NetSuite is one example; it’s in some state approaching disrepair – no reasonable onlooker would say that NetSuite is definitively winning the war against entropy (though it certainly seems to be winning the war for market share). It has so much surface area that it has to dedicate an overwhelming percentage of its resources just to fight decay, leaving little to build new order and structure. Once a company reaches this state, it’s extraordinarily unlikely that it will ever recover.

There are certainly counterexamples, like AWS, GCP, and Stripe. They are good operational stewards of their production services and they routinely create new and valuable order and structure. But even then, we have to dig deeper: do they truly insulate us from the fight against disorder and decay, or do they leak it through in batches? Each time a provider like GCP deprecates a service or makes a breaking API change, they push entropy back onto us.

Same goes for price increases. A SaaS provider brings the code within the boundaries of their own infrastructure – that is, the thermodynamic system for running and operating the code – and we have to give them energy (in the form of money, which is a store of energy) as compensation. When the price goes up, they’re changing the energy bargain we’ve made.

The four horsemen of SaaS entropy, then, are: feature velocity, operational excellence, minimal breaking changes, and price maintenance. This is the reason we prefer working with AWS over most other providers – AWS has an implicit commitment to its customers that it will do everything in its power to avoid deprecating services, breaking APIs, and making price increases. For the same reason, we hold these principles near and dear for our products, too – our customers are entrusting us to fight their business-system-related entropy, and we want to be excellent stewards of that trust.

Every time we move up the ladder to a higher-order system, we have to make some tradeoffs. When we use a library, we lose some flexibility. Maintaining the library’s order is no longer our problem, but we have to live with their decisions unless we want to maintain our own fork. When we use a managed service, we lose even more flexibility – we lose the ability to change the system’s order altogether. Our system becomes harder to test, since it isn’t self-contained, which makes development slower and riskier.

There are big tradeoffs in terms of flexibility, development speed, testing, and more, and they all add up. But on the other hand, there’s the colossus that is entropy working every single minute of every single day to erode everything we dare to build. When I take stock of it, I’d prefer just about anything to fighting the second law of thermodynamics and the inevitable heat death of the universe.

Entropy and our customers

Our overall driving metrics measure usage. I say usage and not revenue because usage begets revenue – in the world of EDI, usage is a proxy for customer value delivered, and it’s exceptionally rare for a software company to deliver customer value and find itself unable to capture some of that value for itself.

Pricing

To facilitate usage, our pricing needs to be as cheap and reliable as running water, where you wouldn’t think twice (in a developed nation, from a cost standpoint) about running the faucet continuously when brushing your teeth or washing the dishes, or worry that the water might not come out when you need it the most. If our pricing were high compared to other developer-focused, API-driven products in adjacent spaces, it would discourage usage – it makes developers think about how to minimize the use of our products, rather than maximize the use of our products. This is precisely the opposite of the behavior we want to incentivize.

There is a second reason to avoid high prices: high prices signal attractive margins to competition. Once we have like-for-like competition – which is inevitable – we’ll be forced to lower our prices; if we’re going to lower our prices in a couple of years, then the only reason to have higher prices now is to capture additional revenue for a short period of time. This sort of "temporary" revenue is short term thinking; the long-term cost of temporary revenue is that you’ve incentivized competitors to enter your space (conversely, very low prices discourage competitors from entering the space, because the revenue potential is small).

Products

To drive usage, we're also expanding our product offering.

To think about our building blocks again in terms of thermodynamics, each building block is a piece of order or structure that we’ve committed to create, maintain, and improve on behalf of our customers; our customers can outsource their fight against entropy by using our building blocks.

We currently have three Generally Available products – three building blocks – that developers can use to build business systems: EDI Core (which translates EDI into developer-friendly JSON, and vice versa), Mappings (which allows transformation of JSON format to another), and Converter (which converts XML and CSV into JSON).

There are a limited number of use cases that customers can address using only these three products – there is a large amount of entropy that customers need to maintain in order to build end-to-end use cases.

To address this, we're launching a wide-reaching range of new products (many in June) – broadly, these products allow developers to build complete, end-to-end integrations all within the Stedi platform. Those products are:

  • Functions: a serverless compute service to run code on Stedi's infrastructure (now in Developer Preview).

  • SFTP: fully managed serverless SFTP infrastructure for exchanging files at any volume (now in Developer Preview).

  • Stash: simple, massively scalable Key-Value store delivered as a cloud API (now in Developer Preview).

  • Buckets: cheap, durable object storage to accommodate any file, in any format (coming soon to Developer Preview).

  • Guides: EDI specifications defined in standardized JSON Schema, and accessible via API (coming soon to Developer Preview).

There's a common theme for these developer-focused building blocks: they're as simple and generic as possible (in order to be easy to adopt and flexible for the extreme heterogeneity of EDI use cases), and they're scalable – both economically and technically – so that our customers never outgrow them, no matter how large they get (or how small they start). In many cases, developers may never exceed our monthly free tiers, and incur no costs at all.

Developers can use these building blocks as standalone items for just about any business application they might need to build – or assemble them together to build powerful, flexible EDI applications and integrations. With each building block we add, the number of use cases we can support rises exponentially.

In talking about this, I’m reminded of a passage from the book Complexity by Waldrop:

“So if I have a process that can discover building blocks,” says Holland, “the combinatorics [of genetic systems] start working for me instead of against me. I can describe a great many complicated things with relatively few building blocks. The cut and try of evolution isn’t just to build a good animal, but to find good building blocks that can be put together to make many good animals.

Focus

We are here to build a world-class business – a serious, first-rate business that the entire world of commerce can depend on, and that each of us can depend on for a first-rate, exceptional professional career with an exceptional financial return. My job is to be a steward of that path.

In a letter like this, it’s easy to get lost. What is most important? What is our focus?

To that end, I’d like to share a few passages.

Running with Kenyans:

For six months I’ve been piecing together the puzzle of why Kenyans are such good runners. In the end there was no elixir, no running gene, no training secret that you could neatly package up and present with flashing lights and fireworks. Nothing that Nike could replicate and market as the latest running fad. No, it was too complex, yet too simple, for that. It was everything, and nothing. I list the secrets in my head: the tough, active childhood, the barefoot running, the altitude, the diet, the role models, the simple approach to training, the running camps, the focus and dedication, the desire to succeed, to change their lives, the expectation that they can win, the mental toughness, the lack of alternatives, the abundance of trails to train on, the time spent resting, the running to school, the all-pervasive running culture, the reverence for running.

When I spoke to Yannis Pitsiladis, I pushed him to put one factor above all the others. “Oh, that’s tough,” he said, thinking hard for a moment. Then he said pointedly:

“The hunger to succeed.”

Guidelines for the Leader and the Commander:

“It follows from that, that you must, through this process of discernment and storing away, create in yourself a balanced man-whereby you can handle concurrently all the different parts of the job. You don't concentrate on one and forget the other, such as maintenance; you don't concentrate on marksmanship and forget something else. The best organizations in the American Army are the organizations that are good or better in everything. They may not make many headlines, they may not be "superior" in any one thing, but they are our best organizations.”

The Most Important Thing:

“Successful investing requires thoughtful attention to many separate aspects, all at the same time. Omit any one and the result is likely to be less than satisfactory.”

The Intelligent Investor:

“The art of investment has one characteristic that is not generally appreciated. A creditable, if unspectacular, result can be achieved by the lay investor with a minimum of effort and capability; but to improve this easily attainable standard requires much application and more than a trace of wisdom.”

It’s remarkable to me that when you look across so many different disciplines – sport, military, investing, and dozens more – the common theme from the people at the pinnacle of their game is that there’s no one single secret, no shortcut to being the best. It requires doing it all.

– Zack Kanter

Founder & CEO

[1] I say can lose out because it doesn't have to happen this way – every senior leader I've talked to at Amazon has pushed back on this and said that Amazon recognizes that failure to address operational toil leads to an inability to deliver value to customers. But ask any front-line Amazon employee what Amazon's #1 focus is, and they'll tell you customer obsession – when faced with delivering a feature or addressing an on-call issue, that's what comes to mind.

May 11, 2022

EDI

The trick to dealing with date and time in an X12 EDI document is to make things easy for yourself. EDI isn’t going to do that for you. EDI makes it possible to put a date on a business document in any way a human being has ever come up with and that flexibility comes at a cost: it’s downright confusing. You don’t have to deal with that confusion, though, because you don’t have to deal with that flexibility. It all starts with the implementation guide that you received from your trading partner.

Implementation guide

The implementation guide tells you what your trading partner expects from you. If your trading partner will send you purchase orders, then the implementation guide will tell you what those purchase orders look like. Every trading partner will format it a bit differently, but it usually looks something like this.

This implementation guide is based on the X12 EDI specification for an 850 Purchase Order. That specification is much broader than you need, though, because it caters for everything anyone could ever want in a purchase order. For example, it contains fields for specifying additional paperwork. Your trading partner doesn’t need that—can’t even handle that—so they send you an implementation guide with only the fields they do expect. In other words, an implementation guide is a subset of the X12 EDI specification and you only have to deal with that subset.

Of course, I don’t know who you’re trading with and I don’t know which type of documents you’re dealing with, so I’ll probably cover a bit more ground here than you need. Just remember: refer to the implementation guide and ignore everything that’s not in there, lest you drive yourself mad.

Segments

If your trading partner sends you a document, you might expect dates to look something like this.

"shippingDate": "2022-04-28T13:05:37Z"

However, EDI is different. It looks like this instead.

DTM*11*20220428*130537*UT

Every field is on its own line, except it isn’t called a field; it’s called a segment. Each segment begins with a code that tells you what type of data the segment contains. DTM means that the segment represents a date and time.

EDI actually has three different segments to represent date and time: DTM, DTP, and G62. DTM is the most common, but it’s also the most complicated, so I’ll talk about that one last. Fortunately, everything I’ll show you about G62 and DTP also applies to DTM.

To add to the complexity, date and time are not always expressed with one of these dedicated segments; sometimes they’re embedded into completely different segments. Those segments take bits and pieces of the dedicated date-and-time segments and call them their own.

G62

Date

The G62-segment represents a date like this.

G62*11*20220428

The last element of the segment isn’t too hard to understand: it’s the actual date. It’s always in the format YYYYMMDD. There’s also an element in the middle and it isn’t immediately clear what it means. Suppose you have a document for a shipment status update that contains two different dates.

"shippedDate": "2022-04-28"
"estimatedDeliveryDate": "2022-04-29"

In EDI, that would look like this.

G62*11*20220428
G62*17*20220429

EDI doesn’t have named fields. Instead it uses a code. 11 stand for Shipped on This Date and 17 stands for Estimated Delivery Date. If you take a look at the full list of possible codes, you’ll see that there are 137 of them. That’s a lot. They all have names, but those don’t do a good job of explaining what they mean. That’s when you turn to the implementation guide your trading partner gave you.

As you can see, your trading partner will only ever send you an 11 or a 17. That makes sense, because a code like BC - Publication Date doesn’t mean anything in the context of a shipment status update.

Time

The G62 example I gave only shows a date, but a G62 can contain a time as well. In fact, the full specification of a G62 looks like this.

Again, the implementation guide will tell you whether you need the full segment, but since we’ve already seen an example of a date, let’s take a look at an example of a time.

G62***8*1547*LT

The elements with information about a date are left empty. The delimiter * is still there, otherwise we wouldn’t know how many fields we skipped. So, that’s what those three stars between G62 and 8 mean: skip the two date fields.

1547 is the time: 3:47 PM. It’s always specified in a 24-hour clock. The example shows the shortest time format possible, but times can also have seconds and even fractions of seconds.

FormatExampleMeaningHHMM09059:05 AMHHMMSS1928517:28:51 PMHHMMSSD104537810:45:37.8 AMHHMMSSDD2307429111:07:42.91 PM

If you want to be sure which format you need to handle, you’ll have to check the implementation guide.

The 8 in the example is a code for the kind of time you’re dealing with, just like 11 and 17 were codes for the kind of date. 8 means Actual Pickup Time. You could look up all the possible codes, but it’s better to refer to your implementation guide.

The last element specifies the time zone. When you look up the time zone codes, some of the descriptions can be a bit confusing, but there’s method to the madness. Equivalent to ISO P08 means UTC+08 and Equivalent to ISO M03 means UTC-03. In other words, the number at the end is the offset from UTC where P means + and M means -.

In the example, the time zone is LT, which means Local Time. It depends on the context which time zone that is. If you’re dealing with a shipment status update, then it stands to reason that the local time for the pickup is local to wherever the pickup occurred.

Date-time

Since G62 can represent both a date and a time, it can also represent a date-time.

G62*11*20220428*8*1547*LT

This segment tells you that the package shipped on April 28, 2022 at 3:47 PM local time. It’s actually unlikely that you will get both a date qualifier (11) and a time qualifier (8) in the same segment, since they convey similar information, but check your implementation guide to be sure.

DTP

The DTP-segment is also used to express dates and times, but it’s more flexible than the G62-segment. DTP can express almost everything G62 can—except for time zones and fractions of seconds—and it has some extra features on top.

Date

A simple date looks like this.

DTP*11*D8*20220428

As you can see, the date is at the end. The first element is the same as with the G62: it tells you what kind of date you’re dealing with. The value 11 means Shipped, so that’s the same as with the G62, even if it’s describes slightly differently. The entire list of possible codes for the DTP is much longer, though; more than a thousand possibilities. Definitely check your implementation guide to see which ones are relevant to you.

The element in the middle is new. It specifies the format to use. D8 means that this is a date in YYYYMMDD format. This is where DTP becomes more flexible than G62: you can use different formats. For example, D6 means a date in YYMMDD format, TT means a date in MMDDYY format, and CY means a date expressed as a year and a quarter, e.g. 20221 means Q1 of 2022. That last one is interesting, because there’s no way to express the concept of a quarter in a regular date format. There’s even the notion of a Julian date, which just numbers days consecutively, starting on January 1st and it’s particularly fun to deal with in leap years. I imagine Julian dates are rare, but hey, if you need it, you need it.

Time

DTP can do time as well.

DTP*11*TM*1547

TM means a time in HHMM format and, as with G62, DTP always uses a 24-hour clock. Another code is TS, which means a time in HHMMSS format. There’s no code that allows you to specify fractions of a seconds, so that’s a feature of G62 that DTP doesn’t have. DTP also doesn’t provide a way to specify a time zone.

Date-time

Expressing a date-time doesn’t require any extra fields, just a different format qualifier.

DTP*11*DT*202204281547

As you may expect, DT means a date-time in YYYYMMDDHHMM format and the date-time in the example is April 28, 2022 at 3:47 PM. If the qualifier is set to RTS instead of DT, the date-time will include seconds as well.

Range

Where DTP truly trumps G62, is in its ability to represent date ranges.

DTP*135*RD8*20211224-20220104

This represents December 24, 2021 to January 4, 2022. As before, the qualifier gives the exact format and RD8 means a date range in YYYYMMDD-YYYMMDD format. There are also ranges that include a time, and ranges that use only years, and everything in between. You can check out the full list of qualifiers, but it’s even better to check your trading partner’s implementation guide.

DTM

The DTM-segment is the most common way of representing date and time in EDI. It’s also the most flexible, because it has the capabilities of both the G62-element and the DTP-element. That would also make it the most confusing, except that you’ve already seen G62 and DTP, so there’s nothing left to say about DTM.

As you can see, it’s just a combination of the G62-elements and the DTP-elements. You do need to figure out which of these elements are used and which are ignored, but by now, you know how to do that as well: check the implementation guide.

Other segments

If you have to deal with an 850 Purchase Order, you’ll find that it contains a segment called BEG and that segment has an element to represent a date. It’s the same element as you find in a G62 or a DTM—a date expressed in YYYYMMDD format—just embedded in a segment that otherwise has nothing to do with dates. In the case of a purchase order, it represents the date of the purchase order as defined by the purchaser.

It’s not limited to the BEG-segment, though. For example, the B10-segment includes both a date and a time. The AT7-segment includes date, time and time code, and the CUR-segment has a date and time plus a date/time qualifier. And there are many, many more segments like this. It’s a lot to handle, but there’s a saving grace: they all use a combination of the elements described in this article, so by now, you should be well equipped to deal with them.

Apr 14, 2022

Products

When a question about JSONata turns up on StackOverflow, we do our best to answer it. Not because it's part of our strategy, or because we are building our corporate image, but because we like to help out. And we like to impress a little bit, too.

We love JSONata. It's a modest little language. Where the likes of Javascript or Python try to be all things to all people, JSONata just tries to do one thing very well: query JSON structures. That's not something every developer runs into every day, but it's exactly what we need for Mappings, which is all about transforming one JSON structure into another.

Since we do use JSONata every day, we're building up quite a bit of expertise with it. We like building complex queries, like: does the total cost of all items in the order that are not in stock exceed our limit? However, if you only use JSONata occasionally, then a challenge like this may be a bit daunting. JSONata isn't a hard language to learn, but you can do some pretty involved stuff with it. You could dig through the documentation and it does cover all aspects of the language, but it doesn't offer much guidance on how to put all the pieces together. No, a situation like this is when you turn to StackOverflow.

Whenever we post an answer, we keep in mind that the easiest way to understand JSONata is by example, but giving an example isn’t as convenient as we’d like. There are three parts to it: the data you’re querying, the JSONata query, and the result. You can show all of that in code snippets, of course, but it lacks a bit of clarity and it’s very static.

{
  "orders": [
    {
      "quantity": 650,
      "pricePerUnit": 3.53,
      "inStock": true
    },
    {
      "quantity": 800,
      "pricePerUnit": 1.12,
      "inStock": false
    },
    {
      "quantity": 3,
      "pricePerUnit": 275.75,
      "inStock": false
    },
    {
      "quantity": 1250,
      "pricePerUnit": 2.45,
      "inStock": true
    }
  ]
}
(
  $costs := $map(orders[inStock=false], function ($order) {
    $order.quantity * $order.pricePerUnit
  });

  $costs ~> $sum > 1000
)
true

It works, but we can do better. We took a few days and created the JSONata Playground. In the playground, it's clear what all of these snippets are, and it's interactive, so you can play around with the JSONata expression and discover how it was constructed. You can also embed the playground in a webpage.

You can bet that from now on our StackOverflow answers will include JSONata Playground examples and yours can as well, because it's completely free for anyone. We hope you'll use it to learn JSONata, to write JSONata, and to share JSONata. We hope this helps you out. And we hope we impressed you a little bit, too.

Apr 13, 2022

Engineering

One deployment per hour is far too slow. Our initial deployment style starts with a paths filter to limit what stacks deploy, based on files and folders changed in a commit. We start our project this way thinking it will give us quick deploys for infrequently changed stacks. As our project grows, though, this is taking too long to deploy. Based on our analysis, our fastest recorded deployment time is 13.5 minutes, but our slowest deployments take up to 40 minutes. We are confident we can get our p99 down to 20 minutes or less.

How

Here on the Billing team our primary application is a single monorepo with 12 CDK stacks deployed using GitHub Actions. When we dive into the pipeline, we realize that we have a number of redundant steps that are increasing deployment times. These duplicate steps are a result of the way CDK deploys dependent stacks.

For instance, let’s take four of our stacks: Secrets, API, AsyncJobs, and Dashboard. The API stack relies on Secrets, while Dashboard relies on API and AsyncJobs. If we only need to update the Dashboard stack, CDK will still force a no-op deployment of Secrets, API, and AsyncJobs. This pipeline will always start from square one and run the full deployment graph for each stack.

We believe we can speed up our slowest deployment time by reducing these redundant deployment steps. Our first improvement is to modify the paths filter by triggering a custom “deploy all” CDK command if certain files (e.g. package.json) were changed. We alter the standard cdk deploy –all because we have some stacks listed that we don’t want to deploy. Instead, we use cdk deploy –exclusively Secrets-Stack API-Stack AsyncJobs-Stack Dashboard-Stack, which decreases our median deployment time by a full minute and decreases our slowest deployment time by 10 minutes.

With our slowest times still clocking in around 30 minutes, we know we need a new idea to reach our goal of 20 minutes or less. We have reached our limit of speed for serial deployments, so we attempt to parallelize the deployments. We use the CDK stack dependency graph, which gives us a simple mechanism to generate parallel jobs. We are now able to build the GitHub Actions jobs and link them together with the stack dependencies using the job’s need: [...] option.

To generate the stack graph, we synthesize the stacks (cdk synth) for each stage we'd like to deploy and then parse the resulting manifest.json in the cdk.out directory.

const execSync = require("child_process").execSync;
const fs = require('fs');
const path = require('path');
const { parseManifest } = require('./stackDeps');

const stages = ['demo'];
const stackGraphs = {};

stages.map((stage) => {
  execSync(`STAGE=${stage} npx cdk synth`, {
    stdio: ['ignore', 'ignore', 'ignore'],
  });
  stackGraphs[stage] = parseManifest();
});

const data = JSON.stringify(stackGraphs, undefined, 2);
fs.writeFileSync(path.join(__dirname, '..', 'generated', 'graph.json'), data);

Our stack graph:

{
  "demo": {
    "stacks": [
      {
        "id": "Secrets-demo",
        "name": "Secrets-demo",
        "region": "us-east-1",
        "dependencies": []
      },
      {
        "id": "Datastore-demo",
        "name": "Datastore-demo",
        "region": "us-east-1",
        "dependencies": []
      },
      {
        "id": "AsyncJobs-demo",
        "name": "AsyncJobs-demo",
        "region": "us-east-1",
        "dependencies": [
          "Datastore-demo",
          "Secrets-demo"
        ]
      },
      {
        "id": "Api-demo",
        "name": "Api-demo",
        "region": "us-east-1",
        "dependencies": [
          "Datastore-demo",
          "Secrets-demo"
        ]
      },
      {
        "id": "Dashboards-demo",
        "name": "Dashboards-demo",
        "region": "us-east-1",
        "dependencies": [
          "AsyncJobs-demo",
          "Api-demo"
        ]
      }
    ]
  }
}

The workflow below is easy to define programmatically from the stack graph, which allows GitHub Actions to do all the heavy lifting of orchestrating the jobs for us.

Due to network latency and variance in the GitHub runner setup, this change sometimes causes our fastest deployments to slow down. However, the median deployment performs 1 minute faster. Most importantly, our p99 deployment times always perform 12 minutes faster than before: 18 minutes!

At last we achieved our goal! With everything sped up and working smoothly, we are able to add it to our team’s projen setup and make this available to all of our other services.

How to get started

To see how this works, you can find a full working sample on GitHub.

The sample repo provides an un-opinionated example with just one stage to deploy. You could build on this in a few ways, such as specifying stage orders, implementing integration tests or whatever else is needed in your stack.

Feb 24, 2022

Products

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

Instead of writing conditions inside of your mapping for simple data conversions, you can now create a lookup table to automatically replace a value that your system (or trading partner) does not recognize with one that they do.

When building B2B integrations, it is expected that your trading partner will send you data in a format that is incompatible with your system. Or they may require you to send the data in a format that your system doesn’t produce.

Lookup tables make it easier to deal with those scenarios; they are designed for developers that want to write and maintain as little code as possible.

Where lookup tables fit in

Let’s examine the following lookup table, which can be used to replace an internal code, like 313630, with a human-readable code your trading partner requires, like FedEx Express.

When you create a lookup table inside of a mapping, you can find and use a matching value for any of these columns. In the example above, you look up any value by internalID, by SCAC code, or by shippingMethod.

Lookup tables are generic and can be used as part of any data mapping exercise, regardless of where in your pipeline you are using Mappings.

Say you have a trading partner that is sending you CSV files, you can:

  • Use Functions to convert the CSV into JSON

  • Use Mappings to transform that JSON into a shape that you need, and

  • Use a lookup table to transform the values of that JSON to what your system needs

In another example, if you are generating EDI and need to change the values that come from your custom JSON API to something that is required on the EDI document, you can:

  • Use Mappings to transform that JSON into a JEDI file

  • Use a lookup table to change the values from your system to the values your trading partner requires

  • Send JEDI to the EDI Core translate API to get EDI back

Lookup tables in action

Let’s assume that you are ingesting an invoice, and you want to map the following source JSON…

{
  "product": {
    "id": "QL-5490S",
    "country": "USA",
    "price": "500"
  }
}

…to the following target JSON:

{
  "product_number": "QL-5490S",
  "price": {
    "currency": "USD",
    "amount": 500
  }
}

Once you upload your target and source to the mappings editor, you will see this:

Your trading partner sends you USA as the country but does not include a currency field, which is required by your system. Additionally, you work internationally so you will also receive invoices from trading partners operating out of Germany (DE) and Australia (AU). It is standard in your industry for trading partners to send invoices in their local currency, so when you need to populate your currency key, your mapping needs to look up what country the invoice is from.

USA is not a valid currency code, so you need to convert USA to USD, and eventually DE to EUR and AU to AUD. To solve this, you need a lookup table. To create one, open the fullscreen view next to currency, click “Lookup tables”, and “Add new”.

When creating a new lookup table, you can enter the table values manually or upload a CSV file with all values that you’d like to populate in your lookup table.

Let’s create a table named Currency_Codes with two columns, Country and Currency, and populate the rows with the relevant values.

On the upper right side of your screen, you will see that the UI provides you with the JSONata snippet you need to use in your mapping. Simply copy the snippet, and click Confirm. Paste your JSONata snippet into the expression editor:

The $lookupTable JSONata function takes three arguments:

  • The name of the lookup table

  • The name of the column we will lookup data by

  • The path to a value

Simply replace <path.to.key> with product.country, and you will see the Output preview shows USD.

Hit the Test button and swap the input value to see how it works!

Lookup tables are flexible. They can be as simple as two columns or be expanded to include multiple columns. In the example above, you could also look up a country by its currency (or by any other column in the table).

Complex transforms with lookup tables

Lookup tables can be used for more complicated transformations, like if you need to use one source value to populate multiple destination fields.

Let’s say a trading partner only sends a location code that represents where your product needs to be shipped to. Your current fulfillment software requires a full address for validation and rate shopping. You can create a lookup table with multiple columns so that each required data point is satisfied.

Let's create the following lookup table:

The example mapping for Name field would be:

To get a mapping Address, change the column at the end of the expression:

Now that this lookup table and logic are complete, any data coming in with just an address code will expand to a full address to meet your data requirements.

Build your first lookup table

You can get started with lookup tables by reading the overview in our documentation, the API Reference, and the $lookupTable custom JSONata function.

There is no additional charge to create or reference lookup tables when using Mappings; you only pay for the requests to the Mappings API.

Feb 3, 2022

Products

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

Developers come to Stedi to build EDI systems of their own. And at the center of any EDI system is data translation: a way of turning EDI – an arcane file format – into a more approachable format, like JSON.

That's why we launched EDI Core – a basic building block that developers can use to build flexible, scalable EDI applications.

Introducing EDI Core

EDI Core is a collection of APIs that make working with EDI simple. Users can translate between EDI and Stedi’s JEDI (JSON EDI) format, which is a lossless and user-friendly representation of EDI. EDI Core is built to be the foundation of any scalable and reliable EDI system, and the JEDI format makes programmatic manipulation and validation easy.

There are two ways to interact with EDI Core:

  • In the browser using the free Inspector

  • Programmatically using the EDI Core API (Note: This API has been deprecated since this blog post was published. It has been replaced by our EDI platform APIs.)

Visualize EDI with Inspector

Inspector is a free tool built on top of the EDI Core API. You can use it to easily understand the contents of an EDI file, to debug errors, and to share the file with others.

Let’s take an example Amazon Direct Fulfillment Purchase Order from a code editor…

Code editor

…and load it into the free Inspector tool.

Inspector view with raw EDI on the left and a rich view on the right

On the right is the Rich view – a human-readable version of the EDI file. This will help you understand the contents and the structure of the file. Hover over the elements in the Rich view to see where they show up in the original EDI file on the left; click on the elements to gain a deeper understanding of the codes and their definitions.

A gif showing how the inspector highlights data in the raw EDI when the user hovers over fields in the rich view

If you toggle to the JSON view, you will see the file in Stedi’s JEDI format.

Working with imperfect EDI files

It’s fairly common to receive a less-than-perfect EDI file. Trying to debug a malformed EDI file can be a frustrating and time-consuming process. If you find yourself in a situation like this, the Inspector has several features that can help make the process less painful:

  • When an EDI document is malformed, we’ll do our best to parse it and display actionable error messages to help troubleshoot

  • If the EDI file is not formatted correctly, you can make it easier to read by clicking the ‘Format EDI’ button

  • If you want to share the file via the Inspector with others, simply copy the URL in your browser and share it, or click the download button to save the EDI file.

Translate EDI programmatically using EDI Core API

Where EDI Core API fits into your system

There are two scenarios: you’re either working to receive EDI files from your trading partner – that is, getting data into your system, or you need to send EDI files, that is, move data out of your system. Perhaps you need to do both.

Regardless, EDI Core can be the foundation of this system.

EDI Core Diagram showing requests and data flow

When you’re ingesting EDI, you will want to translate EDI to JEDI and then map JEDI into your JSON format (Stedi’s Mappings can help with this!).

When you’re generating EDI, you will want to create JEDI (Mappings works here, too!) and then translate JEDI to EDI.

In both cases, you will receive validation errors if the JEDI file does not conform to the EDI release or Guide you configured on request.

Additionally, when generating EDI, these API options will come in handy, you can:

  • Override EDI delimiters,

  • Generate control numbers, including setting control number values,

  • Remove empty segments

EDI Core is now Generally Available

EDI Core translates EDI files to JEDI (JSON EDI) and vice versa, which allows you to treat EDI just like anything else: a problem to solve using your familiar tools.

If you want to build an end-to-end EDI system that utilizes EDI Core, there is some assembly required. You’ll need to handle the actual transmission of EDI files with your trading partners (via SFTP or AS2), orchestrate API calls, handle retries and errors, and more – though we do have developer-focused products in each of these categories coming soon.

Get started today.

Jan 26, 2022

Products

Business integrations are anything but straightforward – they are typically composed of many steps that need to be executed sequentially to process a single business transaction.

At a high level, each step can be divided into one of two categories: transporting data and transforming data. In order to enable developers to implement all manner of data transformations with ease, we've built Mappings.

Introducing Mappings

Mappings is a robust JSON-to-JSON data transformation engine that enables developers to define field mappings in an intuitive UI – and then invoke those mappings programmatically via an API.

Mappings is composed of two parts:

  • a browser-based tool that allows users to create and test JSON-to-JSON data transformations

  • an API that can be used to programmatically invoke defined mappings at web scale.

Here's a look at the UI...

Image an overview of the Mappings UI with a partially created mapping

...and the corresponding API call:



Using Mappings, developers can transform JSON payloads without the need to write, provision, maintain, or monitor any infrastructure. Mappings is powered by an open-source query and transformation language called JSONata, which comes with a large number of built-in functions and expressions.

Mappings allows developers to solve common tasks like retrieving data from nested structures, manipulating strings, writing conditional expressions, parsing/converting dates, transforming data (with filter, map, reduce functions) and much more.

Using Mappings

As an example, let's build a mapping between an 850 Purchase Order and a QuickBooks Online Estimate. The Purchase Order is in the X12 EDI format; since Mappings works with JSON as its input and output formats, we need to translate it to JSON first. You can read how to do that here.

Now that we have a JSON source document to start with, we can create a new mapping by navigating to Mappings in Stedi. We'll input the JSON as the source in the first step of the Mapping wizard…

Image of an EDI 850 purchase order displayed in JEDI (JSON EDI) format

…and we'll input the target JSON in the next step:

Image of a QuickBooks Online Estimate API shape displayed in JSON

With our source and target selected, we can start building a transformation between them by writing simple functions and expressions:

Image of the Mappings fullscreen view showing a complex JSONata transformation

The Mappings UI has autocomplete functionality and built-in documentation. Together, these facilitate a seamless experience for building complex data transformations:

Image of the Mappings Editor with several mapping transformations

Each mapping can be tested in the UI…

Image of the Test Mapping modal

…and executed at web scale using the API.

Mappings is now Generally Available

Mappings allows you to create your own data transformation API - without the need to write, provision, maintain or monitor any code; you focus on building your business logic and we’ll do the rest for you.

Get started today.

Jan 5, 2022

EDI

EDI – Electronic Data Interchange – is an umbrella term for many different “standardized” frameworks for exchanging business-to-business transactions. It dates back to the 1960s and remains a pain point in every commercial industry from supply chain and logistics to healthcare and finance. What makes it so hard? Why is it still an unsolved problem despite many decades of immense usage?

These are the questions we get most often from developers – both developers who want to become Stedi customers and developers who want to join us to help build Stedi. And these are our favorite questions to answer, too – the world of EDI is complex, opaque, and arcane, but it’s also enormously powerful, underpinning huge swaths of the world economy.

The problem is that there just aren’t any good, developer-focused resources out there that can help make sense of EDI from the ground up. The result is that this wonderfully rich ecosystem has been locked away from the sweeping changes that are happening in the broader world of technology.

We’ve had the good fortune of ramping up a number of people from zero to a solid working knowledge of how the whole EDI picture fits together. It helps to start at the highest level and get more and more specific as you go; this piece you’re reading now is the first one that our engineers read when they join our team – a sort of orientation to the wide, wide world of EDI – and we’ve decided to post it publicly to help others ramp up, too.

Trade

At the most basic level, there are many thousands of businesses on Earth; those businesses provide a wide variety of goods and services to end consumers. Since few businesses have the resources or the desire to be completely self-sufficient, they must exchange goods and services between one another in order to deliver finished products. This mechanism is known as trade.

When two businesses wish to conduct trade, they must exchange certain business documents, or transactions, which contain information necessary to conduct the business at hand. There are hundreds of conceivable transaction types for all manner of situations; some are common across many industries, like Purchase Orders and Invoices, and some are specific to a class of business, such as Load Tenders or Bills of Lading, which pertain only to logistics.

Businesses used to exchange paper transactions and record those transactions into a hand-written book called a ledger (which is why we refer to accounting as “the books,” and people who work with accounting systems as “bookkeepers”), but modern businesses use one or many software applications, called business systems, to facilitate operations. There are many types of business systems, ranging from generic software suites like Oracle, SAP, and NetSuite to vertical-specific products that serve some particular industry, like purpose-built systems for healthcare, agriculture, or education.

Each business system uses a different internal transaction format, which makes it impossible to directly import a transaction from one business system into another; even if both businesses were using the exact same version of SAP – say, SAP ERP 6.0 – the litany of configuration and customization options (each of which affects the system’s transaction format) renders the likelihood of direct interoperability extraordinarily improbable. These circumstances necessitate the conversion of data from one format to another prior to ingestion into a new system.

Data conversion

The most popular method of data conversion is human data entry. Customer A creates Purchase Order n in NetSuite (its business system) and emails a PDF of Purchase Order n to Vendor B. A clerical worker employed by Vendor B enters the data from Purchase Order n into SAP (its business system). The clerk “maps” data from one format to another as necessary – for example, if Customer A’s PDF includes a field called “PO No.”, and Vendor B’s business system requires a field called “Purchase Order #”.

People are smart – you can think of a person sort of like AI, except that it actually works – and are able to handle these sort of mappings on-the-fly with little training. But manual data entry is costly, error-prone, and impractical at high volumes, so businesses often choose to pursue some method of transaction automation, or trading partner integration, in order to programmatically ingest transactions and avoid manual data entry.

Since each business has multiple trading partners, and each of its trading partner operates on different business systems, “point to point” integration of these transactions (that is, mapping Walmart’s internal database format directly to the QuickBooks JSON API) would require the recipient to have detailed knowledge of many different transaction formats – for example, a company selling to three customers running on NetSuite, SAP, and QuickBooks, respectively, would need to understand NetSuite XML, SAP IDoc, and QuickBooks JSON. Maintaining familiarity with so many systems isn’t practical; to avoid this explosion of complexity, businesses instead use commonly-accepted intermediary formats, which are broadly known as Electronic Data Interchange – EDI.

EDI

EDI is an umbrella term for many different “standardized” frameworks for exchanging business-to-business transactions, but it is often used synonymously with two of the most popular standards – X12, used primarily in North America, and EDIFACT, which is prevalent throughout Europe. It’s important to note that EDI isn’t designed to solve all of the problems of B2B transaction exchange; rather, it is designed to eliminate only the unrealistic requirement that a trading partner be able to understand each of its trading partner’s internal syntax and vocabulary.

Instead of businesses having to work with many different syntaxes (e.g., JSON, XML, CSV) and vocabularies (e.g., PO No. and Purchase Order #), frameworks like X12 and EDIFACT provide highly structured, opinionated alternatives intended to reduce the surface area of knowledge required to successfully integrate with trading partners. All documents conforming to a given standard follow that standard’s syntax, allowing an adoptee of the standard to work with just one syntax for all trading partners who have also adopted that syntax.

Further, standards work to reduce variation in vocabulary and the placement of fields, where possible. The X12 standard, for example, has an element type 92 which enumerates Purchase Order Type Codes; the enumerated value Dropship reflects X12’s opinion that POs that might be colloquially referred to as Drop Ship, Drop Shipment, or Dropship will all be referenced as Dropship, which limits the vocabulary for which an adoptee might have to account. Similarly, X12 has designated the 850 Purchase Order’s BEG03 element – that is, the value provided in the third position of the BEG segment in the 850 Purchase Order transaction set – as the proper location for specifying the Purchase Order number. This reduces some of the burden of mapping a transaction into or out of an adoptee’s business system; only one value for drop shipping and one location for PO number must be mapped.

Of course, the vast majority of fields cannot be standardized to this degree. Take, for example, the product identifier of a line item on a Purchase Order – while X12 specifies that the 850 Purchase Order’s PO107 element should be used to specify the product identifier value, the standard cannot possibly mandate which type of product identifier should be used. Some companies use SKUs (Stock Keeping Units), while others use Part Numbers, UPCs (Universal Product Codes), or GTINs (Global Trade Item Numbers); all in all, the X12 standard specifies a dictionary of 544 different possible product identifier values that can be populated in the PO106 element.

The problem with standards

What we’re seeing here is that while a standard can be opinionated about the structure of a document and the naming of fields, it cannot be opinionated about the contents of a business transaction – the contents of a business transaction are dictated by the idiosyncrasies of the business itself. If a standard doesn’t provide enough flexibility to account for the particulars of a given business, businesses would choose not to opt into the standard. A standard like X12, then, does not provide an opinionated definition of transactions – it provides a structured superset of all the possible values of commerce.

Intermediary formats – that is, EDI – make life somewhat easier by limiting the number of different formats that a business must understand in order to work with a wide array of trading partners; a US-based brand selling to dozens of US-based retailers likely only needs to work with the X12 format. However, the brand still needs to account for the different ways that each retailer uses X12. Again, X12 is just a dictionary of possible values – since Walmart and Amazon run their businesses in different ways (and on different business systems), their implementation of an X12 intermediary format will differ, too.

A simple example may be that Walmart allows customers to include a gift message at the order level (“Happy Birthday – and Merry Christmas!”), whereas Amazon allows its customers to specify gift messages at the line item level (“Happy Birthday!” at line item #1, “and Merry Christmas!” at line item #2). This difference in implementation of gift message means that a brand selling to both Amazon and Walmart would need to account for these differences when ‘mapping’ the respective fields to its business system.

Such per-trading-partner nuances cannot be avoided – because different businesses operate in different ways, a single, canonical, ultra-opinionated representation of, say, a Purchase Order, is unlikely to ever exist. In other words, the per-trading-partner setup process is driven by inherent complexity – that is, complexity necessitated by the unavoidable circumstances of the problem at hand. And because field mappings such as these affect real-world transactions, they cannot be done with a probabilistic machine learning approach; for example, mapping “Shipper Address” to “Shipping Address” would result in orders being shipped to the shipper’s own warehouse, rather than the customers’ respective addresses. While there are many ways to build business-to-business integrations, any solution must account for a setup process that involves per-trading-partner, human-driven field mappings.

There are other areas of inherent complexity in EDI, too. Because businesses change over time, the configurations of the businesses’ respective business systems must change, too; an example might be a retailer adding DHL as a shipping option, whereas it previously only offered FedEx. Those changes must be communicated to trading partners so that field mappings can be updated appropriately; because such communications and updates involve ‘best efforts’ from humans, some percentage of them will be missed or completed incorrectly, leading to integration failures on subsequent transactions. Even without inter-business changes, errors happen – for example, a business system’s API keys might expire, or the system might experience intermittent downtime. Such errors will need to be reviewed, retried, and resolved. Just as every solution’s setup process will always require per-trading-partner, human-driven field mappings, every solution must also provide functionality for managing configuration changes on the control plane and intermittent errors on the data plane.

Business physics

The 'laws of physics' of the business universe, then, are as follows:

  1. There are many businesses, and those businesses must conduct business-to-business trade in order to deliver end products and services;

  2. Those businesses run on a large but bounded number of business systems;

  3. Due to the heterogeneity of business practices, those business systems offer configuration options that result in an unbounded number of different configurations;

  4. The heterogeneity of configurations makes it impossible for a single unified format, therefore necessitating a per-trading-partner setup process;

  5. The business impact of incorrect setup requires that a human be involved in setup;

  6. Businesses are not static, so configuration will change over time, again necessitating human input;

  7. Business systems are not perfectly reliable;

  8. Because neither human input not business systems are perfectly reliable, intermittent errors will occur on an ongoing basis;

  9. Errors must be resolved in order to maintain a reliable flow of business.

Any generalized business integration system must account for all these constraints. The so-called Law of Sufficient Variety summarizes this nicely: “in order to ensure stability, a control system must be able to, at a minimum, represent all possible states of the system it controls.” Failing that, it must limit scope in some way – say, by only handling a subset of transaction types, industries, business systems, or use cases. But since limiting scope definitionally means limiting market size, any sufficiently-ambitious effort to develop a business integration system must not limit scope at all: it must provide mechanisms to work with any configuration of any business system, in any industry, across 300+ business-to-business transaction types, in any format, as well as provision for any evolutions that might develop in the future.

Such is the challenge of developing a generalized business integration system.

A familiar problem

The good news is that such circumstances (an unbounded set of heterogeneous, complex, mission-critical, web-scale use cases) are not unique to business integrations – they mirror the circumstances found in the broader world of software development.

If, instead of setting out to create a software application for developing business integrations, one were to set out to create a software application for developing software applications, one would encounter the same challenges – to continue with the Law of Sufficient Variety, a system for developing software would need to be able to represent all possible states of the software it wishes to develop.

This, of course, isn’t reasonable – it isn’t feasible to design a single software application for developing software applications. Instead of a single application, successful platforms like Amazon Web Services provide a series of many small, relatively simple building blocks – primitives – that can be composed to represent virtually any larger application. AWS, then, can be thought of as a catalog of software applications for developing software applications. Using AWS’s array of offerings, software developers today can build web-scale applications with considerably less code and less knowledge of underlying concepts.

Stedi’s approach

Whereas AWS is a catalog of developer-focused software applications for developing software applications, Stedi is a catalog of developer-focused software applications for developing business integrations.

Stedi serves three types of customers:

  • Customers that need to build integrations to support their own business (e.g. an ecommerce retailer that wants to exchange EDI files with its suppliers);

  • Customers that need to build EDI functionality into their own software offerings (e.g. a digital freight forwarder, transportation management system, or fulfillment provider that wants to embed self-service EDI integration into their platform);

  • EDI providers or VANs that need to modernize their technology stack (e.g. a retail-specific EDI software provider that wants to replace legacy infrastructure or tools).

With Stedi, you can build massively scalable and reliable integrations on your own without being an EDI or development expert.

Learn more about building on Stedi.

Sep 17, 2025

Guide

The big takeaway: Manually entered payer names are often wrong. Use Stedi's Search Payers endpoint to find the closest matching payer ID. Then verify it's the right payer with an eligibility check.

Healthcare runs on forms. People often fill them out wrong.

One thing we commonly see is the plan name entered as the payer name. A tired nurse sees "UHC Open Access Plus" on an insurance card and enters it as the payer name. But that's actually the plan name. The payer is UnitedHealthcare.

It’s an honest mistake. But if you’re building on top of that data, it can break your workflow.

Clearinghouse transactions – like claims – need a payer ID. It’s like a routing number for the payer. Use the wrong ID, and your claims are likely to be rejected. And that may mean delayed payments for the provider.

When payer names are clean and trustworthy, finding their IDs is easy. You can just use the top result from Stedi’s Search Payers endpoint.

But when the data is dirty, you need a verification workflow. This guide lays out our recommended flow. It’s based on what we’ve seen work with customers.

Step 1. Run a payer search

Use the Search Payers endpoint to search for the payer name:

curl --request GET \
  --url "https://healthcare.us.stedi.com/2024-04-01/payers/search?query=uhc+open+access+plus" \
  --header "Authorization: <api_key>"

Results are sorted by relevance. Store the top result's primaryPayerId as the payer ID:

{
  "items": [
    {
      "payer": {
        "stediId": "KMQTZ",
        "displayName": "UnitedHealthcare",
        "primaryPayerId": "87726",            // Payer ID
        ...
      }
    },
    ...
  ],
  ...
}

The Search Payers endpoint’s search algorithm is greedy. It’ll almost always return something, even for an unmappable payer name like BCBS out of state.

It’s important to verify the patient has coverage with the payer before using the payer ID in a workflow.

Step 2. Verify the payer ID using an eligibility check

Eligibility checks are fast, inexpensive, and easy to automate. This makes them a good choice to verify a patient’s payer.

Run an eligibility check to verify the payer ID. For example, using the Real-Time Eligibility Check JSON endpoint:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
  "tradingPartnerServiceId": "87726",            // Payer ID
  "encounter": {
    "serviceTypeCodes": ["30"]
  },
  "provider": {                                  // Provider data
    "organizationName": "ACME Health Services",
    "npi": "2528442009"
  },
  "subscriber": {                                // Patient data
    "memberId": "1234567890",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19850101"
  }
}'

The results will tell you if the patient has active coverage with the payer. If so, you can use the payer ID in other workflows for the patient. For tips on interpreting the results, see Active and inactive coverage in the Stedi docs.

If the check returns AAA error 75 (Subscriber/Insured Not Found) or a similar error, it means the payer search likely matched the wrong payer. Move on to step 3.

Step 3. Manually check the rest

Some raw payer names can't be mapped automatically. They need human judgment to pick the right ID.

Many payers use separate payer names and payer IDs for different lines of business, states, or coverage types.

For example, searching "Aetna" returns their main commercial ID first, but they also have separate IDs for each state's Medicaid plan. Without more context, you can't pick the right one.

Other payer name strings can’t be mapped to a valid payer ID at all:

  • BCBS out of state (which state?)

  • Insurance pending (not a payer)

  • Typos that match nothing

Mark these for review or flag them with the provider. Don't guess.

Things to watch out for

Don’t send Medicare Advantage checks to CMS

Medicare Advantage isn't Medicare. It's commercial insurance that Medicare has approved.

Medicare Advantage plans and payers often have “Medicare” in their names. With typos, it’s easy to run a search for these payers that returns the National Centers for Medicare & Medicaid Services (CMS) – the government payer for Medicare – instead of the commercial payer you need.

CMS forbids using its system for Medicare Advantage checks. To avoid doing that, filter anything with "Medicare" in the name. Make sure it’s not a Medicare Advantage check before sending it to CMS.

Blue Cross Blue Shield eligibility responses

Blue Cross Blue Shield (BCBS) isn't one payer. It's a collection of 30+ separate entities. However, a BCBS payer can verify any other BCBS member through the BlueCard network.

For example, if you send an eligibility check to BCBS Texas for a BCBS Alabama member, you'll get benefits back. It may not be obvious that BCBS Alabama is the home payer.

To get the home payer, check the response's benefitsRelatedEntities for an entry with entityIdentifier = "Party Performing Verification". The entityIdentificationValue is the home payer’s ID. Use that payer ID for claims and other workflows.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "serviceTypeCodes": ["30"],
      ...
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Party Performing Verification",
          "entityType": "Non-Person Entity",
          "entityName": "Blue Cross Blue Shield of Alabama",
          "entityIdentification": "PI",
          "entityIdentificationValue": "00510BC"
        }
      ]
    },
    ...
  ],
  ...
}

Get help when you need it

We’ve seen the workflow above work about 80% of the time. For the remaining 20%, patterns often emerge.

If there’s a raw payer name string that’s giving you trouble, reach out. Our team can help with payer ID matching.

Sep 14, 2025

Guide

Big takeaway: Duplicate benefits often aren’t duplicates. They cover different scenarios, like in-network vs. out-of-network care.

Imagine you run an eligibility check. A second or two later, you get back the response. It lists three different co-pays – $15, $30, and $0 – each for a physician office visit.

Which one is right?

They all are. Each co-pay applies to a different situation:

  • $15 for in-network providers – providers who have a contract with the patient’s payer for their health plan.

  • $30 for out-of-network providers – providers without a contract.

  • $0 for specific services, like preventive care or maternity visits, with in-network providers.

If you don't know which fields to check in the response, it’s hard to tell them apart. This guide shows you what to look for with real-life examples.

Where to find benefits

If you’re using Stedi’s JSON eligibility APIs, most of a patient’s benefit information is in the response’s benefitsInformation object array.

Each benefitsInformation object – or benefit entry – tells you about one aspect of the patient’s coverage. One entry indicates active coverage. Another contains a co-pay.

For tips on reading the objects, see How to read a 271 eligibility response in plain English.

{
  ...
  "benefitsInformation": [
    {
      "serviceTypeCodes": ["30"],         // General medical
      "code": "1",                        // Active coverage
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network providers
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "serviceTypeCodes": ["30"],         // General medical
      "code": "C",                        // Deductible
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network providers
    },
    {
      "serviceTypeCodes": ["30"],
      "code": "D",                        // Benefit Description
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    },
    {
      "serviceTypeCodes": ["88"],         // Pharmacy
      "code": "B",                        // Co-pay
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network providers
    },
  ],
  ...
}

Fields to check

Benefit entries often look identical except for one or two fields. Check these fields first to spot the difference:

  • serviceTypeCodes
    The Service Type Code (STC) indicates what type of service the benefit applies to. If two benefitsInformation objects have different serviceTypeCodes values, they apply to different services – like pharmacy and mental health services.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include the same STC.


  • coverageLevelCode
    Whether the benefit applies to the plan’s subscriber, their family, etc. A $20 individual deductible and $50 family deductible aren't duplicates. They're separate buckets that apply to different members of the patient’s plan.


    If coverageLevelCode is missing, assume the benefit entry applies to individual coverage.

  • inPlanNetworkIndicatorCode
    Whether the benefit applies to in-network providers, out-of-network providers, or both. This often explains the biggest price differences in what the patient pays.

  • timeQualifierCode
    The time period for the benefits, such as calendar year, remaining year, or visit. A $500 calendar year maximum is different from a $500 per-visit limit.

  • additionalInformation.description
    Free-text notes – these act as fine print. Payers often use these to include specific procedure codes, exclusions, carve outs, or special rules. As a rule of thumb, more specific descriptions override less specific ones.

    In many cases, these descriptions will be in a separate entry for the STC. These entries typically have a code of 1 (Active Coverage) or  D (Benefit Description).

  • eligibilityAdditionalInformation.industryCode
    When eligibilityAdditionalInformation.codeListQualifierCode is set to ZZ (Mutually Defined), this field contains a code for where the service takes place. Some payers offer reduced co-pays or coinsurance for telehealth visits.


    See Place of Service Code Set on CMS.gov for a list of these codes and their descriptions.

Other fields

The above list isn’t exhaustive. If you’ve checked these fields and still can’t spot differences between similar benefit entries, try diffing the entries in a code editor or a similar tool.

Examples

Here are a few examples of near-identical benefit entries we’ve helped customers interpret.

Multiple co-pays for the same service
The following benefits cover mental health outpatient visits (STC CF).

The difference is in the additionalInformation.description field. Primary care providers (PCPs) have a $25 co-pay. Specialists and other providers have a $75 co-pay.

// PCP co-pay
{
  "serviceTypeCodes": ["CF"],              // Mental health outpatient
  "code": "B",                             // Co-pay
  "coverageLevelCode": "IND",              // Individual coverage
  "inPlanNetworkIndicatorCode": "Y",       // In-network
  ...
  "benefitAmount": "25",                   // $25 co-pay
  "additionalInformation": [
    {
      "description": "Provider Role PCP"    // Primary care provider only
    }
  ]
}

// Specialist co-pay
{
  "serviceTypeCodes": ["CF"],
  "code": "B",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "Y",
  ...
  "benefitAmount": "75",                   // $75 co-pay
  "additionalInformation": [
    {
      "description": "Provider Role OTHER"  // All other providers
    }
  ]
}

Different provider network status, different deductibles
The benefits below both cover general medical care (STC 30). Both have an annual deductible.

The only difference is the provider’s network status. In-network providers have a $1000 deductible. Out-of-network providers have a $2500 deductible.

// In-network deductible
{
  "serviceTypeCodes": ["30"],              // General medical
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "timeQualifierCode": "23",               // Calendar year
  ...
  "benefitAmount": "1000",                 // **$1000 deductible**
  "inPlanNetworkIndicatorCode": "Y",       // **In-network only**
}


// Out-of-network deductible
{
  "serviceTypeCodes": ["30"],
  "code": "C",
  "coverageLevelCode": "IND",
  "timeQualifierCode": "23",
  ...
  "benefitAmount": "2500",                 // **$2500 deductible**
  "inPlanNetworkIndicatorCode": "N",       // **Out-of-network only**
}

Co-insurance for different procedures
These dental benefits all cover adjunctive dental services (STC 28). The coinsurance percentage depends on which procedure codes are billed.

In this case, the procedure codes are CDT (Current Dental Terminology) codes, which are used for dental services.

The codes for each coinsurance are listed in the additionalInformation.description field.

// Fully covered procedures
{
  "serviceTypeCodes": ["28"],              // Adjunctive dental services
  "code": "A",                             // Co-insurance
  "coverageLevelCode": "IND",              // Individual coverage
  "inPlanNetworkIndicatorCode": "W",       // Not applicable
  "benefitPercent": "0",                   // Patient pays 0% (fully covered)
  "additionalInformation": [
    {
	// CDT codes for palliative treatment
      "description": "D9110 D9912"
    }
  ]
}

// 20% coinsurance procedures
{
  "serviceTypeCodes": ["28"],
  "code": "A",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "W",
  "benefitPercent": "0.2",                 // Patient pays 20%
  "additionalInformation": [
    {
	// CDT codes for consultation and diagnostic procedures
      "description": "D9910 D9911 D9930 D9942 D9943 D9950 D9951 D9952"
    }
  ]
}

// 50% coinsurance procedures
{
  "serviceTypeCodes": ["28"],
  "code": "A",
  "coverageLevelCode": "IND",
  "inPlanNetworkIndicatorCode": "W",
  "benefitPercent": "0.5",                 // Patient pays 50%
  "additionalInformation": [
    {
	// CDT codes for hospital/facility services and anesthesia
      "description": "D9938 D9939 D9940 D9944 D9945 D9946 D9947 D9948 D9949 D9954 D9955 D9956 D9957 D9959"
    }
  ]
}

Get help from eligibility experts

Sometimes, payers do return conflicting benefit entries. We've seen it. In other cases, descriptions aren’t clear about when a benefit applies.

If you need help, reach out. We offer real-time support and answer questions in minutes. Our experts have helped hundreds of teams interpret confusing eligibility responses.

Sep 11, 2025

Guide

Big takeaway: Use patient control numbers to track a claim from submission to remit.

A patient control number is a tracking ID for a claim.

You create a patient control number when you submit a claim. The payer sends the ID back in follow-up transactions: claim acknowledgments, Electronic Remittance Advice (ERAs), and claim status checks.

This guide gives you best practices for creating patient control numbers and where to find them in each transaction.

Patient control number locations

This table shows the location of patient control numbers across claim-related transactions in Stedi’s JSON API requests and responses.


837P, 837I, 837D claim submissions

277CA claim acknowledgments

835 ERAs

276/277 claim status checks

JSON API endpoint

Professional Claims JSON

Dental Claims JSON

Institutional Claims JSON

277CA Report

835 ERA Report

276/277 Real-Time Claim Status JSON

Location of the patient control number

Request:
claimInformation
└─patientControlNumber

Response:
claimReference
└─patientControlNumber

Response:
claimStatus
└─patientAccountNumber

or
claimStatus
└─referencedTransactionTraceNumber

Response:
claimPaymentInfo
└─patientControlNumber

Response:
claimStatus
└─patientAccountNumber

Best practices for creating patient control numbers

Here's what we found works best in practice:

  • Stick to 17 characters.
    X12 states patient control numbers can be up to 20 characters. But some payers cut off values longer than 17 characters in ERAs and claim acknowledgments.

  • Use a unique patient control number for each claim.
    If multiple claims have the same patient control number, you may match the claim to the wrong ERA or acknowledgment.

  • Use alphanumeric characters only.
    Patient control numbers can contain both letters and numbers. Avoid special characters. Many payers don’t handle them properly.

  • Use random strings.
    Predictable formats, like {patientInitials}-{DOS}, can create duplicates.

Our recommendation: Use nanoid or a similar library to create a strong, unique 17-character patient control number for each claim.

Claim submission

You set the patient control number when you submit the claim.

For JSON-based claims submission endpoints, pass the patient control number in the patientControlNumber field. For example, using the Professional Claims (837P) JSON endpoint:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/professionalclaims/v3/submission" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
  ...
  "claimInformation": {
    "patientControlNumber": "ABCDEF12345267890",  // Patient control number 
    "claimChargeAmount": "109.20",
    ...
  }
  ...
}'

Claim acknowledgments

A 277CA claim acknowledgment indicates whether the claim was accepted or rejected. Payers send claim acknowledgments to the clearinghouse that submitted the claim.

Listen for claim acknowledgments
Use a webhook or the Poll Transactions endpoint to listen for incoming 277 transactions. When a claim acknowledgment arrives, use the transaction ID to fetch the acknowledgment using Stedi’s Claims acknowledgment endpoint.

Retrieve claim acknowledgments
The claim’s claimStatus.patientAccountNumber and claimStatus.referencedTransactionTraceNumber fields contain the claim’s patient control number. You can use either of these fields in your application logic.

{
  "claims": [
    {
      "claimStatus": {
        "claimServiceBeginDate": "20250101",
        "claimServiceEndDate": "20250101",
        "clearinghouseTraceNumber": "01J1SNT1FQC8ABWD44MAMBDYKA",
        "patientAccountNumber": "ABCDEF12345267890", // Patient control number
        "referencedTransactionTraceNumber": "ABCDEF12345267890", // Patient control number
        ...
      },
      ...
    },
    ...
  ]
}

ERAs

An 835 ERA contains details about payments for a claim, including explanations for any adjustments or denials.

ERAs require transaction enrollment
Payers send ERAs to the provider's designated clearinghouse. The provider designates this clearinghouse through transaction enrollment. Providers can receive ERAs at one clearinghouse per payer.

Listen for ERAs
Use a webhook or the Poll Transactions endpoint to listen for incoming 835 transactions. When an ERA arrives, you can use the transaction ID to fetch the ERA using Stedi’s 835 ERA endpoint.

Retrieve ERAs
The endpoint’s response contains the patient control number in the claimPaymentInfo.patientControlNumber field.

{
  ...
  "transactions": [
    {
      ...
      "detailInfo": [
        {
          "assignedNumber": "1",
          "paymentInfo": [
            {
              "claimPaymentInfo": {
                "patientControlNumber": "ABCDEF12345267890", // Patient control number
                "patientResponsibilityAmount": "30",
                ...
              },
              ...
            },
            ...
          ]
        }
      ],
      ...
    }
  ]
}

Claim status checks

Unlike claim acknowledgments or ERAs, you run 276/277 claim status checks in real time.

Claim status requests
The Real-Time Claim Status JSON endpoint doesn’t accept a patient control number as a request parameter. Instead, you pass in information for the payer, provider, patient, and date of service. For detailed guidance, see our recommended base JSON request.

Claim status responses
If the payer has multiple claims on file that match the information you provided, the response may include multiple claims. Each claim’s claimStatus.patientAccountNumber field contains the claim’s patient control number.

{
  "claims": [
    {
      "claimStatus": {
        "patientAccountNumber": "ABCDEF12345267890",  // Patient control number
        "amountPaid": "95.55",
        ...
      },
      ...
    }
  ],
  ...
}

Other tracking IDs

Tracking service line items

This guide covers tracking at the claims level. However, you can also track service line items from a claim in claim acknowledgments and ERAs. For details, check our docs:

Payer claim control numbers

Don’t use payer claim control numbers for tracking.
Payers use payer claim control numbers to internally track claims. You can use them when talking to a payer, but they can’t easily be matched to a claim submission.

Payer claim control numbers are returned in the following fields:

X12 control numbers

If you’re using Stedi’s JSON APIs, you can safely ignore X12 control numbers.
X12 control numbers are used by Stedi and payers for general EDI file processing. If you’re using our JSON APIs, Stedi manages them for you. They’re not useful for tracking claims or individual transactions.

Process claims with Stedi

Stedi’s JSON Claims APIs are available on all paid Stedi developer plans.

To try it out, request a free trial. We get most teams up and running in less than a day.

Sep 10, 2025

Products

You can now submit 837I institutional claims as X12 EDI using the new Institutional Claims X12 API endpoint.

Most Stedi customers submit 837I claims using our JSON-based Institutional Claims endpoint. JSON is familiar and easy to work with. You can build an integration faster.

But if you already work with X12, converting to JSON wastes time. The new endpoint accepts X12 directly. 

Previously, X12 submissions of 837I claims required SFTP. With SFTP, you have to wait for files to get errors or validation. With the API, you get instant responses. Development is faster, and debugging is easier.

Use the Institutional Claims X12 endpoint

To use the endpoint, send a POST request to the https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/raw-x12-submission endpoint. Include an 837I X12 payload in the request body’s x12 field:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/raw-x12-submission" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "x12": "ISA*00*          *00*          *ZZ*001690149382   *ZZ*STEDITEST      *240630*0847*^*00501*000000001*0*T*>~GS*HC*001690149382*STEDITEST*20240630*084744*000000001*X*005010X223A2~ST*837*3456*005010X223A2~BHT*0019*00*0123*20061123*1023*CH~NM1*41*2*PREMIER BILLING SERVICE*****46*TGJ23~PER*IC*JERRY*TE*7176149999~NM1*40*2*AETNA*****46*AETNA~HL*1**20*1~NM1*85*2*TEST HOSPITAL*****XX*1234567890~N3*123 HOSPITAL ST~N4*NEW YORK*NY*10001~REF*EI*123456789~HL*2*1*22*1~SBR*P********CI~NM1*IL*1*DOE*JOHN****MI*123456789~NM1*PR*2*AETNA*****PI*AETNA~HL*3*2*23*0~PAT*19~NM1*QC*1*DOE*JOHN~N3*123 MAIN ST~N4*NEW YORK*NY*10001~DMG*D8*19800101*M~CLM*26403774*150***11>B>1*Y*A*Y*I~DTP*472*D8*20240101~REF*D9*17312345600006351~NM1*82*1*SMITH*JANE****XX*1234567890~PRV*AT*PXC*207Q00000X~LX*1~SV2*0450*HC>99213*150****1~DTP*472*D8*20240101~SE*29*3456~GE*1*000000001~IEA*1*000000001~"
  }'

Stedi validates the EDI and sends the claim to the payer.

The endpoint returns a response from Stedi in JSON. The JSON contains information about the claim you submitted and whether the submission was successful:

{
  "claimReference": {
    "correlationId": "01J1M588QT2TAV2N093GNJ998T",
    "formatVersion": "5010",
    "patientControlNumber": "26403774",
    "payerID": "AETNA",
    "rhclaimNumber": "01J1M588QT2TAV2N093GNJ998T",
    "serviceLines": [
      {
        "lineItemControlNumber": "1"
      }
    ],
    "timeOfResponse": "2024-07-10T22:05:32.203Z"
  },
  "controlNumber": "000000001",
  "httpStatusCode": "200 OK",
  "meta": {
    "traceId": "b727b8e7-1f00-4011-bc6e-e41444d406d8"
  },
  "payer": {
    "payerID": "AETNA",
    "payerName": "Aetna"
  },
  "status": "SUCCESS",
  "tradingPartnerServiceId": "AETNA"
}

For complete details, check out the Institutional Claims (837I) Raw X12 API reference.

Try it free

The Institutional Claims X12 endpoint is available for all paid Stedi developer accounts.

To try it out, request a free trial. Most teams are up and running in under a day.

Sep 8, 2025

Guide

Big takeaway: Most payers omit carveout benefits from eligibility responses, but many include the carveout administrator's information. Run a second eligibility check with the carveout admin for full benefits details.

Carveout benefits can leave gaps in your eligibility workflows.

For example, many Blue Cross Blue Shield (BCBS) plans carve out mental (behavioral) health benefits to Magellan, a mental health payer.

A BCBS eligibility check may confirm the patient has mental health coverage. But the response doesn’t contain details about mental health benefits. No co-pays, deductibles, or limitations – just basic information for Magellan.

To get the full benefit details, you need to run a separate check with Magellan. This guide shows you how and what to look for in the eligibility response.

What’s a carveout?

A carveout is when the primary payer for a plan lets another entity handle certain benefits.

Often, carveout administrators specialize in benefits for a particular service, such as mental health services or pharmacy benefits.

Carveouts in eligibility responses

Payers aren’t required to return carveout benefits in eligibility checks.
Most don't. If they do, Stedi passes along what the payer provides.

Many payers return the carveout admin’s information.
However, it’s not guaranteed. If you’re using Stedi’s JSON Eligibility API, the carveout admin’s information is typically included in a related benefitsInformation entry in the response. Look for:

  • code = U (Contact Following Entity for Eligibility or Benefit Information)
    OR
    code = 1 (Active coverage)

  • serviceTypeCodes containing a related Service Type Code (STC) 

  • benefitsRelatedEntities object containing contact information for the carveout admin.

  • If present, benefitsRelatedEntities.entityIdentificationValue contains the patient’s member ID for the carveout admin.

Also look for key phrases in additionalInformation.description. These may be in a separate benefitsInformation entry with code = D (Benefit Description).

For example:

{
  "benefitsInformation": [
    {
      "code": "U",	// Contact Following Entity for Eligibility or Benefit Information
      "serviceTypeCodes": [
        "MH"		// Mental Health
      ],
      ...
      "benefitsRelatedEntities": [
      {
        "entityIdentifier": "Third-Party Administrator",
        "entityType": "Non-Person Entity",
        "entityName": "Acme Health Payer",
        "entityIdentificationValue": "123456789", // Member ID for the carveout admin
        "contactInformation": {
          "contacts": [
            {
              "communicationMode": "Telephone",
              "communicationNumber": "1234567890"
            }
          ]
        }
      },
      ...
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["MH"],
      "additionalInformation": [
        {
          "description": "BEHAVIORAL HEALTH MANAGED SEPARATELY"
        }
      ]
    }
  ],
  ...
}

Tip: Don’t rely on benefitsRelatedEntities.entityIdentifier to identify carveout admins. The value can vary between payers.

The carveout benefits runbook

Payers don’t consistently return carveout admin information. But when they do, you follow these steps to get the full carveout benefits:

  1. Run an eligibility check for the primary payer.
    Use a related STC. For tips, see our STC testing docs.

  2. Look for the carveout admin’s information.
    Check for benefitsInformation entries with a related STC in serviceTypeCodes and a benefitsRelatedEntities section.

    The benefitsRelatedEntities.entityName field will contain the carveout admin’s name. If present, benefitsRelatedEntities.entityIdentificationValue contains the patient’s member ID for the carveout admin. See the above example.

  3. Get the carveout admin’s ID.
    Use Stedi's Payer Search API or Payer Network to get the payer ID for the carveout admin’s name.

  4. Run an eligibility check for the carveout admin.
    Use the patient’s member ID for the carveout admin. If you use the right STC, many carveout admins will return the missing carveout benefits.

    The STC may differ from the primary payer. See our STC testing docs for tips.

Other ways to get carveout benefit details

If checks alone can’t get you the carveout benefits you need, try one of these methods:

Check the member ID card.
The back often lists information for carveout benefits and payers. The card may provide enough information on its own. If not, it may give you enough to run an eligibility check for the carveout admin.

Make a telephone call to the primary payer.
Use an AI voice agent to do this programmatically.

Check the primary payer’s website or portal.
Some payers post plan coverage documents with carveout details on their public website. Others may require you to log in to their portal. For programmatic access, create a scraper or use a scraping vendor.

Claims for carveout benefits

Claims for carveout benefits are often a form of crossover claim.

Submit the claim to the primary payer first. If the primary payer supports crossover, they’ll automatically forward the claim to the carveout admin. If not, you’ll need to submit a separate claim directly to the carveout admin.

You may need to complete a separate transaction enrollment for the carveout admin. For more guidance, see our crossover claims docs.

Carveouts vs. secondary or tertiary insurance

Carveouts are different from secondary or tertiary insurance:

  • Carveouts are part of a single health plan.
    Secondary and tertiary insurance is when a patient has multiple, separate health plans.

  • Carveouts don’t show up in coordination of benefits (COB) checks.
    COB checks are intended for cases where a patient has multiple health plans.

  • You can have both carveout benefits and a secondary (or tertiary) plan.
    After the primary payer or carveout admin adjudicates a claim for carveout benefits, you can submit a claim for the remaining balance to the secondary health plan. Sometimes, the primary payer will automatically forward the claim to the secondary payer.

    If you’re unsure which plan is the primary or secondary, use a COB check to find out.

Get expert support

Carveouts – and how they show up in eligibility responses – vary widely by payer and plan. Knowledgeable support can make a difference.

Stedi offers real-time support from experts over Slack or Teams. Request a free trial and try it out.

Sep 4, 2025

Guide

Big takeaway: For Medicare Advantage plans, send eligibility checks to the commercial payer, not CMS.

Verification and billing for Medicare Advantage plans can be confusing. It’s easy to send a request to the wrong payer or miss an important detail.

This guide aims to make it simple. It covers how to spot Medicare Advantage plans, where to send eligibility checks, and how to submit claims.

What is Medicare Advantage?

A Medicare Advantage plan – also called Medicare Part C – is a health plan from a private payer that’s approved by Medicare. It serves as an alternative to Original Medicare. 

Medicare Advantage plans cover benefits included in Medicare Part A (hospital benefits) and Part B (medical benefits). Along with those benefits, Medicare Advantage plans often provide extra coverage like prescription drugs, vision, dental, and hearing. 

Eligibility checks for Medicare Advantage plans

When running eligibility checks for Medicare Advantage plans:

  • Send the check to the plan’s commercial payer.
    Don’t send the check to the National Centers for Medicare & Medicaid Services (CMS) – the government payer for Medicare.

  • Check the payer ID.
    Some payers have separate payer IDs for Medicare Advantage plans and other lines of business, like employer-sponsored plans. For example, CareFirst Medicare Advantage vs CareFirst Blue Cross Blue Shield Maryland.

    Use the payer ID for the Medicare Advantage payer. You can get the ID using the Stedi Payer Network or Payers API.

  • Transaction enrollment isn’t typically required.
    Most Medicare Advantage payers don’t require transaction enrollment for eligibility checks. You can check for transaction enrollment requirements using the Stedi Payer Network or Payers API.

  • Use the commercial plan’s member ID – if required.
    Many Medicare Advantage plans allow eligibility checks with just the patient’s first name, last name, and date of birth.

    If a member ID is required, use the commercial plan’s member ID, not the patient’s Medicare Beneficiary Identifier (MBI) used for Medicare.

How to spot a Medicare Advantage plan in an eligibility response

Providers often want to know if a plan is a Medicare Advantage plan. These plans often have different prior authorization requirements and reimbursement rates than traditional Medicare.

To spot a Medicare Advantage plan in a commercial payer’s eligibility response, look for either of the following indicators in the JSON Eligibility API's response:

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)

  • planInformation.hicNumber or benefitsInformation.benefitsAdditionalInformation.hicNumber, which contains the patient’s MBI.

{
  "benefitsInformation": [
    {
      "code": "1",                 // Active Coverage
      "serviceTypeCodes": ["30"],  // Health Benefit Plan Coverage
      "insuranceTypeCode": "MA",   // Medicare Part A
      ...
      "benefitsAdditionalInformation": {
        "hicNumber": "1AA2CC3DD45" // Patient's MBI
      },
      ...
    },
    ...
  ],
  ...
}

What is a HIC number?
The hicNumber property name refers to a Health Insurance Claim (HIC) Number, the old member ID system for Medicare. HIC numbers were usually a Social Security Number plus a letter, such as 123-45-6789A.

CMS now uses MBIs for member IDs – and that’s usually what fills these properties when present. MBIs are 11 characters: numbers and capital letters, no spaces. For MBI formatting rules, see CMS’s Understanding the MBI doc.

Medicare Advantage plans in CMS eligibility responses

Sometimes, a patient may be confused about whether they’re covered by Original Medicare or Medicare Advantage.

You may run an eligibility check with CMS (the Medicare government payer) or an MBI lookup (which returns a CMS eligibility response on a match), only to discover through the response that the patient actually has Medicare Advantage coverage.

If the patient has Medicare Advantage, the eligibility response from CMS will include a benefitsInformation object with the following:

  • code = U (Contact Following Entity for Eligibility or Benefit Information)

  • serviceTypeCodes = 30 (Health Benefit Plan Coverage)
    OR
    serviceTypeCodes = 30 AND CQ (Case Management

  • insuranceTypeCode = HM (HMO), HN (HMO - Medicare Risk), IN (Indemnity), PR (PPO), or PS (POS)

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Primary Payer

The name of the Medicare Advantage payer is usually in the object’s benefitsRelatedEntities.entityName property. For example:

{
  "benefitsInformation": [
    {
      "code": "U",	// Contact Following Entity for Eligibility or Benefit Information
      "serviceTypeCodes": ["30"],	 // Health Benefit Plan Coverage
      "insuranceTypeCode": "HM",	 // HMO
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Primary Payer",
          "entityName": "BLUE CROSS MEDICARE ADVANTAGE",
          ...
        }
      ],
      ...
    },
    ...
  ],
  ...
}

Note: Don’t use CMS eligibility checks to verify Medicare Advantage coverage. CMS prohibits this.

Coordination of benefits (COB) checks for Medicare Advantage

Medicare Advantage patients often have supplemental insurance or employer-sponsored coverage that could be primary or secondary.

A COB check can help you determine the correct billing order. Most Medicare Advantage plans are supported.

Medicare vs. Medigap

Although Medicare Advantage patients often have supplemental insurance, they won’t have Medigap.

Medigap – also called Medicare Supplement Insurance – is supplemental insurance that helps pay out-of-pocket costs (like deductibles and coinsurance) for people with Original Medicare. 

  • Medigap is completely separate from Medicare Advantage.

  • You can’t have Medigap and Medicare Advantage at the same time.
    Payers can’t sell someone Medigap if they’re on a Medicare Advantage plan.

  • Medigap only works with Original Medicare, not Medicare Advantage

Medicare Advantage claims

Submit claims for Medicare Advantage plans to the commercial payer – just as you would for any commercial health plan. Don’t send Medicare Advantage claims to CMS.

Get started

You can run eligibility checks, MBI lookups, and COB checks – and submit claims – using any paid Stedi developer plan.

If you don't have a paid developer plan, request a free trial. We get most teams up and running in under a day.

Sep 3, 2025

Spotlight

Federico Ruiz @ Puppeteer AI

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Federico Ruiz, Founder and CEO of Puppeteer AI.

What does Puppeteer do?

Puppeteer builds AI agents that handle the patient work that clogs your day: calls, follow-ups, symptom checks, scheduling, and routine questions, so clinicians can focus on care. Our agents hold natural phone conversations, ask clinically relevant questions, summarize to the chart, and keep checking in after the visit. Think continuous, proactive navigation of each patient’s journey, with humans in the loop for anything sensitive or complex. The outcome is less admin, fewer missed appointments, and more capacity without more headcount.

How did you end up working in health tech?

I was working at Meta at the time, but my head was already in the space of building something of my own. I had just launched LangAI, and I was really interested in how agents could evolve beyond the narrow use cases we were seeing then. Back in those days, “agents” weren’t a mainstream concept – it still felt like an open frontier.

I stayed close with Alan and the founders of Light-it, and through those conversations, the opportunity came up to work on a project together. Their background in healthcare and my focus on agents fit naturally, and that collaboration turned into the starting point for Puppeteer, a company built around the idea that agents could actually support clinicians and patients in meaningful ways.

So the path wasn’t a grand plan. It was Meta giving me exposure to big-scale problems, LangAI showing me the potential of agents, and trusted friendships that created the right conditions to start something new in healthcare.

How does your role intersect with revenue cycle management (RCM)?

When you think about RCM, a lot of the problems trace back to the same operational gaps: missed appointments, incomplete paperwork, delays in follow-up, or patients not having the right information at the right time. My role is to make sure our agents close those gaps.

For example, if someone misses an appointment, the agent can call right away to reschedule. If benefits need to be confirmed, the agent can collect and structure that information before the visit. And in value-based programs, our agents can keep nudging patients on adherence and preventive care, which not only improves outcomes but also makes sure organizations meet their quality metrics.

So my intersection with RCM isn’t on the billing side, it’s on the upstream side: making sure the right things happen with patients, consistently, so that revenue capture becomes a natural outcome of smoother operations.

What do you think RCM will look like two years from now?

I think we’ll see a shift from RCM being a back-office function to something that’s much closer to the point of care. Conversations with patients, whether over the phone, in the waiting room, or through an AI agent, will generate structured data that flows directly into eligibility checks, claims, and follow-ups. The gap between “talking to a patient” and “having everything ready for billing” will keep getting smaller.

At the same time, value-based care is going to push revenue away from just transactions and more toward outcomes. That means systems will need to stay connected to patients long after the visit, reminding them about meds, nudging them to book labs, checking in on recovery. The financial side will depend on how well organizations can keep patients engaged and adherent, not just how fast they code a claim.

In short, RCM will still be about dollars, but it will feel less like accounting and more like care continuity, because that’s where the revenue will come from.

Sep 3, 2025

Guide

You can turn payer names into payer IDs (or the reverse) in Google Sheets using the Search Payers API endpoint. This guide tells you how.

Tip: If you just want a full list of Stedi’s payers in a spreadsheet-friendly format, you can download the latest payer CSV from the Stedi Payer Network or use the List Payers CSV endpoint.

Requirements

To use the Search Payers API endpoint, you need a paid Stedi developer account. To try it out free, request a trial.

Step 1. Create a production API key

  1. Log in to Stedi.

  2. Click your account name at the top right of the screen.

  3. Select API Keys.

  4. Click Generate new API Key.

  5. Enter a name for your API key.

  6. Select Production mode.

  7. Click Generate. Stedi generates an API key and allows you to copy it.

If you lose the API key, delete it and create a new one.

Step 2. Open Google Sheets

In your Google Sheet spreadsheet, place your payer names or payer IDs in column A. You can mix them if desired. For example:



How this may look in your spreadsheet:

Google Sheets example

Step 3. Create the script

  1. In the Google Sheets menu, click Extensions > Apps Script.

  2. Delete what’s in the editor.

  3. Paste in the following code. Replace YOUR_STEDI_API_KEY with your Stedi API key.

    /**
     * Returns the payer name and primary payer ID from Stedi's Payer Search API.
     * Usage: =STEDI_PAYER_INFO(A2)
     * Output: [[payer name, primary payer ID]]
     */
    function STEDI_PAYER_INFO(query) {
      if (!query) return [["", ""]];
      const apiKey = "YOUR_STEDI_API_KEY"; // Replace with your Stedi API key
      const encodedQuery = encodeURIComponent(query);
      const url = "https://healthcare.us.stedi.com/2024-04-01/payers/search?query=" + encodedQuery;
      const options = {
        "method": "get",
        "headers": { "Authorization": apiKey },
        "muteHttpExceptions": true
      };
      try {
        const response = UrlFetchApp.fetch(url, options);
        const status = response.getResponseCode();
        if (status === 403) {
          return [["Error: Invalid or missing API key", ""]];
        }
        const result = JSON.parse(response.getContentText());
        if (
          result.items &&
          result.items.length > 0 &&
          result.items[0].payer &&
          result.items[0].payer.displayName &&
          result.items[0].payer.primaryPayerId
        ) {
          return [[
            result.items[0].payer.displayName,
            result.items[0].payer.primaryPayerId
          ]];
        } else {
          return [["No match found", ""]];
        }
      } catch (e) {
        return [["Error: " + e.message, ""]];
      }
    }
  4. Click the Save project to Drive icon or press Ctrl + S.

The script creates an =STEDI_PAYER_INFO() formula that outputs the payer name and primary payer ID from the Search Payers API endpoint.

Step 4. Use the Google Sheets formula

In your Google Sheets spreadsheet, use the =STEDI_PAYER_INFO() formula to get the payer name and primary payer ID for each value in column A. For example:

Google Sheets example

The search supports fuzzy matching. The formula returns the closest match available.

Note: The payer names returned by the Search Payers API endpoint are intended for routing transactions – not for display in patient-facing UIs. If you need a patient-facing name, build your own list and map the names to Stedi payer IDs.

Get started

The Payers API is available on Stedi’s paid developer plan.

To try it out, request a free trial. Most teams are up and running in less than a day.

Sep 2, 2025

Products

Today, we’re announcing Stedi integrated accounts and Stedi apps – a new set of capabilities that allows end customers of Stedi’s Platform Partners to have Stedi accounts of their own.

Integrated accounts drastically reduce the amount of clearinghouse functionality that partners need to build themselves.

Instead of partners having to replicate Stedi’s user interfaces within their own applications, providers can use their own dedicated Stedi accounts that are pre-integrated into partner platforms, such as Revenue Cycle Management (RCM) systems, Practice Management Systems (PMS), and Electronic Healthcare Record (EHR) systems. 

Making integrations easier

Platform Partners have used Stedi’s clearinghouse APIs to incorporate healthcare claims and eligibility functionality into their own systems. To build this functionality, a partner typically maintains a single Stedi account that powers all of the transactions for their provider customers. 

For example, an RCM system would submit claims to a single Stedi account using Stedi’s Claims API or SFTP, and would create transaction enrollment requests for each provider’s required ERA enrollments. In other words, the RCM system would use a single Stedi account in a multitenant fashion – that is, with multiple end provider customers (such as an independent practitioner, group practice, or health system) operating within a single Stedi account.

This multitenant pattern has meant that each Stedi partner needed to replicate much of Stedi’s functionality within their own platform, since providers needed the ability to perform all necessary actions within the partner’s system. For example:

  • Any partner that offered functionality related to ERAs needed to build out a complex set of user interfaces and email notifications for transaction enrollment.

  • Any partner that offered the ability to submit claims needed to also have the ability to manually modify and resubmit claims that are rejected or denied.

  • Any partner that offered eligibility checks needed to build out functionality for troubleshooting failed attempts and functionality for batch uploads. These partners were also on the hook for support, even if Stedi was better positioned to help fix the issue.

This functionality is complex and time-consuming to build, and each time Stedi launched a new feature like delegated signature authority or the Stedi Agent, it meant that our partners needed to dedicate engineering, product, and design resources to incorporate Stedi’s latest functionality. 

For these reasons, the number one feature request we’ve heard from our partners is that they want to allow their providers to have Stedi accounts of their own. This way, providers can use Stedi’s functionality directly, rather than partners needing to replicate it.

Integrated Stedi accounts give providers that access. Stedi apps connect integrated accounts to one or more partner platforms.

Integrated accounts

Integrated accounts are simplified Stedi accounts that are designed to be friendly to non-technical users. The more complex developer-focused functionality – such as configuring API access and webhooks, and viewing raw JSON payloads – has been removed. 

The interfaces for common tasks such as running and troubleshooting eligibility checks, modifying and resubmitting claims, and managing transaction enrollments have been streamlined, so platforms can direct providers to their Stedi accounts for this functionality rather than building and maintaining UIs for those workflows themselves. Integrated accounts also include other standard Stedi account features, including member access, role-based access control (RBAC), and multi-factor authentication (MFA).

Create an integrated account

Providers can create integrated accounts using a self-service signup flow:

  1. Create a Stedi sandbox account.

  2. In the sandbox, click Upgrade

  3. Select Integrated Account and follow the prompts.

Stedi apps

Integrated accounts can install Stedi apps, which are Stedi’s pre-built integrations to a growing list of third-party RCM, PMS, EHR, and other platforms.

Stedi apps directory

Stedi apps allow providers to quickly connect their Stedi account to these external systems using preconfigured SFTP credentials and API keys.

They can also grant Stedi portal access for external support and implementation teams to assist with setup and ongoing support.

Aug 28, 2025

Guide

Transaction enrollment registers a provider to exchange specific transaction types with a payer. For Electronic Remittance Advice (ERAs), enrollment is always required. For other types of transactions, enrollment requirements depend on the payer.

Most traditional clearinghouses treat transaction enrollment as a cost center. Because ERA revenue is small, they try to minimize their involvement and put the work on you. You end up filling out PDFs, chasing signatures, and checking portal statuses just to onboard a provider.

Many Stedi customers manage transaction enrollment for hundreds or thousands of providers. At that scale, enrollment can be an operational burden. One team told us that before switching to Stedi, their staff spent 25–30% of their time just managing enrollment requests.

At Stedi, we treat transaction enrollment as a core part of our product experience. We offer fully managed, API-based transaction enrollment designed to reduce operational overhead, eliminate manual steps, and improve visibility. So you can scale provider onboarding without scaling manual work.

This guide explains how transaction enrollment works at Stedi, how we designed it, and how to manage enrollments at scale. It incorporates practices we’ve seen high-scaling teams use to streamline provider onboarding.

Types of enrollment

Transaction enrollment is just one part of the broader enrollment process. It’s the final step – and the only one that involves working with your clearinghouse. Providers typically complete three types of enrollment to work with a payer:

  • Credentialing – Verifies licenses, training, and qualifications.

  • Payer enrollment – Registers the provider with specific insurance plans. This is typically when providers set rates with a payer.

  • Transaction enrollment – Enables the provider to exchange transactions through a clearinghouse.

This guide focuses only on transaction enrollment. For help with credentialing or payer enrollment, contact the payer or a credentialing service, like CAQH, Assured, or Medallion.

Transaction enrollment as a product

At Stedi, we take on as much of the transaction enrollment process as possible and work constantly to automate the rest. If a manual step is required, it’s built into the product with clear next steps and full visibility.

Our goal is to eliminate back-and-forth. You shouldn’t need to track spreadsheets or email threads to onboard a provider.

Key parts of this approach include:

  • API-first design – Submit and track enrollment requests programmatically using the Enrollments API. Built for automation at scale, with full visibility into status and next steps. We also support UI and bulk CSV upload.

  • One-click enrollment – For payers that support one-click enrollment, just submitting an enrollment request is enough. You don’t need to take any additional steps. One-click enrollment is available for 850+ payers. You can check support using the Payers API or the Payer Network.

  • Delegated signature authority – For payers that still require signed forms, you can authorize Stedi to sign on your behalf. It’s a one-time setup that can eliminate 90% of your enrollment paperwork.

  • Streamlined timelines – Enrollment timelines vary by payer, but most enrollments through Stedi complete in 24-48 hours.

When enrollment is required

Before exchanging transactions with a payer, check whether enrollment is required for the transaction type. Common requirement patterns:

  • 835 ERAs. Always require enrollment. Payers can only send a provider’s ERAs to one clearinghouse at a time, so they need to know where to route them.

  • 270/271 eligibility checks. A few payers, including Medicare, require enrollment. Most major payers don’t.

  • 837P/837D/837I claim submissions. Most major payers don’t require enrollment, but certain payers do – TRICARE West Region and TRICARE East are two examples that do.

Use the Stedi Payer Network or Payers API to see which payers require enrollment by transaction type.

For example, the following JSON payer record from the Payers API indicates enrollment is required for 835 ERAs (claimPayment):

{
  "displayName": "Blue Cross Blue Shield of North Carolina",
  "primaryPayerId": "BCSNC",
  ...
  "transactionSupport": {
    "claimPayment": "ENROLLMENT_REQUIRED",
   ...
  }
}

How to submit an enrollment request

If a transaction type requires enrollment, Stedi’s Enrollments API lets you automate related requests at scale. Here’s how it works:

Step 1. Create a provider record.
Use the Create Provider endpoint to submit the provider’s basic information:

  • Name

  • NPI

  • Tax ID – Either an Employer Identification Number (EIN) or Social Security Number (SSN), depending on whether the provider is a corporate entity or not.

  • Contacts – Information for one or more provider contacts.

    Ensure this contact information – excluding email and phone number – matches what the payer has on record for the provider. Some payers reject enrollment requests if the name or address doesn’t match what’s already on file.

    The payer may use the provided email and phone to reach out with enrollment steps. If you’re a vendor representing a provider, you can use your own email and phone to have the payer contact you instead.

    When creating an enrollment request in Stedi, you must select one of these contacts as the primary contact.

curl --request POST \
  --url "https://enrollments.us.stedi.com/2024-09-01/providers" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Bob Dental Inc",
    "npi": "1999999992",
    "taxIdType": "EIN",
    "taxId": "555123456",
    "contacts": [
      {
        "firstName": "Bob",
        "lastName": "Dentist",
        "email": "bob@example.com",
        "phone": "555-123-2135",
        "streetAddress1": "123 Some Street",
        "city": "Chevy Chase",
        "zipCode": "20814",
        "state": "MD"
      },
    ]
  }'

The request returns an id for the provider. You can use this ID to reference the provider record across multiple enrollment requests.

{
  ...
  "id": "10334e76-f073-4b5d-8984-81d8e5107857",
  "name": "Bob Dental Inc",
  "npi": "1999999992",
  ...
}

Step 2. Submit the enrollment request.
Use the Create Enrollment endpoint to submit the actual transaction enrollment request.

In the request, specify:

  • The provider ID, returned by the Create Provider endpoint or fetched using the List Providers endpoint.

  • The payer ID. You can get this using the Payers API.

  • The transaction types (for example,  claimPayment, eligibilityCheck) you want to enroll the provider for.

  • A primary contact – One of the contacts from the provider record.

  • A userEmail address. Stedi will send notifications for enrollment status updates to this email address. If you’re a vendor representing a provider, you can use your own email address here.

  • A status for the request. Set the status to DRAFT if you plan to work on the request later or want to wait to submit the request. Otherwise, set the status to SUBMITTED to submit the request.

curl --request POST \
  --url "https://enrollments.us.stedi.com/2024-09-01/enrollments" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": {
      "id": "db6665c5-7b97-4af9-8c68-a00a336c2998"
    },
    "payer": {
      "idOrAlias": "87726"
    },
    "transactions": {
      "claimPayment": {
        "enroll": true
      }
    },
    "primaryContact": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "test@example.com",
      "phone": "555-123-4567",
      "streetAddress1": "123 Some Str.",
      "city": "A City",
      "state": "MD",
      "zipCode": "20814"
    },
    "userEmail": "test@example.com",
    "status": "SUBMITTED"
  }'

Step 3. Track enrollment status
Each enrollment request moves through a defined set of statuses.

Transaction enrollment statuses

Status

What it means

DRAFT

The request hasn’t been submitted yet. You can still make changes.

SUBMITTED

The request is in our queue. We’re reviewing it and preparing to send it to the payer.

PROVISIONING

We’ve sent the request to the payer and are actively managing follow-up steps.

LIVE

The enrollment is complete. The provider can now exchange the specified transactions.

REJECTED

The payer rejected the request. We’ll include a reason and next steps.

CANCELED

The request was canceled before it was processed (only allowed in DRAFT or SUBMITTED states).

You can track the status of enrollment requests using the List Enrollments endpoint or the Stedi portal.

The endpoint lets you pull the status of every request and filter by status or transaction type. This lets you build custom views into your own systems or dashboards. For example:

curl --request GET \
  --url "https://enrollments.us.stedi.com/2024-09-01/enrollments?status=LIVE" \
  --header "Authorization: <api_key>"

You’ll also get email notifications whenever an enrollment status changes, like when it moves from PROVISIONING to LIVE, or if it's rejected. If an enrollment requires action on your part, we’ll reach out to you using Slack, Teams, or email with clear next steps.

Get started

Fully managed transaction enrollment is available on all paid Stedi plans.

If you’re not yet a customer, request a free trial. We get most teams up and running in under a day.

Aug 28, 2025

Spotlight

Cassandra Bahre @ Included Health

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Cassandra Bahre, Product Manager for Virtual Visit Revenue at Included Health.

What does Included Health do?

Included Health delivers personalized all-in-one healthcare to support members holistically – mind, body, and wallet. We offer a wide range of services, from care navigation to virtual care, including urgent care, primary care, and behavioral health to claims and member advocacy.

How did you end up working in health tech?

After several years in product consulting across numerous industries, I was personally struck by how insurance and benefits complexity directly influence the patient experience. I learned this firsthand when my child was diagnosed with cancer shortly after her first birthday (today she is cancer-free!). I faced numerous billing issues over the subsequent years, from a prior authorization being denied as not medically necessary to serious overbilling by the practice because prepaid funds weren't correctly applied to the claim balance.

My knowledge of revenue cycle management (RCM) granted me the ability to advocate for myself, but the billing process was still a source of unnecessary trauma during my family’s journey. My personal mission is to help others understand and navigate the healthcare system so they can advocate for themselves, too. Health tech is the perfect place to combine my skills and my goal!

How does your role intersect with revenue cycle management (RCM)?

As the Product Manager for Virtual Visit Revenue at Included Health, I'm the voice of our members and our RCM Operations team in our virtual care business.

For our members, I focus on the billing experience, advocating for cost transparency and reliable, secure payment processing. My goal is to help them focus on getting care without the stress of managing the financial side of their healthcare experience.

For our RCM Ops team, I support revenue realization for virtual visits. I collaborate with cross-functional teams across the entire revenue cycle – from eligibility and registration to coding, claims submission, denial management, and payment posting. By optimizing for an efficient claims experience, we reduce administrative overhead on the member, instilling trust that they can rely on us to effectively manage their benefits. 

When my team and I are successful, we provide high-quality, cost-transparent care to our members and recognize revenue for the business in ways that are scalable and sustainable for our operational and care delivery teams. 

What do you think RCM will look like two years from now?

I believe RCM operations are set for a deep evolution over the next couple of years. We're already seeing a rapid rise in AI usage to automate repetitive, manual tasks like data entry, eligibility verification, and payment posting. And AI is being used on the payer side by claims departments to review, approve, and deny claims, prior authorizations, and more.

This shift will allow RCM teams to focus on more complex, high-value work that requires critical thinking, such as managing complex denial appeals and analyzing data to find revenue leakages. The future of RCM will be a hybrid model where humans and AI work together, with humans providing the crucial oversight and strategic direction.

This shift will also require a renewed focus on data integrity for Health Tech teams. Healthcare data can no longer be managed in silos; it needs to be clean, accurate, and accessible across the entire revenue cycle to build effective AI agents and provide patients with a more efficient and transparent billing experience.

Lastly, I think members – as healthcare consumers – will increasingly expect cost transparency and accuracy for the care they receive. Included Health aims to make that the standard, and that’s always my north star.

Aug 27, 2025

Spotlight

Spotlight: Bruno Ferrari @ Light-it

A spotlight is a short-form interview with a leader in health tech. In this spotlight, you'll hear from Bruno Ferrari, Head of Innovation at Light-it.

What does Light-it do?

Light-it is a digital product studio specialized in healthcare. We help healthcare organizations ideate, design, develop, and launch custom software solutions, which range from patient-facing apps all the way to complex internal systems. Our focus is on combining product strategy, technical expertise, and deep knowledge of the healthcare ecosystem to create solutions that are innovative, compliant, and impactful.

How did you end up working in health tech?

I’ve always been passionate about building products that make a real difference – not just nice-to-have tools but solutions that truly impact people’s lives. When I joined Light-it, I quickly realized how uniquely complex and rewarding the healthcare industry is. Unlike many other sectors, the work you do here has a direct influence on people’s well-being, outcomes, and even quality of life. That combination of high stakes and high potential drew me in.

Healthcare comes with its own set of challenges: regulatory requirements, fragmented systems, and a huge diversity of stakeholders. But I’ve always seen those challenges as opportunities for innovation. Technology, when designed right, can unlock new ways of delivering care.

Today, I lead our Innovation and AI initiatives at Light-it. My focus is on exploring how cutting-edge technologies can be applied responsibly to solve some of the industry’s biggest problems. From reducing administrative overhead and clinician burnout to improving clinical documentation and enabling smarter use of data, I’m constantly looking for ways to push the boundaries of what’s possible while keeping compliance and patient trust at the center.

For me, health tech is the perfect intersection of purpose and innovation: you get to experiment with the future of technology, but you also know that every advancement has the potential to make healthcare more humane, efficient, and accessible.

What’s one thing you wish you could change about U.S. healthcare?

I’d love to see more interoperability and the adoption of open, flexible systems across U.S. healthcare. Today, too many providers and platforms operate in silos, which not only leads to inefficiencies but also creates a fragmented, frustrating experience for both patients and clinicians. Every time data is locked within a single system, valuable context is lost, whether it’s a physician missing part of a patient’s history or patients having to repeatedly provide the same information.

If health data could flow more seamlessly and securely across the ecosystem, it would unlock enormous benefits: better coordination of care, reduced administrative costs, improved clinical decision-making, and ultimately healthier, more empowered patients. True interoperability would also accelerate innovation, giving startups and health systems the ability to build on shared infrastructure rather than having to reinvent the wheel each time.

At the core, healthcare should put patients first, and that means enabling them and their providers to access the right information at the right time without unnecessary barriers.

What do you think U.S. healthcare will look like two years from now?

Two years from now, I think U.S. healthcare will look very different. We’ll see a major shift toward AI-enabled workflows and automation. Especially in administrative and clinical documentation, freeing clinicians from routine tasks so they can focus more on patients. Healthcare organizations will increasingly adopt tools that not only reduce the burden on providers but also improve patient engagement and unlock richer, more actionable insights from data. At the same time, trust, safety, and compliance will remain non-negotiable, meaning the solutions that thrive will be those that strike the right balance between innovation and responsibility.

AI is an incredibly powerful force, but its impact depends on how humans guide, test, and validate it before it touches patients' lives. At Light-it, we don’t just watch these trends; we anticipate them. Through our Innovation Lab, we dedicate a team fully focused on exploring, testing, and validating emerging technologies. We run experiments, build proofs of concept, and pressure-test new models so that when clients face new challenges, we’re already a step ahead with proven solutions.

At the end of the day, healthcare is about people. Technology should never replace the human connection; it should empower it. That’s the future we’re working towards: a healthcare system where innovation makes care more human, not less.

Aug 27, 2025

Guide

If you want to get benefits data, eligibility checks are usually the best place to start. They’re fast, accurate, inexpensive, and easy to automate. In most cases, eligibility responses give you everything you need.

But not always. While there is some required data, payers mostly decide what benefits to include in their eligibility responses. Benefits for some services – like medical nutrition therapy – might be hidden in free-text fields or missing entirely. If the payer leaves information you need out, it can create a gap in your workflow.

Stedi has worked with teams who have filled these gaps – for medical nutritional therapy and other services –  to keep their workflows running.

This guide covers how you can follow their practices to get the most out of eligibility responses and what to do when you need more information. It uses medical nutrition therapy benefits as a running example.

Eligibility responses

Eligibility responses organize benefits by Service Type Codes (STCs), which group services into broad categories like "office visit" or "surgery." You can get the full STC list in our docs.

If you use Stedi’s JSON-based Eligibility API, these benefit details are in the benefitsInformation object array. Each object in the array includes a serviceTypeCodes field with the related STC:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["30"],         // General medical
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["30"],
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    }
  ],
  ...
}

For more information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

Why gaps can happen

Though not a one-to-one mapping, the STC you send in the eligibility request determines the benefits you get back in the response.

The problem is that X12 5010 – the X12 version mandated by HIPAA –  doesn’t have a specific STC for medical nutrition therapy or some other healthcare services.

If a payer includes benefits for these services in their eligibility responses, it’s usually under a generic STC like 30 (Health Benefit Plan Coverage) or 98 (Professional Physician Visit - Office), often as a free-text description.

These descriptions appear in the additionalInformation.description field of benefitsInformation objects. Look for them with a code of 1 (Active Coverage), D (Benefit Description), or F (Limitations), but they can also show up in other entries too.

Example: Co-pay with free-text note
For example, the following entries relate to a generic STC – BZ (Physician Visit - Office: Well). The additionalInformation.description values indicate they relate to nutritional therapy.

{
  "code": "1",                               // Active coverage
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "inPlanNetworkIndicatorCode": "W",         // Both in and out-of-network
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
  ]
},
{
  "code": "B",                               // Co-pay
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "benefitAmount": "20",                     // $20 co-pay amount
  "timeQualifierCode": "27",                 // Per visit
  "coverageLevelCode": "IND",                // Individual coverage
  "inPlanNetworkIndicatorCode": "Y",         // In-network benefits
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
    }
  ]
},
{
  "code": "B",                               // Co-pay
  "serviceTypeCodes": ["BZ"],                // Physician Visit - Office: Well
  "benefitAmount": "30",                     // $30 co-pay
  "timeQualifierCode": "27",                 // Per visit
  "coverageLevelCode": "IND",                // Individual coverage
  "inPlanNetworkIndicatorCode": "N",         // Out-of-network benefits
  "additionalInformation": [
    {
      "description": "NUTRITIONAL THERAPY AND COUNSELING"
    }
  ]
}

Example: Service limitations using codes
The following example is a bit more complex. One entry (code = D) defines abbreviations like AQ and 086 for Nutritionist. Another entry (code = A) uses these codes to show that nutritionists and other services are excluded from co-insurance for emergency hospital coverage.

{
  "code": "D", // Benefit description. Contains details or notes about the coverage.
  "additionalInformation": [
    {
      // List of abbreviations and their definitions for various codes.
      "description": "BENEFIT ABBREVIATIONS - B0 = NAPRAPATH - IL1, BY = NAPRAPATH GRP - IL1, D0 = VIRTUAL VISITS VENDOR - IL1, D7 = TELEMEDICINE - IL1, MT1, NM1, OK1, TX1, 097 = NAPRAPATH - IL1, 0I = +NON-PLN FOREIGN CLMS - IL1, MT1, NM1, TX1,"
    },
    ...
    {
      // More abbreviations and definitions
      // Note `AQ` is used for nutritionist.
      "description": "AC = AMBULANCE SERV - IL1, MT1, TX1, AG = OPTICIAN (IND) - IL1, MT1, AH = HEARING AID SUPPLIER - IL1, MT1, AM = PHARMACY - IL1, AQ = NUTRITIONIST - IL1, MT1, AX = SKILLED NURSE GRP - IL1, MT1, BG = OPTICIAN GRP - IL1, MT1, 071 = AMBULANCE SERVICES - IL1, MT1, TX1,"
    },
    {
      // More abbreviations and definitions.
      // Note `086` is also used for nutritionist.
      "description": "076 = HEARING AID & SUPPLIES - IL1, MT1, 080 = REGISTERED NURSE (RN) - IL1, 081 = LICENSED PRACTICAL NURSE (LPN) - IL1, OK1, 086 = NUTRITIONIST - IL1, MT1, 094 = PHARMACY - IL1"
    }
  ]
},
{
  "code": "A",                         // Co-insurance
  "serviceTypeCodes": [
     "51",                             // Hospital - Emergency Accident
     "52"                              // Hospital - Emergency Medical
   ],    
  "benefitPercent": "0.2",             // 20% co-insurance
  "timeQualifierCode": "23",           // Calendar year
  "coverageLevelCode": "IND",          // Individual coverage
  "inPlanNetworkIndicatorCode": "N",   // Out-of-network benefits
  "additionalInformation": [
    ...
    {
      // These provider specialties are NOT covered under this benefit.
      // The list includes `086` (Nutritionist) from the prior entry.
      "description": "EXCLUDED PROVIDER SPECIALTIES - 071, 076, 080, 081, 086, 094, 097,"
    },
    {
      // More provider specialties NOT covered under this benefit.
      // The list includes `AQ` (Nutritionist) from the prior entry.
      "description": "EXCLUDED PROVIDER TYPES - 0I, 0N, 0Q, 0T, 0U, 0V, A9, AC, AG, AH, AM, AQ, AX, B0, BG, BY, D0, D7, D7,"
    }
  ],
  ...
}

Missing benefits
In some cases, eligibility responses like the ones above may provide everything you need. However, sometimes, the payer doesn’t include the benefit at all – even if it’s covered by the patient’s plan. This makes it hard to know what’s covered, which STC to check, or how to automate parsing.

How to get the most from eligibility responses

Eligibility responses vary by payer. The best way to check for the benefit details you need is to test likely STCs with each payer. For each payer:

  1. Compile a list of STCs to test.
    Use our list of STCs for common services as a starting point. For example, for medical nutritional therapy, try 98 (Professional Physician Visit - Office), MH (Mental Health), 1 (Medical Care),  and BZ (Physician Visit - Office: Well).

  2. Send a baseline eligibility request.
    Use STC 30 (Health Benefit Plan Coverage) for general medical benefits or 35 (Dental Care) for general dental benefits.

  3. Compare the baseline response with the response to the specific STC.
    Save the benefitsInformation array for each STC and diff them. If they're different, the payer may include information about your service in the response.

  4. Search the response.
    Look for keywords related to your service in free-text fields. For medical nutrition therapy, you can match on nutrition, dietitian, and MNT.

As a test, we checked recent responses from Stedi’s top eligibility payers to see which returned benefits related to medical nutrition therapy. Out of the 140 we checked, about 40% did. That group included several major payers – like UnitedHealthcare and Blue Shield of California – who make up about 82% of our transaction volume.

There are exceptions. For example, some major payers like Cigna and Blue Cross Blue Shield of Texas didn’t appear to include nutrition therapy benefits in their responses at all.

How to fill in gaps

If the eligibility response is missing benefits you need, you can still reliably get the information. Here’s what we’ve seen work with our most successful customers:

  1. Contact the payer.
    Call or use the payer’s portal to get any missing benefits information you need. You can do this manually or use an AI voice agent or screen scraper to do it programmatically.

  2. Record what you learn.
    Regardless of the method you use, create a system to track the information you get by payer and plan. Depending on your needs, this could just be a database, spreadsheet, or JSON file.

  3. Use the collected data to enrich your eligibility responses.
    Plan benefits for the same payer and plan usually don’t vary from member to member. You can reuse the information you collect to enrich eligibility responses across patients for that plan.

    Even when plan benefits are the same, eligibility checks are still useful for things that change by member like active coverage or service history.

Get expert support

Eligibility checks are a good first line of defense for fetching benefits data. They work for most cases and help automate your workflow. But when gaps appear, you can still get answers.

Support often plays a key role in filling those gaps. Stedi has helped several companies, like Berry Street and others, interpret eligibility responses and optimize their workflows for medical nutrition therapy and more.

Request a free trial and see our support for yourself.

Aug 26, 2025

Products

You can now correct and resubmit claims directly from the transaction detail page of any claim in the Stedi portal. 

We’ve also made other UI improvements that make the portal’s Claims section simpler and faster to use. Here’s what’s new:

  • Streamlined list views and filters on the Transactions and Files pages help you find what you need, fast.

  • Cleaner detail pages for transactions and files put the most important information front and center.

These changes cut the clutter and help anyone using the portal – developer or operator – resubmit claims quickly, which means faster payments for the provider.

Correct and resubmit claims

You can now correct and resubmit claims from the transaction details page in the Claims section of the Stedi Portal. Just open a claim from the Transactions page, and click Edit and resubmit.

Edit and resubmit

The claim opens in an interactive inspector, with the X12 EDI on the left and the EDI specification on the right. Make your changes, and click Review and submit. You can review a diff of the updated X12 before resubmitting.

X12 diff

Improved list page views and filters

The Transactions and Files list pages now have a simplified layout. We’ve removed unneeded columns and simplified each page’s filter. For example, the Transactions page:

Transactions page

The Files page:

Files page

Improved detail pages

We’ve revamped the File detail page so that you can see the file’s X12 alongside the EDI specification. You can also view related transactions in the Transactions tab.

File detail page

The Transaction detail page now includes tabs for X12 and JSON versions of the transaction, along with webhook deliveries, which are useful for 277CA and 835 transactions.

Transaction detail page

Get started

The improved claims UI is now available in the Stedi portal for all paid plans. If you’re not yet a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Aug 21, 2025

Products

You can now run 276/277 real-time claim status checks in the Stedi portal.

A real-time claim status check tells you if a claim was received, is in process, was denied, or has another status with the payer. You typically run claim status checks if you haven’t gotten a claim acknowledgment or an ERA for a claim within an expected timeframe – typically around 21 days.

Previously, you could only run these checks using Stedi’s Real-Time Claim Status API.

Now, you can send real-time claim status requests and view the responses right in the Stedi portal. This makes it easy for anyone – whether you’re an operator or a developer – to check on claims and debug issues. This can help speed up resubmissions and result in faster payments.

Submit a claim status request

You can submit a claim status check using the Stedi portal’s new Create claim status check page. You can access the page by clicking Claims > New claim status in the portal’s nav.

By default, the page shows fields from our recommended base request. Check our docs for best practices on filling out the request.

Create claim status check page

View claim status responses

Claim status checks run synchronously – so you get responses back quickly.

If the payer has multiple claims on file that match the information in the claim status request, the response may contain information about more than one claim. If so, you can use the dropdown to navigate between claims and quickly see each one’s status category code.

You’ll see claim-level statuses in the response, and when available from the payer, you’ll also see service-line level statuses.

Claim status response page

Get started

Claim status checks are available in the Stedi portal for all paid plans. Check the Stedi Payer Network or Payers API to see which payers support 276/277 real-time claim status checks.

If you’re not a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Aug 21, 2025

Guide

Figuring out the right steps for processing out-of-network claims can be confusing.

Some payers require provider registration. Others make you submit a claim before enrolling for Electronic Remittance Advice (ERAs). Some have few requirements at all.

If you’ve run into questions or hurdles, you’re not alone.

While requirements vary between payers, there are common patterns you can use to reliably submit out-of-network claims and – when possible – get back ERAs. 

This guide aims to answer your questions about out-of-network claims. It also covers patterns we’ve seen work.

Provider network status

A provider’s network status indicates whether a provider is in-network or out-of-network for a specific payer. If a provider is out-of-network, any claim they submit is also out-of-network.

Network status is determined by credentialing and payer enrollment, which are handled directly with the payer.

They’re separate from transaction enrollment, which determines what transactions you can exchange with a payer. Your clearinghouse only handles transaction enrollment.

For help with credentialing or payer enrollment, contact the payer or use a credentialing service like CAQH, Assured, or Medallion.

Checking a provider’s network status
You can’t reliably determine whether a provider is in-network or out-of-network using an eligibility check or other pre-claim clearinghouse transaction. However, there are options for checking this programmatically. For a rundown, see our Provider network status docs.

Out-of-network benefits
Even if a payer accepts out-of-network claims, not all patients have out-of-network benefits. You can determine if a patient has out-of-network benefits –and whether they’re eligible for reimbursement – using an eligibility check. See the In Plan Network Indicator docs

Requirements for out-of-network claims

Most payers accept out-of-network claims, but requirements can vary. The two main things to check are:

  • Whether the payer requires registration for out-of-network providers

  • Whether they require transaction enrollment for claims submission

Provider registration
Some payers require any out-of-network provider to register as a “non-participating” provider before they’ll accept claims. This registration is separate from transaction enrollment and happens outside of Stedi. 

You’ll need to contact the payer to confirm what’s needed and set up your provider if required.

Transaction enrollment for claims submission
Most payers don’t require transaction enrollment for claims submission – but some do. If a payer does and enrollment isn’t completed, the claim will be rejected.

Before submitting claims, check the payer’s enrollment requirements using the Payer API or Payer Network. You can filter payers by supported transaction type and enrollment requirements.

Once any needed provider registration or transaction enrollment is complete, the provider can submit out-of-network claims for the payer.

Submit an out-of-network claim

You submit out-of-network claims using the Claims API, the Stedi portal, or SFTP.

There are no special fields or requirements for out-of-network claims. Just submit them as you would an in-network claim. A provider doesn’t have to be enrolled for ERAs with a payer to submit claims.

You’ll receive claim acknowledgments for out-of-network claims. You can also run real-time status checks.

ERAs for out-of-network claims

Unlike claims, all payers require transaction enrollment for ERAs. Providers can only be enrolled for ERAs with one clearinghouse per payer at a time.

Transaction enrollment for ERAs
ERA enrollment requirements for out-of-network providers vary across payers.

Some payers treat ERA enrollment the same for all providers. Others require out-of-network providers to be "on-file" first. Some payers don’t allow out-of-network providers to enroll for ERAs at all.

“On-file” requirements
To become "on-file," out-of-network providers typically need to either:

  • Submit a claim to the payer.

  • Register as a “non-participating” provider with the payer.

Many payers require a submitted claim before ERA enrollment. However, claims submitted before or during enrollment won't generate ERAs. 

Many payers send Explanations of Benefits (EOBs) – typically snail mailed – for out-of-network claims or when no ERA enrollment is on file. EOBs contain the same information as ERAs, but if you and your providers rely on ERAs for reconciliation, this can create issues.

A straightforward workaround is to use Anatomy to convert your EOBs into ERAs. Anatomy sends the converted ERAs to Stedi. You can then use Stedi’s APIs or SFTP to fetch the ERAs as normal.

Set up Anatomy for EOB-to-ERA conversion

Setting up Anatomy is a one-time step. Once configured, you can use it for any EOBs sent to the provider from any payer.

Step 1. Create an Anatomy account
Contact Anatomy to get started. You can upload PDFs directly in Anatomy’s UI or redirect paper EOBs to a PO Box managed by Anatomy.

Step 2. Submit an ERA enrollment request for Anatomy in Stedi
Enroll the provider for ERAs using the Enrollments API or the Stedi portal, with ANAMY (Anatomy) as the payer ID. Enrollment typically takes 1-2 business days.

Step 3. Send EOBs to Anatomy
After enrollment completes, send any paper EOBs for the provider to Anatomy.

Step 4. Fetch the converted ERAs
You can fetch the converted ERAs as normal using our APIs or SFTP. See the ERA docs.

Pricing
Stedi doesn’t charge extra to receive ERAs from Anatomy. You pay the same as you would for any ERA. Contact Anatomy for pricing on their services.

The out-of-network claims runbook

There are several ways to handle out-of-network claims with Stedi. Here's practical steps you can follow based on what we’ve seen work.

Step 1. Set up Anatomy (optional).
Follow the instructions above. You only need to configure Anatomy once. You use the same Anatomy ERA enrollment across multiple payers.

Step 2. Check the payer’s requirements for out-of-network claims and ERAs.
Contact the payer to see if they require registration for “non-participating” for out-of-network claim submissions and ERAs. If so, complete any registration steps for the provider.

Step 3. Check if the payer requires transaction enrollment for claim submission.
Check the payer’s transaction enrollment requirements for claim submission using the Payer API or Payer Network.

If transaction enrollment is required, use the Enrollments API or the Stedi portal to submit a related enrollment request. Wait for enrollment to complete before submitting claims to the payer.

Step 4. Start submitting out-of-network claims.
Submit out-of-network claims using the Claims API, the Stedi portal, or SFTP.

Many payers require a claim on file before processing ERA enrollment. While these initial claims won't get ERAs, waiting risks cash flow delays and missed timely filing deadlines. We'll address this gap in step 6 below.

Step 5. Submit an ERA enrollment request for the payer.
Use the Enrollments API or the Stedi portal to submit an ERA enrollment request for the payer.

Most enrollment requests complete in 1-2 business days, but it varies by payer. You can monitor the enrollment status using the API or the portal.

Step 6. Monitor for ERAs from the payer.
Once enrollment completes, you can listen for and fetch ERAs using our APIs or SFTP.

ERA enrollment isn't retroactive. You'll only receive ERAs for claims submitted after enrollment completes. For claims submitted before or during the enrollment process, you have two options:

  • If you're using Anatomy and the payer sends paper EOBs, Anatomy converts these EOBs into ERAs. You can then fetch the ERAs as normal using our APIs or SFTP.

  • If the payer doesn’t send EOBs or you don’t use Anatomy, use real-time claim status checks and your provider’s actual payments for reconciliation.

Get started

We help teams set up claims processing workflows every day.

When you’re ready, start a free trial and see the workflow end-to-end.

Aug 19, 2025

Products

You can now use the Stedi Agent in sandbox accounts and test mode.

In sandbox accounts and test mode, you run predefined mock eligibility requests that return realistic responses. The Stedi Agent is the Stedi portal’s built-in AI assistant. It helps you recover failed eligibility checks.

Previously, you could only use the Stedi Agent for failed eligibility checks in production. Now, you can try the agent out in sandbox accounts and test mode using a predefined mock check.

Try Stedi Agent in Eligibility Manager

The mock eligibility request is preloaded in the Stedi portal’s Eligibility Manager. To run the check:

  1. Log in to your sandbox account. If you have a production account, switch to test mode.

  2. Create a new eligibility check and select Stedi Agent as the Payer. Leave the Person as is.

  3. Submit the check. It’s expected to fail.

  4. After the check fails, click Resolve with Stedi Agent.

The agent runs in Debug view, where you can watch it work – step by step, in real time.

Stedi Agent in Eligibility Manager

Run a mock API request

You can also run the mock request using the Real-Time Eligibility Check JSON endpoint and a test API key. Sandbox accounts can only create test API keys.

The following curl request uses the request body for the predefined mock request:

curl --request POST \
  --url "https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": {
      "organizationName": "STEDI",
      "npi": "1447848577"
    },
    "tradingPartnerServiceId": "STEDI",
    "controlNumber": "112233445",
    "subscriber": {
      "memberId": "23051322",
      "lastName": "Prohas",
      "firstName": "Bernie"
    },
    "stediTest": true
  }'

Try it today

Stedi Agent is available now and is completely free for sandbox accounts and in test mode.

To get access, create a sandbox account. It’s free and takes less than two minutes.

Aug 19, 2025

Guide

Stedi provides fast, reliable infrastructure for healthcare billing. Anyone doing RCM work could benefit from running on it.

But not everyone should build on Stedi themselves.

Stedi is designed for teams with developers. Think software platforms, MSOs, DSOs, and health tech startups. These teams are often building custom RCM functionality and want full control over their RCM stack. If you have engineers, Stedi’s APIs give you everything you need to do that.

But many healthcare companies don't have developers. Even those with dev teams may not want to build everything from scratch. Some use solutions from our Platform Partners directory. Others need something more custom.

That's where firms like Light-it come in. Light-it is a software development firm that specializes in healthcare. They help teams build on Stedi without writing code. Light-it handles development and other implementation details for them.

Here's how one provider worked with Light-it to verify insurance coverage for nutrition therapy.

Verifying eligibility for nutrition therapy

The provider offers virtual-first medical nutrition therapy. They work with a nationwide network of registered dietitians. Most visits are fully covered by insurance – 95% of patients pay nothing out of pocket.

To deliver that kind of experience, the provider needs to verify coverage before a patient books. That’s harder than it sounds.

Most payers don’t clearly state whether nutrition therapy is covered in their eligibility responses. The X12 270/271 standard lacks a specific Service Type Code (STC) for medical nutrition therapy. This makes it hard to get consistent data across payers.

Most clearinghouses add more friction on top of this. You have to deal with unreliable connections, outdated – or poorly documented – APIs, and lossy responses where the clearinghouse drops data from the payer’s responses. When you do get an unclear response, it can take days to get help. Many teams end up just calling the payer themselves.

Instead of adding friction, Stedi makes the process easier. We offer easy integration, reliable payer connections, and lossless eligibility responses. And we provide real-time support. When you get a confusing eligibility response, we’ll help you interpret or debug it.

From proof of concept to production

Light-it and the provider started small. They ran a proof of concept with real patient data. They tested which payers returned usable coverage.

The results were clear. With Stedi, Light-it and the provider got clean, lossless eligibility responses. When they got errors, there were predictable failure modes. Light-it built reliable logic around those failures. When responses were unclear, they turned to Stedi for help in real time.

After validating the approach, Light-it built a simple frontend. Then they connected it to Stedi's Real-Time Eligibility Check API. It’s modern, JSON-based, and well-documented. Because of that, Light-it was able to move from proof of concept to production in weeks, not months.

The result

Today, the provider runs eligibility checks before every visit. Patients see coverage results immediately.

Stedi handles the transaction layer and APIs. Light-it handles the business logic and patient experience. They tailored the workflow to match how the provider operates. This includes fallback paths when data is unexpected or incomplete.

When to use an implementation partner

If you're evaluating Stedi and don’t have developers – or don’t want to build from scratch – partnering with a healthcare software development service like Light-it might be a good fit. They offer free consultation calls with no commitment.

You can start with a proof of concept to test your specific use case. Then work with the firm to build what's missing. Stedi's modern APIs and real-time support help implementation teams ship faster.

Aug 18, 2025

Products

You can now filter results more precisely in the List Enrollments and List Providers API endpoints.

We’ve also updated the Enrollments page and Providers page in the Stedi portal to surface these filters.

Previously, you had to page through results and filter them in your client. These new options make it easier to find the data you want – faster, with less paging and smaller responses.

List Enrollments endpoint

We’ve added new query parameters to the List Enrollments endpoint. You can use the parameters to filter by:

  • Partial term: filter lets you search across multiple fields at once.

  • Status: Filter by one or more enrollment statuses—such as DRAFT, SUBMITTED, PROVISIONING, LIVE, CANCELED, or REJECTED.

  • Provider details: Filter by NPI (providerNpis), tax ID (providerTaxIds), or name (providerNames).

  • Payer: Filter by payer IDs (payerIds).

  • Source: Limit results by how the enrollment was created: API, UI, or IMPORT.

  • Transaction type: Filter by transaction types, like eligibilityCheck, claimStatus, or claimSubmission.

  • Date: Filter enrollments by when they were created (createdFrom, createdTo) or when their status last changed (statusUpdatedFrom, statusUpdatedTo).

  • Import ID: If an enrollment was created through CSV import, filter by the returned importId.

Several of these query parameters accept arrays. You can include an array parameter more than once in the URL to filter by multiple values.

For example, ?providerNames=John%20Doe&providerNames=Jane%20Doe&status=LIVE returns all enrollments in LIVE status that have either John Doe or Jane Doe as the provider:

https://enrollments.us.stedi.com/2024-09-01/enrollments?providerNames=John%20Doe&providerNames=Jane%20Doe&status=LIVE

List Providers endpoint

The List Providers endpoint now accepts a filter parameter for searching by provider name, NPI, or tax ID. Filtering is case-insensitive and supports partial matches.

For example, ?filter=2385442357 returns all providers whose name, NPI, or tax ID contains 2385442357:

https://enrollments.us.stedi.com/2024-09-01/providers?filter=2385442357

Updated filters in the Stedi portal

Alongside API improvements, we’ve updated the Stedi portal to surface these new filters on the Enrollments page and Providers page.

On the Enrollments page:

Enrollments page filters


On the Providers page:

Providers page search

Try it out

The new filters are available on all paid Stedi plans. For full details, check out the API documentation.

Aug 14, 2025

Company

In February of last year, I gathered our engineering team in a war room. Change Healthcare – the nation’s largest clearinghouse for healthcare claims processing – had been down for almost a full week due to a cyberattack that would ultimately render them unable to process many types of transactions for two months or longer. 

It’s hard to explain the magnitude of the Change Healthcare outage to people outside the healthcare industry. Nearly 40% of healthcare claims processed in the United States flowed through Change’s platform. They processed an aggregate $1.5 trillion of claims volume annually – 15 billion claims – and they were the exclusive designated clearinghouse for dozens of payers ranging from small regional payers to UnitedHealthcare, Change’s parent company. Healthcare spend in the US is $4.9 trillion annually – 18% of total GDP – which means that when Change went down, it was processing roughly 5.5% of US GDP

When the seriousness of Change’s situation became clear, we worked around the clock to accelerate the development of our own clearinghouse that we had planned to launch later that year. We announced a drop-in replacement for Change’s clearinghouse just a few days later, and the 7 weeks afterwards were unlike anything I had experienced in 20 years of business. We couldn’t leave our keyboards during any waking hour of the day for more than a few minutes at a time – 6 and 7 figure deals went from initial phone call or text message to signed terms in under an hour. 

Change Healthcare has long since come back online, but our growth trajectory has only steepened. In April 2025, we were named Ramp’s 3rd-fastest growing software vendor. Last month, we signed 5x the number of customers that we signed at the height of the Change outage. Stedi has become the de facto choice for virtually every new venture-backed health tech company – and as later-stage health tech companies and traditional institutions revisit their legacy clearinghouse dependencies in the wake of the Change outage, Stedi’s cloud-native, API-first platform has become the obvious choice. 

But more and more, our growth is driven by GenAI use cases from all segments of the market – from brand new startups to traditional companies coming to Stedi to build agentic functionality into their existing platform. One-third of our customer base is now made up of fully native GenAI companies, an extremely high-growth cohort that has collectively raised an astounding $5B in funding to date. 

Today, we’re announcing our own $70 million Series B fundraise, co-led by Stripe and Addition, with participation from USV, First Round, Bloomberg Beta, BoxGroup, Ribbit Capital, and other top investors, which includes a $50 million previously-unannounced round plus $20 million of new capital.

Our mission is to make healthcare transactions as reliable as running water. This new funding has allowed us to double down on rebuilding the backbone of healthcare transaction infrastructure as Revenue Cycle Management (RCM) undergoes an AI-driven transformation. In addition to our best-in-class APIs, we’ve made it even easier for development teams to integrate to our clearinghouse using the new MCP server launched last week. Yesterday, we launched the Stedi Agent to power AI functionality within our platform directly.

What’s driving the AI boom in RCM? 

RCM is practically designed to be automatable using AI. Every claim, remittance, and eligibility check is already transmitted in a well-structured transaction, giving AI models clean, labeled data to work with. RCM workflows are rule-bound – that is, every step is governed by explicit payer or regulatory logic. Eligibility follows coverage rules; claim submissions and resubmissions must meet payer-specific requirements; denial appeals hinge on predefined evidence thresholds and deadlines. Because each decision is effectively a yes/no test against a published rule set, accuracy can be measured objectively and improvements validated quickly – ideal conditions for AI agents to learn, iterate, and outperform manual processes.

But when it comes to implementing AI workflows, development teams hit frustrating roadblocks with legacy clearinghouses. Most of the functionality offered by legacy clearinghouses is not accessible programmatically. If you’re lucky, you might be able to do basic eligibility and claim submission using an API – a distant second-class citizen to the clearinghouse’s main focus: traditional EHR integration. More often, you’re relying on brittle portal scraping, email capture, and PDF parsing to attempt to stitch together a passable workflow. 

The legacy clearinghouses are unlikely to get much better. They were built pre-cloud computing (and in many cases, pre-internet), and most are the result of a series of private equity acquisitions with tech stacks that were never harmonized or modernized. As a result, the technology roadmaps move at a glacial pace – the people who built the systems have long since departed and most of the effort is expended on ‘keep the lights on’ maintenance. 

Stedi’s approach is API-first: every piece of functionality available through our user interface is available via API, from the basics like eligibility checks, claims, and remits to the often-neglected aspects like payer search and transaction enrollment. Alongside our APIs, we offer a full suite of modern user interfaces that are easy for non-technical users to use.

Our thesis is simple: as more and more aspects of RCM software are subsumed by agentic workflows, companies will shift ever-greater portions of their workloads to the platforms that offer the best accessibility and legibility to AI agents that are performing actions; since other clearinghouses don’t offer ways to perform tasks programmatically, customers will continue to migrate to Stedi as they build net-new workflows, or as they find that existing workflows come to exceed the requirements afforded by other clearinghouses.

We have a single question that we use to guide our roadmap decisions: does this make it easier for humans and agents to interact with our platform? This has led to dozens of small improvements and major launches over the past several months, and will lead to many more. This latest investment allows us to continue to expand the breadth and depth of our transaction functionality in order to serve the needs of the smallest providers and the largest health systems, and everyone in between.

Most importantly, it allows us to accelerate hiring of world-class talent across engineering, product, design, business operations, and more. If that sounds exciting to you, come work with us.

Aug 13, 2025

Products

Now available in Stedi's Eligibility Manager, the Stedi Agent brings AI-powered automation to healthcare clearinghouse workflows, beginning with eligibility check recovery.

Most failed eligibility checks are recoverable. Common causes include mismatched patient details, an incorrect payer ID, or a temporary outage on the payer’s side. In most cases, these errors have clear, automatable recovery steps.

Now, the Stedi Agent can run those steps for you.

In Eligibility Manager, each eligibility check and any related retries are grouped under a search. If a check fails with a known recoverable error, a Resolve with Stedi Agent option appears next to the related search. The agent runs in Debug view, where you can watch it work – step by step, in real time.

Stedi Agent

How it works

The Resolve with Stedi Agent option only appears where the agent can currently help. We’re starting with the most common errors and expanding coverage based on what works in practice.

When you start the agent, it examines the eligibility search’s failed checks and works through recovery strategies based on the error type. For example, for mismatched patient details, it might try different combinations of patient data or adjust name formats.

The agent can make API calls to the Real-Time Eligibility Check JSON and Search Payers endpoints.  

Each check it runs is added to the same eligibility search and appears in real time in the Debug view. The agent only uses data from the eligibility search it's running on.

Security

The Stedi Agent is hosted on Stedi's HIPAA-compliant infrastructure, which runs completely on AWS. The agent uses Stedi's existing security model, including role-based access control (RBAC) and support for multi-factor authentication (MFA). You must have the Operator role or above to use the agent.

The agent only accesses data from the eligibility search it's working on. It can’t access data from other searches, customers, or systems.

Pricing

The Stedi Agent is available on all Stedi accounts at no additional cost beyond those for additional eligibility checks.

As with our APIs, there’s no charge for non-billable requests. See Billing for eligibility checks.

Try it out

The Stedi Agent is available now. Look for the Resolve with Stedi Agent option next to failed eligibility checks.

If you’re not a Stedi customer, request a trial. Most teams are up and running in less than a day.

Aug 11, 2025

Products

Stedi now enriches most Blue Cross Blue Shield (BCBS) eligibility responses with the member’s home payer name and primary payer ID. 

BCBS is a collection of 33 entities that operate independently. BlueCard is BCBS’s national program that enables members of one BCBS plan to obtain healthcare service benefits while traveling or living in another BCBS plan’s service area.

Each BCBS plan has access to the BlueCard eligibility network, which means that a provider operating in one state can check eligibility for any nationwide BCBS member using the provider’s local BCBS plan payer ID, as long as the eligibility check includes the member’s first name, last name, birthdate, and full member ID (including the 3-character BCBS alpha prefix).

For example, a provider in Texas might send an eligibility check to “Blue Cross Blue Shield of Texas” for a member whose coverage is actually with “Blue Cross Blue Shield of Alabama.” BCBS of Texas will return a successful response as long as all of the member’s details were correct. 

The problem is that the returned eligibility response doesn’t say which BCBS payer is the patient’s home payer. It just lists the payer to whom the request was sent. In the example above, the eligibility response would have no indication that the member belonged to BCBS of Alabama. 

The reason that BCBS doesn’t include this information is that it’s irrelevant for most traditional care scenarios, since the BlueCard program instructs providers to always submit claims to their local payer – not the member’s home plan. However, for multi-state providers or telehealth scenarios, the rules can differ, and it becomes important for the provider to identify the actual home plan.

To simplify this process, Stedi has developed logic to automatically detect and include the home payer’s name and ID in the eligibility response whenever possible.

When detected, this info now appears in a related benefitsInformation.benefitsRelatedEntities entry in our JSON eligibility responses and in a 2120C or 2120D loop in X12 responses.

No action is needed to take advantage of this new functionality. This enhancement is already live for all related Real-Time Eligibility Check API endpoints.

How the data is included

If you’re using our JSON eligibility API, the home payer’s details appear as a benefitsInformation.benefitsRelatedEntities entry in the response. It’s included in the same benefitsInformation entry that includes the patient’s coverage status.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "serviceTypeCodes": ["30"],
      ...
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Party Performing Verification",
          "entityType": "Non-Person Entity",
          "entityName": "Blue Cross Blue Shield of Alabama",
          "entityIdentification": "PI",
          "entityIdentificationValue": "00510BC"
        }
      ]
    },
    ...
  ],
  ...
}

In X12 eligibility responses, the home payer’s information is included in Loop 2120C or 2120D.

LS*2120~
NM1*VER*2*Blue Cross Blue Shield of Alabama*****PI*00510BC~
LE*2120

Try it out

You can see home payer enrichment in action by running a real-time eligibility check for any BCBS member for whom you have the first name, last name, birthdate, and member ID.

If you don’t have production access, request a free trial. Most teams are up and running in less than a day.

Aug 8, 2025

Company

Stedi is now e1 certified by the HITRUST for foundational cybersecurity.

HITRUST e1 Certification demonstrates that Stedi’s healthcare clearinghouse platform is focused on the most critical controls to demonstrate that essential cybersecurity hygiene is in place. The e1 assessment is one of three progressive HITRUST assessments that leverage the HITRUST Framework (HITRUST CSF) to prescribe cyber threat adaptive controls that are appropriate for each assurance type.

“The HITRUST e1 Validated Assessment is a strong fit for cyber-conscious organizations like Stedi that are looking to establish foundational assurances and demonstrate ongoing due diligence in information security and privacy,” said Ryan Patrick, VP of Adoption at HITRUST. “We commend Stedi for their commitment to cybersecurity and congratulate them on successfully achieving their HITRUST e1 Certification.”

Aug 7, 2025

Guide

“How much will this cost?” It’s the first question many patients ask their provider.

Real-time eligibility checks return the data you need to estimate patient responsibility – co-pays, deductibles, limitations – but they don’t hand you the answer. The response is often nuanced and, in some cases, can seem contradictory.

With a few simple patterns in place, though, you can reliably extract the information needed to build cost estimates you – and your providers – can trust.

This guide shows you how. It walks through the structure of a 271 eligibility response and shares practical tips for using Stedi’s Real-Time Eligibility Check JSON API to estimate patient responsibility. It also gives you tips for improving cost estimates using data from Stedi’s 835 ERA Report API.

Choose the right STC

Eligibility responses organize benefits by Service Type Codes (STCs). STCs group services into broad categories like "office visit" or "surgery."

The STC you send in the eligibility request shapes what you get back in the response. Choose the right one, and you'll get the benefits you need. Choose the wrong one, and you might miss them entirely.

Here’s a reliable approach:

  • Use one STC per request. Many payers don’t support multiple STCs. To test payer support for multiple STCs, see Test payer STC support in the Stedi docs.

  • Don't use procedure codes (HCPCS/CPT/CDT) in eligibility requests. While Medicare and some dental payers accept them, most ignore them entirely. Map the procedure codes to STCs first.

  • Only send required patient data in eligibility requests. Payers require that eligibility checks match a single member. Extra data increases the risk of a mismatch. Stick to:

    • Member ID

    • First name

    • Last name

    • Date of birth

  • You may get benefits for more STCs than you request. For example, HIPAA requires medical payers to return benefits for applicable STCs for STC 30 (Health Benefit Plan Coverage).  See General benefit checks in the docs.

  • Missing benefits don't mean missing coverage. Payers aren't required to respond to every STC. Compare the response for your STC to STC 30 for medical or STC 35 for dental. If there's a difference in the response, the STC is likely supported. See Test payer STC support in the docs.

For example, to estimate costs for a level 3 established patient office visit (CPT code 99213), start by mapping the procedure code to the most appropriate STC. In this case, that’s STC 30.

Then, send a real-time eligibility request with a body similar to:

{
    ...
    "encounter": {
      "serviceTypeCodes": ["30"]
    },
    "provider": {
      "organizationName": "ACME Health Services",
      "npi": "1999999984"
    },
    "subscriber": {
      "dateOfBirth": "19900101",
      "firstName": "John",
      "lastName": "Doe",
      "memberId": "123456789"
    }
}

Where to find patient costs in the response

Most of a patient’s benefit information, including patient cost, is in the eligibility response’s benefitsInformation array. For example:

{
  ...
  "benefitsInformation": [
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // In-network only
    },
    {
      "code": "C",                         // Deductible
      "coverageLevelCode": "IND",          // Individual coverage
      "serviceTypeCodes": ["30"],          // General medical (used for CPT 99213)
      "timeQualifierCode": "23",           // Calendar year
      "benefitAmount": "1000",             // $1000 deductible
      "inPlanNetworkIndicatorCode": "Y",   // In-network only
    },
    {
      "code": "A",                         // Co-insurance
      "serviceTypeCodes": ["35"],          // Dental Care
      "benefitPercent": "0",               // 0% co-insurance    
      "inPlanNetworkIndicatorCode": "N",   // Out-of-network only
      "compositeMedicalProcedureIdentifier": {
        "productOrServiceIDQualifierCode": "AD", // American Dental Association (ADA)
        "procedureCode": "D0150"                 // Comprehensive oral evaluation
      },
      "benefitsDateInformation": {
        "latestVisitOrConsultation": "20240404" // Last service
      },
      "benefitsServiceDelivery": [           // 1 visit every 6 months
        {
          "quantityQualifierCode": "VS",     // Visits
          "quantity": "1",                   // 1 visit
          "timePeriodQualifierCode": "34",   // Months
          "numOfPeriods": "6"                // Every 6 months
        }
      ]
    }
    ...
  ],
  ...
}

Each benefitsInformation object includes a few key fields related to patient costs:

  • serviceTypeCodes - The services this benefit applies to.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include its STC.

  • code - What the benefit is. For patient costs, the relevant codes are:

    • ACo-insurance: Percentage the patient pays for the benefit.

    • BCo-pay: Fixed dollar amount the patient pays for the benefit.

    • CDeductible: Total amount the patient must pay before benefits begin.

    • FLimitations (Maximums): Maximum benefit amount. Typically used for dental and vision plans.

    • GOut of Pocket (Stop Loss): Maximum amount a patient can pay per year. Once reached, the plan pays 100% of covered services.

    • JCost Containment: Total amount the patient must pay before benefits begin, similar to a deductible. Typically used for Medicaid benefits.

    • YSpend Down: Total amount the patient must pay before they can receive benefits. Typically used for Medicaid benefits.

  • benefitAmount or benefitPercent - The dollar or percentage value of the patient costs. benefitPercent is used for co-insurance (code = A). All other patient cost types use `benefitAmount`.

  • timeQualifierCode - What the benefit amount represents. It’s often the time period it applies to. For example, if an entry has code = G (Out-of-pocket maximum) and timeQualifierCode = 29 (Remaining Amount), then benefitAmount contains the remaining out-of-pocket maximum.

For the full list of time qualifier codes, see Time Qualifier Codes in the docs.

  • coverageLevelCode - Code indicating the level of coverage for the patient. For example, IND (Individual) or FAM (Family).

  • inPlanNetworkIndicatorCode - Whether the benefit applies to in-network or out-of-network care – not whether the provider is in-network. Possible values are "Y" (In-network), "N" (Out-of-network), "W" (Both), and "U" (Unknown). For more details, see In Plan Network Indicator in the docs.

  • additionalInformation.description - Free-text notes from the payer. For patient costs, these notes often contain limitations and qualifications, as well as carve-outs that don’t align neatly to a full STC. For example for co-pays (code = B), this may contain VISIT OFFICE PCP,TELEHEALTH PRIMARY CARE, which indicates when the co-pay applies.

Service history fields
Some benefits have frequency limits. For example, “one visit every 6 months” or “two cleanings per year.” Others depend on when the patient last received the service.

To estimate patient cost for these types of benefits, look for:

  • benefitsDateInformation – Shows when a service (like a cleaning or exam) was last performed.

  • benefitsServiceDelivery – Indicates how often a service is allowed, such as once every 6 months or twice per year. Many payers don’t populate this field and instead return this information as free text in additionalInformation.description.

These fields show up in responses for dental, vision, and Medicaid. They also apply to some medical services, like annual wellness visits or therapy sessions.

If the patient has already reached the allowed frequency, the next visit may not be covered. In that case, they may owe the full amount.

Some plans, especially dental, apply shared frequency limits across a group of procedures. For example, a plan might allow one X-ray series per year, regardless of the procedure code used later in the claim. If a claim has already been paid for one of the codes in the group, subsequent claims for others may be denied.

Handling multiple benefit entries

The same STC often has different patient costs for different scenarios. When you see multiple benefit entries for the same STC, check these fields to understand which cost applies when:

  • coverageLevelCode - Coverage level, such as individual or family

  • inPlanNetworkIndicatorCode - In-network vs. out-of-network rates

  • additionalInformation.description - Specific limitations and exceptions.

These fields are easy to miss, but without them, entries with the same STC and benefits code can look contradictory when they’re actually describing different conditions.

Example: Multiple deductibles for the same STC
In the following example, both benefitsInformation entries apply to general medical coverage (STC 30), but the inPlanNetworkIndicatorCode differentiates them: the in-network deductible is $1000. The out-of-network deductible is $2500.

// In-network deductible
{
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "serviceTypeCodes": ["30"],              // General medical
  "timeQualifierCode": "23",               // Calendar year
  "benefitAmount": "1000",                 // **$1000 deductible**
  "inPlanNetworkIndicatorCode": "Y",       // **In-network only**
}
// Out-of-network deductible
{
  "code": "C",                             // Deductible
  "coverageLevelCode": "IND",              // Individual coverage
  "serviceTypeCodes": ["30"],              // General medical
  "timeQualifierCode": "23",               // Calendar year
  "benefitAmount": "2500",                 // **$2500 deductible**
  "inPlanNetworkIndicatorCode": "N",       // **Out-of-network only**
}

Improve cost estimates with 835 ERAs

Eligibility responses return benefits at the service type level. But what payers actually pay shows up in the 835 ERA, broken down by procedure code.

You can retrieve ERAs as JSON using Stedi’s 835 ERA Report API endpoint. For example, the previous eligibility check above may correspond to a visit with the following ERA:

{
 ...
 "transactions": [
   {
     ...
     "detailInfo": [
       {
         "paymentInfo": [
           {
             "claimPaymentInfo": {
               "claimPaymentAmount": "500",           // What payer paid provider
               "patientResponsibilityAmount": "300",  // What patient owes
               "totalClaimChargeAmount": "800",       // Original charge
               "patientControlNumber": "1112223333"   // Your tracking number
             },
             "patientName": {
               "firstName": "JOHN",
               "lastName": "DOE",
               "memberId": "123456789"
             },
             "serviceLines": [
               {
                 "serviceAdjustments": [
                   {
                     "adjustmentAmount1": "300",         // Amount adjusted
                     "adjustmentReasonCode1": "1",       // Deductible
                     "claimAdjustmentGroupCode": "PR"    // Patient responsibility
                   }
                 ],
                 "servicePaymentInformation": {
                   "adjudicatedProcedureCode": "99213",   // CPT code
                   "lineItemChargeAmount": "800",         // Charged amount
                   "lineItemProviderPaymentAmount": "500" // Paid to provider
                 }
               }
             ]
           }
         ]
       }
     ],
     "financialInformation": {
       "totalActualProviderPaymentAmount": "1100"   // Total payment for all claims
     },
     ...
   }
 ]
}

To identify patient responsibility in an ERA, look for serviceLines entries with serviceAdjustments.claimAdjustmentGroupCode = PR (Patient responsibility). These adjustments are the amounts the patient is expected to pay. The adjustmentReasonCode tells you why. For example, Claim Adjustment Reason Codes (CARCs) are:

  • 1 – Deductible

  • 2 – Coinsurance

  • 3 – Copay

  • 119 – Benefit max reached for the period

For a full list, see the X12 Claim Adjustment Reason Codes list.

CARCs often align with benefitsInformation.code values in eligibility responses. For example, adjustmentReasonCode = 1 (Deductible) in the ERA corresponds to code = C (Deductible) in the eligibility response. By comparing both sources, you can refine your estimates over time.

Building a cost estimation engine
Several Stedi customers combine 271 eligibility checks with 835 ERAs to refine their patient cost estimates over time. The most reliable setups use a simple feedback loop:

  1. Estimate patient costs using eligibility responses and historical data from prior claims (see step 5).

  2. Submit claims and collect ERAs.

  3. Extract payment patterns by CPT code, payer, and plan.

  4. Compare actual payments to your original estimates.

  5. Update your logic for patient cost estimates based on actual payments.

ERAs don’t give you market-wide benchmarks – only what happened with your own claims. For common procedures with high-volume payers, that’s often enough to make confident estimates.

Real-time support for real-time eligibility

Even clean eligibility requests sometimes return unclear responses. When that happens, our support team can help make sense of it. Our average support response time is under 10 minutes.

It’s one reason teams trust Stedi to stay in the loop while they scale.

To see how it works, start a free trial.

Aug 6, 2025

Products

You can now run real-time eligibility checks using our CAQH CORE–compliant SOAP API endpoint.

CAQH CORE SOAP is a widely adopted XML-based interoperability standard for exchanging healthcare transactions, like eligibility checks. It defines how systems can connect and exchange that data in a consistent, reliable way.

If you're already using CAQH CORE SOAP, our SOAP endpoint is the fastest way to start running eligibility checks with Stedi. Just point your existing integration to our endpoint – no other changes are needed.

Most Stedi customers use our JSON API for real-time eligibility. JSON is familiar, fast to integrate, and easy to work with. But if you’ve already built on SOAP, switching to JSON adds unnecessary overhead.

This endpoint removes that step.

The endpoint supports CAQH CORE Connectivity Rule vC2.2.0. We plan to support CAQH CORE Connectivity Rule vC4.0.0 as industry adoption increases.

How it works

To use the endpoint, send a POST request to:

https://healthcare.us.stedi.com/2025-06-01/protocols/caqh-core

In the request body, use the standard CAQH CORE Connectivity Rule vC2.2.0 SOAP envelope. Wrap your X12 270 eligibility request in the Payload element as CDATA. Authenticate with WS-Security headers using your Stedi account ID and Stedi API key.

You can find your Stedi account ID at the end of any Stedi portal URL. For example, in https://portal.stedi.com/app/healthcare/eligibility?account=1111-33333-55555, the account ID is 1111-33333-55555.

<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope"
  xmlns:cor="http://www.caqh.org/SOAP/WSDL/CORERule2.2.0.xsd">
  <soapenv:Header>
    <wsse:Security soapenv:mustUnderstand="true"
      xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
      xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
      <wsse:UsernameToken>
        <wsse:Username>STEDI-ACCOUNT-ID</wsse:Username>
        <wsse:Password>STEDI-API-KEY</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
  </soapenv:Header>
  <soapenv:Body>
    <cor:COREEnvelopeRealTimeRequest>
      <PayloadType>X12_270_Request_005010X279A1</PayloadType>
      <ProcessingMode>RealTime</ProcessingMode>
      <PayloadID>YOUR-PAYLOAD-ID</PayloadID>
      <TimeStamp>2024-07-29T12:00:00Z</TimeStamp>
      <SenderID>SENDER-ID</SenderID>
      <ReceiverID>RECEIVER-ID</ReceiverID>
      <CORERuleVersion>2.2.0</CORERuleVersion>
      <Payload><![CDATA[ISA*00*          *00*          *ZZ*SENDER         *ZZ*RECEIVER       *231106*1406*^*00501*000000001*0*T*>~GS*HS*SENDERGS*RECEIVERGS*20231106*140631*000000001*X*005010X279A1~ST*270*1234*005010X279A1~BHT*0022*13*10001234*20240321*1319~HL*1**20*1~NM1*PR*2*ABCDE*****PI*11122~HL*2*1*21*1~NM1*1P*2*ACME HEALTH SERVICES*****SV*1999999984~HL*3*2*22*0~TRN*1*11122-12345*1234567890~NM1*IL*1*JANE*DOE****MI*123456789~DMG*D8*19000101~DTP*291*D8*20240108~EQ*MH~SE*13*1234~GE*1*000000001~IEA*1*000000001~]]></Payload>
    </cor:COREEnvelopeRealTimeRequest>
  </soapenv:Body>
</soapenv:Envelope>

Stedi returns a synchronous XML response containing the X12 271 eligibility response or a 999 acknowledgement, depending on the payer's response.

Try it free

The Real-Time Eligibility Check SOAP endpoint is available on all paid Stedi plans. If you’re not a customer, request a trial. Most teams are up and running in under a day.

Aug 6, 2025

Guide

If you’re building a dental eligibility product, you want accurate benefits data fast.

Real-time eligibility checks are quick, reliable, and inexpensive. They return benefits data – like coverage status, deductibles, coinsurance, and plan dates – in seconds and cost pennies per transaction.

Eligibility checks can return any benefit data that a payer chooses to include. Some payers return complete data in every response. Many dental payers don’t.

To fill in those gaps, many teams turn to scraping payer portals. Scraping is quick to roll out and can surface data that checks can’t. But those scrapers require ongoing maintenance.

We’ve worked with dozens of teams building dental eligibility tools. The teams that scale best take a hybrid approach: They start with real-time checks and scrape only when it matters.

Why scraping alone doesn’t scale

In small doses, scraping is effective, but it comes with technical debt. That debt compounds faster than many teams expect. It shows up in a few ways:

1. It’s slow.
Real-time eligibility checks typically respond in 1-3 seconds. Scraping can take 10 times longer. The scraper must log in, navigate pages, and wait for load times. Multiply that by thousands or millions of requests.

Concurrency helps, but only to a point. Most portals rate-limit traffic. Some payers require monthly minimums for automated access. Those minimums often cost more than using their APIs.

2. It’s brittle.
Scrapers frequently break. Portals can make unannounced layout changes or require multi-factor authentication (MFA). When that happens, there are no structured errors. Just screenshots and HTML.

You can’t rely on retry logic. The failure modes are too varied and too unpredictable. That means near-constant patching.

3. It burns engineering time.
One CTO told us they had 60+ scrapers in production. Their best engineers weren’t building the product – they were fixing broken scrapers. Most of the data that those scrapers collected was available in eligibility checks with much lower cost and maintenance overhead.

Scraping isn’t just hard to maintain. For most teams, it’s rarely necessary.

The 85/15 rule

Teams that we’ve seen scale portal scraping follow a simple pattern:

  • They use real-time eligibility checks alone for about 85% of verifications.

  • For the remaining 15%, they combine checks with scraping or payer calls to fill in missing details.

Instead of scraping everything, they focus on high-impact payers: ones that don’t return key data in eligibility responses and that account for a large share of volume or revenue risk. They also use eligibility checks to verify scraper output and catch failures early.

What eligibility responses cover

To put the 85/15 rule to work, you need to know what data real-time eligibility checks reliably return.

We analyzed responses from major dental payers – including MetLife, Delta Dental, and UnitedHealthcare – to find out. Here’s what we saw:

Dental benefits information

In real-time eligibility response?

Requires scraping?

Active coverage

✅ Yes

🚫 Rarely

Coverage dates

✅ Yes

🚫 Rarely

Deductible

✅ Yes

🚫 Rarely

Co-pay

✅ Yes

🚫 Rarely

Coinsurance

✅ Yes

🚫 Rarely

Service History

⚠️ Sometimes

✅ Yes

Downgrade Logic

⚠️ Sometimes

✅ Yes

Frequency Limits

⚠️ Sometimes

✅ Yes

Missing Tooth Clause

❌ No

✅ Yes

Provider network status

❌ No

✅ Yes (may require a call to the payer)

Start with checks

Scraping is a valuable tool. It’s just not something you want to rely on for every verification.

If you’ve already built out scraping, keep it. But try running eligibility checks first. They may cover more than you expect, which can lower costs and reduce engineering effort.

Get started

Try Stedi’s real-time eligibility API for free in our sandbox. You’ll get instant access to run mock checks.

When you’re ready for production, request a free trial. Most teams are up and running in under a day.

Aug 5, 2025

Products

Today, Stedi announces the release of its Model Context Protocol (MCP) server. AI agents can use Stedi’s MCP server to run eligibility checks and search payers.

A third of Stedi’s customers are generative AI companies building AI agents for revenue cycle management (RCM). Until now, connecting those agents to Stedi’s eligibility API meant writing custom integration code or copying parts of Stedi’s docs into instructions.

Stedi’s MCP server changes that.

It gives agents plug-and-play access to Stedi’s Real-Time Eligibility and Search Payers API endpoints, along with built-in guidance for common errors. You can connect your agents without having to write integration code or copy documentation.

I connected our agent to Stedi’s MCP server in minutes. Updated the instructions, and it started running eligibility checks right away.
- Rambo Wu, Engineering at Stratus

How it works

Your AI agent connects to the server using an MCP client. Once connected, the server gives your agent access to two types of capabilities – tools and prompts – as defined by the MCP server spec.

Tools perform specific actions, like running an eligibility check. They’re thin wrappers that let your agent invoke Stedi APIs – currently, the Real-Time Eligibility Check API endpoint and the Search Payers API endpoint.

Prompts help your agent decide what to do next, including how to recover from a failed check.

The following diagram show how your agent can use the MCP server to connect to Stedi’s APIs.

Stedi MCP server diagram

How to connect

The MCP server is a Streamable HTTP MCP server hosted at https://mcp.us.stedi.com/2025-07-11/mcp.

To connect your agent, add this configuration to your agent’s MCP client:

{
  "mcpServers": {
    "stedi-healthcare": {
      "type": "http",
      "url": "https://mcp.us.stedi.com/2025-07-11/mcp",
      "headers": {
        "Authorization": "YOUR_STEDI_API_KEY"
      }
    }
  }
}

Replace YOUR_STEDI_API_KEY with your actual Stedi API key.

Tools

The server provides two tools:

Prompts

The server provides prompts to help your agent recover from common errors. The prompt instructions cover the most common recoverable scenarios we see in production:

  • How to handle common errors

  • When to retry eligibility checks

  • What to do when a payer isn't found

You and your agent stay in control of executing follow-up actions, such as troubleshooting and retries.

Tip: Many LLM clients skip MCP prompts unless explicitly instructed to read them. You'll often get better results by adding “Read the prompts from Stedi's MCP server” to the beginning of your agent’s instructions.

Example usage

You can instruct your agent to use the MCP server to run eligibility checks.

For example, if you’re building a voice agent, you might give your model an instruction like:

Read the prompts from Stedi's MCP server.Then use Stedi’s MCP server to check
the patient’s eligibility programmatically before making a telephone call to
the payer. Only call the payer if the response doesn’t include the benefits you need

You can also use the MCP server to perform one-off checks using MCP clients. If you're using a third-party tool like Claude or ChatGPT, follow your organization’s data handling policies to ensure that you stay compliant with HIPAA and other applicable requirements. For example, your organization likely requires a BAA with any third-party tool before using the tool with Stedi’s MCP server.

When to use the MCP server

The MCP server excels are one-off eligibility checks, especially when your agent needs to retrieve coverage data in real time.

For example:

  • If you're building a voice agent that calls payers for benefits, use the MCP server to check eligibility first. Call the payer only  if the response doesn’t have what you need.

  • If you're building an RCM workflow agent, use Stedi’s MCP server to validate a patient’s coverage before scheduling an appointment or submitting a claim.

For bulk eligibility checks, use our eligibility APIs directly. For tips, see When to use batch eligibility checks.

For benefit interpretation, like determining remaining visits, you'll want to layer your own logic on top, just as you would with our APIs.

Performance

The MCP server adds minimal overhead to our APIs. You’ll get the same fast response times with added intelligence for your agents.

Pricing

The MCP server is available on all paid Stedi plans at no additional cost beyond those for related API calls.

As with our APIs, there’s no charge for non-billable requests. See Billing for eligibility checks.

Security and compliance

The MCP server uses the same security model as our existing APIs, including TLS encryption and API key authentication. If you're using a third-party tool to interact with the MCP server, reach out to your security team and legal counsel to ensure you have the appropriate safeguards in place.

Try it out

If you’re a Stedi customer, you can start building with the MCP server today.

If you’re not, request a trial. We can get you up and running in less than a day.

Jul 31, 2025

Guide

Real-time eligibility checks are built for real-time scenarios: A patient's on the phone. Someone's at the front desk. You need an answer in seconds.

But if you’re checking coverage for upcoming appointments or refreshing eligibility for entire patient panels, the timeline shifts. You don’t need answers in seconds. You need them in hours.

Stedi’s real-time eligibility API works for these bulk use cases until you’re running thousands or millions of checks at once. That’s when teams usually start writing separate logic for large batches, like queuing requests and handling long-running retries.

Stedi's batch eligibility API handles that for you. You can submit multiple checks in one request. Stedi queues and retries them automatically. And batch checks run in a separate pipeline, away from your real-time traffic.

When to use real-time vs. batch checks

Real-time checks are the default. Use them for:

  • When a patient at the desk

  • Verification before a visit

  • Fast feedback loops while testing or debugging

  • Smaller sets of bulk checks

  • Any time-sensitive eligibility need

As your volume grows and you find yourself building custom logic to support it, use batch checks for bulk workflows that aren’t time sensitive:

  • Monthly or weekly coverage refreshes

  • Upcoming appointments

  • Sets of thousands or millions of checks that can run in the background

  • Any workflow where a timeline of minutes to hours works

Most teams start by using real-time checks for everything. They add batch checks when they start running several thousands of time-insensitive checks at once. Until then, using real-time checks is simpler.

Run a batch check

You submit batch checks using the Batch Eligibility Check API endpoint or a CSV upload. Each check in the batch uses the same fields as real-time requests. You can track individual checks in a batch using submitterTransactionIdentifier.

curl --request POST \
  --url "https://manager.us.stedi.com/2024-04-01/eligibility-manager/batch-eligibility" \
  --header "Authorization: <api_key>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "march-2024-eligibility-batch",
    "items": [
      {
        "submitterTransactionIdentifier": "ABC123456789",
        "controlNumber": "000022222",
        "tradingPartnerServiceId": "AHS",
        "encounter": {
          "serviceTypeCodes": [
            "MH"
          ]
        },
        "provider": {
          "organizationName": "ACME Health Services",
          "npi": "1234567891"
        },
        "subscriber": {
          "dateOfBirth": "19000101",
          "firstName": "Jane",
          "lastName": "Doe",
          "memberId": "1234567890"
        }
      },
      ...
    ]
  }'

The response includes a batchId you can use to check results:

{
  "batchId": "01928d19-df25-76c0-8d51-f5351260fa05",
  "submittedAt": "2023-11-07T05:31:56Z"
}

Get batch results

Use the Poll Batch Eligibility Checks API endpoint to poll the batchId:

curl --request GET \
  --url "https://manager.us.stedi.com/2024-04-01/eligibility-manager/polling/batch-eligibility?batchId=01928d19-df25-76c0-8d51-f5351260fa05" \
  --header "Authorization: <api_key>"

You can start polling immediately. After the initial poll, use exponential backoff with jitter. Start at 2 minutes and approximately double the wait between polls, up to 8 hours.

The endpoint returns results incrementally. The response's items array contains full 271 eligibility responses in the same JSON format as real-time checks. Failed checks appear alongside successful ones. You can debug failed checks using Stedi’s Eligibility Manager

You can track batch completion by counting returned results. You can also match submitterTransactionIdentifier values for each check.

Batch processing times

Most batches complete in 15-30 minutes. If a check in the batch fails due to payer connectivity, Stedi retries the check for up to 8 hours.

Pricing

A batch check costs the same as the equivalent number of real-time checks. For example, running a batch of 500 checks costs the same as running 500 real-time checks.

There’s no charge for non-billable checks in a batch. For details, see Billing for eligibility checks.

Get started

If you’re not currently using batch checks but are considering building logic for bulk eligibility, test the pattern with a small batch, then scale up.

Batch eligibility checks are available on all paid Stedi plans. If you don’t have a paid account, request a free trial.

Jul 29, 2025

Spotlight

A spotlight is a short-form interview with a leader in RCM or health tech. In this spotlight, you'll hear from Justin Liu, Co-founder and CEO of Charta.

What does Charta do?

Charta is a proprietary AI-powered platform that optimizes medical billing and coding workflows.

By running a pre‑bill review on every chart, Charta pinpoints documentation gaps, uncovers missed revenue opportunities, and heads off potential denials – boosting accuracy and compliance in one pass.

Our customers typically see up to a 15.2% lift in RVUs per encounter and an 11% increase in revenue, all delivered with a guaranteed ROI and less administrative drag on clinical teams.

How did you end up working in health tech?

My co-founder and CTO, Scott Morris, and I were fortunate to be part of a team at the forefront of AI infrastructure innovation at Rockset, which ultimately became OpenAI’s first ever product acquisition. We saw an opportunity to take what we had learned and apply it to a space where we could drive real, tangible change. AI infrastructure is an exciting field, but we wanted to build something where AI didn’t just optimize processes – it fundamentally improved outcomes.

Healthcare stood out because of the sheer scale of inefficiencies, particularly in administrative tasks like patient chart reviews and medical billing. What really struck us was that patient charts are essentially the data layer of the entire healthcare system – every clinical decision, every billing code, every compliance check is rooted in chart documentation. Yet, reviewing these charts remains a painfully manual process, bogging down providers and leading to lost revenue, denied claims, and time away from patient care.

Instead of optimizing AI for AI’s sake, we wanted to use our expertise to solve real-world problems in a way that directly impacted people’s lives. That perspective – combined with spending a year earning our medical coding credentials and speaking with over 100 healthcare professionals – helped us build a solution that directly addresses the root problems rather than just iterating on legacy systems.

One of those early conversations was with Dr. Caesar Djavaherian, co-founder and former Chief Medical Officer of Carbon Health. The challenges we were tackling – especially around documentation and billing – were so familiar to him that he not only invested in Charta, but ultimately joined the team as our Chief Medical Officer.

How does your role intersect with revenue cycle management (RCM)?

Charta sits squarely in the middle of the RCM stack – between clinical documentation and claim submission – by running pre‑bill AI audits that boost revenue integrity and slash denial risk. 

As CEO, I’m responsible for turning those RCM pain points into product advantages: I spent my first year interviewing medical professionals, diagramming every hand‑off from charge capture to payment posting, and hard‑coding those insights into our roadmap. 

Today we still meet often with rev‑cycle leaders and use real‑world payer & provider feedback to steer cutting-edge model training and new feature prioritization. 

What do you think RCM will look like two years from now?

CMS has widened its Medicare Advantage audit program, signaling increased scrutiny of billing practices, while new federal guidance from HHS and CMS outlines frameworks for responsible AI use in administrative systems. Together, these shifts are laying the groundwork for a new phase of operational automation across the healthcare sector.

For decades, healthcare operations have lagged behind clinical innovation. The revenue cycle – arguably the financial backbone of the healthcare system – remains manual, fragmented, and error-prone. Despite years of outsourced labor and legacy tools, the problems haven’t been solved – they’ve multiplied.

What’s different now is timing. Health systems are facing unprecedented margin pressure, workforce shortages, and growing regulatory scrutiny. At the same time, there's broad consensus that AI is not just viable – it’s urgently needed. Decision-makers are ready to buy. Infrastructure that can deliver step-change improvements, not just incremental gains, is finally in reach.

Charta already reviews 100% of charts in real time; the next wave is about extending that same AI-powered automation across the rest of the revenue cycle. 

Jul 28, 2025

Company

It’s easy to ruin your company’s support by trying to scale it too early – not carelessly or intentionally – but by following the standard support playbook.

That standard playbook – async tickets, chatbots, and rigid KPIs – is meant to improve the customer experience. In practice, it often makes it worse.

Stedi is a healthcare clearinghouse for developers. We sell APIs to highly technical buyers, and we differentiate based on our technology and products. But when we ask customers why they chose us – what we do better than anyone else – they almost always say the same thing:

Support.

As one customer put it:

"I wish I could tell you that I chose Stedi because you have the best APIs or documentation, but the reality is that I went with you because you answered every message I sent within 15 minutes.”

We hear versions of that all the time. It’s not because we’ve mastered support best practices. It’s because we ignore them. Instead, we focus on one thing: solving the customer’s problem as quickly and completely as possible.

When you do support this way – the way that supposedly doesn’t scale – something unexpected happens:

You fix the root causes of customer issues. When you fix root causes, fewer things break. And when fewer things break, you avoid the type of problems that the standard support playbook was designed to manage in the first place.

Why most customer support is bad

Bad customer support is everywhere: useless chatbots, long wait times, unhelpful canned responses. Almost every company offers support. So why is support usually bad?

It’s because typical support tools and practices aren’t designed to help customers. They’re designed to make support easier at scale.

Most support systems assume your company is drowning in support requests. That might be true for some. But, early on, most companies aren’t. So when you implement these systems too soon – to solve a problem you don’t have – you don’t make the customer experience better. You make it worse.

The scale trap

Most teams start with hands-on support. Founders talk to customers, fix their problems, and improve the product. Then they make their first support hire – and assume what they’ve been doing will no longer work. This is typically when companies adopt traditional support systems.

Regardless of intent, those systems cause the company’s incentives around support to shift. You build systems that make support easier to manage at scale – all your tools are optimized for it.

That’s the scale trap.

For example, a typical support workflow looks like this:

  1. The customer creates a ticket.
    A ticketing system makes support easier to manage. Agents can organize work into queues and juggle dozens of conversations at once.

    But those same ticketing systems make it harder for customers to reach out. To get help, customers have to find the (often buried) support portal, create an account, learn a new interface, file a ticket, and, finally, get in line – just when things are already going wrong for them.

  2. Then you make them wait.
    Support teams use queues to manage their workload. Queue-based metrics also help you staff support predictably.

    For the customer, a queue means waiting. You need a quick answer to finish your work. Two hours later, you're still waiting. So customers learn: Don't ask until you're desperate.

  3. Then you don’t fix their problem.
    Support KPIs track things like closed ticket counts and time to resolution, not solved problems. There is no “root causes fixed” metric. What gets measured gets done, so agents master quick workarounds and band-aid fixes.

    For customers, that means they wait days to get shallow responses and temporary fixes. It also means they end up running into the same issues over and over again. The root cause isn’t fixed. The tickets are closed, but the problems stay. Over time, customers stop trusting you to fix their problems at all – and stop reaching out.

If you look at typical support KPIs, this setup works: There are fewer tickets per customer and faster resolution times. But the metrics don’t answer two important questions:

  1. Is the root cause of the customer’s problem really solved?

  2. Does the customer trust you enough to bring you their next problem?

The job of customer support

Support exists to address customer pain. When something breaks, people want the same things anyone in pain wants:

  1. To stop the pain.

  2. To fix the root cause so it doesn’t happen again.

  3. If that’s not possible, to manage the symptoms.

  4. If that’s not possible, to get a direct, honest explanation so they can plan around it.

And they want those things fast. When you’re in pain, you want it to be over as soon as possible.

For support teams, speed isn't just about providing relief. It’s also about empathy. When you respond quickly to someone in pain, it shows you care.

Fixing the scale trap isn’t about tools – it’s about where you focus.

Delivering good support

At Stedi, the job of support is to eliminate customer pain as quickly and thoroughly as possible.

Most people brace for delays and friction when they contact support. Instead, we respond quickly and over-index on being helpful. The contrast often surprises them.

Any company can deliver good support. It just takes hard work, consistency, and the willingness to do things differently. Here’s what that looks like for us:

Work where your customers work.
Every customer gets a dedicated Slack or Microsoft Teams channel. No tickets, no help portals. Slack and Teams are already the customer’s virtual office. When they need help, they don't have to go anywhere. We're already there.

Respond quickly.
We’ve had customers say they were surprised to get a real human response within minutes. We don’t use bots or scripts. Just people who care and can help.

Anyone can help.
Any employee can answer any question in any customer channel. Engineers who built the feature. PMs who designed it. Even the CEO. We have customer success engineers, but support is everyone's job.

If something’s broken, fix it.
Everyone is empowered to make things better. If you have the answer, share it. If something's confusing, explain it and update the docs. If there's a bug, fix it. Don’t wait or assume someone else will do it.

Turn up the volume.
We want customers to bring us more problems, not fewer. Our best customers are often the loudest. They’re pushing our product to the limit, and the questions they ask make our product better.

Go deep.
We tackle questions most healthcare clearinghouses won't touch. We debug errors with customers. We explain complicated responses line by line.

No bots.
We use AI everywhere at Stedi – but not to deflect customer questions. When you need help or have an emergency, you want a human who understands your problem, not a chatbot suggesting knowledge base articles.

Be honest and direct.
When something's broken, we say so. We tell customers why it broke and when we'll fix it.

Do what you say.
If we commit to something, we give customers a date and hit it. If we can't make the deadline, we tell them early. 

Treat every customer the same.
We don’t sell support. Every Stedi customer gets the same level of support, whether you're a two-person startup or processing millions of transactions. There are no support tiers or upsells.

Hire the best people you can.
We hire smart, technical people to do support, and we pay them well. They work hard, understand systems, and can go deep into customer problems.

We didn't start with this playbook. When we started, we were just doing the obvious: trying to help.

Later, we found that our support model has a powerful side effect: it made our product much better.

The support-product flywheel

Good support means fixing things fast. More importantly, it means fixing root causes. We never want customers to hit the same problem twice.

To fix root causes, you have to understand the real problems. Where does our product break? Where do the docs confuse people? Where are users getting stuck?

You can't fix what you don't see, but if you respond quickly and fix things, customers will start to tell you where to look. They’ll point out issues. They’ll trust you to address their pain. And because they trust you, they reach out more. The feedback gets better. The signal gets stronger.

When you fix real problems, your product improves. Fewer customers hit snags. Your support team stops firefighting and starts building. They ship tools, chase harder problems, and spend less time repeating themselves. They’re happier because they’re doing work they’re proud of.

A better product with better support attracts more customers, including ones who push your product to the limit. They find new edge cases. Which you fix. Which makes the product better. Which attracts more customers.

That's the flywheel.

When the flywheel is in effect, the most important support-related metrics can't be measured in CSAT scores or ticket counts. They show up in net revenue retention, churn, and referrals.

But does it scale?

"Your support model won't scale. It works fine with a few customers, but it won’t work past that."

We hear this a lot.

When we were just getting started with our first customers, people would tell us that our approach wouldn’t scale past 10. When we had 10 and didn’t have any issues, people told us that it works fine with 10 customers, but it wouldn’t scale to 100 customers. We keep waiting for the wall, but it still hasn't shown up – even with hundreds of customers.

Why? Our guess is the flywheel is in full effect.

But, to be fair, we have some other things working in our favor. Stedi is built for developers. Our customers are smart and often technical. When they reach out to us, they've done their homework. They ask specific questions. They test things themselves. Smart customers asking real questions keeps our support manageable, even as we grow. Our model might not work if half our tickets were "How do I log in?"

People also assume our support team must cost a fortune. It doesn't. We pay our folks very well, and it pays off in spades. While good-enough support prevents churn, great support encourages customers to use our products even more. That makes for happier customers, and happy customers are our best salespeople.

Will we need to change certain aspects of our support eventually? Probably. But the idea that great human-to-human support can't scale assumes that every customer needs the same amount of help forever.

When you solve problems instead of managing them, support volume doesn't grow linearly with your customer count.

The hard choice

Here’s our pitch. If you run a support org, you have two options:

The safe path.
Buy the ticketing system. Track deflection rates. Answer customer questions with AI slop. This is what most companies do. No one gets fired for it. And your customers might not even complain – they’ve been trained to expect bad support. But no one will rave about it.

The harder path.
Ignore what everyone else does. Do the obvious thing: Be fast, be helpful, fix real problems. Don’t worry about whether it scales.

Our suggestion: Pick the harder path. When done right, support can become your company’s biggest competitive advantage.

Your competitors can copy your features. They can match your prices. But it takes courage to do support in a way that isn’t supposed to scale. Your customers will notice. 

See it for yourself

If you’re evaluating healthcare clearinghouses, give Stedi a try.

You can request a free trial and experience our support firsthand.

Jul 30, 2025

Spotlight

A spotlight is a short-form interview with a leader in RCM or health tech. In this spotlight, you'll hear from Dakota Haugen, Eligibility Lead at Grow Therapy.

What does Grow Therapy do?

Grow Therapy is a leading hybrid mental health company delivering both in-person and virtual therapy and medication management. With a nationwide network of over 20,000 rigorously vetted providers, Grow connects individuals with high-quality, affordable mental healthcare – often at little to no cost through insurance.

Currently available in all 50 states and accessible to 180 million Americans through their health plans, Grow is on a mission to make high-quality mental health care accessible and affordable to all. By removing barriers to care and empowering independent providers, Grow is transforming how people access and experience mental health support. Backed by top investors including Sequoia Capital and Goldman Sachs Alternatives, Grow reached a total of $178 million in funding in 2024 following a Series C raise of $88 million. The round was led by Sequoia and supported by PLUS Capital, alongside artists and athletes such as Anna Kendrick, Lily Collins, Dak Prescott, Joe Burrow, Jrue Holiday, and Lauren Holiday. They joined existing investors Transformation Capital, SignalFire, and TCV – underscoring a strong belief in Grow’s mission to become the most trusted destination for mental healthcare.

Why Grow?

  • Accessible & Affordable: More than 90% of visits are insurance-covered; many clients pay just $0–$20 per session.

  • Diverse & Inclusive Care: Providers reflect a wide range of identities, languages, and specialties – including trauma-informed, mindfulness, and faith-based care.

  • Tech-Enabled Progress: Industry-leading Measurement Informed Care (MIC) platform and AI-assisted tools ensure clients and providers track meaningful progress.

  • Trusted Relationships: 95% of clients report a strong therapeutic alliance; NPS of 85 signifies exceptional client satisfaction.

  • Support for Providers: Grow removes the administrative and financial barriers to starting a private practice, empowering clinicians to focus on care.

How did you end up working in health tech?

I’ve always known that I wanted to help people – especially by addressing barriers to healthcare and creating pathways to access. My career began in the healthcare space as a medical biller, where I saw firsthand just how complex and discouraging it can be for individuals to access care. I realized I wasn’t even well-versed in navigating my own insurance, and that experience fueled my drive to better understand the system and make it easier for others.

Working in billing gave me insight into the structural challenges that stand between people and the care they need. I became passionate about creating a seamless, informed experience for patients. That passion deepened when I began my own mental health journey and experienced the benefits of care personally.

This led me to pivot into mental health, where I found an intersection of purpose and personal connection. When I discovered Grow Therapy, I immediately aligned with the mission. Grow’s commitment to both providers and clients resonated deeply with me – it’s the kind of meaningful, impactful work I’ve always aspired to do.

How does your role intersect with revenue cycle management (RCM)?

As the Eligibility Lead, my role sits at the front line of RCM, where I help ensure that clients are accurately verified for insurance coverage before beginning care. This early step is foundational to the entire revenue cycle – by identifying coverage details, confirming benefits, and addressing potential issues upfront, we minimize claim denials, reduce billing delays, and support timely reimbursement for our providers. But eligibility is more than just a financial checkpoint – it’s a critical access point for care. When done right, it removes barriers that might otherwise prevent someone from starting or continuing treatment. It gives clients clarity and confidence about what their insurance covers and what to expect financially, creating a smoother, more equitable path to mental healthcare. My work in eligibility helps lay the groundwork for both a positive client experience and a sustainable, efficient billing process.

What do you think RCM will look like two years from now?

I believe RCM will continue to evolve into a more specialized, data-driven function – especially within mental health. As the industry matures, I see RCM playing a pivotal role in not just operational efficiency, but also in driving financial outcomes that reflect the value of care. With the integration of AI and automation, particularly in areas like eligibility verification, we’ll be able to optimize workflows, reduce manual errors, and deliver real-time insights that help clients get approved and connected to care faster.

RCM will increasingly align with performance-based and value-driven models, where financial success is tied to meaningful health outcomes. This will allow individuals to see clearer connections between what they’re paying for and the results they experience – ultimately building more trust in the mental healthcare system. As we continue to remove financial and administrative barriers, RCM will help make mental health care more accessible and affordable, reinforcing the overall client experience and encouraging more people to seek and sustain the care they need.

Jul 25, 2025

Guide

It’s 3 AM. You’re on-call and just got paged. A customer – a 24-hour hospital in Des Moines – can’t verify patient coverage. Every eligibility check they send is failing.

You’re staring at a cryptic AAA error. More failures, now from other customers, are starting to come in. Account management wants answers.

You don’t know where to start. Is it your system? Your clearinghouse? The payer?

We’ve been there. Healthcare billing is a distributed system with multiple single points of failure. Even if you do everything right, things break: a payer goes down, a provider enters the wrong data, a request times out.

Stedi’s helped hundreds of teams debug eligibility issues.

This guide outlines the real-world scenarios where we’ve seen eligibility checks fail – and what we’ve found works. You’ll find common failure patterns, diagnostic tips, and how Stedi helps resolve each scenario.

For more tips, check out Eligibility troubleshooting in the Stedi docs.

You’re getting AAA 42 errors – a payer is down

Payer outages are common and often opaque. When you get an AAA 42 (Unable to Respond at Current Time) error or see a spike in timeouts for a specific payer, it usually means one of the following:

  • The payer is having intermittent issues. They’re down for now but may be back up shortly.

  • The payer is having a prolonged outage. This can be because of planned maintenance or a system failure on their end.

  • An intermediary clearinghouse that connects to the payer – not the payer itself – is down.

These scenarios return the same errors, so it's hard to tell them apart.

What you’ll see

  • AAA 42 errors from the payer

  • A spike in timeouts for requests to the payer.

  • Requests that fail even after retries

  • Increased latency on failures

An example AAA 42 response:

{
 "errors": [
    {
      "field": "AAA",
      "code": "42",
      "description": "Unable to Respond at Current Time",
      "followupAction": "Please Correct and Resubmit",
      "location": "Loop 2100A",
      "possibleResolutions": "This is typically a temporary issue with the payer's system, but it can also be an extended outage or the payer throttling your requests (https://www.stedi.com/docs/healthcare/send-eligibility-checks#avoid-payer-throttling)."
    }
  ],
  ...
}

What helps
Whether it’s a full-blown outage or just a flaky payer, the remediation steps are the same:

  • Check with Stedi to confirm the issue.

  • If you’re using Stedi’s Eligibility API, retry the requests with exponential backoff. For suggested retry strategies, see Retry strategy in the Stedi docs.

    This is the most important remediation step. Most Stedi customers simply give up on retries too early. You’re not billed for eligibility checks that return a AAA 42 error, so there’s little incentive not to retry.

Avoid using cached or stale responses. This can cause false positives, which ultimately lead to denied claims.

What Stedi does
Where possible, Stedi uses multiple, redundant payer routes with automatic failover and retries to minimize the impact of intermediary clearinghouse outages. In many cases, our customers don’t notice an outage.

If the payer itself is down – or their designated gateway clearinghouse is down – there's no workaround. No clearinghouse can get traffic through. For widespread outages, we reach out to the payer or the gateway clearinghouse – and we notify affected customers in Slack or Teams.

A patient says they're covered, but eligibility checks return "Subscriber/Insured Not Found"

Payers can only return an eligibility response if the request matches exactly one member in their system.

If they can’t find a match, they return an AAA 75 (Subscriber/Insured Not Found) error. That usually means one of two things:

  • The patient isn’t in the payer’s system.

  • The patient information in the request doesn’t match what the payer has on file

What you’ll see
An AAA 75 error in the eligibility response. For example:

{
  "subscriber": {
    "memberId": "123456789",
    "firstName": "JANE",
    "lastName": "DOE",
    "entityIdentifier": "Insured or Subscriber",
    "entityType": "Person",
    "dateOfBirth": "19001103",
    "aaaErrors": [
      {
        "field": "AAA",
        "code": "75",
        "description": "Subscriber/Insured Not Found",
        "followupAction": "Please Correct and Resubmit",
        "location": "Loop 2100C",
        "possibleResolutions": "- Subscriber was not found."
      }
    ]
  }
}

You may also see:

  • AAA 15 – Required Application Data Missing

  • AAA 71 – Patient DOB Does Not Match That for the Patient on the Database

  • AAA 73 – Invalid/Missing Subscriber/Insured Name

What helps
In your app or support flow, prompt the provider to:

  • Check that the member ID matches what’s on the insurance card. Include any prefix or suffix.

  • Double-check the patient’s name and date of birth (DOB).

  • Use the full legal name. For example, “Robert,” not “Bob.” Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings. Ask about recent name changes.

  • If the details look correct, make sure you’re sending the request to the right payer.

Note: Some payers allow a match even if either the first name or DOB is missing (but not both). If you’re not sure about the first name’s spelling, omitting it may improve your chances of a match. Most payers aren’t strict about matching first names exactly.

What Stedi does
Stedi makes proactive edits to eligibility requests for certain payers that are known to respond incorrectly to specific commonly accepted input formats. These edits increase the likelihood of a valid match.

Stedi also offers real-time support for eligibility issues over Slack and Teams. We’ve helped customers quickly diagnose data mismatches by highlighting exactly where the payer couldn’t find a match. Our support team responds within minutes and can spot common formatting issues that might otherwise take hours to debug.

You get a valid eligibility response, but it’s missing benefits

This usually comes down to the Service Type Code (STC) in the request. In requests, the STC tells the payer what kind of benefits you’re asking for, like medical, dental, or vision.

STCs are standardized, but payers don’t have to support all of them. Some payers only respond to a small set. Payers also don’t return benefits for STCs not covered by the related plan.

The only way to know which STCs a payer or plan supports is to test. For tips, check out How to pick the right STC (when there isn’t one).

What you’ll see
If you’re using Stedi’s Eligibility API, most benefit details come back in the benefitsInformation object array. Each object in the array has a serviceTypeCodes field.

  • If no benefitsInformation objects include the requested STC, the payer or the plan probably doesn’t support the STC.

  • If the STC shows up but the details you need aren't there, be sure to check any other returned STCs. Some payers return patient responsibility information under STC 30, while others may use a more specific STC code. If you still can’t find the details, the payer may not return that data in eligibility responses. 

If the response includes your STC but still lacks enough detail to determine whether the service is covered and the patient responsibility, you may need to get the missing information by calling the payer or using their portal. Some teams do this programmatically with an AI voice agent or portal scraping.

What helps

  • Check that you're using the right STC.

    For a cheat sheet, see How to pick the right STC. For a full list, see Service Type Codes in the Stedi docs.

  • Re-run the request with a baseline STC (like STC 30 for medical or STC 35 for dental).

  • Test different STCs against the baseline to see what each payer supports.

What Stedi does
We’ve tested thousands of payer responses. In addition to our How to pick the right STC blog, we maintain internal payer-specific guidance on which STCs return which benefits. If a payer omits key details, we help teams explore alternatives – whether that’s trying another STC, making a payer call, using the payer portal, or a mix.

The patient doesn’t know their payer or member ID

Eligibility checks only work if the payer can match the request to one specific member. Some payers can match using just name and date of birth. Others require a member ID. All eligibility checks require a payer ID.

What you’ll see
If you don’t know the payer, you can’t submit a check – you don’t know which payer to send it to.

If you leave out the member ID and the payer requires it, you’ll likely get an AAA 72 (Invalid/Missing Subscriber/Insured ID) error.

What helps
Use insurance discovery. It lets you search for active coverage using basic demographic info like name, DOB, ZIP code, and SSN.

Success rates vary depending on the provided patient data – from 25% with basic info to 70%+ with more comprehensive details. For tips, see How to use insurance discovery.

What Stedi does
Getting insurance discovery right out of the gate can be tough. If you’re getting started, our team can help you set up workflows and provide prompts to ensure you get the most out of it.

How would I know if Stedi is down?

Outages are extremely rare for Stedi. We run entirely on AWS with a redundant, multi-region cloud architecture, and we have automated systems for deployment safety. We also maintain redundant routes for payers wherever possible, so a single intermediary clearinghouse or even a widespread payer outage won’t take us down.

What you’ll see

  • A notice on our status page

  • 500 HTTP status code errors when running eligibility checks.

What helps
Contact Stedi.

What Stedi does
If an outage does happen, we reach out to affected customers immediately in Slack and Teams. We also update our status page. You’ll get clear status updates, including timelines and next steps.

Behind the scenes, our team works across regions to fix the issue fast. Once it’s resolved, we share what went wrong with customers and what we’re doing to make sure it doesn’t happen again.

Don’t debug alone

Eligibility failures happen. With so many players – payers, intermediaries, gateways, providers – it’s not a matter of if, but when one makes a mistake. When your eligibility checks fail, your clearinghouse should help.

If you’re seeing one of these issues – or something new – reach out. We can help.

Jul 24, 2025

Guide

When you're evaluating a healthcare clearinghouse, there's one question that matters most:

Do they support my payers?

If you’re looking at Stedi, the Stedi Payer Network is how you check. It’s a public, searchable index of Stedi’s supported payers.

This short guide explains how to use the Payer Network and answers common questions about Stedi's payer coverage.

The Stedi Payer Network

The Payer Network includes every payer Stedi supports – currently 3,500+. Stedi covers virtually every U.S. healthcare payer that accepts electronic claims and eligibility checks.

If one’s missing:

  • It might be listed under a different name. Reach out and we’ll check.

  • If we truly don’t support the payer, submit a request to add it. We can usually connect to new payers quickly.

Some payers can’t be supported at all – by us or other clearinghouses. This may be because they’re low volume, mail-only, or only support portal-based operations.

What each Payer Network entry includes

Each payer entry includes:

  • Payer name.

  • Primary payer ID, plus historical and synonymous aliases. Payer ID aliases let you use the payer IDs you already use with no additional mapping.

  • Supported transaction types, like 270/271 eligibility checks and 837P/837D/837I claim submissions.

  • Transaction enrollment requirements for each transaction type

You can search payers by name or payer ID. You can also filter by supported transaction types.

Filters use AND logic – only payers that support all selected types will appear. For example, if you filter for 270/271 eligibility checks and 837P claim submissions, you’ll only see payers who support both.

How to check Stedi’s supported payers

There are three ways to check if your payers are covered by Stedi:

  1. Send us your payer list.
    This is typically the easiest and fastest way to check. We’ll run a script to check for you and manually review the results. You can send us a message to start.

  2. Download the full CSV.
    You can download our full payer list as CSV from the Payer Network site.

  3. Use the Payers API.
    If you're a Stedi customer on a paid plan, you can use the Payers API to search and fetch our payers programmatically.

Do I need to map my current payer IDs?

No. We support the payer IDs you already use – no mapping required.

Behind the scenes, we maintain an internal mapping of historical and current payer IDs. If a payer changes its ID or merges with another, we’ll handle it automatically.

Do I need a backup clearinghouse?

In most cases, no.

Stedi covers virtually every U.S. healthcare payer. Where possible, we set up redundant routes to each payer with automatic failover. We also use a multi-region cloud infrastructure for redundancy.

How does Stedi work with other clearinghouses?

All clearinghouses rely on other clearinghouses. Some payers have exclusive clearinghouse relationships. For example, Optum is the designated gateway clearinghouse for UnitedHealthcare. No matter which clearinghouse you use, all UnitedHealthcare traffic goes through Optum.

To offer the broadest payer coverage, Stedi integrates with every major clearinghouse. We also build direct connections to payers wherever possible.

Stedi uses multiple routes, direct connections, and built-in redundancy to provide fast, reliable payer access. We’re not a wrapper for any other clearinghouse. As a Stedi customer, you don’t need to worry about which path we take. We pass back full payer responses without dropped fields or lossy normalization.

Next steps

Once you've verified our payer coverage, the typical next step is to explore our docs. When you're ready, you can request a free trial. Setup typically takes less than a day.

If there's a specific payer giving you trouble – whether with claims, enrollments, or eligibility – reach out. We may be able to help.

Jul 21, 2025

Guide

Most payers expect a Service Type Code (STC) in eligibility requests. It tells the payer what kind of benefits you want back – mental health, urgent care, vision, and so on.

But not every healthcare service maps cleanly to an STC.

Take medical nutrition or ABA therapy. There’s no obvious STC. Which one should you use?

The answer depends on the payer. The only way to know which STCs a payer supports – and return the benefits you need – is to test them.

This guide shows how to test for STC support and includes a cheat sheet of STCs to try for services that don't have a clear match. It also gives a quick primer on how STCs work in 270/271 eligibility checks.

Just want the cheat sheet? Scroll to the bottom.

Note: This guide is for medical STCs only. It doesn't cover dental STCs or dental-only payers.

What’s an STC?

A Service Type Code (STC) is a two-character code that groups similar healthcare services into standard categories. For example:

  • 47 – Hospital

  • AL – Vision

  • UC – Urgent care

In eligibility requests, STCs tell the payer what type of benefits you're asking about. In responses, they indicate what type of service each returned benefit entry relates to.

The standard STC list

HIPAA standardizes the list of valid STCs in X12 version 005010. Medical payers should only send these STCs in responses – but they aren’t required to support every code in the list.

For the full set of X12 005010 STCs, see Service Type Codes in the Stedi docs.

Note: X12 maintains a broader Service Type Codes list for later X12 versions. X12’s list includes codes that aren’t part of 005010 and shouldn’t be used by medical payers.

STC 30 - The fallback

If you only send STC 30 (Health Benefit Plan Coverage) in the eligibility request and the patient’s plan covers it, HIPAA requires the payer to return benefits for the following STCs:

STC

Description

1

Medical Care

33

Chiropractic

47

Hospital

86

Emergency Services

88

Pharmacy

98

Professional (Physician) Visit - Office

AL

Vision (Optometry)

MH

Mental Health

UC

Urgent Care

The payer may include benefits for other STCs as well.

How to use STCs in eligibility requests

If you’re using Stedi’s JSON-based Eligibility APIs, include an STC in the request’s encounter.serviceTypeCodes array:

"encounter": {
  "serviceTypeCodes": ["30"]
}

If you don’t include an STC in the request, Stedi defaults to 30 (Health Benefit Plan Coverage).

The array supports multiple STCs, but payer support varies. Unless you’ve tested a payer specifically, only send one STC per request.

To learn how to test for multi-STC support, see How to avoid eligibility check errors.

How STCs show up in eligibility responses

Most benefit details are in the benefitsInformation object array. Each object in the array includes a serviceTypeCodes field:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "INCLUSIONS SPEECH/PHYSICAL/OCCUPATIONAL THERAPY; APPLIED BEHAVIOR ANALYSIS (ABA)"
        }
      ]
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["CF"],         // Mental Health Provider - Outpatient
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: DEVELOPMENTAL TESTING, EDUCATIONAL THERAPY"
        }
      ]
    }
  ],
  ...
}

Responses may include entries for STCs you didn't request. And you'll typically see the same STC repeated across multiple benefit entries. That's normal – each entry covers a different aspect of benefits, like coverage status, co-pays, or deductibles.

Payers also use multiple entries to describe different subsets of services within an STC. For example, the MH STC might have one entry for standard therapy and another that notes coverage for other treatments. Descriptions typically appear in entries with code: "1" (Active Coverage) or code: "D" (Benefit Description), but they can appear in other entries as well. Often, coverage notes, inclusions, and exclusions appear in additionalInformation.description.

To get the full picture of benefits for a service, check all entries with the same serviceTypeCodes value.

For more information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

Picking an STC

When sending an eligibility request, use the most specific STC you can. It narrows the response to the benefits you care about and reduces guesswork. For example, if you send a request for STC 33 (chiropractic) instead of STC 30, you’ll get specific benefits related to chiropractic care.

However, some services, like remote therapeutic monitoring (RTM) or speech therapy, don’t map cleanly to well-supported STCs.

In these cases, your best option is to systematically test the STCs that seem most appropriate and compare the responses to see if you get the benefits information you need.

How to test for STC support

  1. Send a baseline eligibility request.
    Submit an eligibility check to the payer using STC 30 (Health Benefit Plan Coverage). This gives you a fallback response to compare against.

  2. Test your specific STC.
    Send a second request to the payer for the same patient with the STC that best matches the benefit type you’re targeting. Use the STCs in the cheat sheet as a starting point.

  3. Compare the specific STC response to the baseline response.
    If responses change based on the STC, the payer likely supports the specific STC.

If responses are identical, the payer may not support the specific STC – or the patient’s plan might not cover that service. When that happens, medical payers are required to return a fallback response using STC 30.

Example: Testing STC support for ABA therapy

Start with a baseline eligibility request using STC 30 (Health Benefit Plan Coverage):

{
  "encounter": {
    "serviceTypeCodes": ["30"]
  }
}

Then try a more specific STC like BD (mental health):

{
  "encounter": {
    "serviceTypeCodes": ["BD"]
  }
}

If the BD response includes benefitsInformation objects with a serviceTypeCodes field value of BD, the payer supports STC BD. Use this response if it includes the benefits information you need.

If the responses are identical, the payer or plan likely doesn't support the BD STC. You continue testing other related STCs, such as MH (Mental Health).

If you've already tried other STCs and no others seem appropriate, use the response for STC 30. If STC 30 doesn't include the benefits information you need, you may need to call the payer or visit the payer portal.

Tip: Automate your STC tests
To speed up testing, script your requests. Loop through candidate STCs and compare the responses against the baseline STC 30 response, using the same patient. Save the benefitsInformation array for each STC and diff them. This helps you spot what changes, if anything, between requests.

The STC cheat sheet

If you’re testing STC support for a service without a clear mapping, use the following table as a starting point. This list isn’t exhaustive and it isn’t payer-specific, but it’s a starting point for what we’ve seen work in production.

Try the STCs in the order shown – from the most specific to more general alternatives.

For a complete list of STCs, see Service Type Codes in the Stedi docs. For help mapping procedure codes to STCs, see How to map procedure codes to STCs.

Type of Care

STCs to Try

ABA Therapy

BD, MH, CF

Acupuncture

64, 1

Chemotherapy

ON, 82, 92

Chemotherapy, IV push

82, 92

Chemotherapy, additional infusion

82, 92

Chronic Care Management (CCM) services

A4, MH, 98, 1

Dermatology

3, 98

Durable Medical Equipment

DM, 11, 12, 18

IV push

92

IV Therapy/Infusion

92, 98

Maternity (professional)

BT, BU, BV, 69

Medical nutrition therapy

98, MH, 1, BZ

Medical nutrition follow-up

98, MH, 1, BZ

Mental health

MH ,96, 98, A4, BD, CF

Neurology

98

Newborn/facility

65, BI

Non-emergency transportation (taxi, wheelchair van, mileage, trip)

56, 57, 58, 59

Occupational Therapy

AD, 98, CF

Physical therapy

PT, AE, CF

Podiatry

93, 98

Primary care

96, 98, A4, A3, 99, A0, A1, A2, 98

Psychiatry

A4, MH

Psychological testing evaluation

A4, MH

Psychotherapy

96, 98, A4, BD, CF

Rehabilitation

A9, AA, AB, AC

Remote Therapeutic Monitoring (RTM) services

A4, 98, MH, 92, DM, 1

Skilled Nursing

AG, AH

Speech Therapy

AF, 98

Substance Abuse/Addiction

AI, AJ, AK, MH

Telehealth

9, 98

Transcranial magnetic stimulation

A4, MH

Jul 18, 2025

Products

You can now use the Retrieve Payer API endpoint to get a single payer record by its Stedi payer ID:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payer/{stediId} \
  --header 'Authorization: <api-key>'

Every payer in the Stedi Payer Network has a Stedi Payer ID: an immutable payer ID you can use to route transactions to the payer in Stedi.

If you already have a payer’s Stedi Payer ID, this new endpoint is the fastest way to fetch their payer metadata. No filtering, no pagination. Just the one record.

The endpoint returns the same payer information as our existing JSON-based Payer API endpoints: payer ID, name, payer ID aliases, transaction support, enrollment requirements, and more. Just for a single payer.

Why we built this

Payer IDs are used to route healthcare transactions to the right payer. If the ID is wrong, the transaction fails.

We recently introduced List Payers and Search Payers API endpoints to let you retrieve payer IDs and other metadata programmatically. This ensures you can always get an accurate, up-to-date payer ID.

Since then, several customers have asked, “Is there a way to get a single payer without a search?”

Until now, you had to either:

  • Use the Payer Search API endpoint, which always returns an array – even for exact matches.
    OR

  • Fetch and paginate the full list of payers (thousands of records), then filter it client-side.

If you already had a Stedi Payer ID from a previous search or a saved mapping, there was no way to get just that payer’s metadata without a search or filter.

Now you can.

How it works

Make a GET request to the /payer/{stediId} endpoint. Pass the Stedi Payer ID as the {stediId} in the path:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payer/HPQRS \
  --header 'Authorization: <api-key>'

You’ll get back a single JSON object that contains:

  • The payer’s name, primary payer ID, and known payer ID aliases

  • Whether the payer supports medical or dental coverage (or both)

  • Supported transaction types

  • Whether transaction enrollment is required for a transaction type

{
  "payer": {
    "stediId": "KRPCH",
    "displayName": "Blue Cross Blue Shield of Michigan",
    "primaryPayerId": "00710",
    "aliases": [
      "00210I",
      "00710",
      "00710D",
      "00710P",
      "1421",
      "2287",
      "2426",
      "710",
      "CBMI1",
      "MIBCSI",
      "MIBCSP",
      "SB710",
      "Z1380"
    ],
    "names": [
      "Blue Cross Blue Shield Michigan Dental",
      "Blue Cross Blue Shield Michigan Institutional",
      "Blue Cross Blue Shield Michigan Professional"
    ],
    "transactionSupport": {
      "eligibilityCheck": "SUPPORTED",
      "claimStatus": "SUPPORTED",
      "claimSubmission": "SUPPORTED",
      "professionalClaimSubmission": "SUPPORTED",
      "institutionalClaimSubmission": "SUPPORTED",
      "claimPayment": "ENROLLMENT_REQUIRED",
      "coordinationOfBenefits": "SUPPORTED",
      "dentalClaimSubmission": "NOT_SUPPORTED",
      "unsolicitedClaimAttachment": "NOT_SUPPORTED"
    },
    "enrollment": {
      "ptanRequired": false,
      "transactionEnrollmentProcesses": {
        "claimPayment": {
          "type": "ONE_CLICK"
        }
      }
    },
    "parentPayerGroupId": "AWOCR",
    "coverageTypes": [
      "medical"
    ]
  }
}

Try it out

The Retrieve Payer API endpoint is free on all paid Stedi plans.

To see how it works, check out the docs or reach out for a free trial.

Jul 17, 2025

Spotlight



Eliana Berger @ Joyful Health

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from Eliana Berger, CEO at Joyful Health. You'll learn what Joyful Health does and how Eliana thinks RCM will change in the next few years.

What does Joyful Health do?

Joyful Health is a specialized revenue recovery service that focuses exclusively on the hardest part of the revenue cycle: following up on denied and unpaid claims. What makes us true experts in this space is that we hire people with payer-specific expertise who know exactly how to navigate each insurance company's systems, understand their unique resolution pathways, and can get things done quickly and accurately.

What makes us different:

  • Performance-based pricing - We only get paid when we successfully recover revenue for you

  • Cost optimization - As your clean claims rate improves, you pay us less, naturally optimizing your cost to collect

  • Zero risk - No upfront costs, no minimum fees, services pay for themselves

  • Seamless integration - We work entirely within your existing systems with no new software required

  • Specialized focus - We handle denials and aged A/R recovery so your team can focus on scaling your core business

For fast-growing digital health companies, this means you can maintain lean operations while ensuring maximum revenue recovery from day one. We typically help practices recover revenue worth 5-10x our fees, making it a no-brainer investment that scales with your growth.

How did you end up working in health tech?

My path into health tech started at home. Growing up, I watched my mom and grandmother run an independent therapy practice, spending countless hours managing claims and constantly worrying about whether payments would come through. That experience stuck with me.

A few years ago, I became curious about what was really keeping independent practices on the brink of survival. So I started volunteering as a free consultant for several practices - helping with everything from ordering office furniture to analyzing their EHR data. What I discovered was eye-opening: practice owners had no idea where their money was.

Their financial data was scattered across multiple systems with no way to see the full picture. This led me to essentially become a fractional CFO for these practices, spending hours pulling together fragmented reports to give them clarity on their revenue. What I found was shocking - many were losing 10-30% of their revenue without even realizing it. The money was there, just trapped in denied claims and unpaid receivables.

That insight became the foundation for Joyful Health. We're building the modern financial operating system for healthcare, starting with revenue recovery. When you can finally see all that fragmented data in one place, it becomes crystal clear how much money is being left on the table - and more importantly, how to systematically recover what practices are rightfully owed.

How does your role intersect with revenue cycle management (RCM)?

As CEO, I'm constantly thinking about RCM from both a strategic and operational lens - but so is our entire team!

Everyone at Joyful, including operations, engineering, and beyond, regularly fights denied claims alongside our RCM specialists. This helps our entire team understand what the workflows actually look like, from interpreting denial codes to navigating payer portals to executing the right resolution actions. As a result, when our engineers are building features, they understand exactly what data points matter and how billers actually work. When our operations team is designing processes, they know the real bottlenecks and pain points. This deep, practical knowledge allows us to build products and services that are truly integrated within the systems and processes practices already have, rather than creating yet another tool that sits on the side.

From a strategic perspective, this operational depth helps me make better decisions about our product roadmap and service delivery. It's not enough to build cool technology - it has to directly solve real problems in the revenue cycle. And because everyone on our team has fought these battles firsthand, we can build solutions that actually work in the messy reality of healthcare billing.

What do you think RCM will look like two years from now?

I think we're heading toward a world where RCM becomes much more specialized and data-driven, with a clear separation between the "science" and "art" of revenue cycle management.

The "science" - rules-based, routine tasks like eligibility verification, claim scrubbing, and payment posting - will increasingly be handled by AI automation, which is already creating huge efficiencies. But the real breakthrough I'm excited about will be when AI starts getting better at the "art" side too: the complex human judgment required for things like denial management and aged A/R recovery. As AI learns from successful resolution patterns and begins to interpret payer behaviors and policies, it will help tackle problems that have traditionally required deep human expertise.

Another bigger shift I'm excited about is the move toward performance-based pricing models across the entire RCM ecosystem. Practices want better alignment between what they pay and the results they get - they want vendors who tie their success directly to their clients' financial outcomes.

Jul 16, 2025

Guide

The revenue cycle is how healthcare providers get paid. When it slows down, the provider’s cash flow dries up, and everything else breaks.

Less cash flow means providers have to make tough choices: not hiring needed staff, delaying investments, or cutting back on services. And that means worse care for everyone.

Despite the stakes, most of the revenue cycle is spent waiting.
… Waiting for eligibility responses.
… Waiting for claims to get accepted.
… Waiting for payments to show up.

A lot of that wait is avoidable.

If you're building RCM tools for healthcare providers, automating parts of the cycle – the critical path – can cut 10-20 days off the typical 30-60 day cycle. Providers – your customers – get paid faster, and you'll win more of them.

This guide walks you through what the revenue cycle is, tells you the key roles, and breaks down each step of the critical path: enrollment, eligibility, and processing claims. It shows how billing platforms can use Stedi to automate this critical path so it runs faster and at scale.

What is the revenue cycle?

The revenue cycle is everything a healthcare provider does to get paid.

In theory, it’s simple: a provider sees a patient and collects any co-pay or coinsurance. The provider records what happened during the visit and sends the details as a claim to the payer. The claim is approved, and the provider gets paid.

The reality is more complicated.

To avoid claim denials, providers need to check the patient’s insurance coverage before a visit. When submitting a claim, they have to use billing codes – picking from tens of thousands of them – to precisely describe the service they provided. Once submitted, payers don’t process their submitted claims right away. It can take days or weeks. A mistake means a rejection – which means starting over, delaying payment even more.

Revenue cycle management (RCM) is how providers stay on top of it all. It can be software, services, internal processes, or a mix of all three. Regardless of the methods, the goals are the same: prevent mistakes, get the provider paid faster, and avoid lost revenue.

The players

The revenue cycle involves several different systems and organizations. Each plays a specific role:

Providers
Short for “healthcare providers.” These are doctors, hospitals, dentists, therapists – anyone who delivers healthcare. They’re the ones trying to get paid.

Payers
Health insurers, including insurance companies like Aetna or Cigna, and government programs like Medicare. They receive claims, decide what to pay, and send back money (or denials) to the provider.

HIPAA and X12
A federal law that protects healthcare data and sets rules for how certain transactions must be conducted. HIPAA requires some healthcare transactions to be exchanged in the X12 EDI format. For example:

270 and 271 refer to official X12 HIPAA transaction types. Stedi’s APIs let you send and receive data for these transactions as JSON. We handle the translation from JSON and X12 (and the reverse) for you. You can also use Stedi to exchange raw X12.

Healthcare clearinghouses
Clearinghouses sit between providers and payers. Their job is to route transactions between the two and ensure that transactions sent to payers are HIPAA-compliant X12.

Most clearinghouses only connect to medical or dental payers. Stedi connects to both. We also connect to some vision and workers' compensation payers. 

For more information on clearinghouses, check out What is a healthcare clearinghouse?.

Billing platforms
Most providers don’t connect directly to clearinghouses or payers. They use billing platforms to manage the work for them. These include EHRs (electronic health record systems), practice management systems (PMS), and revenue cycle management (RCM) systems – often layered on top of each other.

In addition to billing, some of these platforms may help providers manage appointments, documentation, and clinical tasks.

The critical path

To get paid, every provider needs to complete certain steps in the revenue cycle. This “critical path” includes enrollment, eligibility checks, and claims processing.

There are other steps in the revenue cycle, but these are the most important to get right. Mistakes here slow down provider payments and create more work.

The following sections walk through each step in the critical path, covering its purpose, common pitfalls, and how it can be automated using Stedi.

Healthcare revenue cycle: The critical path

Step 0. Enrollment

Before they can exchange transactions with payers, providers need to complete up to three different types of enrollment. Each enrollment is a one-time process, but providers need to repeat enrollment for each payer:

  • Credentialing – Confirms the provider is licensed and qualified.

  • Payer enrollment – Registers the provider with the payer’s health plan(s) and sets their contracted rates – the agreed dollar amounts for specific services. These rates are used later to calculate how much the payer will reimburse for a service in claims.

  • Transaction enrollment – Lets the provider exchange certain types of healthcare transactions with the payer. 

Billing platforms may handle some or all of these enrollments for their providers. Stedi only helps billing platforms with transaction enrollment – but we make it faster, more automated, and easier to manage than other clearinghouses. 

Transaction enrollment
All payers require providers to complete transaction enrollment to receive 835 Electronic Remittance Advice (ERAs). Some also require enrollment for other transactions, like 270/271 eligibility checks or 837 claims.

If you're a billing platform serving many providers, transaction enrollment becomes a bottleneck. Each payer has its own enrollment process with different requirements: some need PDF signatures, others require portal logins. Just tracking the status of hundreds of enrollment submissions can become overwhelming. Many billing platforms end up hiring entire operations teams just to manage the paperwork.

Stedi automates transaction enrollment requests and lets you avoid most of the manual work. You can submit and track enrollment requests using Stedi’s Enrollments API, the Stedi portal, or a bulk CSV file. For the 850+ payers that support one-click transaction enrollment, that’s it – you’re done. For the rest, we offer delegated signature authority, where Stedi signs enrollment forms on your behalf. It’s a one-time step that eliminates 90%+ of your enrollment-related paperwork.

When manual enrollment steps are needed, we do the work for you. You only take action when absolutely needed – and we tell you exactly what to do next. We complete most enrollment requests in 1-2 business days.

Goal

What to use

Submit and track transaction enrollment requests

Enrollments API, Stedi portal, or CSV file

Eliminate 90%+ of enrollment-related paperwork

Delegated signature authority

Step 1. Eligibility check

Before a patient visit, providers need to know three things:

  • Does the patient have active insurance?

  • Does the patient’s plan cover what they’re coming in for?

  • How much will the patient owe at the time of service? What’s the co-pay, coinsurance, or deductible?

An eligibility check checks a patient’s insurance to answer these questions. The checks help prevent claim denials and surprise bills. It’s especially important for specialty care – like therapy – where coverage can vary by service type.

In some cases, especially for scheduled services, providers may also need to give the patient an estimate of their out-of-pocket costs. For certain services, that estimate is required by the No Surprises Act.

To check coverage, you send a 270 eligibility request to the patient’s payer. The payer responds with a 271 eligibility response that includes benefit details: covered services, copays, coinsurance, deductibles, and more.

Real-time and batch eligibility checks
Most providers need to make eligibility checks in real time – during intake or on the phone – right before a visit. Fast, accurate eligibility responses are important. Stedi’s Real-Time Eligibility API typically returns results in 1-3 seconds.

Many providers also want to run weekly or monthly batch refreshes. These refreshes catch coverage between visits, which is helpful for recurring patients or upcoming visits. You can use Stedi’s Batch Eligibility API or a bulk CSV to run 1,000 checks at a time without interfering with your real-time checks.

Insurance discovery and COB checks
Payers only return a patient’s eligibility data if the request matches a single patient. Some payers can match a patient based on their name and date of birth alone, Many require a member ID.

If a patient doesn’t know their member ID or doesn’t know their payer, you can use an insurance discovery check to try to find active coverage using just their demographics, like name and SSN. Results aren’t guaranteed, but it’s a way forward when eligibility checks aren’t possible. If the discovery check returns multiple plans, use a coordination of benefits (COB) check to determine the primary plan.

MBI lookup
Medicare eligibility checks require the patient’s Medicare Beneficiary Identifier (MBI), the Medicare equivalent of a member ID. If a patient doesn’t know their MBI, you can use Stedi’s MBI Lookup feature to get it. If there’s a match, Stedi automatically runs an eligibility check using the MBI to return the patient’s benefits information.

Using Stedi
The following table outlines how billing platforms can run eligibility and related checks using Stedi.

Goal

What to use

Check eligibility in real time

Real-Time Eligibility API or the Stedi portal

Check eligibility in bulk

Batch Eligibility API or upload a bulk CSV

Find active insurance without a member ID or known payer

Insurance Discovery Check API

Get a patient’s Medicare Beneficiary ID (MBI)

Real-Time Eligibility API with MBI lookup

Determine a patient’s primary payer

Use the Coordination of Benefits Check API

Step 2. Charge capture

Once coverage is confirmed, the provider delivers care. During or after the visit, the provider captures what was done using procedure codes – structured billing codes that describe their services. Providers typically enter the codes into their EHR or PMS.

Later, these codes become the core of the claim sent to the payer. The type of code used depends on the service:

  • Current Procedural Terminology (CPT) codes – Used for most medical services. Maintained by the American Medical Association (AMA).

  • Healthcare Common Procedure Coding System (HCPCS) codes – Used for things like medical equipment, ambulance rides, and certain drugs. Includes CPT codes as Level I.

  • Current Dental Terminology (CDT) codes - Used for dental services. Maintained by the American Dental Association (ADA).

Accuracy is important in this step. The captured codes later become part of the provider’s claim. Mistakes can mean a rejected or denied claim.

Stedi doesn’t handle charge capture directly. But many EHR and practice management platforms that integrate with Stedi do. To find them, check out our Platform Partners directory.

Step 3. Claim submission

Once care and charge capture are done, the provider uses their billing platform to submit a claim to the patient’s payer.

Claims must be submitted using an 837 transaction. There are three types:

  • 837P – Professional claims, used for services like doctor visits, outpatient care, and therapy

  • 837D – Dental claims

  • 837I – Institutional claims, used for services like hospital stays and skilled nursing

You can use Stedi’s Claim Submission API or SFTP to submit 837P, 837D, and 837I claims.

275 claim attachments
Some services require additional documentation – like X-rays or itemized bills – to show that the service occurred or was needed. This is common in dental claims, where payers often require attachments for certain procedures.

Providers must send this documentation to the payer as one or more 275 claim attachments. The type of attachments required depends on the service and the payer. Without required attachments, the payer may delay (pend) or deny the claim.

You submit attachments separately from claims, but the request must reference the original claim. Most claim attachments are unsolicited – meaning they’re sent upfront without the payer requesting them.

You can use Stedi’s Claim Attachments API to upload and submit it as an unsolicited 275 claim attachments.

Using Stedi
The following table outlines how billing platforms can submit claims and claim attachments using Stedi.

Goal

What to use

Submit an 837P, 837D, or 837I claim

Claim Submission API or SFTP

Submit an unsolicited 275 claim attachment

Claim Attachments API

Step 4. Claim acknowledgment

Payers don’t process claims in real time. After you submit a claim, you’ll receive one or more asynchronous 277CA claim acknowledgments: 

  • First from Stedi or your primary clearinghouse. For Stedi, this usually arrives within 30 minutes of submitting the claim.

  • From one or more intermediary clearinghouses, if applicable.

  • Finally from the payer. This acknowledgment is the one you usually care about. You typically receive a payer acknowledgment 2-7 days after claims submission, but it can take longer.

Payers send claim acknowledgments to the provider’s – or their designated billing platform’s – clearinghouse. You can use a Stedi webhook or Stedi’s Poll Transactions API endpoint to listen for incoming 277 transactions. When a 277CA arrives, you can use the transaction ID to fetch the claim acknowledgment’s data using Stedi’s Claims acknowledgment (277CA) API.

Claim acceptance and rejection
The payer acknowledgment tells you whether the claim was accepted for processing or rejected:

  • Acceptance  – The claim passed the payer’s initial checks and made it into their system. It doesn’t mean the claim was approved or paid.

  • Rejection – The claim didn’t meet the payer’s formatting or data requirements and wasn’t processed at all. It doesn’t mean the claim was denied.

If the claim is rejected, fix the issue and try again. If it’s accepted, wait for the 835 ERA – the final word on payment or denial.

Claim repairs
The acknowledgment step is where most claim rejections happen. When you submit a claim using the Claim Submission API, Stedi automatically applies various repairs to help your requests meet X12 HIPAA specifications and individual payer requirements. This results in fewer payer rejections.

Prior authorization
Some services require prior authorization, or prior auth, before you can submit a valid claim. It means getting the payer’s approval for a service in advance – usually through a portal, fax, or EHR. Prior auth isn’t handled by Stedi and isn’t covered in this guide.

Using Stedi
The following table outlines how billing platforms can use Stedi to retrieve claim acknowledgments.

Goal

What to use

Get notified of new 277CA claim acknowledgments

Stedi webhook or Poll Transactions API endpoint 

Retrieve 277CA claim acknowledgments after notification

Claims acknowledgment (277CA) API

Step 5. Remittance and claim status

The 835 Electronic Remittance Advice (ERA) transaction is the final word on a claim. It’s the single source of truth for:

  • What was paid and when

  • What was denied and why

  • What the patient owes

  • How to post payments and reconcile accounts

Like a claim acknowledgment, the ERA is sent from the payer to the provider’s – or their billing platform’s – clearinghouse. You can create a Stedi webhook or Stedi’s Poll Transactions API endpoint to listen for incoming 835 transactions. When an 835 ERA arrives, you can use the transaction ID to fetch the ERA’s data using Stedi’s 835 ERA API

Claim approval and denial
Later, after the claim is processed, you may receive an approval or a denial:

  • Approval – The payer agreed to pay for some or all of the claim.

  • Denial – The claim was processed but not paid, usually because the service wasn’t covered or approved by the patient’s plan.

If the claim is approved, you can post the payment and reconcile the patient’s account.

If it’s denied, review the denial reason in the ERA. If the denial was incorrect or preventable, you can correct the issue and resubmit the claim. Otherwise, you can appeal or escalate the denial with the payer or bill the patient. The steps for appealing and escalating claims differ based on the payer’s rules and the patient’s plan.

Real-time claim status checks
If the claim is approved, an ERA typically arrives 7–20 business days after claim submission. If you haven’t received a claim acknowledgment or ERA after 21 days, you can check the claim’s status using a 276/277 real-time claim status check.

A real-time claim status check tells you whether the claim was received by the payer, is in process, or was denied.

You can make a claim status check any time after claim submission, but most billing platforms wait until day 21. Then they monitor the claim using real-time claim status checks until they receive a final status or an ERA. For example, the following table outlines a typical escalation process.

Days since claims submission

Action

1-20

Wait for the 277CA claim acknowledgment or 835 ERA.

21

Run the first 276/277 real-time claim status check.

24

Run a second 276/277 real-time claim status check.

28

Run a third 276/277 real-time claim status check.

30+

Contact Stedi support in your Slack or Teams channel.

Using Stedi
The following table outlines how billing platforms can use Stedi to retrieve ERAs and make check claim statuses.

Goal

What to use

Get notified of new 835 ERAs

Stedi webhook or Poll Transactions API endpoint

Retrieve 835 ERAs after notification

835 ERA API

Check the status of a claim after 21 days or more

Real-Time Claim Status API

Step 6. Revenue recovery

Sometimes, the provider has already delivered care, but the claim gets stuck. This could be because:

  • They didn’t check eligibility or collect the right insurance information, so they can’t submit a claim.

  • They submitted a claim, but it was rejected or denied.

The result is the same: the claim and its revenue are considered lost.

To recover it, run an insurance discovery check to try to find active coverage for the patient. Results aren’t guaranteed, but even a low success rate is acceptable. This is a last-ditch effort to recover lost revenue, and there’s limited downside. If discovery returns multiple plans, run a COB check to determine the primary.

If you can identify the patient’s primary plan, submit a claim using the Claim Submission API or SFTP, and pick up the revenue cycle from there (Step 3).

Using Stedi
The following table outlines how billing platforms can submit claims and claim attachments using Stedi.

Goal

What to use

Find active insurance without a member ID or known payer

Insurance Discovery Check API

Determine a patient’s primary payer

Use the Coordination of Benefits Check API

Submit an 837P, 837D, or 837I claim

Claim Submission API or SFTP

The rest of the revenue cycle

This guide only covers the critical parts of the revenue cycle. The rest of the cycle happens outside the clearinghouse.

After the 835 ERA comes in, the billing platform or another RCM system typically takes over any remaining steps. These can include:

  • Posting payments to the patient’s account

  • Billing the patient for their share of payment

  • Following up on denied or unpaid claims

  • Collecting payment or writing off balances

  • Running reports and reconciling revenue

If you're looking for tools to handle those downstream steps, check out the Stedi Platform Partners directory.

Start automating your workflows today

If you’re building RCM functionality and running into scaling issues, Stedi can help you out. Our APIs let you automate core workflows so you can onboard more providers with less manual work.

If you want to try Stedi out, contact us for a free trial or sign up for a sandbox account. It takes less than 2 minutes. No billing details required.

Jul 14, 2025

Products

You can now use the Stedi Platform Partners directory to find RCM, EHR, and practice management systems that use Stedi.

Many healthcare companies are looking for modern RCM solutions – and can’t or don’t want to build one themselves. The directory helps you find solutions that are powered by Stedi’s API-first clearinghouse rails.

What is the Stedi Platform Partners directory?

The Stedi Platform Partners directory is a public list of platforms that use Stedi’s APIs to power their RCM functionality. The directory is free and publicly accessible. Platforms don’t pay to be listed.

Why we built it

Stedi isn’t always the right fit for healthcare companies that want to benefit from our modern clearinghouse infrastructure.

We’re built for teams with in-house engineers who want to build their own RCM stack using our healthcare clearinghouse APIs. These can be large MSOs or DSOs with custom RCM requirements that aren’t well-served by off-the-shelf solutions, or they can be engineering teams building software platforms that they resell to others in the form of RCM, EHR, or practice management systems.

Many healthcare companies don't have their own developers (for example, individual provider offices) or don't need to build billing workflows themselves (for example, groups with run-of-the-mill RCM requirements). These companies are looking for plug-and-play solutions that can get them up and running quickly with turnkey functionality, and they want to use platforms that aren’t built on top of legacy clearinghouse infrastructure.

The directory allows those companies to find modern platforms that are built on top of Stedi’s modern infrastructure.

Find a partner today

The Stedi Platform Partners directory is now live. If you can’t find a platform that fits your needs – or you want to be listed – contact us and we’ll help you out.

Jul 15, 2025

Spotlight

Laurence Girard @ Fruit Street

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from Laurence Girard, Founder and CEO of Fruit Street Health. You'll learn what Fruit Street does and how Laurence thinks RCM will change in the next few years.

What does Fruit Street do?

Fruit Street delivers the CDC’s diabetes prevention program through live group video conferencing with registered dietitians. The program is designed to help the 1 in 3 Americans with prediabetes avoid developing Type 2 diabetes.

How did you end up working in health tech?

I was planning to go to medical school when I was 18 years old. I was volunteering in my local emergency room while simultaneously taking a nutrition epidemiology course with a Harvard School of Public Health professor. This led me to realize that many of the patients coming into the emergency room had preventable conditions related to their diet and lifestyle, such as Type 2 diabetes.

I also gained my interest in entrepreneurship by going to talks at the Harvard Innovation Lab on the Harvard Business School campus. I thought that maybe instead of going to medical school, I could have a big impact on lifestyle-related diseases and public health through technology and entrepreneurship. I started my company as a summer project at the Harvard Innovation Lab more than a decade ago and have been working on it ever since.

How does your role intersect with revenue cycle management (RCM)?

Fruit Street recently became a Medicare Diabetes Prevention Program Supplier. We use Stedi to run automated eligibility checks and submit claims.

What do you think RCM will look like two years from now?

I think RCM solutions – like those powered by Stedi – will directly and more deeply integrate with other digital health solutions. They'll use AI to check in advance if a service is covered by a health plan so there are fewer claim denials.

Jul 11, 2025

Guide

Your AI voice agent is making 1,000+ calls a day. Payers are limiting how many questions your agent can ask per call. Your infrastructure costs are spiking. Your queues are overloaded and getting worse.

If you're building an AI voice agent for back office tasks that include insurance eligibility – like many of Stedi’s customers – that might sound familiar. This guide is for you.

In this guide, you’ll learn how to restructure your agent’s workflow to make fewer calls and get faster answers at lower costs. You’ll also see how you can use Stedi’s eligibility APIs to get benefits information before – or instead of – triggering a call.

The problem

For providers, AI voice agents fill a real need. Providers need to check eligibility to determine patient coverage and estimate costs. Sometimes, this requires a phone call to the payer.

Before agents, the provider’s staff or BPOs made those calls. They spent hours waiting on hold, pressing buttons, and navigating phone trees. Many of those teams weren’t using real-time eligibility checks at all – just 100% phone calls. Voice agents are now taking that work, and in many cases, winning business from the BPOs they’re replacing.

The problem is twofold:

  • Payers are getting flooded with AI phone calls and are taking defensive measures. Some payers limit the number of questions per call or just hang up when they hear an AI voice.

  • Voice agents still fail sometimes. When they do, the entire call is wasted.

Calling the payer was never meant to be the first step. A real-time eligibility check does the job faster. In most cases, it can provide all the details needed – like coverage status, deductibles, and co-pays – in seconds. Real-time checks should be your first line of defense for eligibility.

Payer calls should be reserved for cases where the eligibility response doesn’t include the benefit details you need, like doula benefits (which don’t have a specific Service Type Code), medical nutrition therapy coverage, or details on prior authorization requirements.

A better workflow for voice agents

If your agent is placing calls without running an eligibility check first, you’re probably making a lot of unnecessary calls.

Here’s how to fix it: Use Stedi’s real-time eligibility and insurance discovery APIs to resolve more cases upstream – before a call ever needs to happen. Even when a call is required, it’s shorter – because you’re not asking for data you already have from the API.

Stedi’s voice agent customers have found that using eligibility checks first drastically reduces the number and duration of phone calls.

This table outlines each step, when to use it, and how long it takes.

Workflow step

When to use

Expected response time

Step 1. Run a real-time eligibility check

As a first step.

1–3s

Step 2. Run an insurance discovery check

The eligibility check fails.

10–30s

Step 3: Place a call (only if needed)

All else fails or special information is needed.

Minutes

The following flowcharts compare the old and new workflows:

Before and After Flowcharts

Step 1. Run a real-time eligibility check

As a first step, use Stedi’s Real-Time Eligibility Check API to get benefits data from the payer. This step alone – running an eligibility check before triggering a call – can greatly reduce your agent’s call volume.

When using the API, include the following in your request:

  • controlNumber – A required identifier for the eligibility check. It doesn’t need to be unique; you can reuse the same value across requests.

  • tradingPartnerServiceId – The payer ID. If you don’t know the payer ID but know the payer name, use the Search Payers API to look it up.

  • Provider information – The provider’s NPI and name.

  • Patient information – First name, last name, date of birth, and member ID.

    • If verifying a dependent, the format depends on the payer:

      • If the dependent has their own member ID: Put their information in the subscriber object. Leave out the dependents array.

      • If they don’t have their own member ID: Put the subscriber’s information in subscriber, and the dependent’s information in the dependents array.

  • Optional but recommended:

    • serviceTypeCodes – Indicates the type of healthcare service provided. We recommend using one Service Type Code (STC) per request unless you’ve tested that the payer accepts multiple STCs. For a complete list of STCs, see Service Type Codes in the Stedi docs.

An example eligibility request body:

{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "AHS",
  "externalPatientId": "UAA111222333",
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19000101",
    "memberId": "123456789"
  },
  "encounter": {
    "serviceTypeCodes": [
      "MH"
    ]
  },
}

Most patient benefits appear in the benefitsInformation array of the eligibility response. An example eligibility response:

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network services
    },
    ...
  ],
  ...
}

For information on interpreting eligibility responses, see How to read a 271 eligibility response in plain English.

If the response includes the information you need, you’re done. No call needed. If you get a response for the patient, but it doesn’t include the benefits information you need, move to Step 3.

If the eligibility check fails because the patient can’t be identified – indicated by an AAA 72 (Invalid/Missing Subscriber/Insured ID) or AAA 75 (Subscriber/Insured Not Found) error – try the tips in our Eligibility troubleshooting docs. If those don’t work, then move to Step 2.

Step 2. Run an insurance discovery check

Use Stedi’s Insurance Discovery API to find a patient’s coverage using just demographics – no payer ID or member ID needed. It’s less reliable and more expensive than an eligibility check, but often cheaper than making a call.

When making the discovery check, submit the following patient’s demographic information along with the provider’s NPI:

  • First name (required)

  • Last name (required)

  • Middle name (optional)

  • Date of birth (required)

  • Full or partial SSN (even the last 4 digits can help)

  • Gender (optional)

  • Current or previous ZIP code (optional but strongly recommended)

Note: Insurance discovery requires transaction enrollment to set up. See the insurance discovery docs.

An example insurance discovery request body:

{
  "provider": {
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "Jane",
    "lastName": "Doe",
    "middleName": "Smith",
    "dateOfBirth": "19800101",
    "ssn": "123456789",
    "gender": "F",
    "address": {
      "address1": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "postalCode": "62701"
    }
  },
  "encounter": {
    ...
  }
}

Stedi enriches the data, searches across commercial payers, and returns active coverage along with subscriber details and benefits.

If available, the benefits information is returned in benefitsInformation objects like those in the eligibility response from Step 1.

If the response includes the information you need, you’re done. If you get a response for the patient, but it doesn’t include the benefits information you need, move to Step 3.

If the insurance discovery check returns no matches, try asking the patient for more information. In these cases, it’s unlikely even a phone call will help. The patient’s information may be incorrect, or they haven’t provided enough information to check their coverage with the payer – call or not.

Step 3: Place a call (only if needed)

By this point, you should have exhausted your other options. We recommend you only call the payer if the eligibility response doesn’t have the information you need. Common examples include:

  • Missing coverage or benefit rules for specific services or procedures, like nutritional counseling, behavioral health, or missing tooth clauses.

  • Checking the provider’s network status.

  • Secondary or tertiary coverage that needs verification. In these cases, you may want to try a coordination of benefits (COB) check before calling the payer.

  • Referral or prior auth requirements aren’t included in the eligibility response.

  • Coverage dates or amounts are missing or don’t make sense.

In these cases, calling the payer is often the best thing to do.

Benefits

Restructuring your agent’s workflow from "call first" to "check first" gives your system real advantages across performance, cost, and control. Here are a few:

  • Fewer calls.
    Voice agents can see a significant reduction in outbound call volume when they use eligibility and insurance discovery checks before making a call.

  • Shorter calls.
    Most payers limit the number of data points that you can ask for per phone call. When a call is still needed, the agent already has the payer, member ID, and basic plan info. You don’t waste time – or calls – asking for information you already have.

  • Faster answers.
    Eligibility and discovery responses come back in seconds. You’re not waiting in a call queue or retrying after a dropped call.

  • Structured data.
    You can get all eligibility and insurance discovery responses as standardized JSON. That makes it easy to parse, store, and use downstream, whether you’re populating a UI or triggering logic.

Get started

You don’t need to rewrite your agent or rework your pipeline to try out Stedi. Most teams start with a simple proof of concept and expand from there.

If you’d like to try it for yourself, reach out to start a free trial. Our team would love to help get your integration rolling.

Jul 10, 2025

Guide

Most developers don’t hate sales. They hate being blocked.

You shouldn’t need to sit through a demo just to run a curl command.

With most dev tools, you can sign up, test things out, and decide for yourself. Stedi is a healthcare clearinghouse, which means we deal with real PHI. That means HIPAA compliance. And that means a BAA before you can send a production request. You can’t just spin up a prod account and start testing.

But we knew devs would want to try things out before committing. That’s why we created sandbox accounts: a free way to test our eligibility API with mock data.

You can sign up for a sandbox and start sending requests in under five minutes. If you’re considering integrating with Stedi, it’s a quick and painless way to try us out.

This guide shows how to set up a sandbox account and send your first mock eligibility check.

What the sandbox supports

The sandbox lets you test real-time 270/271 eligibility checks using mock data.

You can send requests through the JSON API or the Stedi portal. The data is predefined and simulates payers like Aetna, Cigna, UnitedHealthcare, and CMS (Medicare). You can’t use other patient data or select your own payers.

Supported

Not supported

If you need to test these features, request a free trial.

How to get started

Step 1. Create a sandbox account

Go to stedi.com/create-sandbox.

Sign up with your work email. No payment required.

Once in, you’ll land in the Stedi portal.

Step 2. Create a test API key

If you're using Stedi's APIs, you’ll need an API key to send mock requests – even in the sandbox.

  • Click your account name in the top right.

  • Select API Keys.

  • Click Generate new API key.

  • Name the key. For example: sandbox-test.

  • Select Test as the mode.

  • Click Generate.

  • Copy and save the key. You won’t be able to see it again.

If you lose the API key, delete it and create a new one.

Step 3. Send a mock eligibility check

You can send mock requests using the Stedi portal or the API. For the API, use the Real-Time Eligibility Check (270/271) JSON endpoint.

Only approved mock requests will work. For a list, see Eligibility mock requests in the Stedi docs. You must use the exact data provided. Don’t change payers, member IDs, birthdates, or names.

An example mock request:

curl --request POST \
  --url 'https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3' \
  --header 'Authorization: Key {test_api_key}' \
  --header 'Content-Type: application/json' \
  --data '{
    "controlNumber":"112233445",
    "tradingPartnerServiceId": "60054",
    "provider": {
        "organizationName": "Provider Name",
        "npi": "1999999984"
    },
    "subscriber": {
        "firstName": "John",
        "lastName": "Doe",
        "memberId": "AETNA9wcSu"
    },
    "dependents": [
        {
            "firstName": "Jordan",
            "lastName": "Doe",
            "dateOfBirth": "20010714"
        }
    ],
    "encounter": {
        "serviceTypeCodes": ["30"]
    }
}'

If you're trying to make sense of the 271 response, check out How to read a 271 eligibility response in plain English. It explains the key fields, common pitfalls, and how to tell what a patient owes.

Step 4. Explore the results in Eligibility Manager

Every check you run is stored in the Stedi portal’s Eligibility Manager.

In Eligibility Manager, you’ll see:

  • A list of submitted mock checks

  • Full request and response payloads

  • A human-readable benefits view

  • Debugging tools to inspect coverage, errors, and payer behavior

Each check is grouped under a unique Eligibility Search ID, so you can track related requests and retries.

Eligibility Manager

Eligibility Manager is especially useful for debugging failed checks. To test failure scenarios, try the predefined mock error cases, like an invalid member ID or provider NPI.

What’s next

The sandbox is the fastest way to test Stedi’s eligibility API. But it’s only mock data and doesn’t support most features.

If you want to test real patient data or try things like insurance discovery, request a free trial. We can get most users up and running in less than a day.

Jul 7, 2025

Guide

Eligibility checks are intended for ideal circumstances: the patient has their current insurance card handy, or they know their member ID and are certain of the correct payer. For those situations, Stedi offers a Real-Time Eligibility Check API. But that’s not always reality.

In the real world, patients forget their cards, switch plans, or don’t know who covers them at all.

It’s important to get a successful eligibility response from the patient’s current payer. Without one, patients get surprise bills, staff waste time chasing payment, and providers lose money on denied claims.

But payers can only return results when an eligibility check matches a single patient. While certain payers can match on name and date of birth alone, most need a member ID. If the patient doesn’t have it, the check fails, and the provider is stuck.

When you only have partial details, Stedi’s Insurance Discovery API can help.

With an insurance discovery check, you don’t need to know the member ID or payer. Just send the patient’s known demographic details such as name, birthdate, state or ZIP, and SSN, if available. We enrich these details with third-party data sources and search potentially relevant payers for active coverage, then return what we find.

It’s not guaranteed – match rates vary. But when eligibility checks fail, insurance discovery gives you a way forward. This guide shows how to use it and how to get the best results.

When to use insurance discovery

If you know the payer up front, we recommend that you attempt one or more eligibility checks before running an insurance discovery check (if you know the payer name but not the payer ID, you can use our Payer Search API to find it). If you don’t know the payer or if you’re unable to get a successful eligibility response, insurance discovery is the next step. Some common cases include:

  • The payer isn’t known.

  • The member ID is missing or incorrect.

  • Attempted eligibility checks failed with an AAA 75 (Subscriber not found) or other similar error.

  • The patient gave conflicting or incomplete info.

Discovery checks are a great fallback when a standard eligibility check is impossible or unsuccessful. Since a successful discovery check ultimately returns the full details from a successful eligibility check, they are a direct replacement for eligibility checks. The main tradeoffs are:

  • They’re slower, since Stedi performs an average of 13-16 real-time eligibility checks per discovery check.

  • They’re less reliable than sending an eligibility check with full patient details to a known payer, since it isn’t possible for every discovery check to perform searches against every payer.

  • They’re more expensive, since multiple third-party data and eligibility lookups are performed.

Note: Discovery checks don’t support dental payers. If your use case is dental-only, discovery won’t return results – even if the patient is insured. It's best used for medical coverage.

How to set up insurance discovery

To run discovery checks for a provider, you first need to complete transaction enrollment for their NPI with the DISCOVERY payer ID.

This is a one-time step for each provider. You can submit enrollment requests using the Enrollments API, the Stedi portal, or a bulk CSV.

Enrollment is typically approved within 24 hours. When it’s live, you’ll get an email notification. You can also check enrollment status in real time using the API or Stedi portal. After that, you can use the approved NPI in insurance discovery requests.

If you make an insurance discovery check with an unenrolled provider NPI, you’ll get an error response.

What to include in discovery checks

With eligibility checks, too much data can cause a payer to reject a request that otherwise would have been successful . The opposite is true for insurance discovery checks: the more patient data you include, the better your chances of getting a match.

If you only send the patient’s name and date of birth, success rates will be very low. The reason is that ultimately, Stedi and the payers need to be able to resolve to a single member. Unless the name is extremely uncommon, a name + date of birth is likely to match multiple members and result in a rejection. The same is true for sending a common name + date of birth + ZIP code – for example, John Smith with any date of birth in a New York ZIP code will have dozens of matches, and will therefore result in a failure.

For the best results, include at least the following patient info in Insurance Discovery Check API requests:

  • First name (required)

  • Last name (required)

  • Date of birth (required)

  • Full or partial SSN (even the last 4 digits can help)

  • Gender

  • Full address or ZIP code (current or previous)

An example request body with patient info in the subscriber object:

{
  "provider": {
    "npi": "1999999984"
  },
  "subscriber": {
    "firstName": "John",
    "lastName": "Smith",
    "middleName": "Robert",
    "dateOfBirth": "19800101",
    "ssn": "123456789",
    "gender": "M",
    "address": {
      "address1": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "postalCode": "62701"
    }
  },
  "encounter": {
    "beginningDateOfService": "20240326",
    "endDateOfService": "20240326"
  }
}

How to read discovery responses

Discovery checks usually return a synchronous response within 60-120 seconds. If the result isn’t available by then, you can poll for it using the discoveryId and the Insurance Discovery Check Results endpoint. Most checks don’t require polling.

If the discovery check finds active coverage, you’ll get:

  • Payer name and ID

  • Member ID

  • Group number and plan details

{
  "discoveryId": "12345678-abcd-4321-efgh-987654321abc",
  "status": "COMPLETE",
  "items": [
    {
      "payer": {
        "name": "EXAMPLE INSURANCE CO"
      },
      "subscriber": {
        "memberId": "987654321000",
        "firstName": "John",
        "lastName": "Doe"
      },
      "planInformation": {
        "planNumber": "123456-EXMPL9876",
        "groupNumber": "123456-78",
        "groupDescription": "Individual On-Exchange"
      },
      "benefitsInformation": [
        {
          "code": "1",
          "name": "Active Coverage",
          "serviceTypeCodes": ["30"],
          "serviceTypes": ["Health Benefit Plan Coverage"],
          "planCoverage": "Gold Plan",
          "inPlanNetworkIndicator": "Yes"
        }
      ]
    }
  ]
}

In many cases, the response also includes full benefits data with the same benefitsInformation objects you’d get from a 271 eligibility response. But not always.

If the benefits data is included, you can use it directly. If the data seems incomplete, we recommend running a follow-up 270/271 eligibility check using the returned first name, last name, and member ID to the determined payer, especially if you’re automating downstream logic.

Discovery checks will return multiple payers if multiple coverages are found, but it isn’t guaranteed that they’ll find all of a patient’s plans. If you think the patient may have multiple plans, run a Coordination of Benefits (COB) check after the eligibility check to find other coverage and determine which payer is primary.

How to fix zero matches

A "coveragesFound": 0 result doesn’t always mean the patient is uninsured. It just means the discovery check couldn’t find a match.

{
  "coveragesFound": 0,
  "discoveryId": "0197a79a-ed75-77c3-af58-8ece597ea0be",
  "items": [],
  "meta": {
    "applicationMode": "production",
    "traceId": "1-685c0f14-1b559a954f0bd0127110d161"
  },
  "status": "COMPLETE"
}

Common reasons for no match results include:

  • Recommended fields, like SSN or ZIP code, were missing from the request. Remember that only including name, date of birth, and ZIP code is extremely unlikely to find a single match unless the provided name is extremely uncommon in the provided ZIP code.

  • The patient’s info doesn’t exactly match what the payer has on file. For example, the patient isn’t using their legal name, or their address has changed.

  • The payer doesn’t support real-time eligibility checks, which makes it impossible for Stedi to determine coverage.

  • The patient is covered under a different name, spelling, or demographic variation.

If you think the patient has coverage, try again with corrections or more data. Even small changes like using a partial SSN or legal name can make a difference.

But keep in mind: Even clean, complete input won’t always return a match. Matches aren’t guaranteed.

Since Stedi supports 1,250+ payers for real-time eligibility, it isn’t feasible to check every patient against every payer. Stedi chooses the most probable payers based on the provided demographic details – if the patient has improbable coverage (for example, if the patient has always lived in New York City but has coverage through a small regional payer in the northwest due to their employer’s location), the request is unlikely to find a match.

Limitations

Insurance discovery is a useful tool when used as a fallback. But it has limitations:

  • Hit rates vary. Just sending a name, date of birth, and ZIP code will almost always result in no matches. Including SSN (even last 4), full address, and gender significantly improves results. With strong input data, match rates typically range from 30–70%.

  • It only returns active coverage for the date of service in the request. It can’t return retroactive or future coverage – only what’s active on the date you specify.

  • It doesn’t determine payer primacy. If you get multiple results, use a COB check to figure out which plan is primary.

  • It doesn’t support dental use cases. If your use case is dental-only, discovery won’t return results – even if the patient is insured.

In most cases, you shouldn’t use insurance discovery for your primary workflow. Use it only when eligibility checks fail or aren’t possible.

Get started with insurance discovery

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. But when you can’t get a clean eligibility check, insurance discovery can fill the gap.

If you’re ready to get started, reach out to us for a demo or free trial. We’d love to help you get set up.

Jun 30, 2025

Products

You can now authorize Stedi to sign enrollment forms for you using delegated signature authority, eliminating over 90% of your team’s enrollment paperwork.

Why we built this

Transaction enrollment is the administrative process a provider must complete to exchange certain types of healthcare transactions with a payer. For example, all 835 ERAs require enrollment. Certain payers require enrollment for other transactions, such as 837 claims and 270/271 eligibility checks.

The enrollment process sometimes involves signing a PDF form before submission to a payer. This step can delay enrollments by days or even weeks as signature requests bounce between you and Stedi.

Delegated signature authority solves this by allowing Stedi to sign enrollment forms on your behalf. It removes overhead for your team and can remove days of delay from the enrollment process.

If you manage enrollments for multiple providers, delegated signature authority scales with you. You can onboard more providers faster with less operations work for your team.

How it works

  1. You sign a one-time delegated signature authority agreement with Stedi.

    • If you submit enrollments on behalf of providers, you’ll need to obtain delegated signature authority from your providers during onboarding.

    • Some providers may not allow delegated signing for various reasons, such as their internal policies or legal requirements. In these cases, you can still submit enrollment requests, but the provider must sign the forms directly.

  2. You submit enrollment requests using the Enrollments API, UI, or a bulk CSV upload.

  3. When a payer requires a signature, Stedi checks whether delegated signing is allowed.

    • If allowed, Stedi signs and submits the form.

    • If not allowed, the provider must sign the form directly. Stedi notifies you on the enrollment request and provides instructions to complete the process.

Next steps

Delegated signature authority is available on all paid Stedi plans.

To get started, contact Stedi support in your dedicated Slack or Teams channel.



Jun 30, 2025

Products

You can now see whether a payer supports medical or dental transactions using the Payers API and Stedi Payer Network.

In the API, the new coverageTypes response field helps you filter the list of payers to only those you care about. If you’re using the Search Payers API endpoint, you can also filter by coverage type. Example:

GET /payers/search?query=blue+cross&coverageType=dental

If you’re using the Payer Network, you can also filter by coverage type.

Why we built this

Stedi connects to both dental and medical payers.

Customers building dental applications kept running into the same problem: they couldn’t tell which payers supported dental transactions.

Previously, you could run an eligibility check with STC 35 (General Dental Care) and parse the response – but that was a bit hacky.

We added a coverageTypes field to fix that. It tells you what kind of coverage a payer supports, so you can safely include or exclude them from your workflows.

How it works

Every payer in the network now includes a coverageTypes field in responses from Payers API endpoints – JSON and CSV. For example, in JSON responses:

{
  "items": [
    {
      "displayName": "Blue Cross Blue Shield of North Carolina",
      "primaryPayerId": "BCSNC",
      ...
      "coverageTypes": ["dental"]
      ...
    }
  ]
}

If a payer’s coverageTypes is set to ["medical", "dental"], you can submit supported transaction types for both medical and dental services.

The coverageTypes field applies to any transaction type supported by the payer: eligibility, claims, ERAs, or more.

Try it now

You can filter by coverage type today in both the Payers API and the Payer Network.

Schedule a demo to see it yourself. Or reach out to let us know what else you’d like to see.

Jun 27, 2025

Guide

If you’ve used insurance at a doctor, a healthcare clearinghouse was likely involved.

But most people – even in healthcare – don’t know what a clearinghouse is.

This guide covers what clearinghouses do, who needs one, and why they matter.

What a healthcare clearinghouse does

A clearinghouse helps healthcare providers exchange billing data with payers. Payers include insurance companies, Medicare, and Medicaid.

When your doctor checks your insurance before a visit, that’s a billing transaction – called an eligibility check.

Other common billing transactions include:

  • Claims – a provider asking a payer to pay for their part of a service’s costs

  • Remittances (remits) – a provider receiving payment details or denials from a payer

  • Claim status checks – a provider checking if a claim was received, processed, or delayed

The clearinghouse sits in the middle. It checks the data, keeps it secure, and gets it to the right place.

The jobs of a clearinghouse

The clearinghouse has two main jobs:

  • Connect providers to payers

  • Ensure transactions sent to payers use HIPAA-compliant X12 EDI

HIPAA is a federal law that protects healthcare data and sets rules for how certain transactions must be conducted. It requires that specific billing transactions – like claims and eligibility checks – use the X12 standard of the EDI format.

Without X12, every payer would use a different standard and format. Providers would have to use different formats for different payers. Providers would need custom logic for each one. Billing at scale wouldn’t work.

Connecting providers to payers

In theory, a provider could connect to each payer directly. Some, like large hospital systems, do.

Most don’t. It doesn’t scale.

Even though they all use X12, every payer works differently. Each has its own setup, protocols, and quirks. Connecting to payers takes time and technical skill. Most providers don’t have the staff for it.

That’s where a clearinghouse comes in. They’ve already built payer connections – lots of them – and they keep them running.

But most providers don’t connect directly to a clearinghouse either. Integrating with a clearinghouse still takes engineering work. Most providers don’t have a dev team.

Instead, they use a billing platform that connects to the clearinghouse for them. These platforms can take different shapes:

  • Revenue Cycle Management (RCM) – Software that manages all billing tasks for a provider, including ones that don’t directly involve a payer. That full set of tasks is called RCM.

  • Electronic Health Record (EHR) – Software that stores patient data, like charting, notes, medications, and lab results.

  • Practice Management System (PMS) – Software used by healthcare providers to handle the administrative side of care. In addition to billing, they often help with scheduling and other services.

Providers often layer these platforms on top of each other. For example, a PMS is often used alongside an EHR system.

Handling X12

Clearinghouses don’t just move data between providers and payers. They make sure it’s valid X12.

That process includes:

  • Routing – Sending data to the right payer or provider based on transaction data

  • Translation – Converting common data formats like JSON to X12

  • Validation – Checking for required X12 fields and formatting

  • Delivery – Sending over the right transport protocol

  • Parsing – Turning raw X12 payer responses back into usable data

Some clearinghouses give you raw EDI and expect you to handle it. Others – like Stedi – may also let you use JSON and handle the EDI layer for you.

HIPAA compliance

Healthcare billing data includes protected health information (PHI) – data like names or insurance IDs that can identify patients. Every system that sends, receives, or stores PHI must follow HIPAA rules.

To comply with HIPAA, the clearinghouse must:

  • Encrypt data in transit and at rest

  • Control who can access the data

  • Keep audit logs for every transaction

This matters. It means billing platforms don’t need to build security from scratch. The clearinghouse does it by default.

Why your clearinghouse matters

Billing platforms work with many providers. To scale, they need to write software that automates healthcare transactions. They can't afford the staff – or time – to call payers or use manual payer portals. So they hire developers.

But most legacy clearinghouses weren't built for developers. They have:

  • Poorly documented APIs

  • Cryptic error messages

  • Frequent outages with no updates

  • Slow, unhelpful support

Healthcare transactions are already hard to automate. Most devs don’t know X12. Transactions can fail in strange ways. Error codes don't help. Payers go down without warning. Payer docs don’t match actual responses. And every payer works differently.

When issues hit, you need a clearinghouse that can help. In most cases, they don’t. You submit a ticket, wait days for a reply – then get a boilerplate answer that doesn’t work.

If it’s urgent, you’re on your own. Your team has to scramble to create temporary fixes or call payers themselves.

The right clearinghouse fixes all that. They give you fast, responsive support. Instead of slowing you down, they speed you up and help you scale.

A clearinghouse for developers

If you're building an RCM, EHR, or provider platform, we built Stedi for you. We're an API-first clearinghouse that helps you move fast and scale.

When you hit issues, we give you fast, real-time support from real engineers – not tickets or boilerplate.

Don't take our word for it. See it for yourself. Contact us to set up a demo.

Jun 26, 2025

Spotlight



George Uehling at Ritten

A spotlight is a short-form interview with a leader in RCM or health tech.

In this spotlight, you'll hear from George Uehling, Head of Product at Ritten. You'll learn what Ritten does and how George thinks RCM will change in the next few years.

What does Ritten do?

Ritten builds modern Electronic Health Record (EHR) software tailored specifically for Behavioral Health providers.

Its platform is designed to support mental health practices, group therapy clinics, and treatment centers by streamlining documentation, scheduling, billing, and insurance workflows.

Key features include intuitive clinical documentation tools for a wide range of roles – therapists, psychiatrists, counselors, administrators, and technicians – ensuring ease of use across the board.

Ritten also includes integrated Revenue Cycle Management (RCM) to simplify insurance billing, a built-in CRM to streamline client intake, and a strong emphasis on automating repetitive administrative tasks, particularly in note taking and billing.

Ritten delivers an all-in-one solution built to fit the unique workflows of Behavioral Health providers.

How did you end up working in health tech?

With an engineering background, I've always been focused on building useful technology.

I gravitated toward product management as the perfect blend of customer-facing conversations and technology-driven problem solving, giving me the opportunity to deliver new tools that genuinely improve people’s lives.

When the chance came up to work in behavioral health, I jumped on it. Not only did it allow me to keep doing what I loved, but I also got to do it in service of providers who face some of the most complex, emotionally demanding challenges every day.

How does your role intersect with RCM?

Before stepping into my current role as Head of Product, I served as the Product Manager for RItten's RCM solution.

In that role, I spoke directly with dozens of billers and deeply immersed myself in their day-to-day workflows. That experience gave me firsthand insight into the bottlenecks and pain points across the revenue cycle: from coding and claims submission to denials management and payment posting.

Today, that perspective continues to inform how I prioritize features, guide product strategy, and ensure that RCM features and automations are built for billers.

What do you think RCM ops will look like two years from now?

Over the next year or two, each step of RCM will increasingly incorporate AI layered on top of raw clearinghouse data. This shift is already underway. Vendors are introducing AI-powered Verification of Benefits (VOB) tools that make eligibility data easier to query and understand. AI-driven solutions will become more prominent in other areas such as claim scrubbing, interpreting status updates, and remittances.

The next major shift will be the “agentification” of the RCM workflow. AI agents won’t just surface better insights; they’ll begin taking action autonomously, such as contacting payers to appeal denials or updating systems with the latest claim statuses from external systems.

On the payer side, insurance companies are working to modernize the Prior Authorization process through a new electronic submission standard. Although this initiative has seen fits and starts over the years, momentum is building. It would allow providers to submit authorization requests as seamlessly as they do VOBs and claims, ushering in a future where utilization review is significantly faster and more automated.

Jun 24, 2025

Guide

Most payers don’t support procedure codes in 270 eligibility requests.

This guide explains how to work around that using Stedi's Eligibility Check APIs. It covers how to test a payer for procedure code support and common procedure-to-STC mappings.

Why procedure codes don't work for eligibility

A procedure code is a billing code for a specific healthcare service or product – like a dental cleaning or an ambulance ride. It’s what you use to submit claims. It tells the payer what service was performed.

You'd think procedure codes would also work with eligibility requests. Procedure codes are specific. They’re the same codes you use to bill. And they’re supported by the 270 X12 format.

But most payers ignore them in eligibility requests.

Instead, payers expect a Service Type Code (STC) like 30 (General Medical Care) or MH (Mental Health). STCs are broad categories that group related procedures. This makes things simpler for payers. There are thousands of procedure codes. There are fewer than 200 STCs.

If you send a procedure code anyway, most payers just send a fallback response for STC 30 (General Medical) or 35 (General Dental) – or nothing at all.

While common patterns exist, there's no standard way to match procedures to STCs. Each payer has their own mapping, and they don't document how they do it. Even for a single procedure code, the right STC might vary based on the provider type, place of service, or other modifiers.

Common types of procedure codes

There are a few types – or sets – of procedure codes. Major ones include:

  • Current Procedural Terminology (CPT) codes – Used for most medical services. Maintained by the American Medical Association (AMA).

  • Healthcare Common Procedure Coding System (HCPCS) codes – Used for things like medical equipment, ambulance rides, and certain drugs. Includes CPT codes as Level I.

  • Current Dental Terminology (CDT) codes - Used for dental services. Maintained by the American Dental Association (ADA).

How to use a procedure code or STC in a 270 request

You can send either a procedure code or an STC in an eligibility request - not both. If you’re using Stedi’s JSON eligibility API endpoints, you include them in the encounter object:

// Example using a procedure code (CPT 97802)
"encounter": {
  "productOrServiceIDQualifier": "HC",	// CPT/HCPCS codes
  "procedureCode": "97802"			    // CPT code for medical nutrition therapy
}

// Example using an STC
"encounter": {
  "serviceTypeCodes": ["1"] 		    // STC for medical care
}

If using STCs, send one per request. Some payers accept multiple STCs, but test first. See How to avoid eligibility check errors for testing tips.

Note: Technically, you can send both a procedure code and STC using encounter.medicalProcedures and encounter.serviceTypeCodes respectively. However, no payer in our Payer Network other than CMS HETS supports both properties.

Where to find procedure-level info in 271 responses

Even if a payer doesn’t support procedure codes in 270 requests, they might include procedure details in the 271 response.

If you’re using Stedi’s JSON eligibility API endpoints, most benefits information is in the response’s benefitsInformation objects. Here’s what to look for:

The compositeMedicalProcedureIdentifier field

This means the payer tied benefits to a specific procedure:

{
  "code": "B",			                      // Co-pay
  "benefitAmount": "50",		              // $50 co-pay
  "serviceTypeCodes": ["35"],	              // General dental care
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // American Dental Association (ADA)
    "procedureCode": "D0120"     // Periodic Oral Evaluation - established patient
  },
  ...
}

Check additionalInformation.description

Some payers stuff procedure codes into the free-text notes in additionalInformation.description:

{
  "code": "B",
  "serviceTypeCodes": ["35"],	// General dental care
  ...
  "additionalInformation": [
    {
      "description": "Benefit applies to D0150 - Comprehensive oral evaluation"
    }
  ]
}

How to test a payer for procedure code support

There’s no definitive list of which payers support procedure codes in eligibility checks. The only way to find out is to test.

Here’s our recommended approach:

  • Send a 270 request with your procedure code.

  • Send another 270 with the likely STC. See common mappings below.

  • Compare the 271 responses.

Do this for your most common procedures and payers. Create your own mapping to keep track of what works for each payer.

Common mappings to try

These mappings are starting points. Test them with your payers.

For a complete list of STCs, check out Service Type Codes in the Stedi docs.

Medical procedures (CPT/HCPCS)

Procedure

Description

STCs to try

90834, 90837

Psychotherapy

MH, CF, A6, 98

90867

Transcranial magnetic stimulation

MH, A4

96130

Psychological testing evaluation

MH, A4

96375

IV push

92

96493

Chemotherapy, IV push

82, 92

96494

Chemotherapy, additional infusion

82, 92

97802

Medical nutrition therapy

98, MH, 1, BZ

97803

Medical nutrition follow-up

98, MH, 1, BZ

99214

Psychiatry visits

MH, A4

99490, 99439, 99487, 99489, 99491, 99437

Chronic Care Management (CCM) services

1, 30, 98, MH, A4

98975, 98976, 98977, 98980, 98981

Remote Therapeutic Monitoring (RTM) services

1, 30, 92, DM, MH, A4, 98

E1399

Durable medical equipment, miscellaneous

11, 12, 18

A0100, A0130, A0425, T2003

Non-emergency transportation (taxi, wheelchair van, mileage, trip)

56, 57, 58, 59

Dental procedures (CDT)

For CDT codes, industry bodies like the ADA and NDEDIC have published recommended CDT-to-STC mappings. These are useful starting points – but they’re not enforced. Payers can ignore them.

You can find the recommended mappings in:

You can buy those documents or contact Stedi for help with a specific code.In addition to the guides, you can try the mappings below.

Procedure

Description

STCs to try

D4210

Gingivectomy or gingivoplasty

25

D4381

Local delivery of antimicrobial agent

25

D5110

Complete maxillary (upper) denture

39

Get expert help fast

Want help figuring out the right STC for a specific code? Reach out – Stedi’s eligibility experts have seen a lot. We’re happy to help.

Jun 24, 2025

Products

You can now run batch 270/271 eligibility checks by uploading a CSV file in the Stedi portal.

Batch checks refresh patient eligibility between visits. Run them weekly or monthly to catch insurance issues early – before they cause problems.

You can use the new Eligibility check batches page to run batch checks using a bulk CSV. Each file can include up to 1,000 checks. You can upload and run more than one file at a time.

Upload a batch eligibility check CSV in the portal

Before, you could only run batch refreshes using the Batch Eligibility Check API.

Like the API, CSV batch checks run asynchronously. They don’t count against your concurrency limit. And they won’t slow down real-time checks.

You can pull the results of a CSV or API batch check using the Poll Batch Eligibility Checks API. You can also now track the real-time status of every batch check – whether API or CSV – directly on the Eligibility check batches page.

How to run a CSV batch check

  1. Log in to Stedi.

  2. Go to the Eligibility check batches page. You can also select Eligibility > Batch eligibility checks in the Stedi portal’s nav.

  3. Click New CSV batch and give it a name.

  4. Download the template and fill it out. Use one row per check.

  5. Upload your file.

  6. Click Verify file to check for errors. You can fix and re-upload a file as many times as you need.

  7. Click Execute batch to run the checks.

The batch will move to In progress. When all checks are done, it will show Completed. Some checks may fail – that’s normal. You can review and debug them in Stedi’s Eligibility Manager.

All batch checks in one place

The Eligibility check batches page shows all your batch checks – whether you used the API or uploaded a CSV.

Eligibility check batches page in Stedi portal

Click the batch name to view its details. Here, you can see the status of each check – including any errors – as well as the payer, subscriber, and provider.

Batch eligibility check statuses

If the batch was submitted using the portal, it’ll use the name you entered. If the batch was submitted via the API, it’ll use the name value from the request, if provided. If no name is provided, it’ll default to the auto-generated batchId.

If the batch was submitted as a CSV file, you can also download the original CSV input.

You can pull results from any batch using the API – even ones uploaded in the portal. Just use the batchId.

What’s in the CSV

The template covers the most common fields for eligibility checks, including (non-exhaustively):

  • Patient name

  • Date of birth

  • Member ID

  • Provider NPI

  • Payer ID

  • Service Type Codes (STCs)

If you need extra fields, use the API or contact Stedi Support to request them.

Costs

The costs for running a batch eligibility check – manually or using the API – are the same as running the equivalent number of real-time eligibility checks.

For example, if you run a batch with 500 checks, it will cost the same as running 500 real-time eligibility checks.

Try it now

We built CSV uploads for teams who need to move fast – whether you're testing a new workflow or keeping things simple.

If you're ready to go deeper, reach out. We'll help you get set up.

Jun 24, 2025

Products

You can now use the List Payers CSV API to get a full list of Stedi’s supported payers in CSV format:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/csv \
  --header 'Authorization: <api-key>'

The CSV includes the same data as the Stedi Payer Network UI and other JSON-based Payer APIs:

  • Payer IDs

  • Transaction support flags

  • Transaction enrollment requirements, and more.

No setup or feature flag is needed to access the new endpoint. Just use your Stedi API key.

Why we built this

If you’ve worked with a legacy clearinghouse, you’ve probably dealt with CSV payer lists.

Sometimes they show up in your email inbox. Sometimes you have to dig them out of a portal. Either way, they’re static and go stale fast. You end up guessing what’s still valid – and maintaining brittle mappings to keep things running.

That’s risky. Every healthcare transaction depends on using the right payer ID. If the ID is wrong, the transaction fails. At scale, your system fails. And payer IDs change often.

With most clearinghouses, there’s no easy way to know which IDs still work.

That’s why we built a better way.

We already expose our payer lists programmatically through our JSON-based Payer APIs. Now you can get the same list as a CSV – updated in real time, with one row per payer. It’s easy to load into Google Sheets or Excel, feed into your tools, or compare against your current setup.

If you’re migrating to Stedi, this makes it easier. One API call gives you everything you need.

How it works

Make a GET request to the List Payers CSV API endpoint:

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/csv \
  --header 'Authorization: <api-key>'

You’ll get a plain-text CSV. The first row contains headers. Each row after that is one payer.

Example of Stedi's CSV payer list

The CSV includes:

  • The payer’s immutable Stedi payer ID

  • Their name, primary payer ID, and known payer ID aliases

  • Supported transaction types

  • Whether transaction enrollment is required for a transaction type

Try it out

The List Payers CSV API is free on all paid Stedi plans.

To see how it works for yourself, reach out to schedule a demo.

Jun 18, 2025

Guide

Payers reject eligibility checks for all kinds of reasons. For failed requests, X12 271 eligibility responses typically include a segment called AAA.

AAA errors tell you what went wrong. Payers return them for things like bad member IDs, missing info, and system outages. They also include tips for what to do next.

Stedi’s Eligibility Check APIs let you get 271 responses as JSON or raw X12 EDI. This guide explains how to find AAA errors in 271 responses, what the most common ones mean, and how to fix them.

How to find and read AAA errors

Every AAA error includes three pieces of information:

  • Code – What went wrong, such as a bad member ID or invalid name.

  • Followup Action – What the payer recommends you do next.

  • Location – The location of the error within the original X12 EDI response.

If you’re reading raw X12, the following table outlines possible loop locations:

Loop

Related part

2100A

Payer

2100B

Provider

2100C

Subscriber

2100D

Dependent

If you’re using JSON responses, you don’t need to decode loops. Instead, AAA errors are nested under the related section of the response:

  • subscriber.aaaErrors[] – The most common spot for AAA errors.

  • dependents[].aaaErrors[] – If checking eligibility for a dependent.

  • provider.aaaErrors[] – Typically indicates an NPI or transaction enrollment issue.

  • payer.aaaErrors[] – Usually indicates a connectivity or access issue with the payer.

All errors at these levels are also returned in the top-level errors array. Example:

{
 "errors": [
    {
      "code": "43",
      "followupAction": "Please Correct and Resubmit",
      "location": "Loop 2100B",
      ...
    }
  ],
  ...
}

Common AAA errors

This section covers the most common AAA errors. Errors are listed in numerical order – not by frequency – for easier lookup.

For complete details, see Payer AAA errors in the Stedi docs.

15 – Required Application Data Missing

The request didn’t include enough info to identify the patient. Or the request is missing a required provider field, like Tax ID.

How to fix it:

  • Double-check the patient’s name, date of birth (DOB), and member ID.

  • Include the provider’s Federal Taxpayer Identification Number (EIN) in the request’s provider.taxID field.

33 – Input Errors

The request is missing payer-required fields or includes invalid data.

Some payers issue each dependent their own member ID and don’t support eligibility requests that include a dependents array. These payers may return this error if the request includes the patient in that array.

How to fix it:

  • Double-check that you’re sending all required fields:

    • Patient first name

    • Patient last name

    • Patient date of birth (DOB)

    • Patient member ID

    • Provider NPI

    • Service Type Code (STC) or procedure codes

  • Make sure all values are in the correct format, especially the patient’s DOB and member ID.

  • If the request includes a dependent patient in the dependents array, try sending their info in the subscriber object instead.

41 – Authorization/Access Restrictions

The provider isn’t authorized to submit eligibility checks to the payer.

How to fix it:

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

42 – Unable to Respond at Current Time

The payer is temporarily unavailable.

How to fix it:

  • Automatically retry using the retry strategy outlined in our docs.

  • If retries fail, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

43 – Invalid/Missing Provider Identification

The NPI you sent isn’t valid for the payer, or the provider isn’t enrolled with the payer for eligibility requests.

How to fix it:

  • Make sure the NPI is correct.

  • Confirm the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • Some payers require transaction enrollment for eligibility requests. If so, ensure the provider is enrolled with the payer.

  • If supported, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

50 – Provider Ineligible for Inquiries

The payer doesn’t allow the provider to submit eligibility checks for the Service Type Code (STC).

How to fix it:

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

  • Confirm that the provider is enrolled with the payer for the Service Type Code (STC) you're using. Some payers only allow certain specialties to check eligibility for specific benefits.

51 – Provider Not on File

The payer doesn’t recognize the provider’s NPI. This usually means the provider isn’t registered with the payer.

How to fix it:

  • Make sure the NPI is correct.

  • Confirm the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • Some payers require transaction enrollment for eligibility requests. Ensure the provider is enrolled with the payer.

  • Some payers require credentialing before accepting eligibility checks from a provider. The provider must contact the payer to register.

52 – Service Dates Not Within Provider Plan Enrollment

The provider wasn’t enrolled in the patient’s plan with the payer on the date of service.

How to fix it:

  • Confirm the patient was actively enrolled on the specific date of service.

  • Double-check the date of service in the request. Ensure the date(s) are properly formatted as YYYYMMDD.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

57 – Invalid/Missing Date(s) of Service

The date of service (DOS) is missing, incorrectly formatted, far in the future, or outside the payer’s allowed range.

How to fix it:

  • Double-check the date of service in the request. Ensure the date(s) are properly formatted as YYYYMMDD.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

  • If requests still fail, try omitting encounter.dateOfService from the request.

58 – Invalid/Missing Date-of-Birth

The subscriber or dependent’s date of birth is missing or incorrectly formatted. Some payers require it to locate the member.

How to fix it:

  • Include dateOfBirth in the request, formatted as YYYYMMDD.

  • Double-check that the date is accurate and is not a future date or invalid day/month.

  • Some payers require a date of birth (DOB) even when a member ID is present.

62 – Date of Service Not Within Allowable Inquiry Period

The date of service you submitted is outside the payer’s allowed range.

How to fix it:

  • Don’t send dates more than two years in the past. Some payers only support eligibility checks for dates within the last 12 or 24 months.

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

63 – Date of Service in Future

The date of service is in the future. The payer doesn’t allow eligibility checks for future dates.

How to fix it:

  • Check that the date of service isn’t far in the future. Most payers support future dates through the end of the current calendar month. Only a few, such as CMS, support dates further into the future.

  • Try omitting the date of service from the request.

64 – Invalid/Missing Patient ID

The patient’s ID is missing or doesn’t match what the payer has on file. This usually happens when the payer needs the dependent’s member ID, but the request only includes the subscriber’s.

How to fix it:

  • Check the insurance card. Some plans list separate IDs for dependents. If the patient is a dependent and has their own member ID, treat them as the subscriber and leave out the dependents array.

65 – Invalid/Missing Patient Name

The name of the patient is missing or doesn’t match the payer’s records.

How to fix it:

  • Check the insurance card. Some plans list separate IDs for dependents. If the patient is a dependent and has their own member ID, treat them as the subscriber and leave out the dependents array.

  • Use the full legal name. For example, “Robert” not “Bob.”

  • Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings for compound or hyphenated names. Check if the patient has recently changed names.

67 – Patient Not Found

The payer couldn’t find the patient in their system based on the information you submitted.

How to fix it:

  • Double-check the patient’s name, date of birth (DOB), and member ID.

  • Try sending different combinations of those fields to account for data mismatches – especially if you're missing the member ID.

68 – Duplicate Patient ID Number

The payer found more than one patient record with the ID you submitted. They can’t tell which one you meant.

How to fix it:

  • Include the patient’s name, date of birth (DOB), and member ID to help the payer narrow down the match.

  • In rare cases, this error can occur due to a data issue on the payer side. For example, duplicate records for the same person with the same member ID. If you suspect this, contact Stedi support.

71 – Patient DOB Does Not Match That for the Patient on the Database

The date of birth (DOB) in your request doesn’t match what the payer has on file for the patient.

How to fix it:

  • Double-check the patient’s dateOfBirth. Use the YYYYMMDD format.

  • Confirm the patient’s DOB from a reliable source, like the insurance card or other identification.

72 – Invalid/Missing Subscriber/Insured ID

The member ID doesn’t match the payer’s requirements.

How to fix it:

  • Use the exact ID on the subscriber’s insurance card.

  • If no card is available, run an insurance discovery check using demographic data, like name and date of birth (DOB).

73 – Invalid/Missing Subscriber/Insured Name

The subscriber’s name doesn’t match the payer’s records.

How to fix it:

  • Use the full legal name. For example, “Robert” not “Bob.”

  • Avoid nicknames, abbreviations, or special characters.

  • Try different name orderings for compound or hyphenated names. Check if the patient has recently changed names.

74 – Invalid/Missing Subscriber/Insured Gender Code

The patient’s gender code is missing, incorrect, or not formatted the way the payer expects.

How to fix it:

  • Double-check that the gender matches what’s on file with the payer.

  • Try omitting gender to see if the payer defaults correctly.

75 – Subscriber/Insured Not Found

The payer couldn’t match the subscriber’s details to anyone in their system. This is the most common error.

How to fix it:

  • Verify the member ID matches the patient’s insurance card. Include any prefix or suffix on the patient’s ID.

  • Double-check the patient’s name and date of birth (DOB).

  • Use the full legal name. For example, “Robert” not “Bob.” Avoid nicknames, abbreviations, or special characters. Try different name orderings. Check if the patient has recently changed names.

  • If the info is correct, confirm the request is going to the right payer.

76 – Duplicate Subscriber/Insured ID Number

The payer found more than one member with the subscriber ID you sent. They can’t determine which one to return.

How to fix it:

  • Include the patient’s name, date of birth (DOB), and member ID in the request.

78 – Subscriber/Insured Not in Group/Plan identified

The payer found the member, but they aren’t enrolled in the group or plan you specified.

How to fix it:

  • If possible, include the groupNumber or planNumber in the request. Make sure it matches what’s on the member’s insurance card.

  • Try omitting the groupNumber. Many payers can still return eligibility without it.

79 – Invalid Participant Identification

If the response has a 200 HTTP status, this usually means there’s a connectivity issue with the payer. If the response has a 400 HTTP status, it means the payer ID is invalid or the payer doesn’t support eligibility checks.

How to fix it:

  • If the error comes back with a 200 HTTP status, automatically retry using the retry strategy outlined in our docs.

  • If you get a 400 status, don’t retry. Confirm the payer ID and that the payer supports 270/271 eligibility checks in the Stedi Payer Network.

  • If all requests continue failing, contact Stedi support.

80 – No Response Received - Transaction Terminated

The payer didn’t return any eligibility data. The transaction timed out or failed midstream.

How to fix it:

  • Automatically retry using the retry strategy outlined in our docs.

  • If retries fail, retry with a different patient and NPI to rule out request-level issues.

  • If all requests are failing, contact Stedi support.

97 – Invalid or Missing Provider Address

The address submitted for the provider is missing or doesn’t match what the payer has on file.

How to fix it:

  • Double-check the provider’s address in informationReceiverName.address. Check for formatting issues, like missing ZIP or street line, or common mismatches (e.g. “St.” vs. “Street”).

Retryable AAA errors

Only AAA errors 42, 79, and 80 are retryable. These indicate temporary payer issues like downtime or throttling. All other AAA errors require fixing before retrying.

AAA 79 errors are only retryable if it comes back with a 200 HTTP status. If you get a 400 status, it usually means the payer ID is invalid or not configured. Don’t retry in these cases.

The right retry strategy depends on your use case:

Retry strategy for real-time eligibility checks
If you’re using the real-time endpoint and need a fast response – like checking in a patient – we recommend:

  • Wait 15 seconds before the first retry.

  • Retry every 15 seconds for up to 2 minutes.

  • Don’t send the same NPI to the payer more than once every 15 seconds.

  • If requests still fail, stop retrying and contact Stedi support.

If requests still fail, stop retrying and contact Stedi support.

Retry strategy for batch refreshes
If you’re running eligibility refreshes between appointments, use the batch endpoint. For this endpoint, Stedi automatically retries AAA 42, eligible 79, and 80 errors in the background for up to 8 hours.

If you’re using the real-time endpoint and can tolerate longer delays:

  • Wait 1 minute before the first retry.

  • Then exponentially back off – up to 30 minutes between retries

  • Continue retrying for up to 8 hours.

For full guidance, see our retry strategy docs.

How to handle non-retryable AAA errors

If a 271 response includes any AAA errors, treat it as a failure – even if it includes benefits data. Payers sometimes return benefits alongside AAA errors. In JSON responses, you can check the top-level errors array to quickly detect AAA errors.

If the error isn’t retryable, use Stedi’s Eligibility Manager to debug and try the request again.

How to mock AAA errors

In Stedi’s test mode, you can use mock eligibility checks to simulate common AAA errors and test your application’s error-handling logic. See Eligibility mock requests for more details.

Fast, expert eligibility help

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. When errors do happen, you get help fast. Our average support response time is under 8 minutes.

Want to see how good support can be? Get in touch.

Jun 17, 2025

Guide

Insurance verification is important for dental care. Before the provider can get paid, they need to know what a patient’s plan covers. The patient needs to know too – so they’re not surprised by a bill later.

That’s where Stedi comes in. We make it easy to check dental insurance in real time. Stedi's Eligibility Check APIs let you work with JSON or raw X12 EDI. You can check coverage with thousands of payers, including major dental insurers like Delta Dental, DentaQuest, and Guardian.

This post answers the most common questions we hear from developers using Stedi to check dental eligibility.

What STCs should I use for dental?

A Service Type Code (STC) tells the payer what kind of benefits you're checking. For general dental coverage, use STC 35 (Dental Care) in the eligibility request:

"encounter": {
  "serviceTypeCodes": ["35"]
}

If you leave it out, Stedi defaults to 30 (Health Benefit Plan Coverage). This may return incomplete or irrelevant data. Many payers only return dental benefits for STC 35.

Other common dental STCs include:

  • 4 - Diagnostic X-Ray

  • 5 - Diagnostic Lab

  • 23 - Diagnostic Dental

  • 24 - Periodontics

  • 25 - Restorative

  • 26 - Endodontics

  • 27 - Maxillofacial Prosthetics

  • 28 - Adjunctive Dental Services

  • 36 - Dental Crowns

  • 37 - Dental Accident

  • 38 - Orthodontics

  • 39 - Prosthodontics

  • 40 - Oral Surgery

  • 41 - Routine (Preventive) Dental

Most payers only support one STC per request. Don’t send multiple STCs unless you’ve tested that it works. For testing tips, see our How to avoid eligibility check errors blog.

Which payers support dental eligibility checks?

Use the coverageTypes field in Payers API responses:

{
  "items": [
    {
      "displayName": "Blue Cross Blue Shield of North Carolina",
      "primaryPayerId": "BCSNC",
      ...
      "coverageTypes": ["dental"]
      ...
    }
  ]
}

If you’re using the Search Payers API endpoint, you can also filter by coverage type.

You can also filter for coverage type in the Stedi Payer Network.

The coverageTypes field applies to any transaction type supported by the payer: eligibility, claims, ERAs, or more.

Can I use CDT codes in eligibility checks?

Yes – but support depends on the payer.

Current Dental Terminology (CDT) codes are procedure codes used in dental billing. For example, D0120 is a routine exam.

You can send a CDT code in your request using the productOrServiceIDQualifier and procedureCode fields. But many payers will return the same data you’d get from STC 35. Those results often include CDT code-level benefits.

To test it, send one request with the CDT code and one with STC 35 to the same payer. Then compare what you get back.

What’s included in dental eligibility responses?

This depends on the payer, but most responses include the following.

Basic coverage
These fields confirm whether the member has dental coverage and when it starts and ends:

  • Coverage status (active/inactive): benefitsInformation.code ("1" = Active, "6" = Inactive).

    Plan start and end dates: planInformation.planBeginDate and planInformation.planEndDate.

Patient responsibility
These fields tell you what the patient might owe:

  • Co-insurance and deductible: benefitsInformation.code and benefitsInformation.benefitPercent or benefitsInformation.benefitAmount.

    Coverage levels for common categories, such as diagnostic or preventative: benefitsInformation.serviceTypeCode (35 for basic or 41 for preventive).

Lifetime maximums
Many dental plans include lifetime maximums. These often show up as two entries with the same benefitsInformation.code.

For example, one for the total lifetime maximum:

{
  "code": "F",
  "serviceTypeCodes": ["38"],
  "benefitAmount": 2000,		// $2,000 amount
  "timeQualifierCode": "32"		// Lifetime maximum
}

One for the remaining amount:

{
  "code": "F",
  "serviceTypeCodes": ["38"],
  "benefitAmount": 1200,		// $1,200 amount
  "timeQualifierCode": "33"		// Lifetime remaining
}

CDT-level detail
Many payers return benefits tied to specific CDT codes, using compositeMedicalProcedureIdentifier:

{
  "code": "A",  				              // Co-insurance
  "insuranceTypeCode": "GP",  		          // Group policy
  "benefitPercent": "0",  			          // Patient owes 0% co-insurance for procedure
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // CDT code qualifier
    "procedureCode": "D0372"  		          // CDT code for the procedure
  },
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "202420722"  // Most recent date this procedure was used
  }
}

Cigna is a known edge case. It puts CDT info in additionalInformation.description as free text.

Age limitations
Age limitations use one of the following quantityQualifierCode values:

  • S8 - Age, Minimum

  • S7 - Age, Maximum

The benefitQuantity is the minimum or maximum age allowed.

{
  // Age limit: patient must be at least 18 years old
  "quantityQualifierCode": "S8",	// Age (minimum)
  "benefitQuantity": "18"

}

Frequency limitations
 Frequency limitations typically use one of the following quantityQualifierCode values:

  • P6 - Number of Services or Procedures

  • VS - Visits

The timePeriodQualifierCode defines the time window (such as 7 for per year). The benefitQuantity sets the frequency limit.

{
  // Frequency limit: 2 services per calendar year
  "quantityQualifierCode": "VS",  			// Visits
  "benefitQuantity": "2",
  "timePeriodQualifierCode": "7",  			// Annual
  "numOfPeriods": "1",
}

History
Many payers include the last time a procedure was done. This shows up in benefitsDateInformation.latestVisitOrConsultation.

Example at the STC level:

{
  // STC-level entry
  "code": "A",                                // Co-insurance
  "insuranceTypeCode": "GP",                  // Group policy
  "benefitPercent": "80",                     // 80% covered
  "serviceTypeCodes": ["41"],                 // Routine (Preventive) Dental
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "20240301"   // Last preventive service date
  }
}

At the CDT level:

{
  // CDT code-level entry
  "compositeMedicalProcedureIdentifier": {
    "productOrServiceIDQualifierCode": "AD",  // CDT code qualifier
    "procedureCode": "D0150"                  // Comprehensive oral evaluation
  },
  "benefitsDateInformation": {
    "latestVisitOrConsultation": "20240404"   // Last time this procedure was used
  }
}

Free-text details
Some payers add notes as free text in additionalInformation.description, like:

  • Frequency limits shared between CDT codes.

  • Waiting periods.

  • Restrictions, such as the missing tooth clause.

For more tips on reading eligibility responses, see our How to read a 271 eligibility response in plain English blog.

How “real time” are Stedi’s real-time eligibility checks?

Most responses come back in 3-4 seconds.

But it depends on the payer. Some take longer – up to 60 seconds.

To handle slow responses, Stedi keeps the request open for up to 2 minutes. During that time, we retry the request in the background if needed.

Can I run dental eligibility checks in batches?

Yes. Use the Batch Eligibility Check API to send up to 1,000 checks at once. This works well if you want to refresh coverage data before appointments.

Batch checks are asynchronous. They don’t count toward your real-time concurrency limit. But the response can take longer – sometimes up to 8 hours.

Got more questions?

Contact us to talk to a dental eligibility expert at Stedi.

Jun 16, 2025

Products

When the status of a transaction enrollment request changes, Stedi now sends you an automated email.

No setup is needed. These email notifications replace our previous manual notification process.

How it works

You can submit a transaction enrollment request via API, UI, or bulk CSV import. When you submit a request, you must provide an email address that Stedi can reach out to with updates or questions. If you’re a vendor submitting on behalf of a provider, you typically provide your own email address.

Stedi monitors the status of each transaction enrollment. When a status changes – say, from PROVISIONING to LIVE – we send you an email. The only time we don’t send an email is when the status changes from DRAFT to SUBMITTED.

Status update emails are sent once per hour and batched per email address:

  • If one enrollment updates, we send a single-entry email.

  • If multiple enrollments update, we send a summary email listing up to 100 changes.

  • If more than 100 updates occur for the same email address in an hour, we send multiple summary emails.

We never include PHI in these emails.

If an enrollment requires action on your part, we’ll continue to reach out to you via Slack or email with next steps.

How it looks

Each single-entry email includes:

  • Enrollment status

  • Payer name

  • Transaction type, such 835 ERA or 270/271 eligibility check

  • Whether a note was added

  • Timestamp

If the status is REJECTED, we'll include the reason and how to fix it in an added note. If you have questions, you can reach out to us on Slack.

Single entry enrollment status email

Summary emails that cover multiple enrollments include:

  • Enrollment statuses

  • Counts for each status

  • Timestamp

Multiple entry email

Clicking Open enrollments opens the Enrollments page in the UI. The page is filtered to the specific list of enrollment requests from the email.

Other ways to track enrollments

Email notifications are one way to stay updated on transaction enrollment requests. You can also:

Enrollment – done for you

Transaction enrollment is one of the biggest bottlenecks in healthcare billing.Tt can stop you from scaling RCM operations.

At Stedi, we handle the hard parts for you: status tracking, payer follow-up, and notifications. This lets you onboard more providers faster – and keep building.

Contact us to set up a demo or POC.

Jun 13, 2025

Company

CAQH CORE has officially certified Stedi for real-time eligibility checks.

As a result of HIPAA, payers, providers, and other parties are required to use the X12 270/271 EDI format for eligibility checks. This format is commonly known as X12 HIPAA.

But X12 HIPAA only covers part of the picture: the transaction schema. It leaves a lot of room for interpretation when it comes to topics like response content, error handling, and system availability. This opens the door to inconsistencies and reliability issues that make it harder for systems to talk to each other.

CAQH CORE certification exists to fix that.

What is CAQH CORE?

The Council for Affordable Quality Healthcare (CAQH) is a non-profit group backed by major health insurers and provider groups. In 2005, it created the Committee on Operating Rules for Information Exchange (CORE) to improve how healthcare systems exchange data. The U.S. Department of Health and Human Services (HHS) designated CAQH as the official rule authoring entity for HIPAA administrative transactions.

CORE builds on the HIPAA-mandated X12 standard. It further defines how to create, respond to, and transmit the X12 transactions. The CORE Operating Rules define stricter, more specific standards for how eligibility data should be exchanged, including:

  • Required data content in the 271 eligibility response, so data is complete and consistent.

  • Support for the CAQH CORE SOAP+WSDL and HTTP MIME Multipart protocols, as defined in the CORE Connectivity Rule vC2, to ensure secure and interoperable data exchange.

  • Standardized error messages to simplify troubleshooting.

  • Uptime, availability, and performance benchmarks.

These rules make system behavior more predictable and interoperable across the industry.

What Stedi’s certification covers

This certification validates our implementation as an Information Requestor for real-time eligibility checks. In practice, that means:

  • Our handling of 270/271 transactions follows the CORE spec.

  • Our use of the CAQH CORE SOAP protocol.

  • Our 271 responses are consistently structured, complete, and reliable.

  • We’ve passed an independent certification test confirming our compliance.

Why certification matters

If you’re building around eligibility, Stedi’s CORE certification saves you time and guesswork. You know exactly how our system behaves – and that it matches industry standards for speed, structure, and reliability.

Verify insurance with Stedi

CORE certification is one of the only public signals that a clearinghouse’s systems work as expected. Ours does.

Contact us to book a demo and start a POC.

Jun 12, 2025

Guide

Payer IDs are the routing numbers of healthcare. Every transaction – including eligibility checks and claim submissions – depends on the right one. If the ID is wrong, the transaction fails.

But finding the right payer ID is hard. Payers go by different names. Their IDs change. Most clearinghouses still send out monthly CSVs, which go stale fast. Developers end up maintaining brittle mappings that break every time the list updates.

The Search Payers API fixes that. It lets you search the Stedi Payer Network for payers by name, payer ID, or payer ID alias. You get accurate, up-to-date results in JSON.

You can use it anywhere you need to look up a payer: intake forms, billing tools, internal dashboards, and more.

We use the API in our own Payer Network UI. This post walks through a simplified version of that implementation, using TypeScript and Next.js. You don’t need to be an expert to follow along. If you know basic Node.js and JavaScript, you’ll be fine.

For full details on the Search Payers API, check out the Search Payers API docs.

How it works

We implement search as an API route in Next.js. The front end sends a request when a user types a payer name or applies filters. This route receives search requests from the front end, sends them to the API, and returns cleaned-up payer results in JSON.

Here’s a simplified version of our real implementation. It shows how to handle the request, construct filters, and format the response. It’s not exactly our production code, but it’s close. You can use it as a starting point for integrating the Search Payers API into your own application.

import { NextApiRequest, NextApiResponse } from "next";
import fetch from "node-fetch";

// Stedi API key, pulled from environment variables
const STEDI_API_KEY = process.env["STEDI_API_KEY"]!;

// The Search Payers API endpoint
const STEDI_SEARCH_URL =
  "https://healthcare.us.stedi.com/2024-04-01/payers/search";

// The handler for the POST /api/search route
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // Only allow POST requests
  if (req.method !== "POST") {
    return res.status(405).json({ message: "Method not allowed" });
  }

  // Extract values from the request body
  const {
    query = "*",
    pageSize = 100,
    pageToken,
    eligibilityCheck,
    claimStatus,
    claimPayment,
    professionalClaimSubmission,
    institutionalClaimSubmission,
    dentalClaimSubmission,
    coordinationOfBenefits,
    unsolicitedClaimAttachment,
  } = req.body as SearchPayersInput;

  // Build query string parameters
  const params = new URLSearchParams({
    query,                            // the payer name, payer ID, or payer ID alias
    pageSize: pageSize.toString(),    // how many results to return
    ...(pageToken && { pageToken }),  // pagination token, if provided

    // transaction support filters
    ...(eligibilityCheck && { eligibilityCheck }),
    ...(claimStatus && { claimStatus }),
    ...(claimPayment && { claimPayment }),
    ...(professionalClaimSubmission && { professionalClaimSubmission }),
    ...(institutionalClaimSubmission && { institutionalClaimSubmission }),
    ...(dentalClaimSubmission && { dentalClaimSubmission }),
    ...(coordinationOfBenefits && { coordinationOfBenefits }),
    ...(unsolicitedClaimAttachment && { unsolicitedClaimAttachment }),
  });

  try {
    // Send GET request to the Search Payers API
    const response = await fetch(`${STEDI_SEARCH_URL}?${params.toString()}`, {
      headers: {
        Authorization: STEDI_API_KEY,
      },
    });

    const data = await response.json() as SearchPayersOutput;
    const payers = data.items?.map((item) => item.payer) ?? [];
    const totalPayers = data.stats?.total ?? 0;

    // Return the formatted results and metadata
    res.status(200).json({
      payers,
      totalPayers,
      nextPageToken: data.nextPageToken,
    });
  } catch (err) {
    // Log and return a server error if the request fails
    console.error("Search error", err);
    res.status(500).json({ message: "Error performing search" });
  }
}

We've left out the interfaces and enums to save space.

Search by payer name, payer ID, or alias

You can pass in a payer name, payer ID, or payer ID alias. The API supports fuzzy matching on names and aliases, so even partial or slightly misspelled inputs work. For example: AETNA, ATENA, and 60054 all return Aetna. Results are ranked by how closely they match the input.

The query parameter is required by the API. In our UI, it’s optional. If the user doesn’t provide a value, we default to "*". This returns all payers, optionally filtered by transaction support. It’s useful for showing initial results or populating a custom dropdown.

Here’s how the query gets extracted and passed:

  // Extract values from the request body
  const {
    query = "*",
    pageSize = 100,
    pageToken,
    eligibilityCheck,
    claimStatus,
    claimPayment,
    professionalClaimSubmission,
    institutionalClaimSubmission,
    dentalClaimSubmission,
    coordinationOfBenefits,
    unsolicitedClaimAttachment,
  } = req.body as SearchPayersInput;

Filter by transaction type

The Search Payers API supports filters for specific transaction types. You can filter by any combination of the following:

  • eligibilityCheck (270/271)

  • claimStatus (276/277)

  • professionalClaimSubmission (837P)

  • dentalClaimSubmission (837D)

  • institutionalClaimSubmission (837I)

  • claimPayment (835 ERA)

  • coordinationOfBenefits (270/271 COB check)

Each filter accepts one of the following values:

  • SUPPORTED – The payer supports the transaction type.

  • ENROLLMENT_REQUIRED – The payer supports the transaction type but requires transaction enrollment.

  • EITHER – Includes both SUPPORTED and ENROLLMENT_REQUIRED.

  • NOT_SUPPORTED – The payer doesn’t support the transaction type.

Here’s how the filters are added to the query string:

const params = new URLSearchParams({
  query,
  pageSize: pageSize.toString(),
  ...(eligibilityCheck && { eligibilityCheck }),
  ...(claimStatus && { claimStatus }),
  ...(claimPayment && { claimPayment }),
  ...(professionalClaimSubmission && { professionalClaimSubmission }),
  ...(institutionalClaimSubmission && { institutionalClaimSubmission }),
  ...(dentalClaimSubmission && { dentalClaimSubmission }),
  ...(coordinationOfBenefits && { coordinationOfBenefits }),
  ...(unsolicitedClaimAttachment && { unsolicitedClaimAttachment }),
});

This setup lets you filter for exactly the payers your app can work with and ignore everything else.

Pagination

The Search Payers API returns a maximum of 100 results per request. If there are more results, the response includes a nextPageToken.

You can pass that token in your next request to fetch the next page. For example:

const response = await fetch(
  `${STEDI_SEARCH_URL}?query=blue+cross&pageToken=${nextPageToken}`,
  { headers: { Authorization: STEDI_API_KEY } }
);

This approach avoids offset-based pagination. You don’t need to track indices or compute limits. Just pass the token forward.

In your handler, you can return the nextPageToken like this:

res.status(200).json({
  payers,
  totalPayers,
  nextPageToken: data.nextPageToken,
});

If nextPageToken is undefined, you’ve reached the end of the results.

Format results for the UI

The API response includes detailed records for each matching payer. Each result includes:

  • stediId – A unique, immutable payer ID you can use in transactions with Stedi

  • displayName – The payer’s name

  • primaryPayerId –The most commonly used payer ID

  • aliases – Other payer IDs or names associated with this payer

  • transactionSupport – Which transaction types the payer supports

  • enrollment – Whether transaction enrollment is required, and how it works

Here’s how you might extract the data you need:

const payers = data.items?.map((item) => item.payer) ?? [];

This keeps the front end simple. If you need more data – like payer ID aliases, enrollment type, or COB support – you can include it as needed.

Tips

Here are a few tips to help get you started with your own implementation of the Search Payers API.

Multiple filters use AND logic.
If you pass multiple transaction filters, the API only returns payers that match all of them.

The Search Payers API is designed for routing, not patients.
The payer names and aliases are optimized for clearinghouse routing – not for display. If you need a patient-facing dropdown, build your own list and map to the Stedi payer ID.

Try it yourself

The Search Payers API is available on all paid Stedi plans.

To get started, contact us. We’ll help you set up a proof of concept and walk you through the integration.





Jun 9, 2025

Guide

Stedi’s Eligibility Check APIs let you get 271 eligibility responses as JSON. That makes them easier to use in code – not easier to understand.

The 271 format is standard. The data inside isn’t. Most fields are optional, and payers use them in different ways. Two payers might return different info for the same patient or put the same info in different places. Luckily, there are consistent ways to extract the data you need.

This guide shows you how to read Stedi’s 271 responses in plain English. You’ll learn how to check if a patient has coverage, what benefits they have, and what they’ll owe. It also includes common mistakes and troubleshooting tips.

This article covers the highlights. For complete details, see Determine patient benefits in our docs or contact us.

Where to find most benefits info

Most of a patient’s benefit info is in the 271 response’s benefitsInformation array.

Each object in the array answers a different question about benefit coverage: Is it active? What’s the co-pay? What's the remaining deductible?

{
  ...
  "benefitsInformation": [
    {
      "code": "1",                        // Active coverage
      "serviceTypeCodes": ["30"],         // General medical
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "Y",  // Applies to in-network services
      "additionalInformation": [
        {
          "description": "Preauthorization required for imaging services."
        }
      ]
    },
    {
      "code": "B",                        // Co-pay
      "serviceTypeCodes": ["88"],         // Pharmacy
      "benefitAmount": "10",              // $10 co-pay
      "inPlanNetworkIndicatorCode": "Y"   // Applies to in-network services
    },
    {
      "code": "C",                        // Deductible
      "serviceTypeCodes": ["30"],         // General medical
      "benefitAmount": "1000",            // $1000 annual deductible
      "timeQualifierCode": "23",          // Calendar year
      "inPlanNetworkIndicatorCode": "N"   // Applies to out-of-network services
    },
    {
      "code": "D",                        // Benefit Description
      "serviceTypeCodes": ["30"],
      "additionalInformation": [
        {
          "description": "EXCLUSIONS: COSMETIC SURGERY, EXPERIMENTAL TREATMENTS"
        }
      ]
    }
  ],
  ...
}

Each benefitsInformation object includes a few key fields. Most of them contain codes:

  • code: What the benefit is, like "1" (Active Coverage), "B" (Co-pay), or "C" for (Deductible). Although it's one field, there are two classes of codes: 1-8 for coverage status and A-Y for benefit categories. For more details, see Benefit type codes in the docs.

  • serviceTypeCodes: What kind of care the benefit applies to, like "30" (General Medical) or "88" (Pharmacy). See Service Type Codes in the docs.

    Some Service Type Codes (STCs) are broader categories that include other STCs. For example, "MH" (Mental Health) may include "A4" (Psychiatric), "A6" (Psychotherapy), and more. But this varies by payer.

    You’ll often see the same serviceTypeCodes in more than one benefitsInformation object. That’s expected. To get the full picture for a service, look at all entries that include its STC.

  • timeQualifierCode: What the benefit amount represents – often the time period it applies to, like "23" (Calendar Year). Sometimes, this indicates whether the amount is a total or remaining portion, like "29" (Remaining Amount). For the full list, see Time Qualifier Codes in the docs.

    Use this field to understand how to interpret the dollar amount. For example, whether it’s the total annual deductible or the remaining balance of a maximum.

  • inPlanNetworkIndicatorCode: Whether the benefit applies to in-network or out-of-network care – not whether the provider is in-network. Possible values are "Y" (In-network), "N" (Out-of-network), "W" (Both), and "U" (Unknown). For more details, see In Plan Network Indicator in the docs.

  • additionalInformation.description: Free-text notes from the payer. These often override structured fields. Payers often include important info here that doesn’t fit elsewhere.

Most of these fields have human-readable versions, like codeName for code. Use those for display, not logic. Always use the related code field in your code.

Unless otherwise indicated, the fields referenced in the rest of this guide are in benefitsInformation objects.

Check active coverage

To check if a patient has active coverage, look for two things:

  • A benefitsInformation object with code = "1"

  • A date range that includes the date of service

Start with the code. In the following example, the patient has coverage for general medical care.

{
  "code": "1",                      // Active coverage
  "serviceTypeCodes": ["30"]        // General medical
}

Note: Some payers use code: "D" (Benefit Description) entries to list coverage exclusions or limitations. Check these alongside code: "1" entries for a complete picture of benefits coverage.

Next, check the coverage dates. If there’s a benefitsDateInformation field in the same object, use that:

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "benefitsDateInformation": {
    "service": "20241216-20250114", // Coverage window for this benefit
    "periodStart": "20981216"       // Optional start date (duplicate of above)
  }
}

The benefitsDateInformation dates apply specifically to the benefit in the object. They override the top-level plan dates, so they take precedence.

If that’s missing, use the top-level planDateInformation field:

{
  "planDateInformation": {
    "planBegin": "20250101",       // Plan start date
    "planEnd": "20251231"          // Plan end date
  },
  ...
  "benefitsInformation": [
    {
      "code": "1",                 // Active coverage
      "serviceTypeCodes": ["30"]   // General medical
    }
  ]
}

planDateInformation contains the coverage dates for the patient’s plan.

If the date of service isn’t in the date range, coverage is not active, even if code = "1".

Get patient responsibility

Patient responsibility is what the patient has to pay, usually before or at the time of service. This includes co-pays, co-insurance, or deductibles.

Each cost type uses a different code, and the amount is either a dollar (benefitAmount) or a percent (benefitPercent).

code

What it means

Field to read

A

Co-insurance

benefitPercent (Percentage)

B

Co-payment

benefitAmount (Dollar amount)

C

Deductible

benefitAmount (Dollar amount)

F

Limitations (Maximums)

benefitAmount (Max covered)

J

Cost Containment

benefitAmount (Dollar amount)

G

Out-of-pocket max

benefitAmount (Dollar limit)

Y

Spend down (Medicaid)

benefitAmount (Amount to quality)

Use inPlanNetworkIndicatorCode to see if the cost applies in-network ("Y") or out-of-network ("N"). If both in- and out-of-network costs exist, you’ll see two benefitsInformation objects with the same code, one for each.

Example: $20 co-pay for in-network mental health

{
  "code": "B",                        // Co-payment
  "serviceTypeCodes": ["MH"],         // Mental health
  "benefitAmount": "20",              // $20 per visit
  "inPlanNetworkIndicatorCode": "Y"   // Applies only to in-network services
}

Example: $1,000 annual deductible with $500 left

{
  "code": "C",                      // Deductible
  "serviceTypeCodes": ["30"],       // General medical
  "timeQualifierCode": "23",        // Calendar year total
  "benefitAmount": "1000",          // $1,000 total
  "inPlanNetworkIndicatorCode": "Y"
},
{
  "code": "C",
  "serviceTypeCodes": ["30"],
  "timeQualifierCode": "29",        // Remaining
  "benefitAmount": "500",           // $500 left to meet deductible
  "inPlanNetworkIndicatorCode": "Y"
}

Check prior authorization requirements

Some services need prior authorization, also called preauthorization. That means the payer must approve the service before they’ll cover it.

Check authOrCertIndicator:

authOrCertIndicator

What it means

Y

Prior auth required

N

Not required

U

Unknown

If authOrCertIndicator is missing, it means prior auth isn’t required or the payer didn’t return that info. In practice, most payers set this field to "Y" if prior auth is required for at least some services.

Also check additionalInformation.description. Payers often add notes about prior authorization there.

{
  "additionalInformation": [
    {
      "description": "Preauthorization required for all imaging services performed out-of-network."
    }
  ]
}

If the free text says prior auth (may also be called “preauthorization”) is needed, trust it – even if authOrCertIndicator says otherwise.

Check if benefits apply to in-network providers

The field inPlanNetworkIndicatorCode only tells you whether a benefit applies to in-network care. It doesn’t tell you if the provider is in-network. Example:

{
  "code": "B",                        // Co-payment
  "serviceTypeCodes": ["88"],         // Pharmacy
  "benefitAmount": "10",
  "inPlanNetworkIndicatorCode": "Y"   // Co-pay applies to in-network services
}

This means: If the provider is in-network and the co-pay is $10. It doesn’t say whether the provider actually is in-network.

To check if a provider is in-network:

You can’t tell if a provider is in-network just from the 271. Your best option is to call the payer or provider directly. Some payers may offer FHIR APIs you can use.

Some payers include network status for the provider as free text in additionalInformation.description. However, it’s not standardized and may not be reliable. It's best to confirm via phone. Example:

{
  "description": "Provider is out-of-network for member."
}

Check for a Medicare Advantage plan

A 271 response won’t always say “Medicare Advantage” directly – but you can often infer it.

From a commercial payer:

It’s likely a Medicare Advantage plan if either of the following are true:

  • insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B).

  • A hicNumber is populated in benefitsAdditionalInformation or planInformation. This is the patient’s Medicare Beneficiary Identifier (MBI).

Example: Medicare Advantage indicators

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "insuranceTypeCode": "MA",
  "benefitsAdditionalInformation": {
    "hicNumber": "123456789A"
  }
}

From a CMS response:

Look for code = "U" and serviceTypeCodes = ["30"]. Then check for a message in benefitsInformation.additionalInformation.description that includes MA Bill Option Code: in the free text:

{
  "additionalInformation": [
    {
      "description": "MA Bill Option Code: B"
    }
  ]
}

The bill option code tells you how claims are handled. If you see B, C, or 2, it’s likely a Medicare Advantage plan.

Bill option code

What it means

A, 1

 

Claims go to Medicare

B, 2

Medicare Advantage plan handles some claims

C

Medicare Advantage plan handles all claims

Benefit overrides and free-text messages

Not everything is in a structured field. Some of the most important rules only show up in additionalInformation.description as free text.

This free text can include:

  • Prior auth or referral rules

  • Network status hints

  • Legal notices (like NSA or BBPA)

  • Plan limitations or quirks

This field contains overrides. If it contradicts a structured value, like authOrCertIndicator or code, trust the text.

We recommend you surface this text to end users or flag it for review. Ignoring it means missing critical info.

Example: Prior auth rule not shown in authOrCertIndicator:

{
  "code": "B",
  "serviceTypeCodes": ["MH"],
  "benefitAmount": "20",
  "inPlanNetworkIndicatorCode": "Y",
  "additionalInformation": [
    {
      "description": "Preauthorization required for mental health visits after 6 sessions."
    }
  ]
}

Example: Coverage excluded even though code = "1":

{
  "code": "1",
  "serviceTypeCodes": ["30"],
  "additionalInformation": [
    {
      "description": "Coverage excluded due to missing referral."
    }
  ]
}

Common errors and troubleshooting

Eligibility responses aren’t always clean. Payers sometimes return conflicts or errors.

Here’s how to handle common problems:

No code = "1" for active coverage

That doesn’t necessarily mean coverage is inactive. Check for:

  • code = "6" (Inactive)

  • code = "V" (Cannot Process)

  • code = "U" (Contact Following Entity for Eligibility or Benefit Information)

Some payers send code = "V" or code = "U" first but still include code = "1" later. If you see a valid code = "1", use it.

Top-level errors

If the response includes a populated top-level errors array, the whole response is invalid. Even if it includes benefitsInformation. Use Stedi’s Eligibility Manager to debug and try the request again.

Unclear results? Retry with STC 30 (Medical) or 25 (Dental)

If the response is confusing, resend the eligibility check using STC 30 for medical or STC 35 for dental.

These STCS are the most widely supported and usually give the clearest data.

Fast, expert support for eligibility

Stedi’s Eligibility Check APIs let you build fast, reliable eligibility checks. Even with the best tools, you’ll sometimes hit errors or unclear responses.

When that happens, we can help – fast. Our average support time is under 8 minutes.

Want to see how good support can be? Get in touch.

Jun 5, 2025

Products

You can now submit transaction enrollments to select payers in a single step. No PDFs, no portals, no hassle.

Just submit an enrollment request using Stedi’s Enrollments API, UI, or a bulk CSV import. We do the rest.

One-click enrollment is available for 850+ payers. Check the Stedi Payer Network or Payer APIs to see which payers are supported.

What is transaction enrollment?

For certain transactions and payers, providers must first submit an enrollment with a payer before they can exchange transactions with them.

All payers require enrollment for 835 Electronic Remittance Advice (ERA) transactions. Some require it for other transactions too, like 270/271 eligibility checks.

Each payer has its own requirements and steps. You usually need to include the provider’s NPI, tax ID, and contact information. Some payers also ask you to sign a PDF or log into a portal to complete follow-up steps.

When Stedi is able to collect all of the required information up front in a single form, we classify the enrollment as one-click.

How to find payers with one-click enrollment

You can check whether a payer needs enrollment for a certain transaction type using the Stedi Payer Network or the Payer APIs.

In the Stedi Payer Network UI, one-click enrollment support is indicated in the Payer pane:

One-click enrollment indicator in the Payer pane

Or the Payer page:

One-click enrollment indicator on the Payer Detail page

In the Payer APIs, support is indicated in the enrollment.transactionEnrollmentProcesses.{transactionType}.type field with a value of ONE_CLICK. For example, for 835 ERA (claimPayment) enrollments:

  {
    "stediId": "ABHDB",
    "displayName": "Community Health Plan of Washington",
    ...
    "enrollment": {
      "ptanRequired": false,
      "transactionEnrollmentProcesses": {
        "claimPayment": {
          "type": "ONE_CLICK"   // Supports one-click enrollment for 835 ERAs
        }
      }
    }
  }

For payers that require enrollment, we show the required steps in the Transaction Enrollments Hub and we work with you until the enrollment is complete.

We handle enrollment for you

Most clearinghouses make you figure out enrollments. We do it for you. You send enrollment requests using our API, UI, or a bulk CSV import. Then we:

  • We normalize the request to better match the payer’s requirements.

  • Send the request to the payer and coordinate with them.

  • Let you know if anything is needed from your side.

You can track status using the Enrollments API or UI. We also send updates by Slack/Team and email. Once the enrollment has been approved, the provider can start exchanging the enrolled transaction type with the payer.

Tired of slow enrollments?

Transaction enrollments often slow teams down. We’ve built systems that avoid the usual delays.

If enrollments are a bottleneck, we can help. Contact us to see how it works.

Jun 4, 2025

Products

Stedi now has a direct connection to Zelis, a multi-payer provider platform.

Many payers use Zelis as their primary way of delivering 835 Electronic Remittance Advice (ERA) files through clearinghouses to providers.

To receive ERAs from Zelis, providers must set up an account in the Zelis portal. This involves selecting a clearinghouse from a prepopulated list. Previously, providers using Stedi had to select an intermediary clearinghouse in order to receive ERAs.

With Stedi’s new direct Zelis connection, you can now choose Stedi from the list of integrated clearinghouses. Once set up, you’ll automatically receive ERAs from all Zelis-connected payers directly through Stedi.

For an example, see the ERA transaction enrollment steps for United Healthcare Dental.

New Zelis enrollments

To submit new enrollment requests, use the Enrollments API, UI, or a bulk CSV import. For Zelis-connected payers, we’ll instruct you to select Stedi as the clearinghouse in Zelis when needed.

Existing Zelis enrollments

If you previously submitted a Stedi enrollment for a Zelis-connected payer, you can log into Zelis and select Stedi as the clearinghouse to transition to Stedi’s direct Zelis connection without any interruption or downtime.

Fix enrollment delays with Stedi

Other clearinghouses make transaction enrollment slow and manual. Stedi handles enrollment for you. Our processes remove the usual mistakes and delays.

If transaction enrollment is slowing you down, we can help. Contact us to see how it works.

Jun 2, 2025

Guide

Stedi’s Eligibility Check APIs let you get Medicare 271 eligibility responses as JSON. But your system – or one downstream – might need to display that JSON data in Common Working File (CWF) fields. Many providers still expect a CWF-style layout.

This guide shows how to map Stedi’s JSON 271 eligibility responses to CWF fields. It also covers what the CWF was, how Medicare eligibility checks work today, and why the CWF format still persists.

What Is the Common Working File (CWF)?

The Centers for Medicare & Medicaid Services (CMS) built the CWF in the 1980s to centrally manage Medicare eligibility. It was the source of truth for who was covered, when, and under which Medicare part.

The system produced fixed-format text files – also called “CWFs” – for mainframe terminals and printed reports. Each file had a set layout, with fields like member ID, coverage type, and benefit dates. For example:

Example of a CWF-like layout with fixed fields

How Medicare eligibility checks work today

CMS replaced the CWF in 2019 with the HIPAA Eligibility Transaction System (HETS). HETS returns standard 271 eligibility responses, the same as commercial insurers. Medicare 271s include a lot of Medicare-specific info, including:

  • Medicare Part A and Part B entitlements and dates

  • Part C (Medicare Advantage) and Part D (Prescription Drug) plan info

  • Deductibles, copays, and benefit limits

  • Remaining Skilled Nursing Facility (SNF) days

  • ESRD transplant or dialysis dates

  • Smoking cessation visits and therapy caps

  • Qualified Medicare Beneficiary (QMB), secondary payers, Medicaid crossover, and other coordination of benefits information.

In Stedi’s JSON 271 eligibility responses, that data lives under benefitsInformation. Each object describes a specific coverage, limit, or service type. For example:

{
  ...
  "subscriber": {
    "memberId": "123456789",
    "firstName": "JANE",
    "lastName": "DOE",
    ...
  },
  ...
  "benefitsInformation": [
    {
      "code": "B",
      "name": "Co-Payment",
      "serviceTypeCodes": ["30"],
      "serviceTypes": ["Health Benefit Plan Coverage"],
      "insuranceTypeCode": "MA",
      "insuranceType": "Medicare Part A",
      "timeQualifierCode": "7",
      "timeQualifier": "Day",
      "benefitAmount": "408",
      "inPlanNetworkIndicatorCode": "W",
      "inPlanNetworkIndicator": "Not Applicable",
      "benefitsDateInformation": {
        "admission": "20241231",
        "admissions": [
          {
            "startDate": "20240101",
            "endDate": "20241231"
          }
        ]
      },
      "benefitsServiceDelivery": [
        {
          "unitForMeasurementCode": "Days",
          "timePeriodQualifierCode": "30",
          "timePeriodQualifier": "Exceeded",
          "numOfPeriods": "60",
          "unitForMeasurementQualifierCode": "DA",
          "unitForMeasurementQualifier": "Days"
        },
        {
          "unitForMeasurementCode": "Days",
          "timePeriodQualifierCode": "31",
          "timePeriodQualifier": "Not Exceeded",
          "numOfPeriods": "90",
          "unitForMeasurementQualifierCode": "DA",
          "unitForMeasurementQualifier": "Days"
        },
        {
          "timePeriodQualifierCode": "26",
          "timePeriodQualifier": "Episode",
          "numOfPeriods": "1"
        }
      ]
    }
  ]
}

How to use this guide

Stedi’s JSON 271 eligibility response is easier to use in modern applications. But if your customers need the old CWF layout, you’ll need to map each field from JSON.

This guide isn’t an official spec. It’s a practical reference. Each section shows a CWF-style layout, a table of field mappings, and notes on when to use each field. For details on our JSON 271 eligibility responses, see our API documentation.

Provider information

Provider information in a CWF-like format

CWF field

JSON 271 eligibility response property

Organization Name

provider.providerOrgName

NPI ID

provider.npi

Patient Demographics

The following table includes patient demographics from the SUBMITTED TO PAYER and RETURNED BY PAYER sections of the sample CWF.

Patient demographics section in an example CWF

CWF field

JSON 271 eligibility response property

When to use

SUBMITTED TO PAYER



First Name

subscriber.firstName



Last Name

subscriber.lastName



Member ID (MBI)

subscriber.memberId



D.O.B.

subscriber.dateOfBirth



Eligibility Date(From)

planDateInformation.eligibility (first date)

planDateInformation.eligibility can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Eligibility Date(To)

planDateInformation.eligibility (second date, if present)

planDateInformation.eligibility can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Service Types(s)

benefitsInformation.serviceTypeCodes



benefitsInformation.compositeMedicalProcedureIdentifier.procedureCode



benefitsInformation.compositeMedicalProcedureIdentifier.procedureModifiers



RETURNED TO PAYER



First Name

subscriber.firstName



Middle Name

subscriber.middleName



Last Name

subscriber.lastName



Suffix

subscriber.suffix



Member ID (MBI)

subscriber.memberId



D.O.B.

subscriber.dateOfBirth



Gender

subscriber.gender



Address Line 1

subscriber.address.address1



Address Line 2

subscriber.address.address2



City

subscriber.address.city



State

subscriber.address.state



Zip Code

subscriber.address.postalCode



Benefit Information

In the JSON 271 eligibility response, benefitsInformation objects contain most of the benefits information.

The benefitsInformation.insuranceTypeCode property indicates the type of insurance policy within a program, such as Medicare. A code of MA indicates Medicare Part A. A code of MB indicates Medicare Part B. For a complete list of insurance type codes, see Insurance Type Codes in our docs.

The benefitsInformation.serviceTypeCodes property identifies the type of healthcare services the benefits information relates to. A service type code (STC) of 30 relates to general benefits information. For a complete list of STCs, see Service Type Codes in our docs.

Benefit Information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.plan (first date)



All of the following must be true:

  • benefitsInformation.code = 1 (Active Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.plan (second date, if present)

All of the following must be true:

  • benefitsInformation.code = 1 (Active Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Ineligible Start

benefitsInformation.benefitsDateInformation.plan (first date)



All of the following must be true:

  • benefitsInformation.code = 6 (Inactive)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Ineligible End

benefitsInformation.benefitsDateInformation.plan (second date, if present)

All of the following must be true:

  • benefitsInformation.code = 6 (Inactive)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) or MB (Medicare Part B)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Date of Death

planDateInformation.dateOfDeath



Lifetime Psychiatric Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = K (Reserve)

  • benefitsInformation.serviceTypeCodes = A7 (Psychiatric - Inpatient)

  • benefitsInformation.timeQualifierCode = 32 (Lifetime)

  • benefitsInformation.quantityQualifierCode = DY (Days)

Lifetime Reserve Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = K (Reserve)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A) 

  • benefitsInformation.timeQualifierCode = 32 (Lifetime)

  • benefitsInformation.quantityQualifierCode = DY (Days)

Smoking Cessation Days

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = 67 (Smoking Cessation)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B) 

  • benefitsInformation.timeQualifierCode = 22 (Service Year)

  • benefitsInformation.quantityQualifierCode = VS (Visits)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 29 (Remaining)

Initial Cessation Date

benefitsInformation.benefitsDateInformation.plan

This information is only available if an initial counseling visit was used in the past 12 months.

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = 67 (Smoking Cessation)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 22 (Service Year)

  • benefitsInformation.quantityQualifierCode = VS (Visits)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 29 (Remaining)

ESRD Dialysis Date

benefitsInformation.benefitsDateInformation.discharge

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

ESRD Transplant Date

benefitsInformation.benefitsDateInformation.service

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

ESRD Coverage Period

benefitsInformation.benefitsDateInformation.plan

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = RN (Renal)

Plan Benefits

Plan Benefits in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Medicare Part A

Type

Use Base when all of the following are true:

  • benefitsInformation.timeQualifierCode = 7 (Day)

  • benefitsInformation.benefitsDateInformation.admission contains a full year date range, such as 20980101-20991231.



Use Spell when all of the following are true:

  • benefitsInformation.timeQualifierCode = 7 (Day)

  • benefitsInformation.benefitsDateInformation.admission contains a partial year date range, such as 20980506-20980508.



First Bill

benefitsInformation.benefitsDateInformation.admissions.startDate



Last Bill

benefitsInformation.benefitsDateInformation.admissions.endDate



Hospital Days Full

For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode31 (Not Exceeded).



For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode29 (Remaining).



Hospital Days Colns

For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when:

  • benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode30 (Exceeded).



For a Type of Base, use benefitsInformation.BenefitsServiceDelivery.numOfPeriods when:

  • benefitsInformation.BenefitsServiceDelivery.timePeriodQualifierCode29 (Remaining).



Hospital Days Base

benefitsInformation.benefitAmount



SNF Days Full

benefitsInformation.BenefitsServiceDelivery.numOfPeriods

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 31 (Not Exceeded)

SNF Days Colns

benefitsInformation.BenefitsServiceDelivery.numOfPeriods

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode = 30 (Exceeded)

SNF Days Base

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = B (Co-Payment)

  • benefitsInformation.serviceTypeCodes = AG (Skilled Nursing Care)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.timeQualifierCode = 7 (Day)

Inpatient Deductible

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = C (Deductible)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

Medicare Part B

Deductible Remaining

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = C (Deductible)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Physical Therapy

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AE (Physical Medicine) 

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Occupational Therapy

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AD (Occupational Therapy)

  • benefitsInformation.insuranceTypeCode = MB (Medicare Part B)

  • benefitsInformation.timeQualifierCode = 23 (Calendar Year)

Blood Pints Part A/B

benefitsInformation.benefitAmount

All of the following must be true:

  • benefitsInformation.code = E (Exclusions)

  • benefitsInformation.serviceTypeCodes = 10 (Blood Charges)

  • benefitsInformation.quantityQualifierCode = DB (Deductible Blood Units)

  • benefitsInformation.benefitQuantity = Blood Units Excluded

  • benefitsInformation.benefitsServiceDelivery.quantityQualifierCode = FL (Units)

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifier = Blood Units Remaining

  • benefitsInformation.benefitsDateInformation.plan = A date or date range in the current calendar year

Medicare Part A Stays

Type

Use Hospital when:

  • benefitsInformation.serviceTypeCodes30 (Health Benefit Plan Coverage)



Use Hospital Stay when:

  • benefitsInformation.serviceTypeCodes48 (Hospital - Inpatient)



Use SNF Stay when:

  • benefitsInformation.serviceTypeCodesAH (Skilled Nursing Care - Room and Board)



The following must be true:

  • benefitsInformation.insuranceTypeCode = MA (Medicare Part A)

Start Date

benefitsInformation.benefitsDateInformation.plan (first date)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

End Date

benefitsInformation.benefitsDateInformation.plan (second date, if present)



benefitsInformation.benefitsDateInformation.plan can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Billing NPI

benefitsInformation.benefitsRelatedEntities.entityIdentificationValue

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentification is FA (Facility Identification)

Qualified Medicare Beneficiary (QMB) Status

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.insuranceTypeCode = QM (Qualified Medicare Beneficiary)

Qualified Medicare Beneficiary (QMB) Status information in a CWF example

CWF field

JSON 271 eligibility response property

When to use

Period From

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Period Through

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

QMB Plan

benefitsInformation.planCoverage



Medicare Secondary Payor

Medicare Secondary Payor info in an example CWF file

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage)

  • benefitsInformation.insuranceTypeCode = 14, 15, 47, or WC

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Policy Number

benefitsInformation.benefitsAdditionalInformation.insurancePolicyNumber



Insurer

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Type

benefitsInformation.insuranceType



Medicare Advantage

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = U (Contact Following Entity for Eligibility or Benefit Information)

  • benefitsInformation.serviceTypeCodes = 30 (Health Benefit Plan Coverage) OR both 30 (Health Benefit Plan Coverage) AND CQ (Case Management)

  • benefitsInformation.insuranceTypeCode = HM (HMO), HN (HMO - Medicare Risk), IN (Indemnity), PR (PPO), or PS (POS)

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Primary Payer

Medicare Advantage information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (first date)

benefitsInformation.benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.coordinationOfBenefits (second date, if present)

benefitsInformation.benefitsDateInformation.coordinationOfBenefits can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Plan Code

benefitsInformation.benefitsAdditionalInformation.planNumber



Payer Name

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Plan Name

benefitsInformation.benefitsRelatedEntities.entityName



Website

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (URL)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Uniform Resource Locator (URL)



In most cases, CMS only provides just the payer’s domain name, such as examplepayer.com, not a complete URL.

Phone Number

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (Telephone)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Phone Number

Message(s)

benefitsInformation.additionalInformation.description



Part D

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = R (Other or Additional Payor)

  • benefitsInformation.serviceTypeCodes = 88 (Pharmacy)

Medicare Part D info in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Effective Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Termination Date

benefitsInformation.benefitsDateInformation.benefit (second date, if present)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Plan Code

benefitsInformation.benefitsAdditionalInformation.planNumber



Payer Name

benefitsInformation.benefitsRelatedEntities.entityName



Address

benefitsInformation.benefitsRelatedEntities.address.address1



benefitsInformation.benefitsRelatedEntities.address.address2



benefitsInformation.benefitsRelatedEntities.address.city



benefitsInformation.benefitsRelatedEntities.address.state



benefitsInformation.benefitsRelatedEntities.address.postalCode



Plan Name

benefitsInformation.benefitsRelatedEntities.entityName



Website

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (URL)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Uniform Resource Locator (URL)



In most cases, CMS only provides just the payer’s domain name, such as examplepayer.com, not a complete URL.

Phone Number

benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationNumber (Telephone)

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.contactInformation.contacts.communicationMode = Phone Number

Therapy Caps

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = D (Benefit Description)

  • benefitsInformation.serviceTypeCodes = AD (Occupational Therapy) or AE (Physical Medicine)

Therapy Caps information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Period Begin

benefitsInformation.benefitsDateInformation.benefit

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Period End

benefitsInformation.benefitsDateInformation.benefit

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

PT/ST Applied

benefitsInformation.additionalInformation.description



OT Applied

benefitsInformation.additionalInformation.description



Hospice

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = X (Health Care Facility)

  • benefitsInformation.serviceTypeCodes = 45 (Hospice)

Hospice info in a CWF

CWF field

JSON 271 eligibility response property

When to use

Benefit Period

No direct mapping. Calculated by ordering the episodes by date for the calendar year.



Benefit Period Start Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Benefit Period End Date

benefitsInformation.benefitsDateInformation.benefit (second date, if present)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Provider

benefitsInformation.benefitsRelatedEntities.entityIdentificationValue

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Provider

Provider Name

benefitsInformation.benefitsRelatedEntities.entityName

The following must be true:

  • benefitsInformation.benefitsRelatedEntities.entityIdentifier = Provider

Hospice Elections

Election Date

benefitsInformation.benefitsDateInformation.benefit (first date)

benefitsInformation.benefitsDateInformation.benefit can be either a single date (YYYYMMDD) or a date range (YYYYMMDD-YYYYMMDD).

Election Receipt Date

benefitsInformation.benefitsDateInformation.added



Election Revocation Date

benefitsInformation.benefitsDateInformation.benefitEnd



Election Revocation Code

benefitsInformation.additionalInformation.description



Election NPI

benefitsRelatedEntities.entityIdentificationValue



Home Health Certification

Home Health Certification information in an example CWF

In the JSON 271 eligibility response, this information is only available when all of the following is true:

  • benefitsInformation.code = X (Health Care Facility)

  • benefitsInformation.compositeMedicalProcedureIdentifier.procedureCode = G0180

CWF field

JSON 271 eligibility response property

Certification HCPCS Code

compositeMedicalProcedureIdentifier.procedureCode

Process Date

benefitsDateInformation.periodStart

Rehabilitation Services

Rehabilitation Services information in an example CWF

CWF field

JSON 271 eligibility response property

When to use

Pulmonary Remaining (G0424) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BF (Pulmonary Rehabilitation)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

  • additionalInformation.description = Technical

Pulmonary Remaining (G0424) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BF (Pulmonary Rehabilitation)

  • benefitsInformation.timeQualifierCode = 29 (Remaining)

  • additionalInformation.description = Professional

Cardiac Applied (93797, 93798) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Technical

Cardiac Applied (93797, 93798) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Professional

Intensive Cardiac Applied (G0422, G0423) Technical

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Intensive Cardiac Rehabilitation - Technical

Intensive Cardiac Applied (G0422, G0423) Professional

benefitsInformation.benefitQuantity

All of the following must be true:

  • benefitsInformation.code = F (Limitations)

  • benefitsInformation.serviceTypeCodes = BG (Cardiac Rehabilitation)

  • benefitsInformation.timeQualifierCode = 99 (Quantity Used)

  • additionalInformation.description = Intensive Cardiac Rehabilitation - Professional

May 29, 2025

Guide

Insurance verification is the first step in the revenue cycle – and the first place it can break.

When an eligibility check fails or returns bad data, everything downstream falls apart. Claims get denied. Patients get surprise bills. Providers wait to get paid. Staff waste hours on the phone verifying coverage or fixing claims.

Bad eligibility checks set providers up to fail.

But there’s good news: If you’re using Stedi’s Eligibility Check APIs, most eligibility check errors are avoidable. This guide shows how to prevent them – and how to use Stedi to build a faster, more reliable eligibility workflow.

Only send the required patient data

Payers return AAA errors when they can’t match an eligibility request to a subscriber. It may seem counterintuitive, but this often happens because the request includes too much patient data.

Payers require that each check match a single patient. Extra data increases the risk of mismatches. Even a small typo in a non-required field can cause a failed match.

For the best results, only send:

  • Member ID

  • First name

  • Last name

  • Date of birth

If you’re verifying the subscriber, put this info in the subscriber object.

{
  ...
  "subscriber": {
    "memberId": "123456789",
    "firstName": "Jane",
    "lastName": "Doe",
    "dateOfBirth": "19000101"
  }
}

If you’re verifying a dependent, the format depends on the payer. If the dependent has their own member ID, try this:

  • Put the dependent’s info in the subscriber object.

  • Leave out the dependents array.

If the dependent doesn’t have their own member ID:

  • Put the subscriber’s info in the subscriber object.

  • Put the dependent’s info in an object in the dependents array.

Don’t send SSNs, addresses, or phone numbers in eligibility checks. They often cause mismatches. For medical checks, if you don’t know the insurer, or the payer requires a member ID and you don’t have it, start with an insurance discovery check.

If your eligibility checks still fail, try a name variation. For example, “Nicholas” might work where “Nick” doesn’t. Use Stedi’s Eligibility Manager to test variations and retry requests directly from the UI.

When you get a successful response from the payer, update your records with the returned member ID, name, and date of birth. This improves your chances of a successful response on the next eligibility check, whether it’s for a future visit or a batch refresh.

Use insurance discovery when patient data is missing

If you don’t know the insurer or the payer requires a member ID and you don’t have it, start with an insurance discovery check.

Discovery checks use demographic data, like name and date of birth, to search payers for active coverage. If they find a match, they’ll return a response with the payer and member ID.

While helpful, discovery checks aren’t guaranteed to match. Match rates vary. They’re also slower than eligibility checks. But when you can’t send a clean eligibility check, discovery is your best fallback.

Discovery checks might not return all of a patient’s active insurance plans. If the patient could have multiple payers, follow up with a coordination of benefits (COB) check after the eligibility check. A COB check can find other coverage and figure out the primary payer.

Check eligibility before checking COB

COB checks are more sensitive than eligibility checks. Even small mismatches, like using a nickname instead of a legal name, can cause them to fail.

To reduce errors, run an eligibility check first. Then use the member ID from the eligibility response in your COB request. You’ll get cleaner results and fewer failures.

If you don’t have a member ID, start with an insurance discovery check first. Then follow up with an eligibility check and COB check – in that order.

Keep coverage data fresh with batch refreshes

Insurance changes often. Patients switch plans. Cached coverage data goes stale fast.

Always re-check eligibility before a visit. Between visits, use Stedi’s Batch Eligibility Check API to run weekly or monthly refreshes. Batch checks return full coverage breakdowns, just like real-time checks. They can catch insurance issues before they cause problems.

Batch eligibility checks are asynchronous. They don’t count toward your Stedi account’s concurrency limit. You can run thousands of batch eligibility checks and still send real-time checks at the same time.

Include an MBI for Medicare eligibility checks

Every Medicare eligibility check requires a Medicare Beneficiary Identifier (MBI) for the patient. If it’s missing or wrong, the check will fail.

If the patient doesn’t know their MBI, run an MBI lookup using the same Real-Time Eligibility Check API. You’ll need to:

  1. Set the tradingPartnerServiceId to MBILU (MBI Lookup).

  2. Include the following in the request:

    • subscriber.firstName

    • subscriber.lastName

    • subscriber.dateOfBirth

    • subscriber.ssn

For example:

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "MBILU",
  "externalPatientId": "UAA111222333",
  "encounter": {
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1999999984"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "ssn": "123456789"
  }
}'

Stedi will return the patient’s full coverage details and their MBI. You can then use the MBI as the member ID for eligibility checks.

MBI lookups require setup with Stedi and incur additional costs. Contact us for details.

Test each STC one at a time

Service Type Codes (STCs) tell the payer what kind of benefits info you’re requesting. You can include them in the encounter.serviceTypeCodes array:

"encounter": {
  "serviceTypeCodes": ["30"]
}

You can send multiple STCs in one request, but support varies by payer:

  • Some only respond to the first STC.

  • Some ignore your STCs and always return a default response for STC 30 (Health Benefit Plan Coverage).

  • Some don’t support multiple STCs in a single request.

To figure out what works, test each payer individually:

  1. Send a request with just STC 30 for general medical benefits or 35 for general dental benefits.

  2. Send one with a specific STC you care about, like 88 for pharmacy benefits.

  3. Try a request with multiple STCs.

Compare the responses. If they change based on the STC or the number of STCs, the payer likely supports them. If not, they may be ignoring or only partially supporting STCs.

Success depends on the payer’s implementation. Partial support and fallback behavior are common.

Avoid timeouts with request hedging

Payer responses can be slow. Payers can take up to 60 seconds to respond to eligibility requests. To handle this, Stedi keeps real-time eligibility requests open for up to 120 seconds. Internally, Stedi may retry a request to the payer multiple times during that window.

Don’t cancel and retry your own requests during this window. It can create duplicates, increase your concurrency usage, and further slow things down.

If a check feels stuck, use request hedging instead. Wait 30 seconds, then send a second request without canceling the first. Use whichever response returns first. It’s a simple way to avoid timeouts.

Debug errors with Eligibility Manager

Sometimes, eligibility errors are unavoidable. Payers go down, data is missing, or coverage has changed. Stedi’s Eligibility Manager shows exactly why a check failed so you can fix it instead of guessing.

Screenshot of the Eligibility Manager

Each check is grouped under a unique Eligibility Search ID. Retries stay in the same thread, giving you a full audit trail.

Use Eligibility Manager to:

  • Filter by error code, payer, or status. For example, you can find all checks that failed with a specific AAA code (like 75 for Subscriber Not Found). Or see issues by a specific payer.

  • View raw request and response data.

  • Edit and retry failed checks directly from the UI.

  • Compare retries to see what changed between failures and successes.

If you’re running eligibility at scale, this tool can save you hours of guessing and debugging.

Screenshot of the Eligibility Manager debugger

Eligibility workflows that don’t break

Stedi gives you modern APIs and tools to build accurate, reliable eligibility workflows. When errors do happen, you get help fast. Our average support response time is under 8 minutes.

Want to see how it works? Contact us to set up a proof of concept.

May 28, 2025

Products

To get paid, healthcare providers submit an 837 claim to their patient’s insurer. The payer processes the claim and sends back an 835 Electronic Remittance Advice (ERA). That ERA tells you what got paid, what got denied, and why.

Today, most providers and payers submit claims and receive ERAs this way – all electronically. But not every payer sends ERAs. Some still mail EOBs – explanations of benefits – on paper.

An EOB contains the same information as an ERA… just on dead trees. Providers with digital workflows have to build a separate process to open mail, scan or read documents, and manually key in payment data. It’s full of delays, errors, and extra costs.

Turn every EOB into an 835 with Anatomy

We want to make remittance paper optional. To do that, Stedi has partnered with Anatomy, a modern healthcare lockbox and document conversion service, to help you convert paper EOBs into standard 835s. Providers and billing companies can redirect paper EOBs to a PO Box managed by Anatomy – or upload PDFs directly using Anatomy’s UI. Anatomy converts each document into a standard 835.

Anatomy then securely sends the 835s to Stedi on your behalf. In Stedi, you can enroll to receive 835s from Anatomy just like you would if they were a payer. Once enrolled, you’ll get your ERAs as usual – using the 835 ERA Report API or the from-stedi directory of your Stedi SFTP connection. You can even set up webhooks to get notified when new ERAs are available.

If you're already using Stedi, you likely already have this set up. You just need to contract with Anatomy and then enroll with Anatomy in Stedi. The best part? We do all the enrollment work for you as part of our streamlined process.

Enroll today

Anatomy is now listed as a supported payer in the Stedi Payer Network. If you have a contract with Anatomy and are already using Stedi, it takes just minutes to add Anatomy and start receiving ERA transactions.

If you’re new to Stedi, making remits painless is just one part of what we do. When you sign up, you’ll get access to 3,393+ payers, modern APIs, dev-friendly docs, and legendary support. We promise it’ll be the best clearinghouse experience you’ve ever had.

To get started, contact us or book a demo.

May 16, 2025

Products

You can now use the Search Payers API to programmatically search for payers in the Stedi Payer Network. We’ve also updated the Payer Network UI to use the new API. You now get consistent search results across the UI and API.

Screen capture of a search for "Blue Cross" in the Stedi Payer Network UI

Why payer IDs matter

When you send a healthcare transaction, such as a dental claim (837D) or eligibility check (270/271), you need a payer ID. The payer ID tells your clearinghouse where to send the transaction. If the ID is wrong, the transaction might fail or be rejected.

That sounds simple. It’s not.

Primary payer IDs often change. They can vary between clearinghouses, sometimes even between transaction types. Most clearinghouses send out their IDs in CSV payer lists that are updated once a month at best. These CSVs can grow stale quickly. Worse, they often have duplicate names, typos, and other errors.

For developers building healthcare billing applications, CSV-based payer lists create a recurring pain. Every month, you need to update payer name-to-ID mappings or lookup tables. You end up writing logic to normalize names, match payer ID aliases, and handle edge cases – just to get the right payer ID.

So we built something better.

We created the Stedi Payer Network and Payers API to provide accurate, up-to-date data on thousands of medical and dental payers. You can get the right payer ID without digging through CSVs.

Now, with the Search Payers API, it’s faster to find the right payer and build tools that scale. For example, you can use the API to create an application that lets patients search for and select their insurance provider in a patient intake form.

Find payer IDs with the Search Payers API

The Search Payers API does one thing well: find the payer you're looking for.

You can search by the payer’s name, alias, or payer ID. The search supports fuzzy matching, so it returns close matches even if the provided payer name isn’t exact.

Stedi weights results based on text match relevance and additional factors, such as payer size, market share, and transaction volume, to present the most likely matches first.

You can further filter the results by supported transaction types, like dental claims (837D) or eligibility checks (270/271).

For example, the following request searches for the “Blue Cross” payer name and filters for payers that support eligibility checks and real-time claim status.

curl --request GET \
  --url https://healthcare.us.stedi.com/2024-04-01/payers/search?query=Blue%20Cross&eligibilityCheck=SUPPORTED&claimStatus=SUPPORTED \
  --header 'Authorization: <api-key>'

The response returns a list of matching payers. Each result includes:

  • The payer’s immutable Stedi Payer ID

  • Their name, primary payer ID, and known aliases

  • Supported transaction types

  • Whether transaction enrollment is required

  • A score indicating how relevant the payer is to the search query.

{
 "items": [
   {
     "payer": {
       "stediId": "QDTRP",
       "displayName": "Blue Cross Blue Shield of Texas",
       "primaryPayerId": "G84980",
       "aliases": [
         "1406",
         "84980",
         "CB900",
         "G84980",
         "SB900",
         "TXBCBS",
         "TXBLS"
       ],
       "names": [
         "Blue Cross Blue Shield Texas Medicaid STAR CHIP",
         "Blue Cross Blue Shield Texas Medicaid STAR Children's Health Insurance Program",
         "Blue Cross Blue Shield of Texas",
         "Bryan Independent School",
         "Federal Employee Program Texas (FEP)",
         "Health Maintenance Organization Blue",
         "Health Maintenance Organization Blue Texas",
         "Healthcare Benefits",
         "Rio Grande",
         "Walmart (BlueCard Carriers)"
       ],
       "transactionSupport": {
         "eligibilityCheck": "SUPPORTED",
         "claimStatus": "SUPPORTED",
         "claimPayment": "ENROLLMENT_REQUIRED",
         "dentalClaimSubmission": "SUPPORTED",
         "professionalClaimSubmission": "SUPPORTED",
         "institutionalClaimSubmission": "SUPPORTED",
         "coordinationOfBenefits": "SUPPORTED",
         "unsolicitedClaimAttachment": "NOT_SUPPORTED"
       }
     },
     "score": 14.517873
   },
   ...
 ],
 ...
}

Get started with Stedi

At Stedi, we’re working to eliminate the toil in healthcare transactions. Programmatic access to accurate payer data is just one part.

The Search Payers API is free on all paid Stedi plans. Try it for yourself: Schedule a demo today.

May 2, 2025

Products

You can now include claim attachments in API-based 837P professional and 837D dental claim submissions.

When you need a claim attachment

Some payers require attachments to approve claims for specific services. Claim attachments show a service occurred or was needed. They can include X-rays, treatment plans, or itemized bills.

The type of attachment needed depends on the payer and the service. Without these attachments, the payer may delay (pend) or deny the claim.

How to submit a claim attachment

Follow these steps:

  1. Check payer support.
    While uncommon, some payers may not accept claim attachments or may require transaction enrollment first. Check the Payers API or Stedi Payer Network for support.

  2. Create an upload URL.
    Use the Create Claim Attachment JSON API to generate a pre-signed uploadUrl and an attachmentId. Specify the contentType in the request. Supported file types include application/pdf, image/tiff, and image/jpg.

    Example request:

    curl --request POST \
      --url https://claims.us.stedi.com/2025-03-07/claim-attachments/file \
      --header 'Authorization: <api-key>' \
      --header 'Content-Type: application/json' \
      --data '{
      "contentType": "application/pdf"
    }'


    Example response:

    {
      "attachmentId": "d3b3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e",
      "uploadUrl": "https://s3.amazonaws.com/bucket/key"
    }



  3. Upload your attachment.
    Upload your file to the uploadUrl.

    Example:

    curl --request PUT \
      --url "<your-uploadUrl>" \
      --header "Content-Type: application/pdf" \
      --upload-file /path/to/file.pdf



  4. Submit the claim.
    Submit the claim using the Professional Claims (837P) JSON API or Dental Claims (837D) JSON API. Include the attachmentId in the payload’s claimInformation.claimSupplementalInformation.reportInformations[].attachmentId. In the same reportInformations object, include:

    • An attachmentReportTypeCode. This code identifies the type of report or document you plan to submit as an attachment. See Attachment Report Type Codes for a full list of codes.

    • An attachmentTransmissionCode of EL (Electronically Only). This property indicates the attachment will be sent in a separate, electronic 275 transaction.

      Example:

      curl --request POST \
        --url https://healthcare.us.stedi.com/2024-04-01/dental-claims/submission \
        --header 'Authorization: <api-key>' \
        --header 'Content-Type: application/json' \
        --data '{
         ...
         "claimInformation": {
            "claimSupplementalInformation": {
              "reportInformations": [
                {
                  "attachmentReportTypeCode": "RB",
                  "attachmentTransmissionCode": "EL
                  "attachmentId": "<your-attachment-id>"
                }
              ]
            }
          },
          ...
        }'

Get started

Claim attachments are available for all paid Stedi accounts.

If you’re not a Stedi customer, request a free trial. Most teams are up and running in less than a day.

Apr 29, 2025

Guide

Virtual healthcare visits are now common, but verifying patient eligibility for them isn't always straightforward.

Telehealth benefits vary by payer and plan, and it can be a challenge to accurately retrieve coverage details. Without accurate eligibility checks, providers risk billing issues, denied claims, and upset patients.

Stedi’s Real-Time Eligibility Check API gives you reliable, programmatic access to eligibility data as JSON. But access isn’t enough. To get the right eligibility information, it’s just as important to use the right service type code (STC) for the payer.

Why the STC matters

A service type code (STC) tells the payer exactly what benefit information you want. For example, STC 98 indicates a "Professional (Physician) Visit – Office."

The problem is that individual payers may return coverage details for virtual visits differently. Some payers treat virtual visits as office visits, some as separate telemedicine benefits, and others as general medical services.

As a result, different payers require different STCs for eligibility checks. For example, UnitedHealthcare (UHC) maps “VIRTUAL VISITS/TELEMEDICINE” to STC 9. Other payers use STC 98 or STC 30. Payers may use other STCs, such as MH or CF, for virtual mental health visits.

If you use the wrong STC in an eligibility check, the response may omit benefits or return only partial coverage information. This can lead to denied claims or billing surprises for the patient after care is delivered.

Find the virtual visit STC for each payer

To find the right STC for a payer, you’ll need to try multiple STCs, one at a time.

To check virtual visit STCs using the Real-Time Eligibility Check API:

  1. Send an eligibility check request.
    In the request, use one of the following STCs at a time, in order, for each payer. Inspect each response before moving to the next STC.

    98 – Professional (Physician) Visit – Office

    9 – Other Medical

    30 – Health Benefit Plan Coverage

    An example Real-Time Eligibility Check request using STC 98:

    curl --request POST \
      --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
      --header 'Authorization: <api-key>' \
      --header 'Content-Type: application/json' \
      --data '{
      "controlNumber": "123456789",
      "tradingPartnerServiceId": "AHS",
      "externalPatientId": "UAA111222333",
      "encounter": {
        "serviceTypeCodes": [
          "98"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1999999984"
      },
      "subscriber": {
        "dateOfBirth": "19000101",
        "firstName": "Jane",
        "lastName": "Doe",
        "memberId": "123456789"
      }
    }'



  2. Check the response for a matching code or phrase.
    Check the response’s benefitsInformation objects for the following eligibilityAdditionalInformationList.industryCode value:

    02 (Telehealth Provided Other than in Patient’s Home)

    10 (Telehealth Provided in Patient’s Home)

    The eligibilityAdditionalInformationList.industryCode is a CMS Place of Service code, which indicates the location where healthcare was provided. The 02 and 10 values are used for telehealth services.

    If the eligibilityAdditionalInformationList.industryCode isn’t present, check the benefitsInformation objects for an additionalInformation.description property that contains a phrase like:

    • "VIRTUAL VISITS"

    • "TELEMEDICINE"

    • "E-VISIT"



  3. Stop at the first matching STC.
    Use the first matching STC for any future virtual visit eligibility checks with the payer.

    If you don’t find a match after checking all three STCs, fall back to interpreting the response based on STC 98.

Repeat the process for each required payer.

Interpret the eligibility response

After finding the right STC, use the Real-Time Eligibility Check API’s response to extract any needed eligibility information, such as:

  • Whether the patient is eligible for a virtual visit

  • Whether the patient will owe anything for the visit

  • Whether the patient has a limited number of virtual visits

Most eligibility details are in the response’s benefitsInformation object array. Look for benefitsInformation objects containing the STC used by the payer for virtual visits. Then use the following guidelines to interpret the API response.

Patient eligibility

If benefitsInformation.name contains "Active Coverage", the patient is eligible for a virtual visit.

{
  ...
  "benefitsInformation": [
    {
      "code": "1",
      "name": "Active Coverage",
      ...
    }
  ],
  ...
}

Patient responsibility

Some plans require patients to pay a portion of the cost of care, such as a co-pay or deductible. This amount is called the patient responsibility.

You can use the benefitsInformation objects with benefitsInformation.code values A, B, C, F, G, and Y to determine the patient’s financial responsibility for a given STC. For a detailed guide on determining patient responsibility, see Patient costs in the Stedi docs.

Visit limits

Some payers and plans limit the number of covered visits, including virtual visits, per year. In many cases, these limits aren’t hard caps. Patients may be able to get additional benefits with approval, called prior authorization, from their payer.

If the benefitsInformation object’s code is F, the benefitsInformation object includes details about limitations like a numeric visit cap, time period, or other restrictions. However, not all payers return limitations consistently or in the same way.

{
  ...
  "benefitsInformation": [
    {
      "code": "F",
      "name": "Limitations",
      "additionalInformation": {
        "description": "20 visits per calendar year"
      },
      ...
    }
  ],
  ...
}

Most common pattern

The most common pattern is to return values in the following benefitsInformation object properties. For example:

  • benefitsInformation.timeQualifierCode: "23" (Calendar year)

  • benefitsInformation.quantityQualifierCode: "VS" (Visits)

  • benefitsInformation.benefitQuantity: “<number>” (Number of allowed visits)

{
  ...
  "timeQualifierCode": "23",
  "timeQualifier": "Calendar Year",
  "quantityQualifierCode": "VS",
  "quantityQualifier": "Visits",
  "benefitQuantity": "20"
  ...
}

Benefits service delivery

Some payers may include visit limits in the benefitsInformation object’s benefitsServiceDelivery object array instead. For example:

  • benefitsInformation.benefitsServiceDelivery.timePeriodQualifierCode: "23" (Calendar year)

  • benefitsInformation.benefitsServiceDelivery.quantityQualifierCode: "VS" (Visits)

  • benefitsInformation.benefitsServiceDelivery.quantity: "<number>" (Number of allowed visits)

{
  ...
  "benefitsServiceDelivery": [
    {
      "timePeriodQualifierCode": "23",
      "timePeriodQualifier": "Calendar Year",
      "quantityQualifierCode": "VS",
      "quantityQualifier": "Visits",
      "quantity": "20"
    },
    ...
  ],
  ...
}

Process eligibility checks with Stedi today

Eligibility checks don’t just confirm coverage. They remove uncertainty for patients and providers. With the Stedi Real-Time Eligibility Check and Batch Eligibility Check APIs, you can automate eligibility checks within minutes.

To start testing eligibility checks today, create a free sandbox. Or contact us to speak with our team and book a demo.

Apr 30, 2025

Guide

We’ve noticed that in the healthcare world, Postman seems to be the most popular local client for working with APIs. While we think Postman is a great general-purpose tool for testing APIs, we don’t use it internally and don’t recommend that customers use it for processing PHI (Protected Health Information).

As there seems to be an awareness gap in the healthcare industry about Postman’s shortcomings when it comes to processing PHI, we wanted to publish a post outlining the issues.

The root of the problem is that Postman stores request history – including full request payloads – on its cloud servers, and you can’t turn this feature off without impractical workarounds that we’ve rarely seen used in practice. Many users are unaware that request payloads containing PHI are being synced to Postman’s servers, and if their company does not have a BAA in place with Postman, they may be unintentionally falling short of HIPAA requirements.

Why Postman isn’t safe for requests containing PHI

Postman’s core flaw for HIPAA compliance is its sync feature. Sync makes your Postman data available across devices and Postman’s web client. This lets you reuse prior API requests and share them with others. But if you're sending PHI, you’re leaking sensitive patient data to Postman, a third party, without knowing it.

Sync works by uploading your Postman data, including API request history, to Postman’s cloud servers. There’s no opt-in; syncs occur automatically while you’re logged in to Postman. You can’t stop syncing without logging out, which cuts off basic features like OpenAPI imports.

Despite this, many companies that offer APIs in the healthcare ecosystem – including healthcare clearinghouses – recommend Postman for API testing (Postman itself even highlights these APIs in their curated Healthcare APIs directory, and may be unaware of the necessary caveats).

Postman’s workarounds are impractical

There are multiple GitHub issues and community posts that raise concerns about Postman and HIPAA compliance. Postman’s own docs state:

Some organizations have security guidelines that prevent team members from syncing data to the Postman cloud.

Postman offers two workarounds:

  • A lightweight API client: Essentially just using Postman while logged out. However, if you log back in, syncing starts again.

  • Postman Vault: Secrets that you can reference as variables in requests. Vault secrets aren’t synced to the cloud. However, using variables for every request payload would be tedious.

Neither of these solutions scale – data leakage is one login or one bad request away.

Alternative API clients

Ultimately, proper API client usage is your responsibility. You should do your own research to determine HIPAA requirements – you can use any tool in a non-HIPAA-compliant way. At a minimum, if you are going to test APIs that handle PHI from your local machine, use an API client that defaults to local-only storage.

The following open-source API clients use an offline-first approach, which sidesteps the fundamental Postman problem. Each client also supports OpenAPI imports, which you can use to import the Stedi OpenAPI specs. With that said, you should have your security and compliance teams review any tool carefully, especially because applications evolve – there was a time that Postman was local-only, too.

  • Bruno (repo): A local-only API client built to avoid cloud syncing. Bruno has several interfaces, including a desktop app, CLI, and VS Code extension.

  • Pororoca (repo): A desktop-only API client with no cloud sync, built for secure local testing. Poroca’s data policy states that no data is synced to remote servers.

  • Yaak (repo): A simple, fast desktop API client. Yaak supports several import formats, including Postman collections, OpenAPI, and Insomnia.

Secure defaults matter

Postman is a great tool for general APIs. But healthcare isn't general software. When you’re handling PHI, invisible cloud storage is a failure, not a feature. Secure defaults, like local-only storage, prevent developers from accidentally exposing sensitive data. Of course, even if your requests never leave the machine, every laptop that handles PHI should still be locked down with best practices like full-disk encryption, strong authentication, automatic screen-lock, and remote-wipe.

Security is job zero at Stedi. We build every system and design every API with secure defaults in mind. If you want a healthcare clearinghouse that’s serious about security and developer experience, start with Stedi. Reach out or create a free sandbox today.

Disclaimer: Product features, security controls, and regulations change over time. Your organization must perform its own HIPAA risk analysis, implement appropriate administrative, technical, and physical safeguards, and verify that every vendor and workflow meets current legal and policy requirements.

Apr 10, 2025

Products

Without accurate insurance details, providers can’t run an eligibility check to verify patient benefits. Without a successful eligibility check, patients are often told they’ll need to pay out of pocket, causing them to cancel or never schedule services at all. Uncertainty about coverage status can also cause billing surprises, denied claims, and delayed payments to providers down the line – especially in urgent care scenarios when patients can’t communicate insurance details before receiving care. 

Unfortunately, patients often have trouble providing accurate information about their insurance. They make mistakes on online or in-person intake forms, come to appointments without their insurance cards, or forget to mention that their coverage changed since the last visit.

With Stedi’s Insurance Discovery, you can use an API or user-friendly form to find a patient’s active coverage in minutes with only their demographic data, like name, date of birth, and address. Insurance discovery checks replace standard eligibility checks when you don’t know the payer or the full patient details because they return benefits information for each active health plan found.

Improve eligibility and claims processing workflows

Insurance Discovery augments your existing eligibility workflow to help you reliably verify benefits, even when the patient can’t provide accurate insurance information. We recommend using Insurance Discovery for:

  • Eligibility troubleshooting. When eligibility checks fail with a Subscriber/Insured Not Found error, run an insurance discovery check instead of manually following up with the patient.

  • Walk-in or urgent care visits. Run an insurance discovery check when patients show up without their insurance card.

  • Virtual care and telehealth appointments. Simplify intake by letting patients schedule without insurance details. Then, run an insurance discovery check to verify their coverage before the visit.

Insurance Discovery can also help optimize claims processing. First, you can run insurance discovery checks to retroactively find active coverage for denied claims. Then, you can use the results to run a coordination of benefits (COB) check to identify any overlapping coverage and determine which payer is responsible for paying claims (primacy).

Find active coverage in minutes without knowing the payer

Here’s how Insurance Discovery works:

  1. Enroll one or more provider NPIs with Stedi for Insurance Discovery. You can submit a request and get approval in less than two minutes.

  2. Once the enrollment is live, you can either submit requests programmatically through our Insurance Discovery API or manually through the Create insurance discovery check form. At a minimum, you must provide the patient’s first name, last name, and date of birth (DOB), but we strongly recommend providing additional details like the patient's Social Security Number (SSN), gender, or full address to maximize the chances of finding matching coverage. You’ll also include information like the provider’s NPI and the service dates, similar to a standard eligibility check.

  3. Stedi determines if the patient has active coverage with one or more payers. This process involves demographic lookups to enrich partial patient details, comparisons across third-party data sources to determine member IDs, and submitting real-time eligibility checks to payers to detect coverage.

  4. Stedi returns an array of potential active coverages, along with subscriber details and benefits information. 

You should always review the results to ensure the returned subscriber information for each active health plan matches the patient's demographic information. Once you confirm matching coverage, you can use the benefits information to determine the patient’s eligibility for services.

The following example shows an insurance discovery request for a fictional patient named Jane Doe.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/insurance-discovery/check/v1 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "provider": {
    "organizationName": "THE DOCTORS OFFICE",
    "npi": "1234567891"
  },
  "encounter": {
    "beginningDateOfService": "20250326",
    "endDateOfService": "20250328"

  },
  "controlNumber": "123456789",
  "subscriber": {
    "dateOfBirth": "20010925",
    "firstName": "Jane",
    "lastName": "Doe",
    "address": {
      "address1": "1 MAIN ST",
      "address2": "UNIT 1",
      "city": "ANYTOWN",
      "state": "MO",
      "postalCode": "12341"
    }
  }
}'

In the following example response, Stedi found one instance of potential matching coverage for Jane Doe. The information is available in the items array. 

  • The payer is Aetna.

  • The patient in the request, Jane, is a dependent on the Aetna plan because her demographic information appears in the dependent object in the response.

  • The confidence.level is marked as REVIEW_NEEDED, because the dependent’s last name is slightly different from the patient’s last name in the insurance discovery request. However, all of the other demographic details in the dependent object – first name, date of birth, address – match the patient from the request. The two-part last name, Smith Doe, appears to be the complete version of the last name in the request, Doe. Based on this information, we can confirm that this is active coverage for the patient. 

  • The benefitsInformation object (truncated to keep this post concise) contains the patient’s benefits details. For example, the patient has active medical coverage under their health plan for the service dates in the request. Visit Determine patient benefits to learn more about interpreting the benefits information in the insurance discovery check response.

{ 
  "coveragesFound": 1,
  "discoveryId": "e856b480-0b41-11f0-aee6-fc0434004bca",
  "items": [
    {
      "provider": {
        "providerName": "THE DOCTORS OFFICE",
        "entityType": "Non-Person Entity",
        "npi": "1234567891"
      },
      "subscriber": {
        "memberId": "J9606211996",
        "firstName": "JOHN",
        "lastName": "DOE",
        "groupNumber": "012345607890008",
        "groupDescription": "SAMPLE HEALTH GROUP",
        "insuredIndicator": "Y"
      },
      "dependent": {
        "firstName": "JANE",
        "lastName": "SMITH DOE",
        "gender": "F",
        "dateOfBirth": "20010925",
        "planNumber": "0123654",
        "relationToSubscriber": "Child",
        "relationToSubscriberCode": "19",
        "address": {
          "address1": "1 MAIN ST",
          "address2": "UNIT 1",
          "city": "ANYTOWN",
          "state": "MO",
          "postalCode": "12341"
        }
      },
      "payer": {
        "entityIdentifier": "Payer",
        "entityType": "Non-Person Entity",
        "lastName": "Aetna",
        "name": "Aetna",
        "payorIdentification": "100003"
      },
      "planInformation": {
        "planNumber": "0123654"
      },
      "planDateInformation": {
        "planBegin": "2025-01-01",
        "eligibilityBegin": "2025-01-01",
        "service": "2025-03-27"
      },
      "benefitsInformation": [
        {
           "code": "1",
           "name": "Active Coverage",
           "coverageLevelCode": "FAM",
           "coverageLevel": "Family",
           "serviceTypeCodes": [
               "30"
           ],
           "serviceTypes": [
               "Health Benefit Plan Coverage"
           ],
           "insuranceTypeCode": "PS",
           "insuranceType": "Point of Service (POS)",
           "planCoverage": "Aetna Choice POS II",
           "inPlanNetworkIndicatorCode": "W",
           "inPlanNetworkIndicator": "Not Applicable"
        },
	 .... truncated to preserve space
        {
          "code": "W",
          "name": "Other Source of Data",
          "benefitsRelatedEntities": [
            {
              "entityIdentifier": "Payer",
              "entityType": "Non-Person Entity",
              "entityName": "AETNA",
              "address": {
                "address1": "PO BOX 981106",
                "city": "EL PASO",
                "state": "TX",
                "postalCode": "79998"
              }
            }
          ]
        }
      ],
      "confidence": {
        "level": "REVIEW_NEEDED",
        "reason": "This record was identified as a low confidence match due to a last name mismatch."
      }

    }
  ],
  "meta": {
    "applicationMode": "production",
    "traceId": "1-67e5a730-75011daa6caebf3c6595bf7c"
  },
  "status": "COMPLETE"
}

Visit our Insurance Discovery docs for complete details and API references.

Try Insurance Discovery today

Contact our team for pricing and to learn more about how Stedi can automate and streamline your eligibility and claims processing workflows.

Mar 19, 2025

Products

Last year, we introduced Transaction Enrollment, a streamlined way to submit enrollment requests for transaction types like claim remittances (ERAs) and eligibility checks through either our Enrollments API or user-friendly interface. Once you submit a request, Stedi manages the entire process for you, including submitting the enrollment to the payer, following up as needed, and giving clear guidance for any additional steps that might be required. 

To make transaction enrollment even more convenient, we’re excited to introduce CSV imports, a guided workflow that allows you to submit enrollment requests in bulk through Stedi’s UI. With CSV imports, operations teams can efficiently submit hundreds of enrollment requests in seconds without any development effort. 

Bulk import enrollments in seconds

Stedi guides you through the bulk CSV import process step-by-step: 

  1. Go to the Bulk imports page and click New bulk import.

  2. Download the CSV template with the required fields. The upload page contains detailed formatting instructions. 

  3. Complete and upload the CSV file containing enrollment information—one row equals one enrollment request for a specific transaction to a payer. 

  4. Stedi validates the file and provides clear error messages describing any issues. You can fix the errors and re-upload the file as many times as needed.

Once you execute the import, Stedi automatically creates enrollment requests. When the import is complete, you can download a report that shows the status of each row in the CSV file to ensure all your enrollment requests were submitted successfully. 

You can also track the status of each enrollment request through the Enrollments page.

Clear updates and guidance from Stedi’s enrollment experts

Our network and enrollment operations team knows the nuances of each payer’s enrollment requirements and maintains a public repository of payers that require additional steps through our Transaction Enrollments Hub. When an enrollment is submitted, the Stedi team is notified immediately and kicks off the enrollment process within the same business day.

If payers have additional requirements as part of their standard enrollment process, Stedi contacts you with clear, actionable steps to move the process forward. In addition to updating your enrollment request with action items required for your team, we’ll also reach out in your dedicated Slack channel with resources and to answer any follow-up questions.

Check out the Transaction Enrollment docs for complete details about each stage of the enrollment process. You can also search Stedi’s Payer Network to determine which payers require enrollment for the transaction types you need to process.

Get started with Stedi today

Contact us to learn more about how Stedi can automate and optimize eligibility and claims processing workflows for your business.

Mar 4, 2025

Products

We're excited to introduce sandbox accounts—a free, no-commitment way to explore integrating with Stedi. In a sandbox, you can simulate real-world transactions, test API integrations, and validate processing workflows at your own pace without using real patient data or committing to a paid plan.

You can create a sandbox account in under two minutes without talking to our team or entering any payment information. When you’re ready to start sending production data, you can contact customer support to seamlessly upgrade your account.

Simulate realistic transactions without PHI/PII

In a sandbox account, you can submit mock real-time eligibility checks for popular payers, and Stedi sends back a realistic benefits response so you know what kinds of data to expect in production. Mock transactions are always free for testing purposes and won’t incur any charges from Stedi.

You can send mock requests for a variety of well-known payers, including:

  • Aetna

  • Cigna

  • UnitedHealthcare

  • National Centers for Medicare & Medicaid Services (CMS)

  • Many more - Visit Eligibility mock requests for a complete list

Mock claims aren’t yet supported in Sandbox accounts. Contact us if you’d like to learn more about claims processing with Stedi.

Explore how Stedi can streamline your workflow

A sandbox account allows you to explore how Stedi’s UI tools can help you manage, track, and troubleshoot your eligibility check pipeline.

For example, after you submit a mock eligibility check, you can review all of the request and response details in Eligibility Manager. This includes:

  • Historical logs with filters for status, Payer ID, date, and error codes.

  • Raw API requests and responses.​

  • User-friendly benefits views highlighting patient coverage details, co-pays, and deductibles.​

  • Debugging tools to troubleshoot and resolve issues in transactions.​

Create a sandbox account today

Create a sandbox account to start testing Stedi at your own pace with no fees or commitments. You can also contact our team to learn more about how Stedi can help automate your eligibility checks and claims processing workflows.

Mar 3, 2025

Products

Stedi’s APIs allow you to submit claims programmatically and get notified through webhooks when payers return claim statuses and remittances. These APIs are a great fit for developers who prefer an API integration, especially those who don’t want to deal with the complexities of EDI.

However, many existing claims processing workflows use SFTP to submit claims and fetch payer responses. That’s why we’re excited to announce that fully managed SFTP-based claims submission is now available for all Stedi payers that accept claims. With SFTP connectivity, developers and revenue cycle teams can submit claims through Stedi and fetch claim status and remittances without rebuilding their existing architecture.

Submit claims through fully managed SFTP

We recommend using SFTP-based claims submission when you have an existing system that generates and ingests X12 EDI files via SFTP (if your system sends X12 EDI files via API, we support that too). With SFTP connectivity, you can send X12 EDI claims through Stedi and retrieve X12 EDI 277 Claim Acknowledgments and 835 Electronic Remittance Advice (ERAs) without an API integration.

Here’s how SFTP claims processing works:

  1. Create both test and production SFTP users on your account's Healthcare page. 

  2. Connect to Stedi's server and drop compliant X12 EDI claims into the to-stedi directory. 

  3. Stedi automatically validates the claim data. If there are no errors, Stedi routes production claims to payers and test claims to our test clearinghouse. 

  4. Stedi places claim responses – X12 277s and ERAs – into the from-stedi directory. 

  5. Retrieve these files from Stedi’s SFTP server at your preferred cadence.  

You can also configure Stedi webhooks to send claim processing events to your API endpoint. This allows you to monitor for processing issues, confirm successful claim submissions, and get notified when new payer responses are available.

Visit the Submit claims through SFTP documentation for complete details.

Monitor and debug your claims pipeline in the Stedi UI

You can review all of the claims and claim responses flowing through your SFTP connection on the Files and Transactions pages in Stedi. Each transaction page highlights the key information about the claim, such as the insured’s name, member ID, and line items. Stedi also displays a linked list of related transactions, so you can easily move between the original claim and any responses.

These UI tools allow you to track your entire claim pipeline, review claim submissions, quickly diagnose errors, and more.

Start processing claims with Stedi today

With SFTP claims processing, you can send claims and retrieve payer responses through Stedi in minutes. 

Contact us to learn more about how Stedi can help automate and streamline claims processing for your business.

Feb 25, 2025

Products

A Medicare Beneficiary Identifier (MBI) is a unique, randomly-generated identifier assigned to individuals enrolled in Medicare. It’s required in every eligibility check submitted to the Centers for Medicare and Medicaid Services (Payer ID: CMS), which is also known as the HIPAA Eligibility Transaction System (HETS).

Providers need to run eligibility and benefits verification checks to CMS to verify active coverage, calculate patient costs and responsibility, and route claims to the right state Medicare payer.

Many providers are unable to identify or locate a patient’s MBI due to problems with manual patient intake processes, paper forms, incomplete data from referrals, and patients simply not having their Medicare card on hand. These issues prevent providers from verifying patient eligibility with CMS, delaying patients from receiving critical care and creating unnecessary stress for patients, providers, and billing teams. That’s why we’re excited to introduce MBI lookups for CMS eligibility checks.

You can now use Stedi’s eligibility check APIs to retrieve benefits information from CMS with a patient’s Social Security Number (SSN) instead of their MBI. Stedi looks up the patient’s MBI, submits a compliant eligibility check to CMS, and returns a complete benefits response that includes the patient’s MBI for future reference.

Retrieve complete benefits information with the patient’s SSN

You can perform MBI lookups using Stedi’s Real-Time Eligibility Check and Batch Eligibility Check APIs. To perform an MBI lookup:

  1. Construct an eligibility check request that includes the patient’s first name, last name, date of birth, and Social Security Number (SSN).

  2. Set the tradingPartnerServiceId to MBILU, which is the payer ID for the MBI lookup to CMS.

  3. Stedi uses the patient’s demographic data and SSN to perform an MBI lookup. If there is a match, Stedi submits an eligibility check to CMS.

  4. Stedi returns a complete benefits response from CMS that includes the patient’s coverage status and their MBI in the subscriber object for future reference.

The following sample request uses Stedi’s Real-Time Eligibility Check API to perform an MBI lookup for a patient named Jane Doe.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "MBILU",
  "externalPatientId": "UAA111222333",
  "encounter": {
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1234567890"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "ssn": "123456789"
  }
}'

The following snippet shows part of a CMS benefits response returned from an MBI lookup. Stedi places the patient’s MBI in the subscriber.memberId property, so in this example, the patient’s MBI is 1AA2CC3DD45.

{
   "meta": {
       "senderId": "STEDI",
       "submitterId": "117151744",
       "applicationMode": "production",
       "traceId": "11112222333344445555666677",
       "outboundTraceId": "11112222333344445555666677"
   },
   "controlNumber": "112233445",
   "reassociationKey": "112233445",
   "tradingPartnerServiceId": "CMS",
   "provider": {
       "providerName": "ACME HEALTH SERVICES",
       "entityIdentifier": "Provider",
       "entityType": "Non-Person Entity",
       "npi": "1234567890"
   },
   "subscriber": {
       "memberId": "1AA2CC3DD45",
       "firstName": "JANE",
       "lastName": "DOE",
       "middleName": "A",
       "gender": "F",
       "entityIdentifier": "Insured or Subscriber",
       "entityType": "Person",
       "dateOfBirth": "19000101",
       "address": {
           "address1": "1234 FIRST ST",
           "city": "NEW YORK",
           "state": "WV",
           "postalCode": "123451111"
       }
   },
   "payer": {
       "entityIdentifier": "Payer",
       "entityType": "Non-Person Entity",
       "name": "CMS",
       "payorIdentification": "CMS"
   },
   // truncated; visit the docs for a full CMS benefits response example
}

Visit the MBI lookup docs for a full example response with complete benefits information.

Monitor your entire eligibility pipeline in the Stedi UI

Once you submit an MBI lookup, you can review its complete details in the Stedi UI. This includes the full request and response payload as well as clear status messages designed to help you quickly resolve eligibility issues.  

You can retry failed checks in real time through Stedi’s user-friendly eligibility form and use the Debug view to systematically troubleshoot failed checks until you receive a successful response from the payer.

Start processing eligibility checks with Stedi today

MBI lookup makes it easier for providers to access critical benefits information for Medicare beneficiaries.

Contact us to learn more about how Stedi Clearinghouse can help you streamline eligibility checks for CMS and thousands of other payers.

Feb 5, 2025

Products

When a patient has active coverage with multiple health plans, providers need to know which plan is primarily responsible for paying claims. The process of figuring out which payers to bill and in what order is called coordination of benefits (COB), and it’s one of the leading causes of claim denials and delayed payments in healthcare.

Providers often don’t know patients have overlapping coverage until after a claim is rejected or denied with a message that doesn’t contain any information about where to resubmit the claim: 

[PATTERN 28937] REJECT - THIS PATIENT HAS PRIMARY INSURANCE COVERAGE WITH ANOTHER CARRIER WITH EFFECTIVE DATE 1/01/2025. PLEASE RESUBMIT AS ELECTRONIC SECONDARY ONCE ADJUDICATED BY THE PRIMARY PAYOR.

Back at square one, providers must contact patients to ask about additional coverage and then confirm the new plan’s primacy for payment (usually by calling the payer) before they can resubmit. This tedious process delays payments to providers for months and creates stressful billing surprises for patients.

To help streamline claims processing, we’re excited to announce that you can now perform COB checks through our developer-friendly Coordination of Benefits Check API or user-friendly online form. COB checks help you proactively identify additional coverage for patients and confidently submit claims to the right payer the first time. 

Reduce claim denials with coordination of benefits checks

The best practice is to run COB checks for all new patients who have coverage through one of Stedi’s supported COB payers. In seconds, you can determine if the patient has coverage with other supported COB payers, whether there is coverage overlap, and which plan is responsible for paying for services (primacy).

Here’s how COB checks work: 

  1. You submit a COB check through Stedi’s COB form or COB check API with information for one of the patient's known health plans. The information required is similar to a standard eligibility check – first name, last name, DOB, and either member ID or SSN – and you should first run a real-time eligibility check to ensure that the member’s details are accurate.

  2. Stedi searches a database of eligibility data from regional and national plans. This database has 245+ million patient coverage records from 45+ health plans, ASOs, TPAs, and others, including participation from the vast majority of national commercial health plans. Data is updated at least weekly to ensure accuracy.

  3. Stedi synchronously returns summary information about each of the patient’s active health plans and whether there is coverage overlap. When there is coverage overlap, Stedi returns the responsibility sequence number for each payer (such as primary or secondary), if that can be determined.

Once you receive the results, you should send real-time eligibility checks to each additional payer to verify coverage and view the full details of the patient’s plan before submitting claims.

Real-time COB information in developer-friendly JSON

The following example COB response shows information for a dependent covered by multiple health plans through their parents’ policies. The COB check was submitted to Aetna with a service type code of 30 (Health Benefit Plan Coverage) and a service date of 2024-11-27.

The response indicates the following:

  • Active coverage: The patient has active coverage with Aetna for medical care, pharmacy, and vision services. This is indicated by the three objects in the benefitsInformation array with the code set to 1, which indicates Active Coverage.

  • Coverage overlap: The patient has overlapping coverage for medical care services between two health plans. This is indicated by the benefitsInformation object with its code set to R.

  • Primacy: The other health plan is Cigna, listed in benefitsInformation.benefitsRelatedEntities. Cigna is the primary payer for medical care services.

  • COB instance: There is a COB instance for medical care services on the date of service provided in the request. This is indicated in the coordinationOfBenefits object.

Based on this response, you must send claims first to Cigna as the primary payer for medical care services. Once Cigna adjudicates the claim, you can send another claim, if necessary, to Aetna as the secondary payer (subject to specific payer claims processing rules).

Before sending any claim(s) to Cigna, you should first send a separate eligibility check to Aetna to verify coverage status and confirm the full details of Aetna’s coverage.

{
  "benefitsInformation": [
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "1"
      ],
      "serviceTypes": [
        "Medical Care"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "88"
      ],
      "serviceTypes": [
        "Pharmacy"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "1",
      "name": "Active Coverage",
      "serviceTypeCodes": [
        "AL"
      ],
      "serviceTypes": [
        "Vision (Optometry)"
      ],
      "benefitsDateInformation": {
        "benefitBegin": "2023-03-01"
      },
      "subscriber": {
        "dateOfBirth": "2002-02-27"
      }
    },
    {
      "code": "R",
      "name": "Other or Additional Payor",
      "serviceTypeCodes": [
        "1"
      ],
      "serviceTypes": [
        "Medical Care"
      ],
      "benefitsDateInformation": {
        "coordinationOfBenefits": "2024-07-01"
      },
      "benefitsRelatedEntities": [
        {
          "entityIdentifier": "Primary Payer",
          "entityName": "CIGNA",
          "entityIdentification": "PI",
          "entityIdentificationValue": "1006"
        },
        {
          "entityIdentifier": "Insured or Subscriber",
          "entityFirstName": "JOHN",
          "entityMiddleName": "X",
          "entityIdentification": "MI",
          "entityIdentificationValue": "00000000000",
          "entityLastName": "DOE"
        }
      ],
      "subscriber": {
        "dateOfBirth": "2002-12-31"
      }
    }
  ],
  "coordinationOfBenefits": {
    "classification": "CobInstanceExistsPrimacyDetermined",
    "instanceExists": true,
    "primacyDetermined": true,
    "coverageOverlap": true,
    "benefitOverlap": true
  },
  "dependent": {
    "firstName": "JORDAN",
    "lastName": "DOE",
    "gender": "M",
    "dateOfBirth": "2012-12-31",
    "relationToSubscriber": "Child",
    "relationToSubscriberCode": "19",
    "address": {
      "address1": "1 MAIN ST.",
      "city": "NEW YORK",
      "state": "NY",
      "postalCode": "10000"
    }
  },
  "errors": [],
  "meta": {
    "applicationMode": "production",
    "traceId": "01JDQFT4W3KTWZNTADEZ55BFFX",
    "outboundTraceId": "01JDQFT4W3KTWZNTADEZ55BFFX"
  },
  "payer": {
    "name": "Aetna",
    "identification": "AETNA-USH"
  },
  "provider": {
    "providerName": "ACME Health Services",
    "entityType": "Non-Person Entity",
    "npi": "1999999984"
  },
  "subscriber": {
    "memberId": "W000000000",
    "firstName": "JOHN",
    "lastName": "DOE",
    "address": {
      "address1": "1 MAIN ST.",
      "city": "NEW YORK",
      "state": "NY",
      "postalCode": "10000"

Check out our coordination of benefits docs for a complete API reference, test requests and responses, and more.

Get started with coordination of benefits checks today

Add coordination of benefits checks to your claims processing workflow to increase claim acceptance rates, reduce patient billing surprises, and help providers get paid faster. 

Contact us to learn more and speak to the team.

Jan 16, 2025

Products

We’re excited to announce that our Dental Claims API is now Generally Available. 

Dental claims allow dental providers to bill insurers for services, such as preventive cleanings, fillings, crowns, bridges, and other restorative procedures. Our new Dental Claims API allows you to automate this process and streamline the claims lifecycle for dental providers and payers.

You can use the new API to submit 837D Dental Claims to hundreds of payers in Stedi’s developer-friendly JSON format or raw X12 EDI format. Once submitted, you can programmatically check the claim status in real time and retrieve 277 claim acknowledgments and 835 electronic remittance advice (ERA) from payers, all through the Stedi clearinghouse.

Submit dental claims to hundreds of payers

Submit requests to one of Stedi’s dental claims endpoints:

  • Dental Claims: Submit your claim in user-friendly JSON. Stedi translates your request to the X12 EDI 837D format.

  • Dental Claims Raw X12: Submit your claim in X12 EDI format. This is ideal if you have an existing system that generates X12 EDI files and you want to send them through Stedi’s API.

Stedi first validates your request against the 837D specification to ensure compliance, reducing payer rejections later on. Then, Stedi constructs and sends a valid X12 EDI 837D claim to the payer. Finally, Stedi returns a response containing information about the claim you submitted and whether the submission was successful.

You can also send test claims to Stedi’s QA clearinghouse by setting the usageIndicator to T, as shown in the example below for the JSON endpoint. This helps you quickly test and debug your end-to-end claims processing pipeline.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/dental-claims/submission \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "usageIndicator": "T",
  "tradingPartnerServiceId": "1748",
  "tradingPartnerName": "Tricare West AZ",
  "subscriber": {
    "paymentResponsibilityLevelCode": "P",
    "memberId": "123412345",
    "firstName": "John",
    "lastName": "Doe",
    "groupNumber": "1234567890",
    "gender": "F",
    "address": {
      "address1": "1234 Some St",
      "city": "Buckeye",
      "state": "AZ",
      "postalCode": "85326"
    },
    "dateOfBirth": "20180615"
  },
  "submitter": {
    "organizationName": "ABA Inc",
    "submitterIdentification": "123456789",
    "contactInformation": {
      "phoneNumber": "3131234567",
      "name": "BILLING DEPARTMENT"
    }
  },
  "rendering": {
    "npi": "1234567893",
    "taxonomyCode": "106S00000X",
    "providerType": "RenderingProvider",
    "lastName": "Doe",
    "firstName": "Jane"
  },
  "receiver": {
    "organizationName": "Tricare West AZ"
  },
  "payerAddress": {
    "address1": "PO Box 7000",
    "city": "Camden",
    "state": "SC",
    "postalCode": "29000"
  },
  "claimInformation": {
    "signatureIndicator": "Y",
    "toothStatus": [
      {
        "toothNumber": "3",
        "toothStatusCode": "E"
      }
    ],
    "serviceLines": [
      {
        "serviceDate": "20230428",
        "renderingProvider": {
          "npi": "1234567893",
          "taxonomyCode": "122300000X",
          "providerType": "RenderingProvider",
          "lastName": "Doe",
          "firstName": "Jane"
        },
        "providerControlNumber": "a0UDo000000dd2dMAA",
        "dentalService": {
          "procedureCode": "D7140",
          "lineItemChargeAmount": "832.00",
          "placeOfServiceCode": "12",
          "oralCavityDesignationCode": [
            "1",
            "2"
          ],
          "prosthesisCrownOrInlayCode": "I",
          "procedureCount": 2,
          "compositeDiagnosisCodePointers": {
            "diagnosisCodePointers": [
              "1"
            ]
          }
        },
        "teethInformation": [
          {
            "toothCode": "3",
            "toothSurface": [
              "M",
              "O"
            ]
          }
        ]
      }
    ],
    "serviceFacilityLocation": {
      "phoneNumber": "3131234567",
      "organizationName": "ABA Inc",
      "npi": "1234567893",
      "address": {
        "address1": "ABA Inc 123 Some St",
        "city": "Denver",
        "state": "CO",
        "postalCode": "802383100"
      }
    },
    "releaseInformationCode": "Y",
    "planParticipationCode": "A",
    "placeOfServiceCode": "12",
    "patientControlNumber": "0000112233",
    "healthCareCodeInformation": [
      {
        "diagnosisTypeCode": "ABK",
        "diagnosisCode": "K081"
      }
    ],
    "claimSupplementalInformation": {
      "priorAuthorizationNumber": "20231010012345678"
    },
    "claimFrequencyCode": "1",
    "claimFilingCode": "FI",
    "claimChargeAmount": "832.00",
    "benefitsAssignmentCertificationIndicator": "Y"
  },
  "billing": {
    "taxonomyCode": "106S00000X",
    "providerType": "BillingProvider",
    "organizationName": "ABA Inc",
    "npi": "1999999992",
    "employerId": "123456789",
    "contactInformation": {
      "phoneNumber": "3134893157",
      "name": "ABA Inc"
    },
    "address": {
      "address1": "ABA Inc 123 Some St",
      "city": "Denver",
      "state": "CO",
      "postalCode": "802383000"
    }
  }
}'

Retrieve 277 acknowledgments and 835 ERAs programmatically

After you submit a dental claim, you may receive asynchronous 277CA and 835 ERA responses from the payer. The 277CA indicates whether the claim was accepted or rejected and (if relevant) the reasons for rejection. The 835 ERA, also known as a claim remittance, contains details about payments for specific services and explanations for any adjustments or denials.

You can either poll Stedi for processed 277CAs and 835 ERAs or set up webhooks that automatically send events for processed responses to your endpoint. Then, you can use the following APIs to retrieve them from Stedi in JSON format:

Track claims and responses in Stedi

You can view a list of every claim you submit through Stedi clearinghouse and all associated responses on the Transactions page in Stedi. Click any transaction to review its details, including the full request and response payload.

Start processing claims with Stedi

With the Dental Claims API, you can start automating your claim submission process today. Contact us to learn more and speak to the team.

Dec 17, 2024

Products

All payers require providers to complete an enrollment process before they can start receiving claim remittances (ERAs). Though less common, certain payers also require enrollment before allowing providers to submit transactions like claims and eligibility checks. 

The typical enrollment process is highly manual and notoriously painful. There are no APIs or uniform standards, so every payer has different requirements. Forms, online portals, PDFs, wet signatures, and even fax machines can all be part of the process, causing significant frustration and slowing down the overall revenue cycle. After the enrollment is submitted, it’s a black box; days or weeks go by with no status updates, forcing providers to wait to submit claims, check patient eligibility, and receive information about payments. These issues compound for revenue cycle management companies that do hundreds or thousands of enrollments on behalf of many providers.

That’s why Stedi designed a new enrollment experience from the ground up. Through Stedi's user-friendly interface or modern API, developers and operations teams can now submit enrollments for specific transaction types in a streamlined flow – either one at a time or in large batches.

Once you submit an enrollment request, Stedi manages the entire process for you, including submitting the enrollment to the payer, following up as needed, and giving clear guidance for any additional steps that might be required. You can monitor the enrollment status throughout the process using the API or our searchable Enrollments dashboard.

Manage transaction enrollments through Stedi’s user-friendly app or APIs

You can submit and monitor all of your organization’s transaction enrollments from the Enrollments page in Stedi. For each enrollment request, Stedi displays the provider, status, payer, and transaction type. You can click any enrollment request to view its complete details, including any notes from Stedi regarding the next steps.

You can also manage the entire transaction enrollment process programmatically through Stedi’s Transaction Enrollment API. This approach is especially useful when you need to perform bulk enrollments for many providers at once, or when you want to embed enrollment capabilities into your own application. The following example request enrolls a provider for claim payments (835 ERAs) with UnitedHealthcare (Payer ID: 87726). 

curl --request POST \
  --url https://enrollments.us.stedi.com/2024-09-01/enrollments \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "transactions": {
    "claimPayment": {
      "enroll": true
    }
  },
  "primaryContact": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "test@example.com",
    "phone": "555-123-34354",
    "streetAddress1": "123 Some Str.",
    "city": "A City",
    "state": "MD",
    "zipCode": "20814"
  },
  "userEmail": "test@example.com",
  "payer": {
    "id": "87726"
  },
  "provider": {
    "id": "db6665c5-7b97-4af9-8c68-a00a336c2998"
  }
}'

Streamlined processing

Transaction enrollment requests you submit manually and programmatically follow the same streamlined process:

  1. Add provider: You add a new provider record with the information required for enrollment, including the provider's name, tax ID, NPI, and contact information.

  2. Submit transaction enrollment requests: You submit requests to enroll the provider with required payers, one for each transaction type. For example, you’d submit three separate requests to enroll a provider for 837P (professional) claims, 270 real-time eligibility checks, and 835 ERAs (claim payments). You can save requests in DRAFT status until you're ready to submit them to Stedi. Once you submit an enrollment request, its status is set to SUBMITTED, and it enters Stedi’s queue for processing.

  3. Provisioning: Stedi begins the enrollment process with the payer and sets the enrollment request status to PROVISIONING. Our team leaves clear instructions about what to do next, if required, and provides hands-on help as needed with additional steps.

  4. Enrollment is Live: Once the enrollment is approved, the enrollment request status is set to LIVE, and the provider can start exchanging the enrolled transactions with the payer.

Clear status updates and expert guidance

Our customer success team knows the nuances of each payer’s enrollment requirements and breaks them down into clear, actionable steps. In addition to updating your enrollment request with any action items required for your team, we also reach out to you in your dedicated Slack channel with resources and to answer any follow-up questions. 

The following example shows an enrollment request for Medicaid California Medi-Cal that requires additional steps for the provider. In Step 1, customer support notes that they dropped the required PDF in Slack for easy access.

You can also retrieve this information through the GET Enrollment endpoint. Notes from Stedi about the next steps are available in the reason property. 

{
  "createdAt": "2024-12-11T22:39:16.772Z",
  "id": "0193b7e0-6526-72a2-90a3-4e05dce4775a",
  "payer": {
    "id": "100065"
  },
  "primaryContact": {
    "organizationName": "Test Healthcare Org",
    "firstName": "",
    "lastName": "",
    "email": "test@testemail.com",
    "phone": "4112334567",
    "streetAddress1": "123 Some Street",
    "streetAddress2": "",
    "city": "Pittsburgh",
    "zipCode": "12345",
    "state": "PA"
  },
  "provider": {
    "name": "Test Healthcare Provider",
    "id": "0193b7db-528d-7723-9943-959b955ba103"
  },
  "reason": "The following steps are required by Medi-Cal to complete this enrollment.\n- **Step 1:** You (or your provider who requires this enrollment) must log into the DHCS portal and complete the steps found on page 2 of the PDF sent in your Slack channel on 12/11/2024. This step tells Medi-Cal to \"affiliate\" the provider with Availity (the intermediary clearinghouse we go through for our Medi-Cal connection). Let Stedi know in Slack once you have done this.\n- **Step 2:** Stedi performs the enrollment process. In the following days, you will receive an email from Medi-Cal stating that the affiliation process is complete.\n- **Step 3:** You (or your provider who requires the enrollment) must log back into the DHCS portal and complete the steps found on pages 3-5 of the PDF. This will complete the enrollment process.",
  "status": "SUBMITTED",
  "transactions": {
    "professionalClaimSubmission": {
      "enroll": true
    }
  },
  "updatedAt": "2024-12-17T17:31:42.012Z",
  "userEmail": "demos@stedi.com"
}

Get started with Stedi today

Streamlined transaction enrollments are just one of the ways Stedi accelerates payer integration and offers a modern, developer-friendly alternative to traditional clearinghouses. You can search Stedi’s Payer Network to determine which payers require enrollment for the transaction types you need to process. 

Contact us to speak to the team and learn how Stedi can help you automate and simplify your eligibility and claims workflows.

Dec 5, 2024

Products

Many providers need to perform batch eligibility checks for patients monthly or before upcoming appointments. These refresh or background checks have historically required significant development effort.

First, you need complex logic to avoid payer concurrency constraints; payers can throttle requests or even block the NPI when providers send too many checks at once. Then, you need a robust way to detect payer outages and manage retries. Finally, you need to monitor your clearinghouse concurrency ‘budget’ to ensure you have enough throughput left for time-sensitive real-time checks. 

Stedi’s new Batch Eligibility Check API handles all of this complexity for you. You can submit millions of eligibility checks – up to 500 per batch request – for Stedi to process asynchronously. Stedi manages throughput and retries automatically using industry best practices. Even better, asynchronous checks don’t count toward your Stedi account’s concurrency limits, so you can stage an unlimited number of batch checks while continuing to send real-time checks in parallel.

Under the hood, Stedi’s Batch Eligibility Check API uses the same real-time EDI rails as our normal Eligibility Check API, which means that our batch eligibility check jobs aren’t subject to the delays commonly found in other batch eligibility offerings.

Automate batch eligibility checks with two simple endpoints

You can automate asynchronous batch checks to thousands of payers by calling Stedi’s Batch Eligibility Check endpoint with a JSON payload. You may want to submit batch requests for:

  • All patients at the beginning of every month to identify those who have lost or changed coverage.

  • Patients with upcoming appointments to identify those who no longer have active coverage. 

  • New claims before they are submitted, to ensure they are routed to the correct payer.

Once you submit a batch, Stedi translates the JSON into compliant X12 270 payloads and delivers them to the designated payers as quickly as possible using Stedi’s master payer connections. You can then use the Poll Batch Eligibility Checks endpoint to retrieve the results for completed checks. Stedi automatically handles payer downtime and retries requests as needed.

The following example request demonstrates how to submit a batch of eligibility checks, where each item in the array represents an individual check. The request format is the same as our Real-Time Eligibility Check endpoint, with the addition of the submitterTransactionIdentifier property, a unique identifier you can use to match each check to the payer’s response.

curl --request POST \
  --url https://manager.us.stedi.com/2024-09-01/eligibility-manager/batch-eligibility \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "items": [
    {
      "submitterTransactionIdentifier": "ABC123456789",
      "controlNumber": "000022222",
      "tradingPartnerServiceId": "AHS",
      "encounter": {
        "serviceTypeCodes": [
          "MH"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1234567891"
      },
      "subscriber": {
        "dateOfBirth": "19000101",
        "firstName": "Jane",
        "lastName": "Doe",
        "memberId": "1234567890"
      }
    },
    {
      "submitterTransactionIdentifier": "DEF123456799",
      "controlNumber": "333344444",
      "tradingPartnerServiceId": "AHS",
      "encounter": {
        "serviceTypeCodes": [
          "78"
        ]
      },
      "provider": {
        "organizationName": "ACME Health Services",
        "npi": "1234567891"
      },
      "subscriber": {
        "dateOfBirth": "19001021",
        "firstName": "John",
        "lastName": "Doe",
        "memberId": "1234567892"
      }
    }
  ]
}'

Minimize payer throttling through industry best practices

Stedi processes batch eligibility checks as quickly as possible while implementing best practices to avoid payer throttling, such as:

  • Observing NPI throughput limits in real time and adjusting the rate of requests accordingly.

  • Interleaving requests to multiple payers in a round-robin style to avoid hitting a payer with the same provider NPI too many times consecutively. For example, instead of sending all requests to payer #1 and then all requests to payer #2, Stedi will send one request to payer #1, then one request to payer #2, and then go back to payer #1.

  • Using backoff algorithms and other strategies when retrying requests to the same payer.

We continually incorporate our learnings about individual payer requirements and behavior into the system to ensure the most efficient batch processing time. 

Detect payer downtime and automatically retry

Stedi supports a growing list of payers for eligibility checks and continually monitors payer uptime. 

When Stedi detects a payer outage, our APIs reroute requests through alternate direct-to-payer or peer clearinghouse connections if possible. When requests fail due to payer processing issues, Stedi implements a retry strategy based on best practices and each payer’s connectivity requirements.

Start processing eligibility checks with Stedi

With Stedi’s Real-Time Eligibility Check and Batch Eligibility Check APIs, you can start automating eligibility checks for your organization today. 

Contact us to learn more and speak to the team.

Nov 20, 2024

Products

We’re excited to announce that our Institutional Claims API is now Generally Available. 

Institutional claims are how hospitals, nursing homes, and other healthcare facilities bill insurers for services, such as inpatient care, diagnostics, and therapies. Our new Institutional Claims API allows you to automate this process and streamline the claims lifecycle for healthcare providers.

You can use the new API to submit 837I Institutional Claims to thousands of payers using Stedi’s developer-friendly JSON format. Once submitted, you can programmatically check the claim status in real time and retrieve 277 claim acknowledgments and 835 electronic remittance advice (ERA) from payers through the Stedi clearinghouse. 

Submit institutional claims to thousands of payers

Call the Institutional Claims endpoint with a JSON payload. Stedi first validates your request against the 837I specification to ensure it’s compliant, reducing payer rejections down the line. Then, Stedi translates your request to the X12 837 EDI format and sends it to the payer. Finally, Stedi returns a response containing information about the claim you submitted and whether the submission was successful.

You can also send test claims to Stedi’s QA clearinghouse by setting the usageIndicator to T, as shown in the following example. This helps you quickly test and debug your end-to-end claims processing pipeline.

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/institutionalclaims/v1/submission \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "usageIndicator": "T",
  "tradingPartnerName": "UnitedHealthcare",
  "tradingPartnerServiceId": "87726",
  "controlNumber": "123456789",
  "submitter": {
    "organizationName": "Test Facility",
    "contactInformation": {
      "name": "Test Facility",
      "phoneNumber": "2225551234"
    },
    "taxId": "123456789"
  },
  "receiver": {
    "organizationName": "UnitedHealthcare"
  },
  "subscriber": {
    "memberId": "98765",
    "paymentResponsibilityLevelCode": "P",
    "firstName": "JANE",
    "lastName": "DOE",
    "groupNumber": "67890"
  },
  "claimInformation": {
    "claimFilingCode": "ZZ",
    "patientControlNumber": "00001111222233334444",
    "claimChargeAmount": "500.00",
    "placeOfServiceCode": "11",
    "claimFrequencyCode": "0",
    "planParticipationCode": "C",
    "benefitsAssignmentCertificationIndicator": "Y",
    "releaseInformationCode": "Y",
    "principalDiagnosis": {
      "qualifierCode": "ABK",
      "principalDiagnosisCode": "R45851"
    },
    "serviceLines": [
      {
        "assignedNumber": "0",
        "serviceDate": "20241015",
        "serviceDateEnd": "20241015",
        "lineItemControlNumber": "111222333",
        "institutionalService": {
          "serviceLineRevenueCode": "90",
          "lineItemChargeAmount": "500.00",
          "measurementUnit": "UN",
          "serviceUnitCount": "1",
          "procedureIdentifier": "HC",
          "procedureCode": "H0001"
        }
      }
    ],
    "claimCodeInformation": {
      "admissionTypeCode": "3",
      "admissionSourceCode": "9",
      "patientStatusCode": "30"
    },
    "claimDateInformation": {
      "admissionDateAndHour": "202409091000",
      "statementBeginDate": "20241015",
      "statementEndDate": "20241015"
    }
  },
  "providers": [
    {
      "providerType": "BillingProvider",
      "npi": "0123456789",
      "employerId": "123456789",
      "organizationName": "Test Facility",
      "address": {
        "address1": "123 Mulberry Street",
        "city": "Seattle",
        "state": "WA",
        "postalCode": "111135272"
      },
      "contactInformation": {
        "name": "Test Facility",
        "phoneNumber": "2065551234"
      }
    },
    {
      "providerType": "AttendingProvider",
      "npi": "1234567890",
      "firstName": "Doctor",
      "lastName": "Provider",
      "contactInformation": {
        "name": "name"
      }
    }
  ]
}'

Retrieve 277 acknowledgments and 835 ERAs programmatically

After you submit an institutional claim, you may receive asynchronous 277CA and 835 ERA responses from the payer. The 277CA indicates whether the claim was accepted or rejected and (if relevant) the reasons for rejection. The 835 ERA, also known as a claim remittance, contains details about payments for specific services and explanations for any adjustments or denials.

You can either poll Stedi for processed 277CAs and 835 ERAs or set up webhooks that automatically send events for processed responses to your endpoint. Then, you can use the following APIs to retrieve them from Stedi in JSON format:

Track claims and responses in the Stedi app

You can view a list of every claim you submit through the Stedi clearinghouse and all associated responses on the Transactions page of the Stedi app. Click any transaction to review its details, including the full request and response payload.

Start processing claims with Stedi

With the Institutional Claims API, you can start automating your claim submission process today. Contact us to learn more and speak to the team.

Sep 24, 2024

Products

Every clearinghouse maintains a list of supported payers, supported transaction types, and other key integration details. The problem? This vital information is typically provided in a CSV file over email and updated monthly at best. Worse, updated payer lists often contain breaking changes, duplicate payer names, and typos that cause failed transactions and endless code rewrites. 

Instead of a static payer list, we built the world’s most developer-friendly payer database: the Stedi Payer Network, which is uniquely designed to help you build more efficient and reliable integrations with thousands of medical and dental payers. It’s just one of the ways Stedi's clearinghouse gives modern healthcare companies the developer experience, security, and reliability they need to build world-class products for providers and patients.

Stable, unified payer records

Every EHR system and clearinghouse uses payer IDs to route transactions to payers. It can be hard to know which ID to use for requests because payer IDs vary between clearinghouses, and some clearinghouses assign separate IDs to different transactions with the same payer. Once you determine the right ID, frequent payer list updates require tedious remapping in your codebase.

The Stedi Payer Network eliminates this problem by mapping all of a payer’s commonly used names and IDs (aliases) to a single payer record, and Stedi automatically uses the required ID for the payer on the backend. For example, searching our interactive network page by any of Cigna’s aliases, such as 1841, CIGNA, and GWSTHC returns the same result, even though the most common Payer ID for Cigna is 62308.

This approach means Stedi likely already supports all of the common payer IDs you use today, making it easy to migrate to Stedi from other clearinghouses. And if you need a new alias for any existing payer, just let us know and we’ll add it to the network the same day.

Programmatic access to real-time updates

With the List Payers API, developers can retrieve Stedi’s complete, production payer list in seconds for use in any system or application. For example, you can:

  • Embed Stedi’s payer list within a patient intake form, allowing patients to choose from a list of supported payers.

  • Build simpler EHR integrations using the payer IDs in the provider’s EHR.

  • Create nightly or real-time syncs between your internal payer list and Stedi’s payer list.

  • Migrate or reroute transactions to Stedi’s clearinghouse without dynamically changing payer IDs.

The following Blue Cross Blue Shield of North Carolina response example shows that real-time eligibility checks, real-time claim status requests, and professional claims are supported for this payer. The response also indicates that payer enrollment is required for 835 ERAs (claim remittances).

{
  "stediId": "UPICO",
  "displayName": "Blue Cross Blue Shield of North Carolina",
  "primaryPayerId": "BCSNC",
  "aliases": [
    "1411",
    "560894904",
    "61473",
    "7472",
    "7814",
    "BCBS-NC",
    "BCSNC",
    "NCBCBS",
    "NCPNHP"
   ],
   "names": [
     "Blue Cross Blue Shield North Carolina"
   ],
   "transactionSupport": {
     "eligibilityCheck": "SUPPORTED",
     "claimStatus": "SUPPORTED",
     "claimSubmission": "SUPPORTED",
     "claimPayment": "ENROLLMENT_REQUIRED"
   }
}

Broad, reliable connectivity

Stedi already supports thousands of medical and dental payers, and we add more every week. Here are the number of unique payers supported for specific transaction sets:

  • Real-time eligibility checks - 1,100+ unique payers 

  • Claim submissions - 2,700+ unique payers 

  • Real-time claim status requests - 300+ unique payers 

  • 835 ERAs (claim remittances) - 1,800+ unique payers

On the backend, Stedi has multiple, redundant routes to payers through a combination of direct payer integrations and connectivity through intermediary clearinghouses. Whenever possible, Stedi automatically routes requests and responses to the most reliable connection, increasing uptime and reliability across the network.

You get redundancy and reliability with no additional effort or cost when you integrate with Stedi –  we manage all of the payer routing logic seamlessly behind the scenes. We add new payers daily, so feel free to request new payers or new transaction types for an existing payer.

Get started with Stedi

Stedi’s programmatically accessible payer network is one of the many ways our clearinghouse accelerates payer integration.

"The speed at which we got up and running in production with Stedi was remarkable. From the moment we began integration, it was clear that Stedi was designed with user ease and efficiency in mind.”

- Chris Parker, Chief Technology Officer PQT Health

Contact us to speak to the team and learn how Stedi can help you automate and simplify your eligibility and claims workflows. And if you want to see what percentage of your payer list we support, we’d be happy to do the comparison for you.

Sep 3, 2024

Products

Today, we’re introducing two new features to streamline your claims-processing workflow.

  • Manual claim submission to speed up integration testing, simplify troubleshooting, or handle out-of-band claim submissions. 

  • Auto-generated CMS-1500 Claim Form PDFs for record-keeping, mailing or faxing claims to payers, and more. 

We also revamped the Stedi app to make it easier to track claims from submission through remittance.

Submit claims manually in the Stedi app

We adapted the National Uniform Claim Committee (NUCC) 1500 Claim Form into a user-friendly digital form you can use to submit professional claims to thousands of payers

Our form validates key information, including provider NPIs and diagnosis codes, to reduce errors and payer rejections. You can also review a live preview of the auto-generated JSON payload for the Professional Claims API to understand how the form relates to the request structure.

Get auto-generated CMS-1500 Claim Form PDFs

Even if you submit the majority of claims programmatically, you may still need to create traditional claim forms for internal record-keeping, mailing and faxing claims to payers, reviewing claim information in a familiar format, or troubleshooting a complicated submission. You may also want to make claim form PDFs available for download within an EHR or RCM system.

To make these tasks easier, Stedi now automatically generates a complete CMS-1500 Claim Form PDF for every submitted claim. You can download these PDFs from the app or retrieve them through the CMS-1500 Claim Form PDF API

Monitor your entire claims processing pipeline

You can review complete details about every professional claim (837), claim acknowledgment (277), and claim payment (835 ERA) in the Stedi app. On the Transactions page, you can filter and sort by transaction type, usage (production or test), processed date, and business identifiers to quickly find specific claims and responses.  

On each transaction’s details page, Stedi now automatically puts key information at the top for easy access, such as subscriber information and the claim value. You can also instantly download the auto-generated CMS-1500 Claim Form PDF, review all related transactions, and review the full API request and response payloads.

Start processing claims today

With Stedi’s API-first clearinghouse, you can automate business flows like claims processing and eligibility checks using APIs that support thousands of payers

Contact us to learn more and speak to the team.

Jun 25, 2024

Guide

Your claim was denied—another patient forgot to update their insurance details after switching jobs. Now, you need to submit a new claim and wait an extra month for reimbursement, putting a strain on your business.

To prevent these kinds of delays, many healthcare customers use our Eligibility Check API to perform monthly refreshes. This is the common practice of scheduling eligibility checks for all or a subset of patients so you can proactively contact them when they lose or change coverage.

This post explains how you can optimize monthly eligibility refresh checks and avoid common pitfalls like payer throttling.

How to structure refresh checks

We recommend the following to get the most useful data from payers:

  • Dates of service: Only include the current month. Patients can gain or lose eligibility in the middle of a month, but eligibility usually starts and ends on month boundaries.

  • Service type codes: Dental providers should send a single service type code of 35 (Dental Care), and medical providers should send 30 (Health Benefit Plan Coverage), which returns eligibility for all coverage types included in the subscriber’s plan. Some payers may not support other values. Don’t include multiple service type codes because many payers will either return an error or ignore additional service type codes entirely.

Here’s an example of a refresh check for our Eligibility Check API:

curl --request POST \
  --url https://healthcare.us.stedi.com/2024-04-01/change/medicalnetwork/eligibility/v3 \
  --header 'Authorization: <api-key>' \
  --header 'Content-Type: application/json' \
  --data '{
  "controlNumber": "123456789",
  "tradingPartnerServiceId": "Cigna",
  "encounter": {
    "beginningDateOfService": "20240601",
    "endDateOfService": "20240630",
    "serviceTypeCodes": [
      "30"
    ]
  },
  "provider": {
    "organizationName": "ACME Health Services",
    "npi": "1234567891"
  },
  "subscriber": {
    "dateOfBirth": "19000101",
    "firstName": "Jane",
    "lastName": "Doe",
    "gender": "F",
    "memberId": "123456789"
  }
}'

When to schedule refresh checks

Wait until at least 3:00 AM local time on the first day of the month to begin monthly eligibility refresh checks. This approach helps you avoid delays from payer system updates and time zone differences. It also avoids the large volume of requests that occur around midnight, when many organizations schedule their refresh checks to begin.

You should also perform refresh checks outside your business hours. You don’t want to run into issues with payer throttling or your API’s concurrency limits while processing time-sensitive eligibility requests from customers.

Avoid payer throttling

Payers may throttle high volumes of requests for the same provider at once because they expect to receive requests at a “human scale”, such as a member entering insurance information into a website or presenting their insurance card at a doctor’s office. 

When payers are throttling you, they typically send back payer.aaaErrors.code = “42”, which indicates that the payer is having issues. (Our docs have a complete list of payer error codes for eligibility checks and possible resolution steps that you can use for debugging.)

To avoid throttling during monthly refreshes, we recommend:

  • Only send requests with the same provider NPI ID 1-2 times every 15 seconds. 

  • Use an exponential backoff algorithm to wait progressively longer periods before sending another request to the same payer. This approach differs from “live” eligibility checks where we recommend immediate retries because a patient might be waiting on the results.

  • Wait a few hours to retry if the first day of the month falls on a weekend and all requests to a particular payer are failing. Some payers have scheduled downtime, usually on weekends.

  • Interleave requests to multiple payers in a round-robin style. For example, instead of sending all requests to payer #1 and then all requests to payer #2, send one request to payer #1, then one request to payer #2, and then go back to payer #1.

Minimize waste

Don’t perform more checks than you need to. We recommend: 

  • Periodically purge or archive records for inactive patients. It’s a waste to perform eligibility checks on patients who have died or who haven’t scheduled an encounter for several years.

  • Remove or deactivate patients that are no longer eligible. The payer indicates ineligibility by setting benefitsInformation.code = “6” (Inactive) in the response.

When to follow up 

Flag responses that have benefitsInformation.code = “5”, which stands for Active - Pending investigation, or a benefitsDateInformation.premiumPaidToDateEnd before the current date. Some payers may still show active coverage while the subscriber is behind on premium payments. 

You may want to conduct additional checks on these patients, as they are at a higher risk of losing coverage soon.

Start processing eligibility checks today

Monthly eligibility refresh checks can help improve the care experience for both patients and providers. With Stedi’s API-first clearinghouse, you can automate business flows like eligibility checks and claims processing using APIs that support thousands of payers

Contact us to learn more and speak to the team.

May 6, 2024

Products

Modern companies want to integrate using modern tools. Until now, there hasn’t been a modern, developer-friendly clearinghouse to serve the growing healthcare technology ecosystem. 

That’s why we built Stedi – the only API-first healthcare clearinghouse that provides companies with the developer experience, security, and reliability they’ve come to expect.

Developer-friendly APIs

With Stedi, developers can automate business flows like eligibility checks and claims processing using APIs that support thousands of payers. Here is a list of the APIs available today, with more in the works: 

These developer-friendly APIs enable growing health tech companies, digital healthcare providers, and mature, technology-forward enterprises to build critical services that enable providers to determine what services are covered, create cost estimates, and automate revenue cycle management. 

Under the hood, Stedi transforms JSON API requests into HIPAA-compliant X12 EDI and sends those transactions to payers. Stedi then automatically processes payer responses – benefits information, claim statuses, and electronic remittance advice transactions (ERAs) – and returns them in an approachable JSON format. 

Each API has built-in validation and repair – commonly referred to as “edits” – to help reduce payer rejections that can cause delays and hours of manual labor to fix. We continue to expand our capabilities to address common issues, including data validation, formatting, code usage, payer-specific requirements, and more. 

Security-first, multi-region architecture with redundant connectivity

Security and reliability have always been job zero at Stedi.  

Our clearinghouse is built 100% on AWS infrastructure, with multi-region ‘active-active’ APIs and a growing set of payers covered by either redundant clearinghouse connectivity or direct-to-payer integrations. When possible, Stedi will dynamically route traffic to the most reliable connection, eliminating single points of failure.

Beyond maintaining SOC 2 Type II compliance and HIPAA eligibility, Stedi healthcare accounts are secured by mandatory multi-factor Authentication (MFA) and have role-based access control (RBAC) powered by Amazon Verified Permissions

You can learn more about our approach to security and compliance at trust.stedi.com.

Lightning-fast onboarding and support

“We submitted claims three days after signing, and the support is excellent in quality and timeliness of response. Stedi's team regularly resolves our engineers’ issues within minutes. This used to take months or just never happen at all with our previous vendor.” 

- Craig Micon, Head of Product at Pair Team

Our customer success engineers provide hands-on support to get you into production as quickly as possible. Within an hour of signing up, we provide you with a dedicated Slack channel, a Stedi account, and an API key so you can immediately start processing transactions. After you’re in production, we will keep the Slack channel open for communication between you and our support, engineering, and product teams.

We also prioritize comprehensive documentation, including step-by-step guides for claims and eligibility, reference implementations, and helpful API error messages. You can also download our public OpenAPI spec and access a user-friendly version of every X12 HIPAA transaction from our online reference site. 

Better UX 

The Stedi web application supports real-time manual eligibility checks that help developers test new payer integrations and help operations teams easily understand coverage for a specific member. You can search and filter responses for specific benefits (like copays and deductibles), coverage types (individual vs. family), coverage dates, and more.

Get started with Stedi

Stedi makes healthcare transactions more secure, efficient, and reliable for developers across the health tech industry. Contact us to learn more and speak to the team.

Apr 9, 2024

Engineering

It’s not AWS.

There’s no way it’s AWS.

It was AWS.



We use AWS IAM extensively throughout our codebase. Last year, we extended our use of IAM to build and enforce role-based access control (RBAC) for our customers using AWS Security Token Service (STS), an IAM service you can use to provide temporary access to AWS resources. Along the way, we discovered a vulnerability in STS that caused role trust policy statements to be evaluated incorrectly. 

Yes, you read that right – during the development process, we found an edge case that would have allowed certain users to gain unauthorized access to their AWS accounts.

We caught it before rolling out our RBAC product, and AWS has since corrected the issue and notified all affected users, so you don’t need to hit the panic button. However, we wanted to share how we discovered this vulnerability, our disclosure process with AWS, and what we learned from the experience.

How Stedi uses IAM and STS

To understand how we found the bug, you need to know a bit about Stedi’s architecture.

Behind the scenes, we assign a dedicated AWS account per tenant – that is, each customer account in the Stedi platform is attached to its own separate AWS account that contains Stedi resources, such as transactions and trading partner configurations. Our customers usually aren’t even aware that the underlying AWS resources exist or that they have a dedicated AWS account assigned to them, but using a dedicated AWS account as the tenancy boundary helps ensure data isolation (which is important for regulated industries like healthcare) and also eliminates potential noisy neighbor problems (which is important for high-volume customers).

When a customer takes an action in their account, it triggers a call to a resource using a Stedi API, or by calling the underlying AWS resource directly. One example is filtering processed transactions on the Stedi dashboard – when a customer applies a filter, the browser makes a direct request to an AWS database that contains the customer’s transaction data. This approach significantly reduces the code we need to write and maintain (since we don’t need to rebuild existing AWS APIs) and allows us to focus on shipping features and fixes faster.

To facilitate these requests, Stedi uses AWS STS to provide temporary access to AWS IAM policies, allowing the user’s browser session to access their corresponding AWS account. Specifically, we use the STS AssumeRoleWithWebIdentity operation, which allows federated users to temporarily assume an IAM role in their AWS account with a specific set of permissions.

IAM tags

Our IAM role trust policies use tags to control who can view and interact with resources. 

A tag is a custom attribute label (a key:value pair) you can add to an AWS resource. There are three tag types you can use to control access in IAM policies

  • Request: A tag added to a resource during an operation. You can use the aws:RequestTag/key-name condition key to specify what tags can be added, changed, or removed from an IAM user or role. 

  • Resource: An existing tag on an AWS resource, such as a tag describing a resource’s environment (“environment: production”). You can use the aws:ResourceTag/key-name condition key to specify which tag key-value pair must be attached to the resource to perform an operation.

  • Principal tag: A tag on a user or role performing an operation. You can use the aws:PrincipalTag/key-name condition key to specify what tags must be attached to the user or role before the operation is allowed.

Assuming roles

Here’s how we set up RBAC for Stedi accounts. 

We give Stedi users a JSON Web Token (JWT) containing the following AWS-specific principal tags:

"https://aws.amazon.com/tags": {
    "principal_tags": {
      "StediAccountId": [
        "39b2f40d-dc59-4j0c-a5e9-37df5d1e6417"
      ],
      "MemberRole": [
        "stedi:readonly"
      ]
    }
  }

The user can assume a role (specifically, they’re granted a time-bound role session) in their assigned AWS account if the following conditions are true:

  1. The token is issued by the referenced federation service and has the appropriate audience set.

  2. The role has a trust relationship granting access to the specified StediAccountId.

  3. The role has a trust relationship granting access to the specified MemberRole

The following snippet from our role trust policy evaluates these requirements in the Condition object. For example, we check whether the StediAccountId tag in the JWT token is equal to the MappedStediAccountId tag on the AWS account.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
      "Principal": {
        "Federated": {
          "Ref": "ProdOidcProvider"
        }
      },
      "Condition": {
        "StringEquals": {
          "tokens.saas.stedi.com/v1:aud": "tenants",
          "aws:RequestTag/StediAccountId": "${iam:ResourceTag/MappedStediAccountId}",
          "aws:RequestTag/MemberRole": "${iam:ResourceTag/MemberRole}"
        }
      }
    }
  ]
}

Caption: If the IAM role's resource tag for MappedStediAccountId and MemberRole matches the StediAccountId and MemberRole request tag (the JWT token principal tag), the user can access this role. Otherwise, role access is denied.

When assuming a role from a JWT token (or with SAML), STS reads the token claims under the principal_tags object and adds them to the role session as principal tags. 

However, during the AssumeRoleWithWebIdentity operation (within the policy logic), you must reference the principal tags from the JWT token as request tags because the IAM principal isn’t the one making the request, instead the tags are being added to a resource. Existing tags on the role are referenced as resource tags because they are tags on the subject of the operation.

These naming conventions are a bit confusing – more on that later.

Discovering the vulnerability

We set up our role trust policy based on this AWS tutorial, using JWT tokens instead of SAML. Another difference from the tutorial is that our policy uses variables to reference tags instead of hardcoding the values into the condition statements.

For example, "${aws:RequestTag/StediAccountId}": "${iam:ResourceTag/MappedStediAccountId}" instead of "${aws:RequestTag/StediAccountId}": 39b2f40d-dc59-4j0c-a5e9-37df5d1e6417".

During development, we began testing to determine whether our fine-grained access controls were working as expected. They were not. 

Finding the bug

Again and again, our tests gained access to roles above their designated authorization level.

We scoured the documentation to find the source of the error. The different tag types, IAM statement templating, and different (aws vs. iam) prefixes caused extra confusion, and we kept thinking we weren’t reading the instructions correctly. We attempted to use the IAM policy simulator but found it lacked support for evaluating role trust policies.

Eventually, we resorted to systematically experimenting with dozens of configuration changes. For every update, we had to wait minutes for our changes to propagate due to the eventual consistency of IAM. Four team members worked for several hours until we finally made a surprising discovery – the tag variable names affected whether trust policy conditions were evaluated correctly.

If the request tag referenced a principal tag called MemberRole in the JWT token, and the IAM role referenced a resource tag with the same variable name, the condition was always evaluated as true, regardless of whether the tag's values actually matched. This is how test users with stedi:readonly permissions in Stedi gained unauthorized admin access to their AWS accounts. 

Changing one of the tag variable names appeared to fix the issue. For example, the snippet below changes the resource tag variable name to MemberRole2. The policy only functioned properly when the variable names for the request and resource tags were different. 

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
      "Principal": {
        "Federated": {
          "Ref": "ProdOidcProvider"
        }
      },
      "Condition": {
        "StringEquals": {
          "tokens.saas.stedi.com/v1:aud": "tenants",
          "aws:RequestTag/StediAccountId": "${iam:ResourceTag/MappedStediAccountId}",
          "aws:RequestTag/MemberRole": "${iam:ResourceTag/MemberRole2}"
        }
      }
    }
  ]
}

Caption: Initial IAM vulnerability workaround – ensuring request tag and resource tag names did not match.

Alerting AWS

We used the documentation to construct a model of the role assumption process and contacted AWS Support and AWS Security on June 20, 2023 with our findings. We also contacted Chris Munns, Tech Lead for the AWS Startups team, who engaged directly with AWS Security and escalated the issue internally.

AWS was initially skeptical that the problem was with STS/IAM, which is understandable – we were too. They first suggested that we used the wrong prefixes in our condition statements (aws vs. iam), but we confirmed the issue occurred with both prefixes. Then, they suggested that the tag types in our condition statements were incorrect. After some back and forth, we ruled that out as well, once again noting that the tag naming conventions for the AssumeRoleWithWebIdentity operation are confusing.

In the following days, we investigated the issue further and found we could trigger the bug with STS AssumeRole calls, meaning the vulnerability was not limited to assuming roles with web identity or SAML. We also found that hard-coding one of the tag values in the policy statement did not expose the vulnerability. Only role trust policies that used a variable substitution for both the request tag and the resource tag in the policy statement resulted in the policy evaluating incorrectly.

We implemented a workaround (changing one of the variable names), confirmed our tests passed, and kept building.

Resolution

On July 6th, we received an email from AWS stating that their engineering team had reproduced the bug and was working on a fix. On October 30th, STS AssumeRole operations for all new IAM roles used an updated tag handling implementation, which provided the corrected tag input values into the logic to fix the role evaluation issue. This same change was then deployed for existing roles on January 9, 2024. AWS typically rolls out changes in this manner to avoid unexpectedly breaking customer workflows.

AWS also discovered the issue was not limited to role trust policies, which are just resource policies for IAM roles (as a resource) – it also extended to statements within IAM boundary policies and SCP policies that contained the same pattern of STS role assumption with tag-based conditions.

AWS notified customers with existing problematic roles, SCP trust policies, and boundary policies that had usage in the past 30 days. They also displayed a list of affected resources in each customer’s AWS Health Dashboard.

Timeline

  • 2023-06-20 - Role access issue discovered, AWS alerted

  • 2023-06-21 - Minimal reproduction steps provided using STS assume role, AWS acknowledges report and the issue is picked up by an engineer

  • 2023-07-06 - AWS acknowledges issue and determines root cause

  • 2023-10-30 - STS tag handling implementation updated for new IAM roles

  • 2024-01-09 - STS tag handling implementation updated for IAM roles for customers impacted in a 30-day window

What we learned

After we implemented our workaround, we conducted a retrospective. Here are our key takeaways: 

Even the most established software has bugs. 

This might seem obvious, but we think it’s an important reminder. We spent a lot of time second-guessing ourselves when discovering and diagnosing this bug. We were well aware of IAM’s provable security via automated reasoning, and the documentation is so comprehensive (and intimidating at times) that we were sure it had to be our fault. Of course, you should do your due diligence before reporting issues, but no system is infallible. Sometimes, it is AWS.

Glossaries and indexes are underrated. 

Defining service-specific terminology in a single location can be game-changing for users onboarding to a new product and can dramatically speed up the debugging process. 

We struggled to understand the difference between global condition keys with the  “aws:” namespace and service-specific keys with the  “iam:” namespace. We were further confused by how these keys can overlap; the “iam:ResourceTag” and “aws:ResourceTag” resolve to the same value. Finally, it was hard to keep track of the lifecycle from a jwt principal tag becoming a request tag before finally being a resource tag. 

The AWS documentation provides all this information, but we lacked the proper vocabulary to search for it. A comprehensive glossary would have saved us significant time and effort. We’re now adding one to the Stedi docs to better serve our own users.

We need better tools for testing IAM policies. 

The IAM policy simulator does not support role trust policy evaluation. Proving the security of a system to grant federated identities access to IAM roles continues to rely on both positive and negative end-to-end tests with long test cycles. Developing more mature tooling would massively improve the developer experience, and we hope AWS will consider investing in this area moving forward. 



Thank you to all the Stedi team members who contributed to uncovering this issue and the AWS team for working with us to find a solution.

Apr 4, 2024

Engineering

Editor’s note: Every company has a way of working. I wrote this document several years ago when we were still figuring out our way of working – “how we do things here” – and were spending a lot of time aligning on decisions of all sizes. We talked about it a lot at the time, but we rarely have to reference it now – as Alfred North Whitehead said, "Civilization advances by extending the number of important operations which we can perform without thinking of them."

The most difficult part of hiring for any company is finding people who are philosophically aligned with a certain way of working. We are hiring across multiple engineering, product, and design roles right now, so we wanted to post this publicly to give a sense of what it’s like to work here. If this resonates with you, we would love to hear from you. – Zack Kanter

——

This document captures the cornerstones of Stedi’s engineering culture. As a result of following the framework, product development may sometimes come to a screeching halt. The work itself may be tedious and frustrating. Progress may take many multiples of the time it would take using other methods.

This impact is short term, immaterial, and ultimately irrelevant. The framework aligns with our long-term ambitions; nothing is more important than keeping a high bar across these dimensions.

That's the difference between principles and preferences: principles are what you do regardless of the cost – everything else is just a preference.

Key tenets

  • Minimize long-term operational toil.

  • Eliminate waste.

Practices

The following practices represent the bare minimum expectation of all teams:

  • Security is job zero.

    • There is nothing more important than securing our systems.

  • Prioritize on-call.

    • Implement thorough, pragmatic alerting and alarming.

    • The baseline for alerts and alarms is expected to be zero.

    • Prioritize work to mitigate the causes of pages above all else except for security.

  • Define all infrastructure as code.

    • Define all production infrastructure as code, from build and deployment pipelines to operational dashboards.

    • Automate dependency updates and deployments to production.

    • Keep all dependencies up to date across all non-archived repositories.

    • Dependencies can be updated individually or in batches, in real-time or subject to some delay (for security or convenience), but the process must be automated.

  • Investigate service metrics, build times, and infrastructure costs.

    • Review service metrics, build times, and infrastructure costs (both per-request and total spend).

    • The goal is not to strictly minimize these metrics via unbounded investment that has diminishing returns, but rather to surface flaws in configuration or implementation that are causing unnecessary waste.

  • Pull pain forward.

    • When a service’s future is assured (that is, when it reaches or will soon reach GA), pull forward technical problems that get harder over time (e.g., backwards-incompatible changes; major architectural shortcomings).

Practices for practices

‘Best effort’ is ineffective – if ‘best effort’ were all it took, everything would be in place already!

Instead, implement mechanisms for continuous improvement – generally: preventative controls and/or detective controls wherever possible, with a root cause analysis process for any exceptions.

Component selection

In the fight against entropy, we use the following prioritization framework for component types:

  1. Managed, ‘serverless’ primitives.

    • Use managed, serverless (defined as ‘usage-based pricing with little to no capacity planning required’) primitives wherever possible, even at the cost of functionality.

    • Within those managed service options, use the highest-order primitive that fits a given use case.

  2. Open source libraries and open standards.

    • Where a suitable managed service cannot be found, use open source libraries and adopt open standards wherever possible.

  3. Our own code and formats.

    • When all other options have been exhausted, build it ourselves.

Selection framework for component sources

  • Optimize for a cohesive set of components rather than selecting best-in-breed options, even when more compelling or popular alternatives exist.

  • When choosing a set of components, invest in continuously-compounding ecosystems built by high-velocity organizations who are philosophically aligned with Stedi. Current ecosystems include AWS (infrastructure) and GitHub (source control, build, and deployment).

  • Introduce new ecosystems at a clear boundary (e.g., GitHub for source control, build, and deployment of code; AWS for running the code), rather than in the middle of a system. For example, we would not use a database hosted at PlanetScale in an otherwise all-AWS backend.

Refactors

We often identify more suitable ways of building something (that is, more suitable according to the framework listed above) after we’ve begun, or after we’ve finished. For example, we might start with writing our own code, only to later realize that what we’ve written is similar to some open source library instead.

When the refactor:

  • will get easier over time, we’ll wait until it gets easier.

  • will get harder over time, we'll do it without delay.

Generally, the framework when considering a refactor is to ask: if we were starting again today knowing everything we know now, would we build it this new way that we’re considering? If yes, will it get easier or harder over time? If you’d build it the new way and it will get harder over time, do it now.

That said, large-scale lateral migrations (lifting-and-shifting that results in a functionally-equivalent customer experience) are extremely costly. We try to avoid these when possible.

Communication

Discussing tradeoffs

Like all approaches to building software, Stedi’s approach comes with many tradeoffs – including, but certainly not limited to:

  • Managed services, open source libraries, and open standards often have steep learning curves and poor documentation; they are often inflexible and lacking functionality.

  • Managed services are harder to test, slower to iterate against, and harder to diagnose; they are expensive and have unpredictable roadmaps.

  • Maintaining infrastructure as code is tedious and painful.

  • Automated dependencies updates are distracting and error-prone.

These same tradeoffs will show up again and again; enumerating them at each juncture is distracting and demoralizing. Instead, focus discussions on mitigation. For example:

  • “Given that this managed service will be hard to unit test. Let’s come up with a plan for how we can ship with confidence.”

  • “Since the cloud deployment feedback loop is slower than local development, we should invest in tooling to speed this up.”

  • “The documentation in this AWS library is sparse and outdated, so let’s make sure we contribute back early and often before we lose the context.”

  • “This AWS service doesn’t support this feature we’ll want down the line, so let’s schedule a meeting with the AWS PM to see if it’s on their roadmap before building it ourselves.”

Discussing roadblocks

Technology evolves rapidly. All features and functionality we use today did not exist at one point; features and functionality that don’t exist today might exist tomorrow. Most importantly, features and functionality you think don’t exist today might already exist.

When hitting a roadblock or an apparent dead end – for example, when it appears that a certain managed service or library isn’t able to do something – draw a distinction between when you’ve definitively determined that something is not supported, vs. when you’ve exhausted ideas that you’ve come up with but have not definitively proven it isn’t possible.

In other words: ‘Absence of evidence’ is not ‘evidence of absence.’ False determinations of impossibilities are extremely costly to us, particularly because the false determination in one corner of Stedi spreads to all of Stedi.

Something is definitive when you can provide a link to source documentation confirming that it isn’t possible.

  • Acceptable: “A Lambda can’t run for more than 15 minutes – see documentation: Function timeout – 900 seconds (15 minutes).”

  • Unacceptable: “X-ray doesn’t support cross-account traces [no citation].”

When you have tried a number of things but haven’t been able to make it work, that is not definitive.

  • Acceptable: “I haven’t been able to lock down this AWS resource via a tenant-scoped role. Here’s what I’ve tried…”

  • Unacceptable: “Tenant scoped IAM access won’t work for this.”

If you have tried a number of things and haven’t been able to make something work, post somewhere: “I haven’t been able to make this work, and I’m running out of ideas/time. Here’s what I’ve tried…” The fastest way to lose credibility here is to falsely and authoritatively proclaim that something isn’t possible without having done the work to back it up.

On the flip side, if you see something pronounced as impossible without the supporting documentation, ask for the documentation. If you see this happening and fail to ask for the work to back it up, you have lowered our bar for technical rigor.

Written communication

Our standard of “Write important things down” doesn’t mean “record actions already taken.”

The most important function of writing is as a tool for thinking. It follows that writing should almost always precede action, particularly in software development.

Paul Graham explained this nicely in an essay that I’ve pulled passages from below:

“Writing about something, even something you know well, usually shows you that you didn't know it as well as you thought. Putting ideas into words is a severe test.

Once you publish something, the convention is that whatever you wrote was what you thought before you wrote it. These were your ideas, and now you've expressed them. But you know this isn't true.

It's not just having to commit your ideas to specific words that makes writing so exacting. The real test is reading what you've written. You have to pretend to be a neutral reader who knows nothing of what's in your head, only what you wrote.

There may exist people whose thoughts are so perfectly formed that they just flow straight into words. But I've never known anyone who could do this, and if I met someone who said they could, it would seem evidence of their limitations rather than their ability. Indeed, this is a trope in movies: the guy who claims to have a plan for doing some difficult thing, and who when questioned further, taps his head and says "It's all up here." Everyone watching the movie knows what that means. At best the plan is vague and incomplete. Very likely there's some undiscovered flaw that invalidates it completely.

In precisely defined domains it's possible to form complete ideas in your head. People can play chess in their heads, for example. And mathematicians can do some amount of math in their heads, though they don't seem to feel sure of a proof over a certain length till they write it down. But this only seems possible with ideas you can express in a formal language.

The reason I've spent so long establishing this rather obvious point is that it leads to another that many people will find shocking. If writing down your ideas always makes them more precise and more complete, then no one who hasn't written about a topic has fully formed ideas about it. And someone who never writes has no fully formed ideas about anything nontrivial.

It feels to them as if they do, especially if they're not in the habit of critically examining their own thinking. Ideas can feel complete. It's only when you try to put them into words that you discover they're not. So if you never subject your ideas to that test, you'll not only never have fully formed ideas, but also never realize it.

Putting ideas into words is certainly no guarantee that they'll be right. Far from it. But though it's not a sufficient condition, it is a necessary one.”

Writing a doc is not a perfunctory gesture, and asking someone for a doc on something is not a punishment or a mechanism for control. Writing a doc is a way of:

  • surfacing requirements and assumptions, and

  • driving clarity of reasoning stemming from those requirements and assumptions.

Without this step, our software has little hope of delivering the results we want over the long term.

Note that a doc doesn’t necessarily have to take the form of prose – in some cases, the right format for a doc could be a proposed API spec with bullet point lists of constraints, principles, or requirements. The goal of a doc is to reify your thinking and to share it with others.

As a final thought, not everyone has to write docs here. Some people just want to execute, and there is plenty of room for that, too – but if you just want to execute, you’ll be executing on the plan, architecture, or implementation described in someone else’s doc. Our domain is too complex, and our ambitions are too large to build software willy-nilly.

Mar 3, 2024

Products

The prolonged Change Healthcare outage due to a cyberattack has left thousands of healthcare providers unable to submit claims, eligibility checks, and other critical transactions to payers. The outage is expected to continue for weeks. Companies have been scrambling to implement solutions to get back online but are faced with large development efforts to switch to bespoke API formats offered by other clearinghouses.

Stedi's drop-in replacement for Change Healthcare's Professional Claims V3 and Eligibility V3 APIs allow customers who have been using Change to directly switch with minimal development effort. Customers can use the same Change API JSON or X12 request format with Stedi’s APIs, and Stedi will automatically submit transactions to payers and other clearinghouses as necessary. Our APIs are Generally Available today and we are able to get customers live on a same-day turnaround.

Our primary goal is to help providers submit claims and eligibility checks as quickly as possible. We’ve created a streamlined contracting process along with a standardized price list and the ability to match volume pricing previously provided by Change. We’ve set up a dedicated email address for inquiries – change@stedi.com – and can establish a dedicated Slack channel to start working with engineering teams in under an hour from first contact.

Stedi’s Claims and Eligibility APIs

Our APIs are designed as drop-in replacements for Change Healthcare’s Professional Claims V3 and Eligibility V3 APIs. The APIs:

  • Accept Change Healthcare's JSON request format.

  • Translate it to X12 837P for claims and X12 270 for eligibility.

  • Submit the transactions to payers and clearinghouses using SFTP and real-time EDI.

  • Return the Claims and Eligibility API response in Change Healthcare's JSON format.

  • Have full support for returning 277/835 files as JSON-formatted webhooks to endpoints of your choosing (to replace Change’s Claims Responses and Reports V2 API functionality), as well as 999s and other transaction types.

Visit our API documentation for details on API endpoints available as well as example request and response payloads.

Security-first architecture

Stedi is SOC 2 Type II and HIPAA compliant. We view these certifications as a minimum floor and have built security into our platform from the ground up:

  • Stedi’s Healthcare APIs are built 100% on AWS infrastructure with no other external vendor dependencies.

  • Customer resources are stored in dedicated single-tenant AWS accounts for strict tenancy isolation, with one customer per AWS account.

  • Stedi HIPAA accounts are secured by mandatory multi-factor Authentication (MFA) and have role-based access control (RBAC) powered by Amazon Verified Permissions.

You can see more in our Trust Center.

Get in touch

To get into our priority queue for an immediate response, visit stedi.com/healthcare to submit a contact request or email us at change@stedi.com. We can start working with you immediately to get back online.

Jan 15, 2024

Products

Today, we’re excited to showcase the Stedi EDI platform, a modern, developer-friendly EDI system that includes everything you need to build end-to-end integrations with your trading partners.

No-code EDI

The Stedi EDI platform includes all of the turn-key functionality you need to send and receive EDI. Within minutes, you can configure a new trading partnership, import EDI specifications from Stedi’s extensive Network of pre-built partner integrations, enable SFTP/FTPS/AS2 connectivity, ingest EDI files, and post JSON transactions to any API endpoint – all without writing a single line of code.

Once configured, Stedi turns EDI into just another API integration. As your trading partner sends you EDI, Stedi outputs JSON webhooks that you can ingest into your system. For outbound transactions, a single API endpoint enables you to generate and deliver fully-formed EDI files back to your trading partner.

All of Stedi’s functionality is available instantly via self-service, but you don’t need to do it all yourself. We can help you configure a new trading partner integration end-to-end on a free 30-minute demo call – and then support you all the way to production and beyond.

World-class support

Over the past 6 years of working with customers via Slack, Zoom, Teams, email, text, phone, forums, and more, we have built a lightning-fast, exceptionally knowledgeable support team. Every day, they work with customers to solve onboarding and operational problems ranging from troubleshooting EDI errors and meeting with key trading partners to helping extend Stedi for custom integration needs.

This world-class premium support is included at no additional cost.

Get started with Stedi

The Stedi EDI platform provides everything you need to build end-to-end EDI integrations in days. There's no faster way to get started with EDI.

To get started, book a demo with our team.

Nov 30, 2023

EDI

So, you’ve been tasked with “figuring out EDI.” Maybe you’re in the supply chain or logistics world, or maybe you’re building a product in the healthcare or insurance space – chances are that you’re reading this because one of your large customers, vendors, or partners said that if you want to move the relationship forward, you have to do something called EDI: Electronic Data Interchange.

Maybe they sent you a sample file that looks like it came off a dot matrix printer in 1985, or maybe they sent you a PDF “mapping guide” that doesn’t seem to make much sense.

Regardless of what you’re starting with, this post will give you, a modern software developer, a practical crash course in EDI. It will also explain how to build an EDI integration on Stedi that transforms EDI to and from JSON.

  • The two most common questions about EDI

  • Overview of an EDI integration

  • Step 1: Add your trading partner to Stedi

  • Step 2: Transform JSON from Stedi to your API shape

  • Step 3: Configure a webhook to send transactions to your API

The two most common questions about EDI

This section provides some context. If you want to just start building, skip to Add your trading parter to Stedi.

Raw EDI next to a page from an EDI mapping guide

What is EDI?

You may have heard a lot of jargon or acronyms when researching EDI – things like AS2, VANs, or MFT. Put simply, EDI is a way to get data from one business system into another.

Your customer, vendor, or partner wants to exchange data with you, and they need you to work with a very specific file format. The process of exchanging that specific file format is called EDI. The file itself is called an EDI file or EDI document. Each EDI file can contain one or more transaction types - called transaction sets – each with a unique numeric code and name to identify it. In the EDI world, your customer, vendor, or partner is called a trading partner. This post will use all these terms going forward.

The subject of EDI typically comes up because a company like yours wants to achieve some business goal with your trading partner. Those business goals vary widely, but they all require either sending data to a trading partner, receiving data from a trading partner, or both.

Here are a few common examples. EDI spans many industries, so if you don’t see your use case listed, it’s not because it isn’t supported – it’s just because we can’t list out every possible flow here.

Logistics

  • 204 Request pickup of a shipment (load tender)

  • 990 Accept or reject the shipment (response to a load tender)

  • 214 Shipment status update

  • 210 Invoicing details for a shipment

Warehousing

  • 940 Request shipping from a warehouse

  • 945 Communicate fulfillment to a seller

  • 944 Indicate receipt of a stock transfer shipment

  • 943 Stock transfer shipment notification

Retail

  • 850 Send a purchase order to a retailer

  • 855 Send a purchase order acknowledgment back to a customer

  • 856 Send a ship notice to a retailer

  • 810 Send an invoice

Healthcare

  • 834 Send benefits enrollments to an insurance provider

  • 277 Indicate the status of a healthcare claim

  • 276 Request the status of a healthcare claim

  • 278 Communicate patient health information

  • 835 Make a payment and/or send an Explanation of Benefits (EOB) remittance advice

If it’s just data exchange, can’t we use an API instead?

The short answer is no.

Exactly why your trading partner wants to exchange files instead of integrating with an API is a much longer story (and is out of the scope of this blog post), but EDI has been around since the 1960s and even hypermodern companies like Amazon use it as their primary method for trading partner integration. If you plan to wait around for your trading partner to roll out an API, you’ll likely be waiting a long time.

Overview of an EDI integration

To achieve some business goal, your trading partner wants to either send EDI files to you, receive EDI files from you, or both. But what is it that you are trying to accomplish? In all likelihood, you’re trying to get data into or out of your system.

For the purposes of simplifying this post, we’re going to assume two things: first, that your business system has an API, and second, that the API can accept JSON as a format. We’re also going to focus only on receiving EDI files from your trading partner – getting data into your system – because otherwise this post would get too long.

When you build an EDI integration on Stedi, the end-to-end flow for an inbound file (from your trading partner) will look something like this:

End-to-end inbound EDI integration on the Stedi platform.
  1. Receive an EDI file through a configured connection (SFTP/FTPS or AS2).

  2. Translate the file to JSON according to your partner’s EDI requirements.

  3. Use a Destination webhook to automatically send the JSON to your API. The webhook may be configured to use a Stedi mapping to transform the JSON to a custom shape before sending. You can also just receive raw JSON from Stedi and use other methods to transform it into the shape you need for your API (more on that in step 2).

Step 1: Add your trading partner to Stedi

Adding a trading partner configuration to the Stedi platform takes about 10-15 minutes and doesn’t require any code.

End-to-end inbound EDI integration on the Stedi platform.

Here’s a video of adding a trading partner that demonstrates each of the following steps in detail.

  • Create a partnership: A partnership defines the EDI relationship between you and your trading partner. Within the partnership, you’ll define a connection to exchange files, specify the EDI transactions you plan to send and receive, and configure other settings, such as whether to send automatic acknowledgments.

  • Configure a connection: Set up a connection to exchange files with your trading partner. Stedi supports SFTP/FTPS, remote SFTP/FTPS, and AS2.

  • Add transaction settings: Create a transaction setting for each EDI transaction you plan to send or receive. For example, an inbound transaction setting to receive 850 Purchase Orders and an outbound transaction setting to send 855 Purchase Order Acknowledgments. This process involves specifying a Stedi guide for the transaction type. Stedi guides are a machine-readable format for trading partner EDI specifications, and Stedi uses them to validate data according to your partner’s requirements.

Stedi automatically translates the EDI files your trading partner sends over the connection into JSON. The next step is to map the JSON fields in Stedi’s output to the JSON shape you need for your API.

Step 2: Transform JSON from Stedi to your API shape

There are three ways you can transform Stedi transaction data (JSON) into a custom format: Stedi Mappings, writing custom code, and using an iPaaS platform. This post demonstrates the Stedi Mappings method, and you can check out our docs for more details about the alternatives.

Guide JSON: Stedi’s JSON EDI format

Stedi converts EDI files into a human-readable JSON format called Guide JSON.

The following EDI example is is an Amazon 850 Purchase Order that follows Amazon’s specific requirements for constructing a Purchase Order transaction (each company has their own requirements for each transaction type). It’s formatted according to the X12 Standard, which is the most popular standard in North America.

You can play around with it by opening the example file in our Inspector tool. On the right, Stedi shows what each data element in the EDI transaction means. Open in Inspector →

ISA*00*          *00*          *ZZ*AMAZON         *ZZ*VENDOR         *221110*0201*U*00401*000012911*0*P*>~
GS*PO*AMAZON*VENDOR*20221110*0201*12911*X*004010~
ST*850*0001~
BEG*00*NE*T82Z63Y5**20221110~
REF*CR*AMZN_VENDORCODE~
FOB*CC~
CSH*N~
DTM*064*20221203~
DTM*063*20221209~
N1*ST**92*RNO1~
PO1*1*8*EA*39*PE*UP*028877454078~
PO1*2*6*EA*40*PE*UP*028877454077~
CTT*2*14~
SE*12*0001~
GE*1*12911~
IEA*1*000012911

The following example shows the Guide JSON shape for the translated Amazon 850 Purchase Order.

{
  "heading": {
    "transaction_set_header_ST": {
      "transaction_set_identifier_code_01": "850",
      "transaction_set_control_number_02": 1
    },
    "beginning_segment_for_purchase_order_BEG": {
      "transaction_set_purpose_code_01": "00",
      "purchase_order_type_code_02": "NE",
      "purchase_order_number_03": "T82Z63Y5",
      "date_05": "2022-11-10"
    },
    "reference_identification_REF_customer_reference": [
      {
        "reference_identification_qualifier_01": "CR",
        "reference_identification_02": "AMZN_VENDORCODE"
      }
    ],
    "fob_related_instructions_FOB": [
      {
        "shipment_method_of_payment_01": "CC"
      }
    ],
    "sales_requirements_CSH": [
      {
        "sales_requirement_code_01": "N"
      }
    ],
    "date_time_reference_DTM_do_not_deliver_before": [
      {
        "date_time_qualifier_01": "064",
        "date_02": "2022-12-03"
      }
    ],
    "date_time_reference_DTM_do_not_deliver_after": [
      {
        "date_time_qualifier_01": "063",
        "date_02": "2022-12-09"
      }
    ],
    "name_N1_loop": [
      {
        "name_N1": {
          "entity_identifier_code_01": "ST",
          "identification_code_qualifier_03": "92",
          "identification_code_04": "RNO1"
        }
      }
    ]
  },
  "detail": {
    "baseline_item_data_PO1_loop": [
      {
        "baseline_item_data_PO1": {
          "assigned_identification_01": "1",
          "quantity_ordered_02": 8,
          "unit_or_basis_for_measurement_code_03": "EA",
          "unit_price_04": 39,
          "basis_of_unit_price_code_05": "PE",
          "product_service_id_qualifier_06": "UP",
          "product_service_id_07": "028877454078"
        }
      },
      {
        "baseline_item_data_PO1": {
          "assigned_identification_01": "2",
          "quantity_ordered_02": 6,
          "unit_or_basis_for_measurement_code_03": "EA",
          "unit_price_04": 40,
          "basis_of_unit_price_code_05": "PE",
          "product_service_id_qualifier_06": "UP",
          "product_service_id_07": "028877454077"
        }
      }
    ]
  },
  "summary": {
    "transaction_totals_CTT_loop": [
      {
        "transaction_totals_CTT": {
          "number_of_line_items_01": 2,
          "hash_total_02": 14
        }
      }
    ],
    "transaction_set_trailer_SE": {
      "number_of_included_segments_01": 12,
      "transaction_set_control_number_02": 1
    }
  }
}

Let’s compare a smaller snippet of the Guide JSON to the original EDI file.

Stedi turns this EDI line:

BEG*00*NE*T82Z63Y5**20221110

into the following object in Guide JSON:

"beginning_segment_for_purchase_order_BEG": {
"transaction_set_purpose_code_01": "00",
"purchase_order_type_code_02": "NE",
"purchase_order_number_03": "T82Z63Y5",
"date_05": "2022-11-10"
},

Notice how Guide JSON makes it easier to understand each data element in the transaction.

Create a Stedi Mapping

Now that you understand Guide JSON, you can map fields from Stedi transactions into a custom shape for your API. If you plan to do this outside of Stedi, you can skip to step 3.

The following example shows a JSON payload for a very simple Orders API endpoint. This happens to be an API for creating e-commerce orders, but it could just as easily be an API for anything from railroad waybills to health insurance eligibility checks.

The Orders API:

{
  "records": [
    {
      "fields": {
        "purchaseOrderNumber": "PO102388",
        "orderDate": "223-11-24",
        "lineItems": [
          {
            "sku": 123,
            "quantity": 2,
            "unitPrice": 10.0
          },
          {
            "sku": 456,
            "quantity": 2,
            "unitPrice": 12.0
          }
        ]
      }
    }
  ]
}

Assuming that each one of these fields is mandatory, you can extract this data out of the EDI files - in this case, 850 Purchase Orders - by creating a Stedi mapping.

Stedi Mappings is a powerful JSON-to-JSON transformation engine. To transform the Guide JSON from an 850 Purchase Order into the shape for the Orders API, you would create the following Stedi mapping. Note that you don’t need to map every field from the original transaction, only the fields you need for your API.

While is a very simple one-to-one mapping, Stedi Mappings supports the full power of the JSONata language, allowing you to combine fields, split text, and more. Mappings also supports lookup tables that you can use to replace fields from the original transaction with a list of static values (for example, automatically replacing a country code like USA with its full name United States).

For detailed instructions and more examples, check out the Mappings documentation.

Step 3: Configure a webhook to send transactions to your API

Once you create your mapping, you can attach it to a Destination webook to automatically transform JSON transactions from Stedi into a custom shape before automatically sending them to your API.

You can set up an event binding for transaction.processed.v2 events that triggers the webhook every time Stedi successfully processes a 850 Purchase Order.

End-to-end inbound EDI integration on the Stedi platform.

Get started on Stedi

Now you know how to take an EDI file, translate it into Guide JSON, transform it into your target API shape, and automatically send it to your API. Check out the following resources as you keep building:

  • EDI 101 for a deeper dive into EDI standards and format.

  • Stedi Network for free access to EDI guides for hundreds of popular trading partners that you can use to configure integrations.

To get started building EDI integrations on Stedi, book a demo with our team.

Nov 14, 2023

EDI

Large EDI files are common across many industries, including healthcare (such as 834 Benefit Enrollments), logistics (210 Motor Carrier Freight Details and Invoices), and retail (846 Inventory Advice). Unlike most other EDI solutions, Stedi has virtually no file size limitations and can process EDI files that are gigabytes in size.

However, large files that have been translated into JSON still pose significant challenges for downstream applications. The translation ratio of an EDI file to JSON is typically 1:10, which means that a large EDI file can easily produce a multi-gigabyte payload that is difficult for downstream applications to receive and ingest.

To solve this problem, we’re excited to introduce Fragments, a new feature in our Large File Processing module. Fragments allow you to automatically split processed transactions into smaller chunks for easier downstream ingestion.

Use fragments to split large transactions

Large files are often the result of transactions containing many repeated loops or segments. A healthcare provider may send an 834 Healthcare Benefit Enrollment file containing tens of thousands of individual members – each one in the INS segment – or a brand may send an 846 Inventory Inquiry/Advice file containing millions of SKUs – each one in the LIN segment.

With Fragments, you can use repeated segments like these to split the transaction. For example, when you enable fragments on the INS loop in an 834, Stedi emits batches of INS loops instead of a single giant JSON payload.

For each fragment batch, Stedi emits a fragment.processed.v2 event, which you can use to automatically send Destination webhooks to your API. Fragment processed events contain details about the original transaction as well as the actual fragment payload itself.

An 834 Benefit Enrollment split into fragments

Configure fragments

You can configure fragments for any transaction in minutes without writing any code. Before you begin, create a partnership in Stedi between you and your trading partner.

Set up the Stedi guide

First, you need to enable fragments in the Stedi guide for the transaction.

Stedi guides are a machine-readable format for the trading partner EDI specifications you typically receive as PDF or CSV files. The Stedi Network has pre-built guides for hundreds of popular partners that you can import into your account and use in your integrations for free.

You can enable fragments on one repeated segment within each guide. To enable fragments, open the guide in your Stedi account, click the segment, toggle Set as fragment to ON, and click Publish changes.

Enable fragments in a Stedi guide

Create a transaction setting

Next, you need to create an inbound transaction setting. Transaction settings define the EDI transactions you plan to exchange with your trading partner and the guide Stedi should use to validate data.

When creating the transaction setting, choose the guide you previously configured with fragments and then toggle Enable fragments to ON.

Create an inbound transaction setting with fragment-enabled guide

Send fragments to your API

Finally, you can automatically deliver fragments to your API. To do this, add an event binding for fragment.processed.v2 events to a Destination webhook.

Set up a destination webhook for fragment events

You can also use the Get Fragment API to manually retrieve fragments for processed transactions as needed.

  curl --request GET \
    --url https://core.us.stedi.com/2023-08-01/transactions/{transactionId}/fragments/{fragmentIndex} \
    --header 'Authorization: Key {STEDI_API_KEY}'

Get started on Stedi

Stedi allows you to seamlessly process large EDI files of virtually any size. You can also use fragments to efficiently manage large transactions and reduce the development work required to integrate with new trading partners. Check out the Fragments documentation for complete details.

To get started building EDI integrations on Stedi, book a demo with our team.

Jul 12, 2023

Products

We recently launched Stedi Core, an event-driven EDI system that does most of the heavy lifting for EDI integrations. Core can validate, parse, and generate EDI for any trading partner and provides complete visibility into your real-time transaction data.

After configuring Core, you can use Stedi Functions to create an end-to-end flow between Core and your internal systems and business applications. Functions can react to Core events to run custom code. You can transform the data shape, call out to external applications and APIs, or extend Stedi to fulfill any requirement.

We want to showcase two features you can use to automatically invoke functions in your EDI integration: event bindings and function schedules.

Event bindings

Core emits events for every file, functional group, and transaction set it processes successfully. Core also emits events for processing failures. Visit the Core events documentation for details.

You can configure event bindings on a function so the function is automatically invoked in response to specific Core events.

Create event binding in Functions UI

Example: Send inbound purchase orders to an ERP system

The following function has an event binding that listens to transaction.processed events for inbound 850 Purchase Order transactions from a specific trading partner. When Core successfully processes an 850 transaction, it fires an event. The event binding invokes this function when the filter criteria are met.

The function uses a Stedi mapping to transform the translated JSON output from Core to the JSON shape required by the ERP system. It then calls the ERP system’s API and sends the transformed payload to create the purchase order within the system.

const buckets = bucketsClient();
const mappings = mappingsClient();

export const handler = async (event) => {
  const transactionObject = await buckets.getObject({
    bucketName: event.detail.output.bucketName,
    key: event.detail.output.key,
  });
  const content = JSON.parse(await transactionObject.body?.transformToString());

  const erpJson = await mappings.mapDocument({
    id: "<your_mapping_id>",
    content,
    validationMode: "strict",
  });

  await fetch(process.env.ERP_CREATE_PURCHASE_ORDER, {
    method: "POST",
    body: JSON.stringify(erpJson),
    headers: {
      "Authorization": `Key ${process.env.ERP_API_KEY}`,
      "Content-Type": "application/json",
    },
  });
};

You can find additional example code on GitHub.

Example: Publish processing failures to Slack

Stedi Core emits file.failed events when it cannot process an inbound file or when it cannot deliver a generated outbound file to a partner. For example, Core emits a file.failed event when your partner sends an invalid EDI file.

The following function has an event binding that listens to file.failed events and posts a Slack webhook to notify a support team to take action. You can use this approach to integrate with your own alerting system.

export const handler = async (event) => {
  const lines = [
    `ISA IDs: receiver: ${event.interchange.receiverId},
       sender: ${event.interchange.senderId}`,
    `File ID: ${event.fileId}`,
    `Direction: ${event.direction}`,
    ...event.errors.map((err) => `Error: ${err}`),
  ];

  await fetch(process.env["SLACK_URL"], {
    method: "POST",
    body: JSON.stringify({
      blocks: [
        {
          type: "header",
          text: {
            type: "plain_text",
            text: "⚠️ Stedi Core File Processing Failed",
            emoji: true,
          },
        },
        ...lines.map((line) => ({
          type: "section",
          text: {
            type: "mrkdwn",
            text: line,
          },
        })),
      ],
    }),
  });
};

You can find full template code on GitHub.

Custom schedules

You can quickly set up a basic schedule in the Functions UI to run your function periodically at a set frequency (every X minutes, hours, or days). You can also define advanced schedules with custom expressions to set the frequency of execution, such as specific dates and times in a week.

Create schedule in the Functions UI

Example: Poll for inventory data

The following function polls to check inventory levels for product SKUs. When products run low on inventory, the function automatically generates an 850 purchase order based on current stock and preferred vendor information.

Stedi Core uses Stedi guides to generate EDI according to partner-specific requirements. Each guide’s JSON schema represents the shape of the EDI transaction set that the guide defines. This example uses a Stedi mapping to transform the JSON inventory payload into the required schema for 850 purchase orders.

const mappings = mappingsClient();
const core = coreClient();

export const handler = async () => {
  const lowInventoryItems = await fetch(process.env.ERP_LOW_INVENTORY_URL, {
    method: "GET",
    headers: {
      Authorization: `Key ${process.env.ERP_API_KEY}`,
    },
  });

  // Use Stedi mapping to transform JSON schema
  const lowInventoryOrdersPromises = (await lowInventoryItems.json()).map(async (item) => {
    const orderJson = await mappings.mapDocument({
      id: "<your_mapping_id>",
      content: item,
      validationMode: "strict",
    });

    return core.generateEdi({
      partnershipId: orderJson.partnershipId,
      transactionGroups: [
        {
          transactionSettingsId: "004010-850",
          transactions: orderJson.transactions,
        },
      ],
    });
  });

  await Promise.all(lowInventoryOrdersPromises);
};

Complete your integration with Stedi Functions

Functions allow you to extend Stedi’s event-driven architecture to fulfill any requirement and build end-to-end EDI integrations tailored to your business.

Book a demo with our onboarding team, and we'll help you set up Stedi Core for your first trading partner and create the functions you need to complete your integration.

Jun 20, 2023

Products

EDI formats are designed to use as little space as possible, but most companies still eventually need to process files that are tens or hundreds of megabytes. Unfortunately, large EDI files are a notorious pain point for EDI systems, as most cannot handle payloads over a few dozen megabytes and sometimes take hours or days to work through larger payloads.

We are excited to announce that Stedi Core now supports validating and parsing EDI files up to 500MB without pre-processing or custom development required.

Large file support in Stedi Core

Stedi Core is an EDI integration hub that translates inbound EDI to JSON and generates outbound EDI from JSON—for any of the 300+ transaction sets across every X12 version release.

With the latest enhancements, Core can reliably ingest files up to 500MB and individual transaction sets up to 130MB, with processing times ranging from seconds to minutes.

If your integration requires parsing EDI files or transaction sets beyond Core’s current upper limits, Stedi can automatically split files and process them in manageable pieces. With this approach, you can use Core to process transaction sets and files of virtually any size.

Is there a size limit for EDI files?

No. Neither the X12 nor EDIFACT standards explicitly specify an upper limit for file size.

Even though there are no explicit limits, your EDI system should list upper bounds for the file size and individual transaction set size it can handle successfully. However, it can be hard to predict whether files approaching the upper limit will produce failures.

This is because the upper limit of what an EDI system can successfully process often depends not on the file size but on the size and complexity of the largest EDI transaction set within the file. Sometimes files are 500MB, but no single transaction is larger than 1MB. In other cases, a 60MB file contains a single transaction set that's 50MB. Depending on how the system is designed, it may have no problem parsing the former but a lot of trouble parsing the latter.

Large EDI file examples

There are many scenarios in which it’s important to have an EDI solution that can handle large files.

  • Many transaction sets within a single file: Your trading partner may send batches of business data instead of generating a new EDI file for every transaction set. For example, a carrier may batch multiple 210 Motor Carrier Freight Details and Invoice transactions into a single file at regular intervals.

  • Very large transaction sets: Some transaction sets can contain many repeated segments or loops. For example, a healthcare provider could send an 837 HealthCare Claim that contains multiple, separate insurance claims, or a brand may send an 846 Inventory Inquiry/Advice file containing millions of SKUs.

  • A lot of data in one or more elements: For example, a single 275 Patient Information transaction containing x-ray images could produce a very large file.

Try Stedi Core

Stedi Core seamlessly processes large EDI files and allows you to search, inspect, and debug all real-time transaction data.

Book a demo, and we’ll help you set up Stedi Core for your first trading partner. Stedi has a generous free tier for evaluation and transparent pricing with no hidden fees.

Jun 6, 2023

Products

Over the past 18 months, Stedi launched eight key building blocks for developing EDI systems.

We designed these developer-focused products to address the major flaws in existing EDI solutions: a lack of control, extensibility, or both. While our building blocks solved these problems, stitching them together into an end-to-end EDI integration still required substantial development effort—until today.

We are excited to introduce the new, integrated Stedi platform. Stedi is a turn-key, event-driven EDI system that allows you to configure EDI integrations in minutes without being an EDI or development expert.

Stedi: The hub for EDI integrations

Stedi is bi-directional and can translate inbound EDI to JSON and generate outbound EDI from JSON—for any of the 300+ transaction sets across every X12 version release. With Stedi, you can integrate with any trading partner and maintain complete visibility into your transactions.

The Stedi platform allows you to:

  • Manage relationships between you and your trading partners.

  • Configure secure file exchange, including SFTP, FTPS, FTP, AS2, and HTTP.

  • Use machine-readable Stedi Guides to validate and generate EDI according to partner requirements.

  • Monitor, filter, and inspect real-time data for all inbound and outbound transactions.

  • Troubleshoot and retry errors to unblock your pipeline without opening a support case.

Configure an EDI integration in minutes

For each trading partner, you'll create partnerships to define the transaction types you plan to exchange and how Stedi should process each transaction. You'll also configure a connection protocol to securely exchange EDI files. Stedi supports file exchange via SFTP, FTPS, FTP, AS2, and HTTP. Finally, you'll set up Destination webhooks to automatically send processed transaction data and events from Stedi to your business system.

Once you've completed this configuration, you can start exchanging EDI files with your partner.

  • Stedi automatically validates and translates files your partner sends over the connection.

  • With a single API call, you can generate and send fully-formed EDI files, complete with autogenerated control numbers, to your partners through the connection.

A diagram of an end-to-end EDI integration on Stedi

Own your integrations and your data

Stedi gives you complete control over your EDI integrations. You can build, test, and modify every integration quickly and on your timeline. Our onboarding team is always here to help, but we'll never block you.

Stedi allows you to search, inspect, and debug real-time transaction data. You can view both individual transactions and entire files as well as filter by transaction type, use case (production or test), sender or receiver, specific business identifiers (such as PO numbers), and more.

To help with testing and debugging, Stedi shows detailed error messages and resolution tips based on the X12 EDI specification. After you fix an issue, you can retry files anytime to unblock your pipeline.

Inspecting errors

Get started

Stedi handles all the hardest parts of building an EDI integration, allowing you to onboard new trading partners faster and focus development efforts on the functionality unique to your business.

"Stedi has made me look like a superstar to my clients and trading partners.

EDI setup and trading partner testing have become seamless, and the full visibility into file executions and transaction flows makes Stedi the new industry standard for organizations of all sizes. I have worked in the EDI space since 2012, and I can say unequivocally that Stedi is the future!"

– Paul Tittel - Founder, Surpass Solutions Inc.

Book a demo with our onboarding team, and we’ll help you set up Stedi for your first trading partner, or check out the docs to learn more.

Feb 23, 2023

Engineering

There are many different ways to provision AWS services, and we use several of them to address different use cases at Stedi. We set out to benchmark the performance of each option – direct APIs, Cloud Control, CloudFormation, and Service Catalog.

When compared to direct service APIs, we found that:

  • Cloud Control introduced an additional ~5 seconds of deployment latency

  • CloudFormation introduced an additional ~13 seconds of deployment latency

  • Service Catalog introduced an additional ~33 seconds of deployment latency.

This additional latency can make day-to-day operations quite painful.

How we provision resources at Stedi

Each AWS service has its own APIs for CRUD of various resources, but since AWS services are built by many different teams, the ergonomics of these APIs vary greatly – as an example, you would use the Lambda CreateFunction API to create a function vs the EC2 RunInstances API to create an EC2 instance.

To make it easier for developers to work with these disparate APIs in a uniform fashion, AWS launched the Cloud Control API, which exposes five normalized verbs (CreateResource, GetResource, UpdateResource, DeleteResource, ListResources) to manage the lifecycle of various services. Cloud Control provides a convenient way of working with many different AWS services in the same way.

That said, we rarely use the ‘native’ service APIs or Cloud Control APIs directly. Instead, we typically define resources using CDK, which synthesizes AWS CloudFormation templates that are then deployed by the CloudFormation service.

Over the past year, we’ve also begun to use AWS Service Catalog for certain use cases. Service Catalog allows us to define a set of CloudFormation templates in a single AWS account, which are then shared with many other AWS accounts for deployment on-demand. Service Catalog handles complexity such as versioning and governance, and we’ve been thrilled with the higher-order functionality it provides.

Expectations

We expect to pay a performance penalty as we move ‘up the stack’ of value delivery – it would be unreasonable to expect a value-add layer to offer identical performance as the underlying abstractions. Cloud Control offers added value (in the form of normalization) over direct APIs; CloudFormation offers added value over direct APIs or Cloud Control (in the form of state management and dependency resolution); Service Catalog offers added value over CloudFormation (in the form of versioning, governance, and more).

Any performance hit can be broken into two categories: essential latency and incidental latency. Essential latency is the latency required to deliver the functionality, and incidental latency is the latency introduced as a result of a chosen implementation. The theoretical minimum performance hit, then, is equal to the essential latency, and the actual performance hit is equal to the essential latency plus the incidental latency.

It requires substantial investment to achieve something approaching essential latency, and such an investment isn’t sensible in anything but the most latency-sensitive use cases. But as an AWS customer, it’s reasonable to expect that the actual latency of AWS’s various layers of abstraction is within some margin that is difficult to perceive in the normal course of development work – in other words, we expect the unnecessary latency to be largely unnoticeable.

Reality

To test the relative performance of each provisioning method, we ran a series of performance benchmarks for managing Lambda Functions and SQS Queues. Here is a summary of the P50 (median) results:

  • Cloud Control was 744% (~5 seconds) and 1,259% (500 ms) slower than Lambda and SQS direct APIs, respectively.

  • CloudFormation was 1,736% (~13 seconds) and 21,076% (8 seconds) slower than Lambda and SQS direct APIs, respectively.

  • Service Catalog was 4,339% and 86,771% (~33 seconds, in both cases) slower than Lambda and SQS direct APIs, respectively.

The full results are below.

We experimented with Service Catalog to determine what is causing its staggeringly poor performance. According to CloudTrail logs, Service Catalog is triggering the underlying CloudFormation stack create/update/delete, and then sleeping for 30 seconds before polling every 30 seconds until it’s finished. In practice, this means that Service Catalog can never take less than 30 seconds to complete an operation, and if the CloudFormation stack isn’t finished within 30 seconds, then Service Catalog can’t finish in under a minute.

Conclusion

Our hope is that AWS tracks provisioning latency for each of these options internally and takes steps towards improving them – ideally, each provisioning method only introduces the minimum latecy overhead necessary to provide its corresponding functionality.

Full results

Lambda

|                 | Absolute |        |        |        | Delta |       |       |      |
|-Service---------|-P10------|-P50----|-P90----|-P99----|-P10---|-P50---|-P90---|-P99--|
| Lambda          | 464      | 744    | 2,301  | 5,310  |       |       |       |      |
| Cloud Control   | 6,098    | 6,278  | 7,206  | 12,971 | 1214% | 744%  | 213%  | 144% |
| CloudFormation  | 13,054   | 13,654 | 14,591 | 15,906 | 2713% | 1736% | 534%  | 200% |
| Service Catalog | 32,797   | 33,013 | 33,389 | 34,049 | 6967% | 4339% | 1351% | 541

Methodology:

  • Change an existing function's code via different services, which involves first calling UpdateFunctionCode then polling GetFunction.

  • In the case of CloudFormation and Service Catalog, the new code value was passed in as a parameter rather than changing the template.

  • The "Wait" timings represent how long it took the resource to stabilize. This was determined by polling the applicable service operation every 50 milliseconds.

SQS

|                 | Absolute |          |        |        | Delta   |         |         |         |
|-Service---------|-P10------|-P50------|-P90----|-P99----|-P10-----|-P50-----|-P90-----|-P99-----|
| SQS             | 34       | 38       | 45     | 51     |         |         |         |         |
| Cloud Control   | 444      | 516      | 669    | 1,023  | 1,205%  | 1,259%  | 1,382%  | 1,904%  |
| CloudFormation  | 7,417    | 8,047    | 8,766  | 11,398 | 21,714% | 21,076% | 19,337% | 22,239% |
| Service Catalog | 32,785   | 33,011   | 33,320 | 33,659 | 96,327% | 86,771% | 73,780% | 65,873

Methodology:

  • Change an existing queue's visibility timeout attribute via different services, which involves calling SetQueueAttributes.

  • In the case of CloudFormation and Service Catalog, the new visibility timeout value was passed in as a parameter rather than changing the template.

  • The "Wait" timings represent how long it took the resource to stabilize. This was determined by polling the applicable service operation every 50 milliseconds.

Feb 21, 2023

Products

EDI is more prevalent in healthcare than in any other industry, yet EDI specifications for healthcare are some of the most challenging and opaque standards to understand.

The format, commonly known as X12 HIPAA, is captured in a series of PDFs that are up to 700 pages long. These PDFs aren't available to the general public and they don't provide machine-readable schemas for parsing, generating, or validating files. These challenges have made working directly with X12 HIPAA transactions out of reach for all but the largest companies - until now.

We are excited to announce the availability of Stedi's X12 HIPAA guides, a free catalog of X12 HIPAA specifications that make it easier to understand, test, and translate healthcare EDI.

EDI in Healthcare

In 1996, the Healthcare Insurance Portability and Accountability Act (HIPAA) required that the United States Department of Health and Human Services (HHS) establish national standards for electronic transactions so that health information could be transmitted electronically. Essentially, HIPAA mandated the use of EDI.

HHS ultimately adopted the X12 standard, which was already in use throughout retail, supply chain, and other industries. While the X12 standard was extremely comprehensive, it was also extremely flexible, so HHS created a much narrower, opinionated subset of X12: X12 HIPAA.

With few exceptions, all HIPAA covered entities must support electronic transactions that conform to the X12 HIPAA format. Covered entities include health plans, healthcare clearinghouses, and healthcare providers who accept payment from health plans.

User-friendly, machine-readable X12 HIPAA specifications

The EDI Guide Catalog now has Stedi guides for every X12 HIPAA transaction set. Stedi guides are interactive, machine-readable EDI specifications that let you instantly validate EDI test files.

Validate and debug EDI files

Each guide's EDI Inspector automatically identifies errors in EDI test files, including missing or wrong codes, incorrect formatting, and invalid segments. EDI Inspector runs completely in your browser and doesn't send EDI payloads to Stedi, so you can debug production data without data privacy concerns.

View accurate samples

Each guide contains sample transactions that demonstrate valid usage patterns. You can add samples to EDI Inspector, edit them, and download the result to share with collaborators and trading partners.

Parse and generate compliant EDI

You can import any X12 HIPAA guide directly into your Stedi account and customize it to fit your use case. If you need to integrate X12 HIPAA parsing and generation into your workflows, you can use Stedi Core to programmatically validate against any X12 HIPAA guides and generate X12 EDI that conforms to the specifications.

Build a healthcare EDI integration

We're incredibly excited to make X12 HIPAA specifications accessible to businesses of any size so they can easily build healthcare integrations.

"Stedi’s X12 HIPAA guides are a great way to skip the traditional back-and-forth of PDF guides and sample files. We have been able to cut implementation fees with some partners by five-figure amounts because we can generate valid data so quickly. Our partners also appreciate that we can handle integrations ourselves."

– Russell Pekala, Co-Founder of Yuzu Health

If you're new to EDI, check out our EDI Essentials documentation. Our experts explain the basics of the EDI format and provide answers to common X12 HIPAA questions.

Book a call with our technical team to learn how you can build an EDI integration using Stedi.

Jan 26, 2023

Engineering

Stedi’s cloud EDI platform handles sensitive customer data, from business transactions like Purchase Orders and Invoices to healthcare data like Benefits Enrollments and Prior Authorizations. While we maintain a SOC 2 Type II compliance certification and our products have been certified as HIPAA-eligible by an external audit, we view these industry standards as minimum baselines and are constantly evaluating ways to elevate our security posture.

One key area of focus for us is evaluating and restricting our own access to customer data. Last summer, we set out to prove that our least-privilege access policies work as intended by applying automated reasoning. Unlike traditional software testing or penetration testing often used in security audits, this method uses mathematical proof that the security properties we intend to protect are always upheld.

We are thrilled to present the results of the work in a paper written by software engineering intern Hye Woong Jeon, a mathematics student at the University of Chicago and Susa Ventures Fellow, who designed and implemented this approach.

Overview of the paper

Stedi’s cloud infrastructure runs on Amazon Web Services (AWS). We use AWS’s Identity Access Management (IAM) Policies at every level of our stack to enable least-privilege access. We grant our engineers and software processes only the minimum necessary permissions required to perform their tasks. Using IAM, we define the specific actions that can be taken on specific resources under specific conditions.

By carefully separating the resources that Stedi needs to make our systems function from customer-owned resources, such as stored data, we can craft access policies that allow us to operate the platform securely while having no access to customer data. IAM independently enforces these access policies – its mechanisms cannot be circumvented. However, this raises a question: how do we know that we have crafted the access control policies correctly? And further, how can we provably demonstrate this not just to ourselves, but also to our customers?

When customers access Stedi’s systems via our API or web UI, they perform actions under their own identity using a specific IAM role that gives them full access to their data – for example, to EDI files stored in Stedi Buckets. When our engineers operate Stedi systems, or our automated processes perform tasks such as deployments, they use a different set of IAM roles. This enables us to manage configuration, deploy software, and access the operational logs and metrics necessary to run our services with the high level of availability that our customers expect.

With a clear separation of roles built on top of a security-first, battle-tested system like IAM, the key concern becomes ensuring that the underlying IAM policies for those roles are written as intended – in other words, ensuring that the role assigned to Stedi employees does not inadvertently include an IAM policy that can potentially allow access to customer data. While we carefully peer-review all software configuration changes with particular attention paid to security policies, we wanted to achieve a higher level of certainty. We wanted to prove definitively to ourselves that we hadn’t made an error, and put in place a mechanism that provides ongoing assurance that our access controls match our intent.

Concrete and complete proof is difficult to come by – especially for the sort of complex systems that Stedi builds – but, given a well-enough defined problem, automated reasoning makes it possible.

Our proof used a Satisfiability Modulo Theory (SMT) solver, a class of logic statement solvers concerned with identifying if some mathematical formula is satisfiable (i.e. is there some combination of values that will make some formula true). Examples of satisfiability questions are not difficult to come by – e.g. if there are twenty apples and oranges, and four more oranges than apples, then how many apples/oranges are there? In this example, twelve oranges and eight apples satisfies the formula.

The problem of access control can also be formulated as a satisfiability problem. For any Stedi-affiliated account X, can X access customer data? A more rigorous characterization of this problem requires understanding the different components that make up the formula of “accessing customer data.”

AWS encodes IAM policies using a specialized JSON grammar, which breaks IAM into several core components: Actions, Principals, Effects, and Resources. In brief, an IAM policy consists of an Effect, which describes if a Principal is allowed to perform an Action on a Resource. These core components hence act as the apples and oranges of our elementary example: under the various combinations of Actions, Principals, Effects, and Resources ascribed to Stedi account X, does there exist some combination that allows X to access a customer account?

Under the well-defined structure of IAM policies, it is relatively straightforward to encode access control for use in the SMT solver. Once each grammar component has been translated into its appropriate logical formulation, encoded policies can be tested against various template policies to check if a policy is allowed to (or prohibited from) accessing customer data.

Our implementation sorted roles into three categories: allowed, prohibited, and inconclusive. We ensured that items in the allowed and prohibited categories were appropriately categorized and invested time in making changes to IAM policies in the inconclusive category to get further assurance that our policies functioned as intended. Overall, we found that using the proof techniques gave us an even higher level of confidence that our security controls work as intended, isolating our own systems from the data our customers have entrusted to us.

More details about the tools and methods we used to conduct this research and our full conclusions are available in the paper.

Jan 25, 2023

Products

At Stedi, we have always prioritized the security and privacy of our customers and their data. That is why we are thrilled to announce that Stedi has received SOC 2 Type II compliance certification and our products have been certified as HIPAA eligible by an external audit.

SOC 2 Type II compliance is a widely recognized standard for data security and privacy in the technology industry. It involves a rigorous evaluation process that assesses a company's systems, policies, procedures, and controls related to data security, availability, and confidentiality. The certification is an ongoing process, and we will maintain and improve our security posture to meet or exceed industry standards.

We understand that HIPAA eligibility is important for our customers in the healthcare industry as it ensures their sensitive patient data and protected health information (PHI) will be safe and kept confidential when using Stedi products. This is critical when you are exchanging health information such as claims, eligibility, and enrollment information with your partners.

We are committed to providing our customers and their trading partners with the peace of mind that their data is always handled safely and securely. These certifications are just one validation of the high bar we set for security and operational rigor, and one of the many ways we’re fortifying our products to handle customer data safely and securely.

Visit our trust page for more information on our security and compliance policies, or contact us with any compliance questions related to your business case.

Book a demo to learn how you can run a modern EDI system on Stedi.

Nov 29, 2022

Products

This post mentions Stedi’s EDI Translate API. Converting EDI into JSON remains a key Stedi offering, however EDI Translate has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

They say that “no two businesses are the same,” and these differences are shown in the wide variation between EDI implementations.

Every EDI implementation is unique in some way – you’ll rarely find a company that doesn’t need some sort of customization. The problem with typical off-the-shelf EDI software is that customers quickly hit limitations and then wait months or years for new features to address their specific needs or unlock valuable new use cases. And when platforms do offer customization capabilities, they often require deep EDI expertise in order to implement.

Introducing Stedi Functions

Stedi Functions is a powerful way for customers to extend Stedi’s functionality to meet any unique requirement. Functions enables you to run your own purpose-built code on Stedi’s platform. You can use Functions to build end-to-end workflows using Stedi’s APIs – Guides, EDI Translate, Mappings, Stash, Buckets, and SFTP – or build custom functionality using open-source libraries, external web APIs, and even custom business logic to address virtually any requirement.

Functions can serve as an orchestration layer to link various steps in an EDI workflow. For example, you can build a function to pick up an EDI file from a designated location (e.g. an SFTP folder), perform validation and translate the EDI to JSON, transform the JSON to an internal schema, post this data to a custom backend API, and send an acknowledgment to the trading partner – all in one seamless flow.

Functions are ready to execute as soon as they are created, without the hassle of provisioning or managing infrastructure. You can invoke a function directly using the API or SDK, or trigger it through an event (e.g. a new EDI file uploaded to a Bucket via SFTP).

Features

  • Built for extensibility: Include functionality from open-source libraries, external web APIs, or custom business logic to handle virtually any business use case.

  • Seamless orchestration: Stitch together various functional steps with tailored sequence and logic to build an EDI transaction flow.

  • Event-driven execution: Trigger functions in response to events from Buckets (such as file uploads via SFTP). You can also invoke functions directly via API or SDK.

  • Automated scaling: Functions run on high-availability infrastructure that automatically scales up with the number of requests.

  • Automated deployment: Your code is ready to execute within seconds. No need to provision or manage infrastructure to deploy your code.

Functions currently supports JavaScript or any other language that compiles to JavaScript (such as TypeScript).

Using Stedi Functions

You can create, update, and invoke functions using the web UI, or programmatically with the Functions API and Functions SDK. See Functions tutorial for a walkthrough of each approach.

The web UI allows you to experiment, build and test your functions. Below is a high-level overview of creating and executing a function using the web UI.

Creating a function: In the web UI, you can create a function by clicking Create function, adding code to the editor, and clicking Save.

Your code must include a handler function with two parameters: event parameter that lets you pass JSON data into the function, and context parameter that contains information about the environment. You can include any logic, including external API calls, and return any value from the function. You cannot include libraries or additional packages using web UI, for that you will have to use the SDK or API.

Invoking a function: You can invoke your function by clicking Execute. You can also execute functions in the web UI that you created using the API. When the function completes execution, you can view the results, which include the return value and the logs.

Functions UI

Using Stedi function for an outbound EDI flow

Now that we have seen the basics, let’s look at a real-world solution built using Stedi Functions. In this example, a Stedi function is used to connect various Stedi offerings to generate an EDI document and then send it to a trading partner.

The function is called with a JSON payload representing the source data for the EDI transaction. The function performs several steps, as illustrated in the diagram below:

Outbound EDI diagram
  1. Accepts a JSON payload (source) for the transaction.

  2. Calls Stash to generate a control number for the EDI document.

  3. Passes the JSON payload to Mappings using a predefined mapping that maps the internal schema of the payload to a target JSON Schema of a guide specific to the trading partner.

  4. Combines the output from step 3, the envelope information (with control number from step 2), and guide reference before calling the EDI Translate API.

  5. The EDI Translate API validates that the input conforms to the guide schema, and generates the X12 EDI document.

  6. The function saves the EDI output as a file in a bucket.

  7. A trading partner can retrieve the EDI from Buckets using SFTP.

Stedi Functions is now Generally Available

Stedi Functions is the connecting tissue that lets you orchestrate various steps in an end-to-end EDI workflow. Stedi Functions also gives you unlimited extensibility to bring in functionality from third-party APIs, open-source libraries, or custom business logic to build an EDI implementation for your needs. There is no need for deployment or provisioning infrastructure to run a function. With the API access, you can integrate a Stedi function into your own tech stack.

Try it out for yourself – build a Function using our web UI. We have a generous free tier, so there is no need to worry about costs when you are experimenting.

Start building today.

Nov 3, 2022

Products

Every EDI relationship begins with an implementation guide. These EDI implementation guides have been shared in the same format for decades – they’re typically PDFs with dozens or hundreds of pages of details to sift through, and a successful EDI implementation requires getting the details just right.

After reviewing and implementing a partner’s guide, companies have no way of validating their EDI files locally. Their only option to test is to send test files and hope that they’re correct, wasting time and effort on both sides. The fixing of one error reveals another, dragging the partner onboarding process on for weeks. Throw in the fact that many PDF implementation guides and sample files have errors within them, you begin to understand why EDI gets a bad reputation.

To solve these problems, we’ve built Public Guides – a radically new and improved way for businesses to share EDI specifications and speed up the trading partner onboarding process. A public guide is a live, interactive web page that is accessible by anyone with a link. Links can be shared directly with trading partners or included in an EDI portal – they provide the same information as a traditional PDF guide, but allow users to instantly validate and troubleshoot EDI files right in the browser. An intuitive UI and helpful error messages lead them to quickly build EDI files that conform to the guide’s specifications, reducing the testing process from weeks to hours – and eliminating dozens of back-and-forth emails.

Stedi guides can be built out in minutes by anyone using a no-code visual interface, regardless of technical ability. Existing PDFs can be quickly replicated into a Stedi guide, or new guides can be built from scratch. Once built, a guide can be made public instantly.

Features

  • Interactive documentation for trading partners to easily navigate specifications with contextual references. No more scrolling through dozens of pages in a PDF.

  • Embedded EDI Inspector to help your partners instantly validate EDI payloads against your requirements without sending you any traffic.

  • Built-in support for all X12 transaction sets and releases, allowing you to configure X12-conformant specifications without custom development.

  • Customizable specifications, so you can make the X12 standard work for you and not be constrained by strict interpretation.

  • No coding experience required to build a public guide using the visual interface.

  • Brand your guide with a business logo and name, a custom URL slug, and a link to your own website.

Public Guides is now generally available

Try it for yourself by creating a Stedi guide using the guide builder interface. Creating guides and sharing them within your organization is entirely free. Public Guides has no setup fees or long-term subscriptions, and are a flat $250 per month per public guide.

Start building today.

Using Public Guides

Creating a guide: You can create a Stedi guide based on a PDF guide from a trading partner, or from scratch.

Using the guide builder UI, you can create a new guide starting with the X12 release (e.g. 5010) and transaction set (e.g. 810 Invoice). You’ll then select the necessary segments and elements and, if necessary, customize each of them based on your requirements.

Guides UI

Customizing the branding: To customize the appearance of your guide, you can click on “published guide settings” on the guides listing page.

Making your guide public: Once your changes are published, you can make your guide public by choosing the “Actions → Make public” option on the guide. You can view the guide by clicking on “Actions → View public guide”. You can also make this guide private at any time. You can also see a preview of your guide page before making it public.

Sharing the guide: Once a guide is made public, you can view the guide using “Actions → View public guide” option on the guide builder. You can use the "Share" option on this page to get the link to be shared with your partners or to embed into your EDI resources page.

Accessing the public guide (partners): To access a public guide, partners just need to click on the link you send them. The guide will be available to them on a web browser.

You or a trading partner can also export the guide as a PDF. The PDF still retains all the references so partners can look up information on the segments and elements in the transaction set that they don’t fully understand from the guide. However, you’ll miss the ability to validate EDI files.

guide preview

Validating EDI files (partners): Once a partner is on the public guide page, they can use EDI Inspector to validate an EDI file instantly. To use Inspector, one can click the EDI Inspector link and paste their EDI file contents into the designated area. The validation and error messages will be specific to the underlying guide.

EDI Inspector in Guides

Oct 26, 2022

Products

Developers building EDI integrations often find that receiving EDI documents is relatively straightforward compared to the difficulty of sending EDI documents. What makes these use cases so different?

Imagine you’ve switched cell phone providers and you receive your first bill. Though you’ve never seen an invoice from this cell phone provider before, you can quickly ‘parse’ out the relevant details – the total, the due date, your address, how many units of data you’ve consumed, and so on.

But what if you had to create an invoice in this exact format – from scratch? How would you know which fields were required, what order to put them in, and details like whether to use the full name of states or just the abbreviation?

What’s needed is a template, and a way to validate an input against it – which is precisely what we’ve launched with EDI Translate, a new API for creating and validating EDI files that conform to precise trading partner specifications. EDI Translate accepts user-defined JSON payloads and translates them into valid EDI files, and vice versa.

EDI Translate works in conjunction with recently-launched Stedi Guides, which enables users to quickly and accurately define EDI specifications using a no-code visual interface. Once a guide has been defined, users can use the EDI Translate API to create outbound EDI files that conform to the guide. Users can also use EDI Translate API to validate that inbound EDI files meet the defined specification, as well as parse these EDI files into JSON for further processing.

Stedi Guides uses the popular, open-source JSON Schema format, giving developers a familiar way to ensure that their JSON payloads have all the information required to create a valid EDI file.

Features of EDI Translate

  • Read and write any X12 EDI transaction.

  • Validate EDI documents automatically against trading partner specifications.

  • Seamlessly integrate with your own tech stack – or build your end-to-end EDI workflow on Stedi.

How EDI Translate works

EDI Translate makes EDI integrations a lot simpler, taking the EDI format out of the equation and enabling you to work with JSON. Behind the scenes, the nuances of EDI are handled for you to ensure every transaction meets your or your trading partners’ requirements.

For outbound EDI: Once you map your internal payload to the JSON Schema generated by your Stedi guide, you can use EDI Translate to create an EDI document that conforms to the guide’s specifications.

For inbound EDI: When you receive an EDI file from a trading partner, you can use EDI Translate to convert it to JSON. You can use this output JSON directly, or map the output to your own data schema for additional processing (e.g. posting data to an ERP system).

Using EDI Translate

Generating EDI: The following example generates EDI using a custom JSON payload. For a detailed walkthrough, see the demo for writing EDI.

You need three pieces of data to generate EDI:

  1. A Stedi guideId that refers to the trading partner specification. For more information, see Creating a Stedi Guide.

  2. A JSON payload that conforms to the JSON Schema of your guide. If you need help creating this payload, you can use Stedi Mappings to map your internal format to the guide’s JSON Schema.

  3. The X12 envelope data, including interchange control header and functional group headers. Here is an example:

const envelope = {
  interchangeHeader: {
    senderQualifier: "ZZ",
    senderId,
    receiverQualifier: "14",
    receiverId,
    date: format(documentDate, "yyyy-MM-dd"),
    time: format(documentDate, "HH:mm"),
    controlNumber,
    usageIndicatorCode,
  },
  groupHeader: {
    functionalIdentifierCode,
    applicationSenderCode: "WRITEDEMO",
    applicationReceiverCode: "072271711TMS",
    date: format(documentDate, "yyyy-MM-dd"),
    time: format(documentDate, "HH:mm:ss"),
    controlNumber,
  },
};

Once you have these three inputs, you can call the EDI Translate API.

// Translate the Guide schema-based JSON to X12 EDI
const translation = await translateJsonToEdi(mapResult.content, guideId, envelope);

The output of the API call is an EDI payload that you can send to your trading partner using Stedi SFTP or another file transfer mechanism. The sample output below is for the X12-5010-850 transaction set.

{
    "output": "ISA*00*          *00*          *ZZ*AMERCHANT      *14*ANOTHERMERCH   *220915*0218*U*00501*000000001*0*T*>~GS*OW*WRITEDEMO*072271711TMS*20220915*021828*000000001*X*005010~ST*850*000000001~BEG*00*DS*365465413**20220830~REF*CO*ACME-4567~REF*ZZ*Thank you for your business~PER*OC*Marvin Acme*TE*973-555-1212*EM*marvin@acme.com~TD5****ZZ*FHD~N1*ST*Wile E Coyote*92*123~N3*111 Canyon Court~N4*Phoenix*AZ*85001*US~PO1*item-1*0008*EA*400**VC*VND1234567*SK*ACM/8900-400~PID*F****400 pound anvil~PO1*item-2*0004*EA*125**VC*VND000111222*SK*ACM/1100-001~PID*F****Detonator~CTT*2~AMT*TT*3700~SE*16*000000001~GE*1*000000001~IEA*1*000000001~"
}

Parsing EDI: To parse an EDI file, you only need the raw EDI payload (file contents) and the relevant Stedi guideId.

{
    "input": "ISA*00*          *00*          *ZZ*ANOTHERMERCH   *14*AMERCHANT      *220914*2022*U*00501*000001746*0*T*>~\nGS*PR*072271711TMS*READDEMO*20220914*202222*000001746*X*005010~\nST*855*0001~\nBAK*00*AD*365465413*20220914*****20220913~\nREF*CO*ACME-4567~\nN1*SE*Marvin Acme*92*DROPSHIP CUSTOMER~\nN3*123 Main Street~\nN4*Fairfield*NJ*07004*US~\nN1*ST*Wile E Coyote*92*DROPSHIP CUSTOMER~\nN3*111 Canyon Court~\nN4*Phoenix*AZ*85001*US~\nPO1*item-1*8*EA*400**VC*VND1234567*SK*ACM/8900-400~\nPID*F****400 pound anvil~\nACK*IA*8*EA~\nPO1*item-2*4*EA*125**VC*VND000111222*SK*ACM/1100-001~\nPID*F****Detonator~\nACK*IA*4*EA~\nCTT*2~\nSE*17*0001~\nGE*1*000001746~\nIEA*1*000001746~",
    "guideId": "01GEJBYTQCHWK59PKANTKKGJXM"
}

EDI Translate API is now generally available

EDI Translate is the core of any modern EDI system. Backed by Stedi Guides, EDI Translate enables you to convert data between JSON and EDI while conforming to your trading partners’ specifications. Use EDI Translate regardless of whether you are building an EDI system from scratch or simplifying your existing architecture. You can build your end-to-end flow entirely on Stedi, or integrate EDI Translate into your existing tech stack.

Try it for yourself – start by creating a Stedi guide using the guide builder UI. Use the guide with EDI Translate to parse and generate EDI files. We have a generous free tier for API calls, so there is no need to worry about costs when you are experimenting. Creating guides and sharing them within your organization is entirely free.

Start building today.

Did you know? You can build an end-to-end EDI integration on Stedi. Capture EDI specifications from your trading partner in a Stedi Guide, convert data between JSON and EDI using EDI Translate, transform data between schemas using Stedi Mappings, handle transmission of EDI files with your trading partners using Stedi SFTP (backed by Stedi Buckets), and orchestrate API calls and business logic with Stedi Functions. Use Stash as a reliable key-value store to keep track of transactions, reference lookup tables, and more.

Oct 20, 2022

Products

This post mentions Stedi’s EDI Translate API. Converting EDI into JSON remains a key Stedi offering, however EDI Translate has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

The first step in any EDI relationship is agreeing on a set of EDI specifications. These requirements are typically shared as a PDF document, which is helpful for human reference but does not enable automated translation or validation of EDI documents.

Stedi Guides is a new product that enables users to quickly and accurately create specifications using a visual interface. These specifications can be used to programmatically parse and generate EDI documents via API using a modern JSON Schema format, or to validate EDI manually in a web browser against interactive documentation.

Features of Stedi Guides

  • Machine-readable JSON Schema artifacts to read, write, and validate EDI documents.

  • Built-in support for all X12 transaction sets and releases, allowing you to configure X12-conformant specifications without custom development.

  • Customizable specifications, so you can make the standard work for you and not be constrained by strict X12 interpretation.

  • No coding experience required to build a Stedi guide using the visual interface.

Guides UI

How Stedi Guides can help your business

If you are reading this, you probably encountered one of these situations:

  • You’ve received EDI requirements from a trading partner in a PDF document, and are wondering about how best to incorporate those into your development workflow.

  • You meticulously built and sent an EDI file to your trading partner, only to be told days or weeks later that the file has errors.

  • You receive EDI files from a partner and want to validate and parse them according to your specifications.

Stedi Guides is built to address these use cases, and more.

The fundamental problem with EDI is that while conforming to EDI specifications is critical to every EDI implementation, PDFs are a poor format for communicating a schema. Trading partners are left trying to visually validate an EDI file against a PDF and attempting to capture the nuances of a complex guide in custom validation logic. To complicate matters, EDI specifications almost always differ from one trading partner to another, making it unmanageable to keep track of differences while generating accurate EDI files for every partner.

Stedi Guides solves these problems by giving you a visual interface for building robust artifacts – guides – to encapsulate EDI specifications, while making them available for programmatic access.

Stedi Guides is natively integrated with the EDI Translate API, which allows you to generate outbound EDI that conforms to a specified guide, and to validate and parse inbound EDI against a guide defined by you or your trading partner.

Using Stedi Guides

In the browser

Creating and publishing a guide: You can create a Stedi guide based on a PDF guide from a trading partner, or from scratch.

Using the guide builder UI, you can create a new guide starting with the X12 release (e.g. 5010) and transaction set (e.g. 810 Invoice). You’ll then select the necessary segments and elements and, if necessary, customize each of them based on your requirements.

guide builder UI

You can publish the guide privately within your organization for others to access it as an interactive documentation on a web page.

guide preview

Validating EDI files in the browser: Once you’ve built a guide, you can use the Inspector view to validate an EDI file instantly, without having to reach out to your trading partner.

To use Inspector, click the EDI Inspector link within the guide builder, or on the published guide page. The validation and error messages will be specific to the underlying guide.

EDI Inspector in Guides

Via API

To generate EDI: To use a Stedi guide to generate EDI, you can call the EDI Translate API with the guide reference (guideId) and a JSON object, and the API will return a conforming EDI file – or a list of validation errors encountered during translation. See the demo for writing EDI for a detailed walkthrough.

// Translate the Guide schema-based JSON to X12 EDI
const translation = await translateJsonToEdi(mapResult.content, guideId, envelope);

To parse EDI: Similarly, you can call EDI Translate API to parse or validate EDI by passing the EDI file and the guideId, and the API will return a translated JSON representation of your EDI file.

const translation = await translateEdiToJson(ediDocument, guideId);

Stedi Guides is now Generally Available

Stedi Guides is the foundation of every modern EDI system. Stedi Guides allows you to build integrations that are reliable, easy to troubleshoot, and scalable as you onboard with new trading partners and transaction sets. The no-code interface makes it easy for anyone to create and maintain EDI specifications, regardless of their technical ability or EDI knowledge. In addition to reading and writing EDI programmatically, Stedi Guides renders human-readable documentation in the browser, allowing for real-time validation of EDI files using Inspector.

Try it for yourself by creating a Stedi guide using the guide builder interface. Creating guides and sharing them within your organization is entirely free. Validate EDI files for free using the Inspector – or use the EDI Translate API to parse and generate EDI files programmatically. The EDI Translate API also has a generous free tier, so there is no need to worry about costs when experimenting.

For more details, check out our user guide.

Aug 2, 2022

Products

Stedi Buckets is worth getting excited about, or at least so I’m told. We launched it together with Stedi SFTP and I can’t help but feel that SFTP stole the show that day. Receive documents from customers without hassle? That is solving a business problem. But storing those documents? Sure, it’s necessary, but there’s not a lot for me to do with documents that just sit there. Unless you give me programmatic access. Then I can do with them whatever I want and that gets my coder’s heart pumping.

Setting a challenge

Stedi SFTP and Stedi Buckets are related and I can access both of them using the SDK. I need a starting point though, so I’ll set myself a challenge. I want to generate a report that tells me how many files each of my customers has sent me. I also want to know how many of those files contain EDI. What I’m looking for is something like this.

user: Paper Wrap - files: 10 - EDI: 8
user: Blank Billing - files: 4 - EDI: 2
user: Blank Shipping - files: 11 - EDI: 7
user: Blank Management - files: 17 - EDI: 9

Let me stop pretending that I’m making this up as I go: I already finished the challenge. It turned out easier than I thought. There, I spoiled the ending, so you might as well stop reading. Actually, that’s not a bad idea. If you feel you can code this yourself, then open the Stedi SFTP SDK reference and the Stedi Buckets SDK reference and go for it.

  • If you want a little more help, continue reading Programming with the SDK.

  • If you want a lot more help, jump to Walkthrough.

Programming with the SDK

Assuming that installing Node.js is something you’ve done already, you can get started with the SDK by running the following commands in your project directory.

npm install @stedi/sdk-client-buckets
npm install @stedi/sdk-client-sftp

Since I’ve already finished the challenge, I know exactly which operations I’m going to need.

  • List all SFTP users.

  • List all objects in a bucket.

  • Download the contents of an object.

List all SFTP users

const sftp = require("@stedi/sdk-client-sftp");

async function main() {
  const sftpClient = new sftp.Sftp({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const listUsersResult = await sftpClient.listUsers({});
  const users = listUsersResult.items;
  console.info(users);
}

main();

List all objects in a bucket

const buckets = require("@stedi/sdk-client-buckets");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const listObjectsResult = await bucketsClient.listObjects({
    bucketName: "YOUR BUCKET NAME HERE",
  });
  const objects = listObjectsResult.items || []; // items is undefined if the bucket doesn’t contain objects
  console.info(objects);
}

main();

Download the contents of an object

const buckets = require("@stedi/sdk-client-buckets");
const consumers = require("stream/consumers");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  const getObjectResult = await bucketsClient.getObject({
    bucketName: "YOUR BUCKET NAME HERE",
    key: "YOUR OBJECT KEY HERE",
  });
  const contents = await consumers.text(getObjectResult.body);
  console.info(contents);
}

main();

Paging

The list-operations only return the first page of results. Let’s not settle for that.

const buckets = require("@stedi/sdk-client-buckets");

async function main() {
  const bucketsClient = new buckets.Buckets({
    region: "us",
    apiKey: process.env.STEDI_API_KEY,
  });

  let objects = [];
  let pageToken = undefined;
  do {
    const listObjectsResult = await bucketsClient.listObjects({
      bucketName: "YOUR BUCKET NAME HERE",
      pageSize: 5,
      pageToken: pageToken,
    });
    objects = objects.concat(listObjectsResult.items || []);

    pageToken = listObjectsResult.nextPageToken;
  } while (pageToken !== undefined);

  console.info(objects);
}

main();

Walkthrough

The devil is in the details. The code above shows you what you need to program with the SDK, but it doesn’t explain anything. Allow me to rectify.

From time to time, I’ll do something on the command line. To follow along, you need to run a Linux shell, the Mac terminal, or Windows Subsystem for Linux.

Installing Node.js

  1. Install Node.js.

The Stedi SDK is written for JavaScript. It also works with languages that compile to JavaScript—like TypeScript, CoffeeScript, and Haxe—but it requires a JavaScript environment. In practice, that means Node.js.

How you get up and running with Node.js depends on your operating system. I’m not going to cover all the options here.

Creating a project

  1. Create a folder for your project.

mkdir stedi-challenge
cd stedi-challenge
  1. Create a file called main.js.

touch main.js
  1. Open the file in your preferred code editor.

  2. Paste the following code into the file.

console.info("Hello, world!");
  1. Run the code.

node main.js

This should output the following:

Hello, world

Creating a Stedi-account

  1. Go to the sign-up page and create a free Stedi-account.

If you already have Stedi-account, you can use that one. If you’re already using Stedi SFTP, then your results might look a little different, but it will all still work.

Create an API-key

  1. Sign in to the Stedi Dashboard.

  2. Open the menu on the top left.

Dashboard menu
  1. Under Account, click API Keys.

API Keys
  1. Click on Generate API key.

Generate API key
  1. Enter a description for the API key. If you have multiple API keys in your account, you can use the description to tell them apart. Other than that, it doesn’t matter what you fill in. I usually just type my name.

  2. Click on Generate.

Generate
  1. Copy the API key and store it somewhere safe.

  2. Click Close.

You need the API key to work with the SDK. It tells the SDK which account to connect to. It’s important you keep the API key safe, because anyone who has your API key can run code that access your account.

Test the Stedi Buckets SDK

  1. Install the Stedi Buckets SDK.

npm install @stedi/sdk-client-buckets
  1. Open main.js.

  2. Remove all code.

  3. Import the Stedi Buckets package.

const buckets = require("@stedi/sdk-client-buckets");
  1. Create a Stedi Buckets client.

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. List all buckets in your account.

async function main() {
  const listBucketsResult = await bucketsClient.listBuckets({});
  const buckets = listBucketsResult.items;
  console.info(buckets);
}

main();
  1. On the command line, set the environment variable STEDI_API_KEY to your API key.

export STEDI_API_KEY=YOUR.API.KEY.HERE
  1. Run the code.

node main.js

This should output the following:

[]

The client allows you to send commands to Stedi Buckets. You must pass it a region, although we only support us right now. You also give it your API key, so it knows which account to connect to. You could paste your API key directly into the code, but then everyone who has access to your code, can see your API key and you should keep it safe.

Instead, this code reads the API key from an environment variable that you set on the command line. This way, the API key is only available to you. If someone else wants to run the code, they need to have their own API key and set it on their command line.

To get a list of all buckets, you call listBuckets(). The functions in the SDK expect all their parameters wrapped in an object. listBuckets() doesn’t have any required parameters, but it still expects an object. That’s why you have to pass in {}.

listBuckets() is an async function, so you have to await it, but you can only use await inside another async function. That’s why the code is wrapped inside the function main().

Since you don’t have any buckets in your account yet, the output is an empty array.

Test the Stedi SFTP SDK

  1. Install the Stedi SFTP SDK.

npm install @stedi/sdk-client-sftp
  1. Remove all code from main.js.

  2. Import the Stedi SFTP package.

const sftp = require("@stedi/sdk-client-sftp");
  1. Create a Stedi SFTP client.

const sftpClient = new sftp.Sftp({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. List all SFTP users in your account.

async function main() {
  const listUsersResult = await sftpClient.listUsers({});
  const users = listUsersResult.items;
  console.info(users);
}

main();
  1. Run the code.

node main.js

This should output the following:

[]

As you can see, this works just like the Stedi Buckets SDK. The same notes apply.

Create an SFTP user

  1. Open the Stedi Dashboard.

  2. Open the menu on the top left.

  3. Under Products, click on SFTP.

SFTP
  1. Click on Create User.

  2. Fill in the form with the values you see below.

Create SFTP User: Paper Wrap
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

This creates a user that has access to the directory /paper-wrap on the SFTP server.

Paper Wrap is a store that sells edible gift wrapping. They care greatly about preventing paper waste, so they do all their business electronically.

The SFTP server is ready for use, even though you didn’t create a bucket for it. Stedi does this for you automatically.

Find the bucket name

  1. Add the following code at the end of main().

if (users.length == 0) {
  console.info("No users.");
  return;
}

const bucketName = users[0].bucketName;
console.info(bucketName);
  1. Run the code.

node main.js

This should output the following, although your bucket name and username will be different.

[
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Paper Wrap',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/paper-wrap',
    lastConnectedDate: undefined,
    username: 'P7EGRE9H'
  }
]
e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp

It’s convenient that Stedi creates the SFTP bucket for you, but you need the name of the bucket if you want to access it from code. You could get the name from the dashboard, but you didn’t get into programming to copy and paste things from the UI.

When you retrieve information about a user, it includes the bucket name in a field conveniently called bucketName. All SFTP users use the same bucket, so you can get the bucket name from the first user and use it throughout.

Create more SFTP users

  1. In the Stedi Dashboard, click on Create User.

  2. Fill in the form with the values you see below.

Create SFTP User: Blank Billing
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

  4. Click on Create User.

  5. Fill in the form with the values you see below.

Create SFTP User: Blank Shipping
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

  4. Click on Create User.

  5. Fill in the form with the values you see below.

Create SFTP User: Blank Management
  1. Click Save.

  2. Copy the connection string and password and store it somewhere safe.

  3. Click Done.

Blank is a manufacterer of invisible ink. They went fully digital after the court ruled that their paper contracts weren’t legally binding.

Blank has separate departments for billing and shipping and both departments get their own user with their own directory. This way, Shipping can’t accidentally put their ship notices among the invoices. The general manager likes to keep an eye on everything, so he has a user that can see documents from both Shipping and Billing, but can’t access Paper Wrap’s documents.

Create test files

  1. Create a file called not_edi.txt.

  2. Add the following content to not_edi.txt.

This is not an EDI file
  1. Create a file called edi.txt.

  2. Add the following content to edi.txt.

ISA*00*          *00*          *ZZ*PAPER WRAP     *ZZ*BLANK          *210901*1234*U*00801*000000001*0*T*>~
GS*PO*SENDERGS*007326879*20210901*1234*1*X*008020~
ST*850*000000001~
BEG*24*SP*PO-00001**20210901~
PO1**5000*04*0.0075*PE*GE*Lemon Juice Gift Card~
SE*1*000000001~
GE*1*1~
IEA*1*000000001

If the script is going to count files, you should give it some files to count. The contents don’t really matter, other than that some files should contain EDI. You’ll upload copies of these two files to the SFTP server.

A word about SFTP clients

I’m about to show you how to upload files to the SFTP server and I’m going to do it from the command line. I think that’s convenient since I’m doing a lot of things on the command line already, but it’s not the only way. If you prefer dragging and dropping your files, you can install an FTP client like FileZilla. You won’t be able to follow the instructions in the next paragraph step by step, but it shouldn’t be too hard to adapt them. Here are some pointers.

  • You can find the username and host on the Stedi Dashboard.

  • Host and SFTP endpoint are the same thing.

  • If you need to specify a port, use 22.

  • Call the files on the SFTP server whatever you like; it doesn’t matter to the code.

  • The exact number of files you upload doesn’t matter; just gives every user a couple of files.

Upload test files

  1. On the command line, make sure you’re in the directory that contains the files edi.txt and not-edi.txt.

  2. Connect to the SFTP server with the connection string from the Paper Wrap user. Your connection string will look a little different than the one in example.

sftp 9UE5Z386@data.sftp.us.stedi.com
  1. Enter the password for the Paper Wrap user.

  2. Upload a couple of files.

put edi.txt coconut-christmas
put edi.txt toffee-birthday
put edi.txt blueberry-ribbon
put edi.txt cinnamon-surprise
put edi.txt salty-sixteen
put edi.txt beefy-graduation
put edi.txt cashew-coupon
put edi.txt bittersweet-valentine
put not-edi.txt whats-that-smell
put not-edi.txt dont-ship-the-fish
  1. Disconnect from the SFTP server.

bye
  1. Connect to the SFTP server with the connection string from the Blank Billing user.

  2. Enter the password for the Blank Billing user.

  3. Upload a couple of files.

put edi.txt pay-me
put edi.txt seriously-pay-me
put not-edi.txt i-know-where-you-live
put not-edi.txt thank-you
  1. Disconnect from the SFTP server

  2. Connect to the SFTP server with the connection string from the Blank Shipping user.

  3. Enter the password for the Blank Shipping user.

  4. Upload a couple of files.

put edi.txt too-heavy
put edi.txt is-this-empty
put edi.txt poorly-wrapped
put edi.txt other-side-down
put edi.txt handle-with-gloves
put edi.txt negative-shelf-space
put edi.txt destination-undisclosed
put not-edi.txt empty-labels
put not-edi.txt boxes-and-bows
put not-edi.txt be-transparent
put not-edi.txt how-to-drop-without-breaking
  1. Disconnect from the SFTP server.

  2. Connect to the SFTP server with the connection string from the Blank Management user.

  3. Enter the password for the Blank Management user.

  4. Upload a couple of files.

put not-edi.txt strategic-strategizing
put not-edi.txt eat-my-spam

List all files

  1. Create a Stedi Buckets client. Add the following code at the top of main.js.

const buckets = require("@stedi/sdk-client-buckets");

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});
  1. Get a list of files from the SFTP server. Add the following code to the end of main().

const listObjectsResult = await bucketsClient.listObjects({
  bucketName: bucketName,
});
const objects = listObjectsResult.items || [];
console.info(objects);
  1. Run the code.

node main.js

This should output the following:

[
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Management',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank',
    lastConnectedDate: '2022-07-28',
    username: '8X0HLJJW'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Shipping',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank/shipping',
    lastConnectedDate: '2022-07-28',
    username: '4UBJ9H9C'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Paper Wrap',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/paper-wrap',
    lastConnectedDate: '2022-07-28',
    username: 'P7EGRE9H'
  },
  {
    bucketName: 'e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp',
    description: 'Blank Billing',
    endpoint: 'data.sftp.us.stedi.com',
    homeDirectory: '/blank/billing',
    lastConnectedDate: '2022-07-28',
    username: '23SO1YKM'
  }
]
e97e59c8-d5d4-4dde-97ab-52cdf777eaf5-sftp
[
  {
    key: 'blank/billing/i-know-where-you-live',
    updatedAt: '2022-07-28T15:21:23.000Z',
    size: 24
  },
  {
    key: 'blank/billing/pay-me',
    updatedAt: '2022-07-28T15:21:22.000Z',
    size: 295
  },
  {
    key: 'blank/billing/seriously-pay-me',
    updatedAt: '2022-07-28T15:21:22.000Z',
    size: 295
  },
  {
    key: 'blank/billing/thank-you',
    updatedAt: '2022-07-28T15:21:24.000Z',
    size: 24
  },
  {
    key: 'blank/eat-my-spam',
    updatedAt: '2022-07-28T15:25:22.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/be-transparent',
    updatedAt: '2022-07-28T15:22:26.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/boxes-and-bows',
    updatedAt: '2022-07-28T15:22:25.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/destination-undisclosed',
    updatedAt: '2022-07-28T15:22:24.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/empty-labels',
    updatedAt: '2022-07-28T15:22:24.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/handle-with-gloves',
    updatedAt: '2022-07-28T15:22:23.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/how-to-drop-without-breaking',
    updatedAt: '2022-07-28T15:22:26.000Z',
    size: 24
  },
  {
    key: 'blank/shipping/is-this-empty',
    updatedAt: '2022-07-28T15:22:21.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/negative-shelf-space',
    updatedAt: '2022-07-28T15:22:23.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/other-side-down',
    updatedAt: '2022-07-28T15:22:22.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/poorly-wrapped',
    updatedAt: '2022-07-28T15:22:21.000Z',
    size: 295
  },
  {
    key: 'blank/shipping/too-heavy',
    updatedAt: '2022-07-28T15:22:20.000Z',
    size: 295
  },
  {
    key: 'blank/strategic-strategizing',
    updatedAt: '2022-07-28T15:25:21.000Z',
    size: 24
  },
  {
    key: 'paper-wrap/beefy-graduation',
    updatedAt: '2022-07-28T15:20:11.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/bittersweet-valentine',
    updatedAt: '2022-07-28T15:20:12.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/blueberry-ribbon',
    updatedAt: '2022-07-28T15:20:09.000Z',
    size: 295
  },
  {
    key: 'paper-wrap/cashew-coupon',
    updatedAt: '2022-07-28T15:20:12.000Z',
    size: 295
  }
]

You can’t get files per user, because Stedi Buckets doesn’t know anything about users. You also can’t get files per directory, because technically Stedi Buckets doesn’t have directories. That’s a topic for another time, though.

You can list all the files. The result from listObjects() contains an array called items with information on each file. If there are no files on the SFTP server, items will be undefined. For our code, it’s more convenient to have an empty array if there are no files, hence the expression listObjectsResult.items || [].

If you take a close look at the output—and you’ve been following this walkthrough to the letter—you’ll discover that it doesn’t contain every single file. listObjects() only return the first 25. To get the rest as well, you’ll need paging.

Paging through files

  1. Page through the files on the SFTP server. Replace the code from the previous paragraph with the following.

let objects = [];
let pageToken = undefined;
do {
  const listObjectsResult = await bucketsClient.listObjects({
    bucketName: bucketName,
    pageToken: pageToken,
  });
  objects = objects.concat(listObjectsResult.items || []);

  pageToken = listObjectsResult.nextPageToken;
} while (pageToken !== undefined);

console.info(objects.length);
  1. Run the code.

node main.js

This should output the following:

27

When there are more results to fetch, listObjects() will add the field nextPageToken to its result. If you then call listObjects() again, passing in the page token, you will get the next page of results. The first time you call listObjects(), you won’t have a page token yet, so you can pass in undefined to get the first page.

You can use concat() to put the files of each page into a single array.

Paging through users

  1. Replace the line let pageToken = undefined; with the following.

pageToken = undefined;
  1. Page through all SFTP users. Replace the code that lists users, at the beginning of main(), with the following.

let users = [];
let pageToken = undefined;
do {
  const listUsersResult = await sftpClient.listUsers({
    pageToken: pageToken,
  });
  users = users.concat(listUsersResult.items);

  pageToken = listUsersResult.nextPageToken;
} while (pageToken !== undefined);

There are only four users, so for this challenge, paging through the users won’t make a difference, but it makes the scripts more robust. It works just like paging through files.

Counting files

  1. For every user, loop through all files and count the ones that are in their home directory. Add the following code to the end of main().

for (let user of users) {
  const homeDirectory = user.homeDirectory.substring(1);

  let fileCount = 0;
  for (let object of objects) {
    if (object.key.startsWith(homeDirectory)) {
      fileCount++;
    }
  }

  console.info(`user: ${user.description} - files: ${fileCount}`);
}
  1. Run the code.

node main.js

This should output the following:

user: Blank Management - files: 17
user: Blank Shipping - files: 11
user: Paper Wrap - files: 10
user: Blank Billing - files: 4

Every file has a field called key, which contains the full path, for example blank/billing/thank-you. If the start of the key is the same as the user’s home directory, then the user owns the file. The only problem is that the home directory has a / at the start and the key doesn’t. user.homeDirectory.substring(1) strips the / from the home directory.

Detecting EDI

  1. Import the stream consumer package. Add the following code at the top of main.js.

const consumers = require("stream/consumers");
  1. Download each file from the SFTP server. Add the following code right below fileCount++.

const getObjectResult = await bucketsClient.getObject({
  bucketName: bucketName,
  key: object.key,
});
const contents = await consumers.text(getObjectResult.body);
  1. Add a counter for EDI files. Add the following code right below let fileCount = 0.

let ediCount = 0;
  1. Determine if the file contents is EDI. Add the following right below the previous code.

if (contents.startsWith("ISA")) {
  ediCount++;
}
  1. Output the result. Replace the last line that calls console.info() with the following code.

console.info(`user: ${user.description} - files: ${fileCount} - EDI: ${ediCount}`);

This should output the following:

user: Blank Management - files: 17 - EDI: 9
user: Blank Shipping - files: 11 - EDI: 7
user: Paper Wrap - files: 10 - EDI: 8
user: Blank Billing - files: 4 - EDI: 2

When you call getObject() the result includes a stream that allows you to download the contents of the file. The easiest way to do this, is using consumer.text() from Node.js’s stream consumer package.

To detect if a document is EDI, you can check if it starts with the letters ISA. It’s easy to do and I expect it covers at least 99% of cases, so I call that good enough.

Challenge completed

const buckets = require("@stedi/sdk-client-buckets");
const sftp = require("@stedi/sdk-client-sftp");
const consumers = require("stream/consumers");

const bucketsClient = new buckets.Buckets({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});

const sftpClient = new sftp.Sftp({
  region: "us",
  apiKey: process.env.STEDI_API_KEY,
});

async function main() {
  let users = [];
  let pageToken = undefined;
  do {
    const listUsersResult = await sftpClient.listUsers({
      pageToken: pageToken,
    });
    users = users.concat(listUsersResult.items);

    pageToken = listUsersResult.nextPageToken;
  } while (pageToken !== undefined);

  if (users.length == 0) {
    console.info("No users.");
    return;
  }

  const bucketName = users[0].bucketName;
  console.info(bucketName);

  let objects = [];
  pageToken = undefined;
  do {
    const listObjectsResult = await bucketsClient.listObjects({
      bucketName: bucketName,
      pageToken: pageToken,
    });
    objects = objects.concat(listObjectsResult.items || []);

    pageToken = listObjectsResult.nextPageToken;
  } while (pageToken !== undefined);

  console.info(objects.length);

  for (let user of users) {
    const homeDirectory = user.homeDirectory.substring(1);

    let fileCount = 0;
    let ediCount = 0;
    for (let object of objects) {
      if (object.key.startsWith(homeDirectory)) {
        fileCount++;

        const getObjectResult = await bucketsClient.getObject({
          bucketName: bucketName,
          key: object.key,
        });
        const contents = await consumers.text(getObjectResult.body);
        if (contents.startsWith("ISA")) {
          ediCount++;
        }
      }
    }

    console.info(`user: ${user.description} - files: ${fileCount} - EDI: ${ediCount}`);
  }
}

main();

Jul 12, 2022

Products

Building an EDI system or B2B integration requires a secure, scalable way to exchange files with trading partners. With Stedi SFTP, developers can provision users and begin transferring files in seconds. Files received via Stedi SFTP are immediately available in Stedi Buckets - a simple, reliable data store – for further processing. With a usage-based pricing model and no servers to manage, builders can easily offer SFTP connectivity to their trading partners as part of a new or existing B2B workflow without incurring fixed costs or operational overhead.

Starting today, both Stedi SFTP and Buckets are now Generally Available.

Features

  • Provision SFTP users via the Stedi dashboard or the API

  • Securely manage credentials

  • Scale to an unlimited number of trading partners

  • Send, receive, and store an unlimited number of files

Where Stedi SFTP fits in

Stedi SFTP works for any workflow requiring file transfer between trading partners. If you’re building an EDI integration, you could use Stedi SFTP to:

  • Receive EDI files from your trading partner via SFTP, and retrieve those files using the Buckets SDK

  • Using the Buckets SDK, send EDI files for your trading partner to pick up via SFTP.

Stedi SFTP is fully integrated with Stedi Buckets. When you upload files programmatically via the Buckets SDK, those files are available to your trading partner via their SFTP credentials. And each time your trading partner uploads files via SFTP, those files are available via the Buckets SDK, too. Check out the Buckets documentation for more details.

Using Stedi SFTP

The first step is to create an SFTP user, which you can do via the SFTP UI or via the API. Once you have the generated credentials, you can connect to the SFTP endpoint using your favorite SFTP client:

sftp WL9F11A9@data.sftp.us.stedi.com

WL9F11A9@data.sftp.us.stedi.com's password:
Connected to data.sftp.us.stedi.com.
sftp

After you've uploaded a file via your SFTP client, you can retrieve the file via Stedi's Buckets SDK using the following code:

// Stedi SDK example for GetObject from your local machine

import { BucketsClient, GetObjectCommand } from "@stedi/sdk-client-buckets";
import consumers from 'stream/consumers';

// Enter your Stedi API key here
const apiKey = "<your-stedi-api-key>"; // Change this to your Stedi API key

async function main() {

 // create a new BucketsClient to the Stedi US region
 const stediclient = new BucketsClient({
  region: "us",
  apiKey: apiKey
 });

 // Prepare a GetObject command
 const getObject = new GetObjectCommand(
  {
   bucketName: "your-stedi-bucket" // Change this to your existing Bucket name
   key: "document.txt" // Change this to an existing object name
  }
 );

 // Send the request to GetObject request to Stedi
 const getObjectOutput = await stediclient.send(getObject);

 // Pretty print the object output
 console.log(await consumers.text(getObjectOutput.body));

}

// Run the main function
main();

In this example, we've printed the file as a string, but you could also write it to a file on your disk.

Stedi SFTP pricing

Stedi SFTP is billed based on the number of files and the amount of data uploaded and downloaded. There are no minimum fees, no monthly commitments, and no upfront costs to use Stedi SFTP.

SFTP is backed by Stedi Buckets for file storage and retrieval, and related storage and data transfer charges will be billed separately.

Stedi SFTP and Stedi Buckets are now Generally Available

Stedi SFTP offers a hassle-free way to provision SFTP access as part of your EDI solution or B2B integration, without any operational overhead or minimum cost. It gives developers the ability to configure SFTP access for their trading partners via the UI or API. Developers can programmatically access the same files using the Stedi Buckets SDK.

Get started today.

Jun 24, 2022

EDI

A colleague told me about transaction set variants and now I want to get some more experience with them myself. In a nutshell, the problem is that one transaction set can represent different kinds of documents. For example, he showed me an implementation guide that specified that the 204 Motor Carrier Load Tender could be used to create, cancel, modify, or confirm a load tender. That feels like four different documents to me, but they are all represented by the same transaction set. So, how do you deal with that in practice?

My goal isn’t to come up with a solution, but to understand the problem. I’ll take an implementation guide that describes some transaction set variants and then I’ll try to come up with a strategy to deal with them. I don’t want to build a full implementation; I want to get a feel of what it takes to handle this in practice.

I’m going to look at the implementation guide for the 850 Purchase Order from Amazon, because I happen to have a copy. This means that today, I’m a wholesaler! Amazon sends me a purchase order, now what.

Understanding the variants

I don’t sell directly to the consumer; I leave that to Amazon, so they need my product in their warehouse. That’s why they send me an 850 Purchase Order. How do I know that I have to deal with transaction set variants? The implementation guide from Amazon doesn’t contain any preamble. It goes straight into the default information.

The start of the implementation guide, with the heading '850 Purchase Order' followed by a generic description of the transaction set.

That generic description of the transaction set doesn’t do me any good. Fortunately, I know to keep an eye out for transaction set variants, thanks to my colleague. The first segment of the transaction set (I’m not counting the envelopes) seems to contain what I’m looking for.

The details of the BEG segment as described by the implementation guide, with the BEG02 element highlighted. The element lists four codes: CN, NE, NP, and RO.

There seem to be 4 variants. Although, as you can see above, the guide mentions that there are 69 total codes. I assume that means that the standard defines 69 codes, but Amazon only uses 4 of them. Surely, Amazon doesn’t mean to say that they use 65 other codes as well, but they’re not going to tell you any more about them. Let me check that assumption with a look at the standard transaction set. Yes, that one has 69 codes. Good.

The details of the BEG segment as described by the standard. A highlight shows that the standard contains 69 codes for the BEG02 element.

So, what variants do we have? New Order I understand: that’s a new order. I feel so smart right now! Rush Order is an order that needs to be rushed, but I’m not sure how this is supposed to be treated differently than a new order. Maybe it isn’t different in terms of EDI messaging and it’s just a notification to the business: do whatever you need to do to rush this. Perhaps alarm bells should go of to let everyone know to drop whatever it is they’re doing and take care of this order. I have no idea what a consigned order is, so I should ask a domain expert, but I’m going to ask Google instead.

If I understand correctly, with a normal order, Amazon buys a product from me, puts it in their warehouse, and can then do whatever they want with it, although presumably they’ll try to sell it. With a consigned order, Amazon puts the product in their warehouse, but it still belongs to me until Amazon sells it. At that point, Amazon handles the transaction with the customer and they forward me the money, minus a transaction fee they keep for their work. If the product doesn’t sell, that’s my loss and Amazon can ship it back to me to free up some shelfspace.

Let’s see if I can get this confirmed or corrected by someone more knowledgeable than the Internet. Zack confirmed it.

That leaves New Product Introduction. Does that mean that the first time Amazon orders that product from you, it will have a separate code? Why would that be the case? Again, I’m lacking domain knowledge. For now, I’ll assume you can handle it like New Order.

Handling the variants

Now that I have an idea of the variants, I need to decide how to deal with them. That’s not a technical issue; it depends on the business needs. Since this exercise takes place in a fictitious context, there’s no one I can ask questions about the business. No matter, I might not be able to get answers, but I still should be able to figure out the questions.

The first one is whether we use consignment or not. It seems to me that we either do all our orders with consignment or none of them. That implies that we receive either NE-orders (New Order) or CN-orders (Consigned Order). That’s an understanding we need to come to with Amazon before we start processing their EDI messages. Other than that, I don’t expect a difference in the way the orders need to be routed. In both cases, the warehouse needs to fulfill the order and Finance needs to process it. How Finance processes each order will be different, but that’s beyond my concern.

A rush order may be a simple boolean flag in the system, or it may have to kick off its own, separate flow, depending on how the business operates. Does the business need me to do something special in this case? If there’s a separate flow, can we kick that off based on the simple boolean flag? That seems reasonable to me and it means that we can map the message regardless of the impact a rush order has on the business. I might need to route it somewhere else if there’s a special department handling rush orders, though.

The considerations for a new product introduction are similar to those for rush orders. If there’s a special flow, can we kick it off based on a boolean flag? Because that would make mapping simple. If we need to inform specific people of a new product introduction, we can do that based on the flag as well.

A minimal mapping

Even without the answers, I can make some reasonable assumptions about how the system should work and create a mapping based on that. I’m not going to map the entire 850, because that’s not what I’m trying to figure out here. I’m interested in handling the different variants and for that, I only need to map the BEG-segment. I can get away with that, because all three variants—NE, NP, and RO—are basically the same, except for a flag or two. Yes, I’m assuming non-consigned orders, but even that doesn’t make a big difference. For handling consigned orders, just swap out NE for CN and keep the flags. Okay, let’s open Mappings and start mapping.

The starting page for Mappings.

I’ll come up with my own custom target JSON based on the implementation guide. Yes, this is cheating, because typically you have a defined JSON shape you need to map to, but the principle is the same.

An example of the target JSON, with four fields: purchaseOrderNumber, orderDate, isRushOrder, isProductIntroduction.

I decided to go with two flags: one for rush order, one for product introduction. Another option is to create a field type that can be normal, rush, or introduction. Doesn’t matter too much; I just like the flags.

The source JSON is just a BEG-segment. That’s small enough that I can type the EDI by hand. However, in order to do the mapping, I need to convert it to JSON, because that’s what Mappings uses. I’ll use Inspector to translate my handcrafted segment to JEDI, which is Stedi’s JSON representation of EDI.

A screenshot of the Inspector parsing a BEG segment.

Inspector doesn’t like that single segment very much. I guess I should tell it which transaction set it’s dealing with.

Hey, I just noticed that the Amazon implementation guide provides sample data for each individual segment, so I didn’t have to type it by hand. That’s marvelous.

An example of a BEG segment, as provided by the Amazon implementation guide.

I add a transaction set identifier code. That’s still not enough to make Inspector happy, but it’s enough to keep it satisfied and it does its best to produce some JEDI. It looks fine for my purposes, so kudos to Inspector.

A screenshot of the Inspector parsing a BEG segment wrapped in a transaction set envelope.

The purchase order is trivial to map, because it’s a one-to-one copy of a specific field.

The mapping expression for the field purchaseOrderNumber.

The date isn’t much harder. It just requires a minimal bit of conversion, because I like dashes in my date.

The mapping expression for the field orderDate.

Will the flags be more challenging?

The mapping expression for the field isRushOrder.

That’s not quite right. The code in the sample JEDI isn’t NE, but new_order_NE. JEDI makes the values more readable, but now I have to figure out what the JEDIfied versions are of RO and NP. Fortunately, Inspector can help me here.

A screenshot of the Inspector showing the JEDI values for RO and NP.

Now that I know that, the mapping expressions for the flags will be simple.

The corrected mapping expression for the field isRushOrder.The mapping expression for the field isProductIntroduction.

That’s it?

The whole exercise was less of a hassle than I expected to be honest. Granted, I made things easy on myself by assuming only a single transaction set in the EDI message and by mapping only a single segment, but why would I want to make things hard for myself?

That’s not the real reason it was easier than expected, though. The real reason is that the variants of the Amazon 850 purchase order are all pretty much the same. I was able to handle them with nothing more than two flags. Most transaction sets that have variants will be more difficult to handle. If you’re disappointed by the lack of suffering I had to go through this time, then you can take comfort in the fact that the next transaction set with variants is bound to cause me more of a headache.

Jun 22, 2022

EDI

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

This document is the tl;dr of EDI. It won’t contain everything. It may not answer all your questions. It will only apply to X12. But it will save you a ton of time if you’re trying to decipher your first EDI document.

For this document, I’ll be using an 856 Ship Notice as an example. If you’re wondering what the heck an 856 Ship Notice is, you’re in the right place. Anyway, here’s the example.

ISA*00*          *00*          *ZZ*MYORG          *ZZ*Interchange Rec*200831*2324*U*00501*100030537*0*P*>~
GS*SH*MYORG*Interchange Rec*20200831*2324*200030537*X*005010~
ST*856*300089443~
BSN*00*P1982123*20200831*2324*0001~
DTM*011*20200831*2324~
HL*1**S~
TD1*CTN*1****G*2*LB~
TD5**2*FDE*U********3D*3D*3D~
DTM*011*20200831~
N1*ST*SOO KIM*92*DROPSHIP CUSTOMER~
N3*26218 QUENTIN TURNPIKE~
N4*REANNAFORT*MS*51135~
HL*2*1*O~
PRF*1881225***20200831~
HL*3*2*P~
MAN*CP*037178019302492~
HL*4*3*I~
LIN*1*VC*11216.32*SK*RGR-11216.32~
SN1*1*1*EA~
PID*F****ALL TERRAIN ENTRY GUARD KIT 2020 JEEP WRANGLER J~
CTT*4~
SE*20*300089443~
GE*1*200030537~
IEA*1*100030537

What is EDI?

EDI a structured way for businesses to send documents back and forth. It can be:

  • a purchase order (“I want to buy this!”),

  • an invoice (“Pay me for this!”),

  • a ship notice (“I sent you this!”),

  • or any of 300+ other transaction types.

Think of it as a JSON Schema to cover all possible business transactions and details, but before JSON, or XML, or the World Wide Web were invented.

You might hear people talk about X12 EDI. This is just a specific format of EDI. Another common one is EDIFACT. We’ll focus on X12 EDI here.

You might also hear of 5010 or 4010 . These are specific versions (formally known as releases) of EDI. Think of a versioned API, which might have small but breaking changes as you increase versions. EDI is an old standard and it required some changes over the years.

Structure

Segments, elements, and separators

When you first look at the 856 Ship Notice, you’ll see the following:

ISA*00*          *00*          *ZZ*MYORG          *ZZ*Interchange Rec*200831*2324*U*00501*100030537*0*P*>~
GS*SH*MYORG*Interchange Rec*20200831*2324*200030537*X*005010~
ST*856*300089443~
BSN*00*P1982123*20200831*2324*0001~
DTM*011*20200831*2324~

This feels overwhelming. Don’t let it be.

Notice that the example of this particular EDI document is split into 5 lines. (The full document has 24 lines, as you can see at the start of the article.) Each line is called a segment. Each segment is broken into multiple elements.

Let’s start by breaking down what’s happening on line 1.

  1. It starts with ISA. This is the segment name. We’ll often refer to the segment by its name. “Ohh, they’re missing an ABC segment”, or “That’s an invalid value in the XYZ segment”. This is the ISA segment.

  2. There are a bunch of letters and numbers separated by *. An asterisk serves as a separator between elements (technically, the fourth character—right after ISA—is the element separator, but it’s commonly a *). Elements are often referred to by their numerical index within a particular segment. For example, notice the 200831 near the end of the ISA segment. That is called ISA09 as it is the ninth element in the ISA segment. When you look at the definition of the segment, you’ll see that it refers to the Interchange Date in YYMMDD format. Thus, 200831 is August 31, 2020.

In the file we’ve looked at so far, each segment is on a different line. Sometimes you’ll get a file that’s all on one line. EDI documents will generally use ~ or a newline \n to distinguish between segments. (It’s technically the 105th byte in the ISA segment.) When reviewing a document, I find it much easier to split it into new lines based on the segment delimiter.

Common segments

Now that we know the basics of segments, elements, and separators, let’s look at some common segments. Specifically, we’re going to look at ISA, GS, and ST.

ISA

We looked at the ISA segment in the previous section. This will always be the first line in an EDI document. It’s called an interchange and contains metadata that applies to the document as a whole. This includes the document’s date and time, a control number (similar to an auto-incrementing primary key), whether it’s a test document, and more.

Typically, there’s only one ISA segment in a document, but some organizations combine multiple EDI documents into one.

GS

The GS segment will always be next. It’s the functional group and it includes additional metadata about the transactions within the group. The most important value is the last one (GS08) which indicates the X12 version that is used by the document. In the example above, the value of this element is 005010. That indicates it’s using the 5010 version.

The X12 spec allows multiple functional groups in a single document.

ST

The ST segment is for the actual transaction being sent. This is the star of the show and the reason we’re all here. It could be an invoice, a purchase order, or any other of the many transaction types.

Each functional group can have multiple transactions.

ST01 identifies the transaction type for the given transaction. In the example above, it says 856, which maps to a ship notice.

Other common transaction types are:

Closing segments

Finally, for each of these key sections, there is a closing segment (formally a trailer segment) to indicate when they end. These closing segments include a checksum and a control number.

The code fragment above shows the last three lines of our ship notice example. The SE segment indicates the end of the ST block. SE01 refers to the number of segments within the ST block. The value 20 indicates there were 20 segments in that transaction. SE02 is a control number that matches the control number from the ST segment.

Likewise, the GE segment indicates the end of the functional group, which started with GS. GE01 states how many transactions were included within the functional group, and GE02 is a control number for the group. Same thing with IEA which closes the ISA block.

Implementation guides

The X12 spec is expansive, and most companies don’t need all of it. Accordingly, companies will limit what they support. The terms may be dictated and standardized by the trading partner with the most market power, or it may be by mutual agreement between the two trading partners.

As a result, you may get an implementation guide indicating what is supported by a your trading partner. We’ll take a look at an example for a mapping guide for an 856 Ship Notice. There are two kinds of pages here: the overview page, and the segment detail page.

Overview

The overview page shows all the segments that are allowed in this particular transaction.

The Req column shows whether a segment is mandatory (M) or optional (O).

For this transaction, only two segments are optional.

The Max Use column shows the maximum number of times a segment can be repeated.

For example, the DTM segment can be used up to 10 times, which means a ship notice can have 10 dates associated with it.

A loop is a collection of segments that can be repeated, in this case up to 200 times.

A loop can also contain other loops.

Note that repeats are multiplicative. The top-level loop can be repeated 200,000 times and the inner loop 200 times, so the N1, N3, and N4 segment can be included 200,000 × 200 = 40,000,000 times.

Another look at the example document shows multiple iterations of the HL loop.

Details

The overview usually doesn’t take up more than a page or two. The rest of the implementation guide is made up of segment detail pages. Here’s one for the PRF segment.

The details page describes each element within the segment.

For example, the PRF01 segment is mandatory and can have at most 22 characters.

The PRF06 segment is optional and can have at most 30 characters.

At times, an element will accept a small number of codes that map to specific values, similar to an enum. You can see an example of that on the details page for DTM.

Transforming EDI

So far, we’ve looked at a raw EDI document and you may have noticed that the format is hard to use with your favorite programming language. It would take a lot of string parsing, array indexing, and other tedium. Stedi offers an EDI parser, converter, and validator called EDI Core.

We can take our EDI example and give it to EDI Core. EDI Core will return a JSON representation of the EDI that we call JEDI.

{
  "interchanges": [
    {
      "interchange_control_header_ISA": {
        "authorization_information_qualifier_01": "no_authorization_information_present_no_meaningful_information_in_i02_00",
        "authorization_information_02": "",
        "security_information_qualifier_03": "no_security_information_present_no_meaningful_information_in_i04_00",
        "security_information_04": "",
        "interchange_id_qualifier_05": "mutually_defined_ZZ",
        "interchange_sender_id_06": "MYORG",
        "interchange_id_qualifier_07": "mutually_defined_ZZ",
        "interchange_receiver_id_08": "Interchange Rec",
        "interchange_date_09": "200831",
        "interchange_time_10": "2324",
        "repetition_separator_11": "U",
        "interchange_control_version_number_12": "00501",
        "interchange_control_number_13": "100030537",
        "acknowledgment_requested_14": "no_interchange_acknowledgment_requested_0",
        "interchange_usage_indicator_15": "production_data_P",
        "component_element_separator_16": ">"
      },
      "groups": [
        {
          "functional_group_header_GS": {
            "functional_identifier_code_01": "ship_notice_manifest_856_SH",
            "application_senders_code_02": "MYORG",
            "application_receivers_code_03": "Interchange Rec",
            "date_04": "20200831",
            "time_05": "2324",
            "group_control_number_06": "200030537",
            "responsible_agency_code_07": "accredited_standards_committee_x12_X",
            "version_release_industry_identifier_code_08": "005010"
          },
          "transaction_sets": [
            {
              "set": "856",
              "heading": {
                "transaction_set_header_ST": {
                  "transaction_set_identifier_code_01": "856",
                  "transaction_set_control_number_02": "300089443"
                },
                "beginning_segment_for_ship_notice_BSN": {
                  "transaction_set_purpose_code_01": "original_00",
                  "shipment_identification_02": "P1982123",
                  "date_03": "20200831",
                  "time_04": "2324",
                  "hierarchical_structure_code_05": "shipment_order_packaging_item_0001"
                },
                "date_time_reference_DTM": [
                  {
                    "date_time_qualifier_01": "shipped_011",
                    "date_02": "20200831",
                    "time_03": "2324"
                  }
                ]
              },
              "detail": {
                "hierarchical_level_HL_loop": [
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "1",
                      "hierarchical_level_code_03": "shipment_S"
                    },
                    "carrier_details_quantity_and_weight_TD1": [
                      {
                        "packaging_code_01": "CTN",
                        "lading_quantity_02": "1",
                        "weight_qualifier_06": "gross_weight_G",
                        "weight_07": "2",
                        "unit_or_basis_for_measurement_code_08": "pound_LB"
                      }
                    ],
                    "carrier_details_routing_sequence_transit_time_TD5": [
                      {
                        "identification_code_qualifier_02": "standard_carrier_alpha_code_scac_2",
                        "identification_code_03": "FDE",
                        "transportation_method_type_code_04": "private_parcel_service_U",
                        "service_level_code_12": "three_day_service_3D",
                        "service_level_code_13": "three_day_service_3D",
                        "service_level_code_14": "three_day_service_3D"
                      }
                    ],
                    "date_time_reference_DTM": [
                      {
                        "date_time_qualifier_01": "shipped_011",
                        "date_02": "20200831"
                      }
                    ],
                    "party_identification_N1_loop": [
                      {
                        "party_identification_N1": {
                          "entity_identifier_code_01": "ship_to_ST",
                          "name_02": "SOO KIM",
                          "identification_code_qualifier_03": "assigned_by_buyer_or_buyers_agent_92",
                          "identification_code_04": "DROPSHIP CUSTOMER"
                        },
                        "party_location_N3": [
                          {
                            "address_information_01": "26218 QUENTIN TURNPIKE"
                          }
                        ],
                        "geographic_location_N4": {
                          "city_name_01": "REANNAFORT",
                          "state_or_province_code_02": "MS",
                          "postal_code_03": "51135"
                        }
                      }
                    ]
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "2",
                      "hierarchical_parent_id_number_02": "1",
                      "hierarchical_level_code_03": "order_O"
                    },
                    "purchase_order_reference_PRF": {
                      "purchase_order_number_01": "1881225",
                      "date_04": "20200831"
                    }
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "3",
                      "hierarchical_parent_id_number_02": "2",
                      "hierarchical_level_code_03": "pack_P"
                    },
                    "marks_and_numbers_information_MAN": [
                      {
                        "marks_and_numbers_qualifier_01": "carrier_assigned_package_id_number_CP",
                        "marks_and_numbers_02": "037178019302492"
                      }
                    ]
                  },
                  {
                    "hierarchical_level_HL": {
                      "hierarchical_id_number_01": "4",
                      "hierarchical_parent_id_number_02": "3",
                      "hierarchical_level_code_03": "item_I"
                    },
                    "item_identification_LIN": {
                      "assigned_identification_01": "1",
                      "product_service_id_qualifier_02": "vendors_sellers_catalog_number_VC",
                      "product_service_id_03": "11216.32",
                      "product_service_id_qualifier_04": "stock_keeping_unit_sku_SK",
                      "product_service_id_05": "RGR-11216.32"
                    },
                    "item_detail_shipment_SN1": {
                      "assigned_identification_01": "1",
                      "number_of_units_shipped_02": "1",
                      "unit_or_basis_for_measurement_code_03": "each_EA"
                    },
                    "product_item_description_PID": [
                      {
                        "item_description_type_01": "free_form_F",
                        "description_05": "ALL TERRAIN ENTRY GUARD KIT 2020 JEEP WRANGLER J"
                      }
                    ]
                  }
                ]
              },
              "summary": {
                "transaction_totals_CTT": {
                  "number_of_line_items_01": "4"
                },
                "transaction_set_trailer_SE": {
                  "number_of_included_segments_01": "20",
                  "transaction_set_control_number_02": "300089443"
                }
              }
            }
          ],
          "functional_group_trailer_GE": {
            "number_of_transaction_sets_included_01": "1",
            "group_control_number_02": "200030537"
          },
          "release": "005010"
        }
      ],
      "interchange_control_trailer_IEA": {
        "number_of_included_functional_groups_01": "1",
        "interchange_control_number_02": "100030537"
      },
      "delimiters": {
        "element": "*",
        "segment": "~",
        "sub_element": ">"
      }
    }
  ],
  "__version": "jedi@2.0"
}

This is more verbose, but it’s much easier to deal with from code. You can see the EDI and the JSON side-by-side by using the Inspector. It’ll show an explanation of each segment, too.

Getting started

This is certainly not all you need to know, but it’s enough to get you started. Don’t forget to check out our blog for more information on working with EDI.

Jun 13, 2022

EDI

I don’t blame EDI for failures in logistics any more than I blame kittens for messing up my floor. I only got Sam and Toby a day ago, but they’re the cutest balls of fuzz ever to grow legs and they jumped straight into my heart. Still, as I’m staring at the puddle on my floor, I’m convinced that there's a design flaw in their digestive system. I don’t remember their breakfast being this chunky, or this sticky. Sputter gunk kittens are a new experience for me, so I call a friend and she recommends a steam cleaner.

There’s a store that sells steam cleaners about a thirty-minute drive from here, but that means I’d have to leave the kittens alone and I don’t want to do that. After a short search, I find an online store with same-day delivery, called Howard’s House of Cleanliness—How Clean, for short. They sell steam cleaners they do not own. That’s not a scam; it’s dropshipping and it’s a marvel of online logistics.

Traditionally, How Clean would order a whole bunch of steam cleaners from Vaporware and keep them in their own warehouse. Then, if I order one, How Clean would ship it to me.

  1. How Clean sends an order to Vaporware.

  1. Vaporware hands of a bunch of steam cleaners to Truckload.

  1. Truckload ships a bunch of steam cleaners to How Clean.

  1. How Clean stores the steam cleaners in their own warehouse.

  1. I order a steam cleaner from How Clean.

  1. How Clean hands off one steam cleaner to Last Mile.

  1. Last Mile drives the steam cleaner to me.

  1. I receive my steam cleaner.



Dropshipping

In the case of dropshipping, the steam cleaners don’t need to go to How Clean’s warehouse.

  1. I order a steam cleaner from How Clean.

  1. How Clean forwards the order to Vaporware.

  1. Vaporware hands over one steam cleaner to Last Mile.

  1. Last Mile drives the steam cleaner to me.

  1. I receive my steam cleaner.

This is convenient for How Clean, because they don’t have to order a bunch of steam cleaners up front in the hope of selling them. It’s convenient for Vaporware, because they don’t have to open an online shop and deal with customers. And customers don’t even notice, unless something goes wrong.

Toby is finding out that gravity doesn’t always work in his favor. A monstera is a good house plant for decoration, but not so much for climbing. It’s certainly not strong enough to carry his full weight.

My floor is now decorated with one cat, one plant, and a lot of dirt, so I check my email. Toby gets up, a little confused. He knows where the plant came from, of course, but the dirt is new to him. I hit refresh; nothing. He decides to do some digging and discovers that underneath all the dirt is more dirt. Refresh, refresh, refresh; nothing, nothing, nothing. Sam joins in. To her, the unpotted plant is more of an artwork than a research project and the carpet is her canvas. I need a drink. The steam cleaner should’ve shipped by now, but apparently How Clean can’t be bothered to send me a confirmation. It’s time to give them a call. Of course, now I’m on hold.

Somewhere, the logistics failed. There’s always quite a bit of communication going on between different parties. That doesn't happen by someone picking up the phone; it all happens electronically. The documents that are sent back and forth adhere to a specific format. EDI is that format. There are different documents involved and they each have a number and a name. For example, the document that How Clean sends to Vaporware is called an 850 Purchase Order.

  1. How Clean sends an 850 Purchase Order to Vaporware to place an order.

  1. Vaporware sends a 216 Motor Carrier Shipment Pick-up Notification to Last Mile to request delivery.

  1. After Last Mile picks up the package, Vaporware sends an 856 Ship Notice to How Clean to let them know the order has shipped.

That’s how it should work.

Sam has found a spot near the window in the warm sunlight and starts dancing with her own shadow. Toby looks at her from across the room and is content observing the merriment, but that won’t do for Sam. She wants him to join in the fun. She runs over to him and on the way, she knocks over my drink.

“Good afternoon. Howard’s House of Cleanliness. This is Mira speaking. How may I help you?”
“Do you have kittens?”
“No sir, we don’t sell pets.”
“You sell steam cleaners, though, don’t you? I need my steam cleaner.”
Mira pulls up my record.
“That one hasn’t shipped yet.”
“It’s supposed to arrive today. Will it arrive today? I need it today.”
“Let me check. Oh, that’s odd: no delivery date. I don’t know yet. Perhaps if you ask again later.”
“How much later?”
“If you call me tomorrow, I should have an answer for you.”
“Well, tomorrow I don’t really need your help to figure out if it arrived today.”
“Oh no, sir, you’re right.”
Mira laughs. I don’t know why. I decide to make myself another drink, but then I think better off it.
“You know what, just cancel my order. I’ll get it somewhere else.”
“I’m sorry, but I can’t seem to cancel it. Maybe because it hasn’t shipped yet.”
“Wouldn’t it be more difficult to cancel it after it has shipped?”
Mira promises to keep an eye on my order and cancel it when she can.
“You’ll get your refund, sir, don’t worry.”

I don’t want a refund; I want my steam cleaner. I grab a roll of paper towels and try to get rid of the puddle. Sam grabs the end of the roll with her mouth, gives it a tug, then offers it to me. Someone messed up here and it isn’t my kittens.

How Clean’s website works, otherwise Mira wouldn’t have been able to pull up my details. Maybe they didn’t place the order with Vaporware. Or maybe the order picker at Vaporware got lost in their own warehouse. I don’t know, but someone didn’t send the document they were supposed to.

  1. The order I placed with How Clean arrived just fine, because Mira can see it in the system.

  1. It’s possible the 850 Purchase Order from How Clean to Vaporware didn’t arrive. It would explain why How Clean didn’t receive a delivery date from Vaporware; Vaporware doesn’t know there’s anything to deliver.

  1. It’s possible the 216 Motor Carrier Shipment Pick-up Notification from Vaporware to Last Mile didn’t arrive. It would explain why the order hasn’t shipped yet; Last Mile doesn’t know there’s something for them to ship.

  1. It’s possible the 856 Ship Notice from Vaporware to How Clean didn’t arrive. It would explain why Mira can’t see the delivery date; Vaporware didn’t give one to How Clean.

They’ll probably blame EDI; that’s what they always do. That’s wrong, though. EDI tells you what a document should look like; it’s not responsible for making sure the document arrives.

The paper towels have turned into papier mache. Enough! I put on my coat and grab my car keys. The kittens can manage for an hour, because whatever the state of my floor and my plant and my drink, Sam and Toby are just fine. Then I notice the clock and I realize: the store is closed. There’s nothing left for me to do.

I still want to know what went wrong. Maybe I can call Vaporware. No phone number, but I do have their address now. I know where that is; that’s not an industrial area. Judging from Street View, Vaporware is a pretty small business. That would explain— Is that the doorbell?

I call How Clean.
“It’s here,” I say.
“Let me check,” Mira says.
“There’s no need to check; it’s here.”
“No, it isn’t.”
“It isn’t?”
“No sir, I’m looking right at my computer screen and it clearly says it isn’t.”
“And I’m looking right at my steam cleaner and it clearly is. It’s here!”
“Well, it’s not supposed to be.”
“You and I disagree on that one.”
“Are you sure it’s the steam cleaner you ordered from us?”
“It’s not like I ordered steam cleaners from five different— It doesn’t matter. Could you please just cancel the cancellation, so that you get properly paid?”
“I can’t find the button for canceling a cancellation.”
“…”
“But I’ll figure it out.”
“Thank you.”
“Before you go, sir, one more thing. Seeing that this kind of concludes your order and all that, I wanted to ask you a question.”
“Go ahead.”
“Are you satisfied with your new steam cleaner?”

All of this could’ve been prevented. I have my steam cleaner, which means Last Mile picked it up from Vaporware, which means that Vaporware sent it to me, which means How Clean did put in the order. In other words, the 850 Purchase Order from How Clean arrived at Vaporware just fine, and Last Mile did receive the 216 Motor Carrier Shipment Pick-up Notification from Vaporware, so the problem must be that the 856 Ship Notice never got back to How Clean. That’s why How Clean thinks the steam cleaner hasn’t shipped yet.

Showing the ordering process with the 856 as the point of failure.

Since Vaporware is a small vendor, they don’t have the resources to properly automate this process. They could hire a third party to do the automation for them, but chances are they’re perfectly happy filling out the occassional 856 Ship Notice manually and sending it when they get around to it. That may not be fast enough for my taste, but it makes good business sense to Vaporware. It also makes Mira’s job harder.

How Clean does have another option, though. Since they are a large retailer, dealing with many more shipments than just the ones from Vaporware, they could strike a deal with Last Mile. Every time Last Mile is asked to deliver a package to one of How Clean’s customers, they can send a message to How Clean. And yes, EDI has a template for that message as well. It’s called a 214 Transportation Carrier Shipment Status Message.

The ordering process with Last Mile sending a 214 to How Clean.

How Clean can use this message from Last Mile as a backup. It’s bound to be more reliable than the 856 Ship Notice from Vaporware, because a large carrier like Last Mile does have this process automated. As soon as a courier picks up the steam cleaner from Vaporware, they will scan its bar code and the system will generate and send the 214 Transportation Carrier Shipment Status Message. Even if Vaporware is tardy with their 856 Ship Notice, How Clean can let me know with confidence that the order has shipped and Mira will never have to deal with me.

That was yesterday. Today, I’m watching how Toby discovers that a water bowl isn't just for drinking. At the same time, Sam indulges her creativity with some paw painting. It’s fine. There’s no mess they can make that my steam cleaner can’t handle. I sit down on the couch with my drink and then the doorbell rings.

My best guess is that Mira didn’t figure out how to undo the cancellation and, to be on the safe side, she put in another order for me. EDI can’t fix a broken system, nor can it prevent human error. If you’ve enriched your life with kittens, or puppies, or gerbils and you now find yourself in the market for a top-of-the-line steam cleaner, don’t order one from Howard’s House of Cleanliness. Just drop me a line; I have a spare.

Editor: Kasia Fojucik. Illustrator: Katherine Meng.

Jun 7, 2022

Guide

Big takeaway: If you use Stedi’s JSON APIs, you can safely ignore X12 control numbers. If a top-level controlNumber is required, use a dummy value.

Control numbers in X12 can be confusing. There are three different types, and they have weird formatting rules.

Luckily, if you use Stedi’s JSON APIs, you don’t need to deal with X12 control numbers at all. Even if you pass X12 directly to Stedi, you only need to follow a few validation rules. This guide covers:

  • What control numbers are

  • The different types of control numbers

  • How to use them with Stedi

What are control numbers?

Every X12 file contains three nested containers called envelopes:

  • Interchange – The outer envelope containing the entire EDI file. The file may contain one – and only one – interchange.

  • Functional group – A functional group of related transactions inside that envelope. An interchange can contain many groups.

  • Transaction set – An individual transaction in a group. A group can contain many transactions.

Each envelope has its own control number. The control number acts as an ID for that specific container.

Why have different control numbers?

Each control number serves a different purpose.

Interchange control numbers confirm whether an X12 file was received.
For example, Stedi sends an X12 file with interchange control number 000000001 to a payer. The payer can send back an acknowledgment for 000000001. Stedi knows that the file wasn’t lost.

Group control numbers help with batching and routing.
If the receiver indicates there was an error with group 234, the sender can just resend the transactions in that group. They don’t have to resend the entire file.

In some use cases, group control numbers are also used for internal routing. The receiver can split the X12 file by group and route each group’s transactions to a different system or department in their organization.

A transaction set control number identifies a specific transaction.
If there’s only an error with transaction 123456789, the sender can just resend that transaction – not the entire group or file.

Control numbers in Stedi’s JSON APIs

If you’re using Stedi’s JSON APIs, you can ignore X12 control numbers.
Our JSON APIs handle any needed X12 translation for you.

Don’t use X12 control numbers for tracking.
Don't use the controlNumber in API responses for transaction tracking. These values won't match what you pass in requests and aren't guaranteed to be unique to the transaction.

If you need to track claims, use the patient control number instead. See our How to track claims blog.

Control numbers in Stedi X12

Where control numbers are located
In an X12 file, each control number appears twice in an X12 message: once in the envelope's opening header and once in its closing trailer. The control number in the header must match the control number in its respective trailer.

The following table outlines each control number’s location and requirements.

Control number

Header location

Trailer location

Data type

Length

Interchange

ISA-13

IEA-02

Numeric (integer)

Exactly 9 digits. Can include leading zeroes.


Functional group

GS-06

GE-02

Numeric (integer)

1-9 digits.

Transaction set

ST-02

SE-02

Numeric (integer)

4-9 digits. Only include leading zeroes to meet length requirements.

Don’t worry about uniqueness.
If you pass X12 directly to Stedi using our X12 APIs or SFTP, you only need to ensure the ISA/IEA, GS/GE, and ST/SE envelopes are valid X12.

Stedi creates its own ISA /IEA and GS/GE envelopes before sending them to the payer. This means you can use any valid transaction set control number in the ST/SE envelope. The transaction set control number only has to be unique within its groups.

Get started

You don’t have to learn EDI or wrestle with control numbers to process healthcare transactions. Stedi’s modern JSON APIs can handle it for you.

Sign up for free and request a trial to try them out. It takes less than two minutes and gives you instant access.

May 31, 2022

Engineering

At Stedi, we are fans of everything related to AWS and serverless. By standing on the shoulders of giants, the engineering teams can quickly iterate on their ideas by not having to reinvent the wheel. Serverless-first development allows product backends to scale elastically with low maintenance effort and costs.

One of the serverless products in the AWS catalog is Amazon DynamoDB – an excellent NoSQL storage solution that works wonderfully for most of our needs. Designed with scale, availability, and speed in mind, it stands at the center of our application architectures.

While a pleasure to work with, Amazon DynamoDB also has limitations that engineers must consider in their applications. The following is a story of how one of the product teams dealt with the 400 KB Amazon DynamoDB item size limit and the challenge of ensuring data integrity across different AWS regions while leveraging Amazon DynamoDB global tables.

Background

I'm part of a team that keeps all its application data in Amazon DynamoDB. One of the items in the database is a JSON blob, which holds the data for our core entity. It has three large payload fields for customer input and one much smaller field containing various metadata. A simplified control flow diagram of creating/updating the entity follows.

The user sends the JSON blob to the application API. The API is fronted with Amazon API Gateway and backed by the AWS Lambda function. The logic within the AWS Lambda function validates and applies business logic to the data and then saves or updates the JSON blob as a single Amazon DynamoDB item.

The product matures

As the product matured, more and more customers began to rely on our application. We have observed that the user payload started to grow during the application lifecycle.

Aware of the 400 KB limitation of a single Amazon DynamoDB item, we started thinking about alternatives in how we could store the entity differently. One of such was splitting the single entity into multiple sub-items, following the logical separation of data inside it.

At the same time, prompted by one of the AWS region outages that rendered the service unavailable, we decided to re-architect the application to increase the system availability and prevent the application from going down due to AWS region outages.

Therefore, we deferred splitting the entity to the last responsible moment, pivoting our work towards system availability, exercising one of the Stedi core standards – "bringing the pain forward" and focusing on operational excellence.

We have opted for an active-active architecture backed by DynamoDB global tables to improve the service's availability. The following depicts the new API architecture in a simplified form.

Amazon Route 53 routes the user request to the endpoint that responds the fastest, while Amazon DynamoDB global tables take care of data replication between different AWS regions.

The tipping point

After some time, customers expected the application's API to allow for bigger and bigger payloads. We got a strong signal that the 400 KB limit imposed by the underlying architecture no longer fits the product requirements – that was the last responsible moment I alluded to earlier.

Considering prior exploratory work, we identified two viable solutions that would enable us to expand the amount of data the entity consists of, allowing the service to accept bigger payloads.

The first approach would be to switch from Amazon DynamoDB to Amazon S3 as the storage layer solution. Changing to Amazon S3 would allow the entity, in theory, to grow to the size of 5 TB (which we will never reach).

The second approach would be to keep using Amazon DynamoDB and take advantage of the ability to split the entity into multiple sub-items. The sub-items would be concatenated back into a singular entity object upon retrieval.

After careful consideration, we decided to keep using Amazon DynamoDB as the storage solution and opted to split the entity into multiple Amazon DynamoDB sub-items. Sticking with the same storage layer allowed us to re-use most of our existing code while giving us much-needed leeway in terms of the entity size. Because we have four sub-items, the entity could grow up to 1.6 MB.

After splitting up the core entity into four sub-items, each of them had the following structure.

{
  pk: "CustomerID",
  sk: "EntityID#SubItemA",
  data: ...
}

The challenges with global replication and eventual consistency

The diagrams above do not take data replication to secondary AWS regions into account. The data replication between regions is eventually consistent and takes some time – usually a couple of seconds. Due to these circumstances, one might run into cases where all four sub-items are only partially replicated to another region. Reading such data without performing additional reconciliation might lead to data integrity issues.

The following sequence diagram depicts the problem of potential data integrity issues in the read-after-write scenario.

Below the orange box, you can see four replications of the entity's sub-items. Below the red box, you can see a query to retrieve the entity. The query operation took place before all sub-items had the chance to replicate. Consequently, the fetched data consists of two sub-items from the new version and two ones from the old version. Merging those four sub-items leads to an inconsistent state and is not what the customer expects. Here's a code example that shows how this query would work:

import { DocumentClient } from "aws-sdk/clients/dynamodb";
const ddb = new DocumentClient();
const NUMBER_OF_SUB_ITEMS = 4;

const subItems = (
  await ddb
    .query({
      TableName: "MyTableName",
      KeyConditionExpression: "pk = :pk and begins_with(sk, :sk_prefix)",
      ExpressionAttributeValues: {
        ":pk": "CustomerID",
        ":sk": "EntityID#",
      },
      Limit: NUMBER_OF_SUB_ITEMS,
    })
    .promise()
).Items;

const entity = merge(subItems);

The application's API should not allow any data integrity issues to happen. The logic that powers fetching and stitching the entity sub-items must reconcile the data to ensure consistency.

The solution

The solution we came up with is a versioning scheme where we append the same version attribute for each entity sub-item. Attaching a sortable identifier to each sub-item, in our case – a ULID, allows for data reconciliation when fetching the data. We like ULIDs because we can sort them in chronological order. In fact, we don't even sort them ourselves. When we build a sort key that contains the ULID, DynamoDB will sort the items for us. That's very convenient in the context of data reconciliation.

Below, you can see how we added the version to the sort key.

{
  pk: "CustomerID",
  sk: "EntityID#Version1#SubItemA",
  data: ...
}

The version changes every time the API consumer creates or modifies the entity. Each sub-item has the same version in the context of a given API request. The version attribute is entirely internal to the application and is not exposed to the end-users.

By appending the version to the sub-items' Amazon DynamoDB sort key, we ensure that the API will always return either the newest or the previous entity version to the API consumer.

The following is a sequence diagram that uses the version attribute of the entity sub-item to perform data reconciliation upon data retrieval.

Similar to the previous diagram, we again see that two sub-items are replicated before we try to read the entity. Instead of getting the four sub-items, we now query at least the two latest versions by utilizing the version in the sort key. Here's a code example that shows how this query would work:

import { DocumentClient } from "aws-sdk/clients/dynamodb";
const ddb = new DocumentClient();
const NUMBER_OF_SUB_ITEMS = 4;

const subItems = (
  await ddb
    .query({
      TableName: "MyTableName",
      KeyConditionExpression: "pk = :pk and begins_with(sk, :sk_prefix)",
      ExpressionAttributeValues: {
        ":pk": "CustomerID",
        ":sk": "EntityID#",
      },
      // This line changed: We now load two versions if possible.
      Limit: 2 * NUMBER_OF_SUB_ITEMS,
    })
    .promise()
).Items;

const latestSubItems = filterForHighestReplicatedVersion(subItems);
const entity = merge(latestSubItems);

Instead of returning possibly inconsistent data to the customer hitting the API in the AWS region B, the API ensures that the entity consists of sub-items with the same version.

We do it by fetching at minimum two latest versions of the entity sub-items, sorted by the version identifier. Then we merge only those sub-items that are of the same version, favoring the newest version available. The API behaves in an eventually consistent manner in the worst-case scenario, which was acceptable for the product team.

As for the delete operation – there is no need for data reconciliation. When the user requests the entity's removal, we delete all of the entity sub-items from the database. Every time we insert a new group of sub-items into the database, we asynchronously delete the previous group, leveraging the Amazon DynamoDB Streams. This ensures that we do not accumulate a considerable backlog of previous sub-item versions, keeping the delete operation fast.

Engineering tradeoffs

There is a saying that software engineering is all about making the right tradeoffs. The solution described above is not an exception to this rule. Let us consider tradeoffs related to costs next.

The logic behind querying the data must fetch more items than necessary. It is not viable to only query for the last version of sub-items as some of them might still be replicating and might not be available. These factors increase the retrieval costs – see the Amazon DynamoDB read and write requests pricing.

Another axis of cost consideration for Amazon DynamoDB is the storage cost. Storing multiple versions of the entity sub-items increases the amount of data stored in the database. To optimize storage costs, one might look into using the Amazon DynamoDB Time to Live or removing the records via logic triggered by DynamoDB Streams. The product team chose the latter to keep storage costs low.

Conclusion

DynamoDB Global Tables are eventually consistent, with a replication latency measured in seconds. With increased data requirements, splitting entities into multiple sub-items becomes an option but must be implemented carefully.

This article shows how my team used custom logic to version entities and merge sub-items into the entity that our customers expect. It would be nice if DynamoDB Global Tables provided global consistency. Still, until then, I hope this article helps you understand the problems and a possible solution to implement yourself.

May 24, 2022

Company

Below is an edited version of our internal annual letter.

Our business is the business of business systems – specifically, we offer a catalog of tools that developers can use to build integration systems for themselves and their customers.

I started Stedi, though, not just to solve the problem of business integrations – nor just to build a big business or a successful business – but to build a world-class business.

A world-class business is a business that doesn't just win a market – it defines a category, dominates a field. Its name becomes synonymous with its industry. Its hallmark characteristic is not some single milestone or achievement, but enduring success over a very long period of time.

Types of businesses

When the topic of world-class businesses comes up, someone inevitably mentions Amazon. Amazon bills itself as a customer-obsessed company. It seems sensible that customer obsession is a path to building a world-class business; customers are the lifeblood of a business – without customers, a business will cease to exist.

The problem with customer obsession is that optimizing for delivering value to customers can cause a company to make structural choices that are detrimental to the company's long term operational efficiency. A company that is inefficient becomes painful to work for; when a company becomes painful to work for, the best people leave – and when the best people leave, a company becomes disadvantaged in its pursuit of delivering value to customers. Paradoxically, customer obsession as a company's paramount operating principle can lead to a declining experience for the customer over time.

The alternative to a customer-obsessed company is a company that is operations-obsessed. The operations-obsessed company optimizes for efficient operations – it works to continuously eliminate toil.

What exactly is toil? Toil has many definitions – a popular one is from Google's SRE book:

“The kind of work tied to running a production service that tends to be manual, repetitive, automatable, tactical, devoid of enduring value, and that scales linearly as a service grows.”

If "fear is the mind killer," toil is the soul-killer. Eliminating toil allows people to focus on the inherent complexity of the difficult, interesting problems at hand, rather than the incidental complexity caused by choices made along the way. Of course, most companies are neither customer- nor operations-obsessed, and, without an explicit optimization function, drift around aimlessly with middling results.

To be sure, a customer-obsessed company can also prioritize operational excellence, just as an operations-obsessed company can also – must also – prioritize delivering exceptional customer value. The difference between the two comes at the margins: when push comes to shove, what takes priority? In other words: will the company incur toil in order to further its success with customers? Or will it leave upside – that is, revenue or profit – on the table in order to further its long-term operational advantage?

Amazon is the former: a customer-obsessed company that also prioritizes operational excellence. Said in a slightly different way, Amazon is an operationally-excellent company subject to the constraint of customer demands. It is operations-obsessed up until the point that operational concerns conflict with customer demands, at which point Amazon – as a customer-obsessed company – runs the risk of prioritizing the latter. This is one reason why on-call at Amazon can get notoriously bad: when push comes to shove, operational concerns can lose out.[1]

Costco and ALDI, on the other hand, are examples of the latter: operations-obsessed companies that also excel at delivering customer value. They are customer-focused companies subject to the constraint of minimal operational overhead. When customer demands conflict with minimizing operational overhead, these companies sacrifice customer experience. This is why, as a Costco customer looking for paper towel, you have to walk through a harshly-lit warehouse with a giant shopping cart and pick a 48-pack off of a pallet – certainly not the most customer-friendly experience in the world, but in exchange for it, you get to purchase exceptional quality items at 14% over cost.

Stedi is definitively in this latter category: we are an operations-obsessed company. When push comes to shove, we are willing to leave revenue or profit on the table in order to reach ever-lower levels of operational toil.

I made this choice early on for two reasons. First, because I believe that over a long enough time horizon, an operations-obsessed company will lead to better outcomes for our customers. And second, because, well, I think it's just an enormously rewarding way to work. Eliminating toil brings me tremendous, tremendous satisfaction. If you feel the same way, you've come to the right place.

Thermodynamics and toil

Most of us have heard the first law of thermodynamics, though likely aren't familiar with it by name – it's also called the law of conservation of energy, and it states that energy can neither be created nor destroyed.

But the lesser-known second law is, in my opinion, a lot more interesting and applicable to our daily lives. The second law of thermodynamics states that entropy - the measure of disorder in a closed system – is always increasing.

What exactly does this mean? Eric Beinhocker sums it up nicely in The Origin of Wealth:

“The universe as a whole is drifting from a state of order to a state of disorder[…]over time, all order, structure, and pattern in the universe breaks down, decays, and dissipates. Cars rust, buildings crumble, mountains erode, apples rot, and cream poured into coffee dissipates until it is evenly mixed.”

The law of entropy applies to any thermodynamic system – that is, "any defined set of space, matter, energy, or information that we care to draw a box around and study." But while the total entropy – the disorder – of the universe at large is always increasing, the entropy of a given system can ebb and flow. "[A] system can use the energy and matter flowing through it to temporarily fight entropy and create order, structure, and patterns for a time[...]in [a system], there is a never-ending battle between energy-powered order creation and entropy-driven order destruction."

Stedi itself is a thermodynamic system. It imports energy in the form of capital (either from investors or from customers), which it uses to pay humans – that is, all of us – to create order and structure in the form of software that our customers can use, along with the various business systems required to support that customer-facing software.

You and I are thermodynamic systems, too – we use some of that capital to buy food, and we use the energy stored in that food to repair that damage that entropy has done since our last meal and to power the energy-hungry brain that works to build Stedi. From a thermodynamic standpoint, the food we eat becomes the intellectual assets – code, pixels, docs, emails, and more – that makes Stedi, Stedi. I think that's pretty remarkable (it's also another great reason why Stedi pays for lunch).

Anyways, some portion of our energy (your and my time, and Stedi's capital) is spent on building new order and structure, and some portion of it is dedicated to fighting the decay brought on by entropy.

Our word for this latter energy expenditure – energy expended just to stay in the same place – is toil. Our goal as a company is to spend as little energy on toil as possible, allowing us to allocate the maximum amount of energy possible to building new order and structure – that is, new value for our customers.

The practice of mitigating toil has a quality of what economists call increasing returns or agglomeration effects – the more toil we mitigate, the more attractive it is to work at Stedi, and the more attractive it is to work at Stedi, the more great people we can hire to build new order and structure and mitigate more toil, which starts the process over again.

Another way of thinking about this is that if we continue to mitigate toil wherever we find it, we'll find ourselves with ever-larger amounts of time to work on building new order and structure for our customers. That's the basic reason why an operations-obsessed business model wins out over a long enough time horizon: it's designed to minimize the surface area upon which entropy can do its work.

Ways to mitigate toil deserves its own memo, but there are two basic tactics that I wanted to touch on briefly: automation and elimination.

If you think about entropy as automatic decay, then one tactic is automatic [mitigation of] toil. One example of this is Dependabot. Dependabot doesn’t eliminate toil – rather, it employs a machine to fight automatic decay with automatic repair. Automatic repair is better than manual repair, but it’s not as good as eliminating it altogether; automation inevitably fails in one way or another, and if we’re going to disappear to a different dimension, there isn’t going to be anyone there to give our automatic decay-mitigation systems a kick when they get stuck.

Toil can be eliminated (from our thermodynamic system, at least) by drawing the system boundary a bit differently. When we use an external service instead of an external library, we’re moving the code outside of our system – thereby outsourcing the entropy-fighting toil to some third party. Not our entropy, not our problem.

As a general rule of thumb, when trying to reduce our surface area in the fight against entropy, we want to use the highest-order (that is, the lowest amount of entropy) system that satisfies our use case. A well-supported open source library is preferable to a library of our own (because the code entropy is someone else’s problem), and a managed service is preferable to open source (because fighting the code entropy and the operational entropy are someone else’s problem).

When we choose to whom and to what we outsource our fight against entropy, there are a number of questions to ask ourselves. Is the entity in question – be it a library or a service or a company – winning, losing, or standing still in the fight against entropy? Pick a GitHub library at random, and the overwhelming odds are that there hasn’t been a commit made in recent history – the library is rotting. Amongst more popular libraries, the picture is better, with brisk roadmaps and regular housekeeping.

When it comes to managed services, though, it’s quite rare to find a company that's winning – the vast majority of Software-as-a-Service companies are either treading water or falling behind. NetSuite is one example; it’s in some state approaching disrepair – no reasonable onlooker would say that NetSuite is definitively winning the war against entropy (though it certainly seems to be winning the war for market share). It has so much surface area that it has to dedicate an overwhelming percentage of its resources just to fight decay, leaving little to build new order and structure. Once a company reaches this state, it’s extraordinarily unlikely that it will ever recover.

There are certainly counterexamples, like AWS, GCP, and Stripe. They are good operational stewards of their production services and they routinely create new and valuable order and structure. But even then, we have to dig deeper: do they truly insulate us from the fight against disorder and decay, or do they leak it through in batches? Each time a provider like GCP deprecates a service or makes a breaking API change, they push entropy back onto us.

Same goes for price increases. A SaaS provider brings the code within the boundaries of their own infrastructure – that is, the thermodynamic system for running and operating the code – and we have to give them energy (in the form of money, which is a store of energy) as compensation. When the price goes up, they’re changing the energy bargain we’ve made.

The four horsemen of SaaS entropy, then, are: feature velocity, operational excellence, minimal breaking changes, and price maintenance. This is the reason we prefer working with AWS over most other providers – AWS has an implicit commitment to its customers that it will do everything in its power to avoid deprecating services, breaking APIs, and making price increases. For the same reason, we hold these principles near and dear for our products, too – our customers are entrusting us to fight their business-system-related entropy, and we want to be excellent stewards of that trust.

Every time we move up the ladder to a higher-order system, we have to make some tradeoffs. When we use a library, we lose some flexibility. Maintaining the library’s order is no longer our problem, but we have to live with their decisions unless we want to maintain our own fork. When we use a managed service, we lose even more flexibility – we lose the ability to change the system’s order altogether. Our system becomes harder to test, since it isn’t self-contained, which makes development slower and riskier.

There are big tradeoffs in terms of flexibility, development speed, testing, and more, and they all add up. But on the other hand, there’s the colossus that is entropy working every single minute of every single day to erode everything we dare to build. When I take stock of it, I’d prefer just about anything to fighting the second law of thermodynamics and the inevitable heat death of the universe.

Entropy and our customers

Our overall driving metrics measure usage. I say usage and not revenue because usage begets revenue – in the world of EDI, usage is a proxy for customer value delivered, and it’s exceptionally rare for a software company to deliver customer value and find itself unable to capture some of that value for itself.

Pricing

To facilitate usage, our pricing needs to be as cheap and reliable as running water, where you wouldn’t think twice (in a developed nation, from a cost standpoint) about running the faucet continuously when brushing your teeth or washing the dishes, or worry that the water might not come out when you need it the most. If our pricing were high compared to other developer-focused, API-driven products in adjacent spaces, it would discourage usage – it makes developers think about how to minimize the use of our products, rather than maximize the use of our products. This is precisely the opposite of the behavior we want to incentivize.

There is a second reason to avoid high prices: high prices signal attractive margins to competition. Once we have like-for-like competition – which is inevitable – we’ll be forced to lower our prices; if we’re going to lower our prices in a couple of years, then the only reason to have higher prices now is to capture additional revenue for a short period of time. This sort of "temporary" revenue is short term thinking; the long-term cost of temporary revenue is that you’ve incentivized competitors to enter your space (conversely, very low prices discourage competitors from entering the space, because the revenue potential is small).

Products

To drive usage, we're also expanding our product offering.

To think about our building blocks again in terms of thermodynamics, each building block is a piece of order or structure that we’ve committed to create, maintain, and improve on behalf of our customers; our customers can outsource their fight against entropy by using our building blocks.

We currently have three Generally Available products – three building blocks – that developers can use to build business systems: EDI Core (which translates EDI into developer-friendly JSON, and vice versa), Mappings (which allows transformation of JSON format to another), and Converter (which converts XML and CSV into JSON).

There are a limited number of use cases that customers can address using only these three products – there is a large amount of entropy that customers need to maintain in order to build end-to-end use cases.

To address this, we're launching a wide-reaching range of new products (many in June) – broadly, these products allow developers to build complete, end-to-end integrations all within the Stedi platform. Those products are:

  • Functions: a serverless compute service to run code on Stedi's infrastructure (now in Developer Preview).

  • SFTP: fully managed serverless SFTP infrastructure for exchanging files at any volume (now in Developer Preview).

  • Stash: simple, massively scalable Key-Value store delivered as a cloud API (now in Developer Preview).

  • Buckets: cheap, durable object storage to accommodate any file, in any format (coming soon to Developer Preview).

  • Guides: EDI specifications defined in standardized JSON Schema, and accessible via API (coming soon to Developer Preview).

There's a common theme for these developer-focused building blocks: they're as simple and generic as possible (in order to be easy to adopt and flexible for the extreme heterogeneity of EDI use cases), and they're scalable – both economically and technically – so that our customers never outgrow them, no matter how large they get (or how small they start). In many cases, developers may never exceed our monthly free tiers, and incur no costs at all.

Developers can use these building blocks as standalone items for just about any business application they might need to build – or assemble them together to build powerful, flexible EDI applications and integrations. With each building block we add, the number of use cases we can support rises exponentially.

In talking about this, I’m reminded of a passage from the book Complexity by Waldrop:

“So if I have a process that can discover building blocks,” says Holland, “the combinatorics [of genetic systems] start working for me instead of against me. I can describe a great many complicated things with relatively few building blocks. The cut and try of evolution isn’t just to build a good animal, but to find good building blocks that can be put together to make many good animals.

Focus

We are here to build a world-class business – a serious, first-rate business that the entire world of commerce can depend on, and that each of us can depend on for a first-rate, exceptional professional career with an exceptional financial return. My job is to be a steward of that path.

In a letter like this, it’s easy to get lost. What is most important? What is our focus?

To that end, I’d like to share a few passages.

Running with Kenyans:

For six months I’ve been piecing together the puzzle of why Kenyans are such good runners. In the end there was no elixir, no running gene, no training secret that you could neatly package up and present with flashing lights and fireworks. Nothing that Nike could replicate and market as the latest running fad. No, it was too complex, yet too simple, for that. It was everything, and nothing. I list the secrets in my head: the tough, active childhood, the barefoot running, the altitude, the diet, the role models, the simple approach to training, the running camps, the focus and dedication, the desire to succeed, to change their lives, the expectation that they can win, the mental toughness, the lack of alternatives, the abundance of trails to train on, the time spent resting, the running to school, the all-pervasive running culture, the reverence for running.

When I spoke to Yannis Pitsiladis, I pushed him to put one factor above all the others. “Oh, that’s tough,” he said, thinking hard for a moment. Then he said pointedly:

“The hunger to succeed.”

Guidelines for the Leader and the Commander:

“It follows from that, that you must, through this process of discernment and storing away, create in yourself a balanced man-whereby you can handle concurrently all the different parts of the job. You don't concentrate on one and forget the other, such as maintenance; you don't concentrate on marksmanship and forget something else. The best organizations in the American Army are the organizations that are good or better in everything. They may not make many headlines, they may not be "superior" in any one thing, but they are our best organizations.”

The Most Important Thing:

“Successful investing requires thoughtful attention to many separate aspects, all at the same time. Omit any one and the result is likely to be less than satisfactory.”

The Intelligent Investor:

“The art of investment has one characteristic that is not generally appreciated. A creditable, if unspectacular, result can be achieved by the lay investor with a minimum of effort and capability; but to improve this easily attainable standard requires much application and more than a trace of wisdom.”

It’s remarkable to me that when you look across so many different disciplines – sport, military, investing, and dozens more – the common theme from the people at the pinnacle of their game is that there’s no one single secret, no shortcut to being the best. It requires doing it all.

– Zack Kanter

Founder & CEO

[1] I say can lose out because it doesn't have to happen this way – every senior leader I've talked to at Amazon has pushed back on this and said that Amazon recognizes that failure to address operational toil leads to an inability to deliver value to customers. But ask any front-line Amazon employee what Amazon's #1 focus is, and they'll tell you customer obsession – when faced with delivering a feature or addressing an on-call issue, that's what comes to mind.

May 11, 2022

EDI

The trick to dealing with date and time in an X12 EDI document is to make things easy for yourself. EDI isn’t going to do that for you. EDI makes it possible to put a date on a business document in any way a human being has ever come up with and that flexibility comes at a cost: it’s downright confusing. You don’t have to deal with that confusion, though, because you don’t have to deal with that flexibility. It all starts with the implementation guide that you received from your trading partner.

Implementation guide

The implementation guide tells you what your trading partner expects from you. If your trading partner will send you purchase orders, then the implementation guide will tell you what those purchase orders look like. Every trading partner will format it a bit differently, but it usually looks something like this.

This implementation guide is based on the X12 EDI specification for an 850 Purchase Order. That specification is much broader than you need, though, because it caters for everything anyone could ever want in a purchase order. For example, it contains fields for specifying additional paperwork. Your trading partner doesn’t need that—can’t even handle that—so they send you an implementation guide with only the fields they do expect. In other words, an implementation guide is a subset of the X12 EDI specification and you only have to deal with that subset.

Of course, I don’t know who you’re trading with and I don’t know which type of documents you’re dealing with, so I’ll probably cover a bit more ground here than you need. Just remember: refer to the implementation guide and ignore everything that’s not in there, lest you drive yourself mad.

Segments

If your trading partner sends you a document, you might expect dates to look something like this.

"shippingDate": "2022-04-28T13:05:37Z"

However, EDI is different. It looks like this instead.

DTM*11*20220428*130537*UT

Every field is on its own line, except it isn’t called a field; it’s called a segment. Each segment begins with a code that tells you what type of data the segment contains. DTM means that the segment represents a date and time.

EDI actually has three different segments to represent date and time: DTM, DTP, and G62. DTM is the most common, but it’s also the most complicated, so I’ll talk about that one last. Fortunately, everything I’ll show you about G62 and DTP also applies to DTM.

To add to the complexity, date and time are not always expressed with one of these dedicated segments; sometimes they’re embedded into completely different segments. Those segments take bits and pieces of the dedicated date-and-time segments and call them their own.

G62

Date

The G62-segment represents a date like this.

G62*11*20220428

The last element of the segment isn’t too hard to understand: it’s the actual date. It’s always in the format YYYYMMDD. There’s also an element in the middle and it isn’t immediately clear what it means. Suppose you have a document for a shipment status update that contains two different dates.

"shippedDate": "2022-04-28"
"estimatedDeliveryDate": "2022-04-29"

In EDI, that would look like this.

G62*11*20220428
G62*17*20220429

EDI doesn’t have named fields. Instead it uses a code. 11 stand for Shipped on This Date and 17 stands for Estimated Delivery Date. If you take a look at the full list of possible codes, you’ll see that there are 137 of them. That’s a lot. They all have names, but those don’t do a good job of explaining what they mean. That’s when you turn to the implementation guide your trading partner gave you.

As you can see, your trading partner will only ever send you an 11 or a 17. That makes sense, because a code like BC - Publication Date doesn’t mean anything in the context of a shipment status update.

Time

The G62 example I gave only shows a date, but a G62 can contain a time as well. In fact, the full specification of a G62 looks like this.

Again, the implementation guide will tell you whether you need the full segment, but since we’ve already seen an example of a date, let’s take a look at an example of a time.

G62***8*1547*LT

The elements with information about a date are left empty. The delimiter * is still there, otherwise we wouldn’t know how many fields we skipped. So, that’s what those three stars between G62 and 8 mean: skip the two date fields.

1547 is the time: 3:47 PM. It’s always specified in a 24-hour clock. The example shows the shortest time format possible, but times can also have seconds and even fractions of seconds.

FormatExampleMeaningHHMM09059:05 AMHHMMSS1928517:28:51 PMHHMMSSD104537810:45:37.8 AMHHMMSSDD2307429111:07:42.91 PM

If you want to be sure which format you need to handle, you’ll have to check the implementation guide.

The 8 in the example is a code for the kind of time you’re dealing with, just like 11 and 17 were codes for the kind of date. 8 means Actual Pickup Time. You could look up all the possible codes, but it’s better to refer to your implementation guide.

The last element specifies the time zone. When you look up the time zone codes, some of the descriptions can be a bit confusing, but there’s method to the madness. Equivalent to ISO P08 means UTC+08 and Equivalent to ISO M03 means UTC-03. In other words, the number at the end is the offset from UTC where P means + and M means -.

In the example, the time zone is LT, which means Local Time. It depends on the context which time zone that is. If you’re dealing with a shipment status update, then it stands to reason that the local time for the pickup is local to wherever the pickup occurred.

Date-time

Since G62 can represent both a date and a time, it can also represent a date-time.

G62*11*20220428*8*1547*LT

This segment tells you that the package shipped on April 28, 2022 at 3:47 PM local time. It’s actually unlikely that you will get both a date qualifier (11) and a time qualifier (8) in the same segment, since they convey similar information, but check your implementation guide to be sure.

DTP

The DTP-segment is also used to express dates and times, but it’s more flexible than the G62-segment. DTP can express almost everything G62 can—except for time zones and fractions of seconds—and it has some extra features on top.

Date

A simple date looks like this.

DTP*11*D8*20220428

As you can see, the date is at the end. The first element is the same as with the G62: it tells you what kind of date you’re dealing with. The value 11 means Shipped, so that’s the same as with the G62, even if it’s describes slightly differently. The entire list of possible codes for the DTP is much longer, though; more than a thousand possibilities. Definitely check your implementation guide to see which ones are relevant to you.

The element in the middle is new. It specifies the format to use. D8 means that this is a date in YYYYMMDD format. This is where DTP becomes more flexible than G62: you can use different formats. For example, D6 means a date in YYMMDD format, TT means a date in MMDDYY format, and CY means a date expressed as a year and a quarter, e.g. 20221 means Q1 of 2022. That last one is interesting, because there’s no way to express the concept of a quarter in a regular date format. There’s even the notion of a Julian date, which just numbers days consecutively, starting on January 1st and it’s particularly fun to deal with in leap years. I imagine Julian dates are rare, but hey, if you need it, you need it.

Time

DTP can do time as well.

DTP*11*TM*1547

TM means a time in HHMM format and, as with G62, DTP always uses a 24-hour clock. Another code is TS, which means a time in HHMMSS format. There’s no code that allows you to specify fractions of a seconds, so that’s a feature of G62 that DTP doesn’t have. DTP also doesn’t provide a way to specify a time zone.

Date-time

Expressing a date-time doesn’t require any extra fields, just a different format qualifier.

DTP*11*DT*202204281547

As you may expect, DT means a date-time in YYYYMMDDHHMM format and the date-time in the example is April 28, 2022 at 3:47 PM. If the qualifier is set to RTS instead of DT, the date-time will include seconds as well.

Range

Where DTP truly trumps G62, is in its ability to represent date ranges.

DTP*135*RD8*20211224-20220104

This represents December 24, 2021 to January 4, 2022. As before, the qualifier gives the exact format and RD8 means a date range in YYYYMMDD-YYYMMDD format. There are also ranges that include a time, and ranges that use only years, and everything in between. You can check out the full list of qualifiers, but it’s even better to check your trading partner’s implementation guide.

DTM

The DTM-segment is the most common way of representing date and time in EDI. It’s also the most flexible, because it has the capabilities of both the G62-element and the DTP-element. That would also make it the most confusing, except that you’ve already seen G62 and DTP, so there’s nothing left to say about DTM.

As you can see, it’s just a combination of the G62-elements and the DTP-elements. You do need to figure out which of these elements are used and which are ignored, but by now, you know how to do that as well: check the implementation guide.

Other segments

If you have to deal with an 850 Purchase Order, you’ll find that it contains a segment called BEG and that segment has an element to represent a date. It’s the same element as you find in a G62 or a DTM—a date expressed in YYYYMMDD format—just embedded in a segment that otherwise has nothing to do with dates. In the case of a purchase order, it represents the date of the purchase order as defined by the purchaser.

It’s not limited to the BEG-segment, though. For example, the B10-segment includes both a date and a time. The AT7-segment includes date, time and time code, and the CUR-segment has a date and time plus a date/time qualifier. And there are many, many more segments like this. It’s a lot to handle, but there’s a saving grace: they all use a combination of the elements described in this article, so by now, you should be well equipped to deal with them.

Apr 14, 2022

Products

When a question about JSONata turns up on StackOverflow, we do our best to answer it. Not because it's part of our strategy, or because we are building our corporate image, but because we like to help out. And we like to impress a little bit, too.

We love JSONata. It's a modest little language. Where the likes of Javascript or Python try to be all things to all people, JSONata just tries to do one thing very well: query JSON structures. That's not something every developer runs into every day, but it's exactly what we need for Mappings, which is all about transforming one JSON structure into another.

Since we do use JSONata every day, we're building up quite a bit of expertise with it. We like building complex queries, like: does the total cost of all items in the order that are not in stock exceed our limit? However, if you only use JSONata occasionally, then a challenge like this may be a bit daunting. JSONata isn't a hard language to learn, but you can do some pretty involved stuff with it. You could dig through the documentation and it does cover all aspects of the language, but it doesn't offer much guidance on how to put all the pieces together. No, a situation like this is when you turn to StackOverflow.

Whenever we post an answer, we keep in mind that the easiest way to understand JSONata is by example, but giving an example isn’t as convenient as we’d like. There are three parts to it: the data you’re querying, the JSONata query, and the result. You can show all of that in code snippets, of course, but it lacks a bit of clarity and it’s very static.

{
  "orders": [
    {
      "quantity": 650,
      "pricePerUnit": 3.53,
      "inStock": true
    },
    {
      "quantity": 800,
      "pricePerUnit": 1.12,
      "inStock": false
    },
    {
      "quantity": 3,
      "pricePerUnit": 275.75,
      "inStock": false
    },
    {
      "quantity": 1250,
      "pricePerUnit": 2.45,
      "inStock": true
    }
  ]
}
(
  $costs := $map(orders[inStock=false], function ($order) {
    $order.quantity * $order.pricePerUnit
  });

  $costs ~> $sum > 1000
)
true

It works, but we can do better. We took a few days and created the JSONata Playground. In the playground, it's clear what all of these snippets are, and it's interactive, so you can play around with the JSONata expression and discover how it was constructed. You can also embed the playground in a webpage.

You can bet that from now on our StackOverflow answers will include JSONata Playground examples and yours can as well, because it's completely free for anyone. We hope you'll use it to learn JSONata, to write JSONata, and to share JSONata. We hope this helps you out. And we hope we impressed you a little bit, too.

Apr 13, 2022

Engineering

One deployment per hour is far too slow. Our initial deployment style starts with a paths filter to limit what stacks deploy, based on files and folders changed in a commit. We start our project this way thinking it will give us quick deploys for infrequently changed stacks. As our project grows, though, this is taking too long to deploy. Based on our analysis, our fastest recorded deployment time is 13.5 minutes, but our slowest deployments take up to 40 minutes. We are confident we can get our p99 down to 20 minutes or less.

How

Here on the Billing team our primary application is a single monorepo with 12 CDK stacks deployed using GitHub Actions. When we dive into the pipeline, we realize that we have a number of redundant steps that are increasing deployment times. These duplicate steps are a result of the way CDK deploys dependent stacks.

For instance, let’s take four of our stacks: Secrets, API, AsyncJobs, and Dashboard. The API stack relies on Secrets, while Dashboard relies on API and AsyncJobs. If we only need to update the Dashboard stack, CDK will still force a no-op deployment of Secrets, API, and AsyncJobs. This pipeline will always start from square one and run the full deployment graph for each stack.

We believe we can speed up our slowest deployment time by reducing these redundant deployment steps. Our first improvement is to modify the paths filter by triggering a custom “deploy all” CDK command if certain files (e.g. package.json) were changed. We alter the standard cdk deploy –all because we have some stacks listed that we don’t want to deploy. Instead, we use cdk deploy –exclusively Secrets-Stack API-Stack AsyncJobs-Stack Dashboard-Stack, which decreases our median deployment time by a full minute and decreases our slowest deployment time by 10 minutes.

With our slowest times still clocking in around 30 minutes, we know we need a new idea to reach our goal of 20 minutes or less. We have reached our limit of speed for serial deployments, so we attempt to parallelize the deployments. We use the CDK stack dependency graph, which gives us a simple mechanism to generate parallel jobs. We are now able to build the GitHub Actions jobs and link them together with the stack dependencies using the job’s need: [...] option.

To generate the stack graph, we synthesize the stacks (cdk synth) for each stage we'd like to deploy and then parse the resulting manifest.json in the cdk.out directory.

const execSync = require("child_process").execSync;
const fs = require('fs');
const path = require('path');
const { parseManifest } = require('./stackDeps');

const stages = ['demo'];
const stackGraphs = {};

stages.map((stage) => {
  execSync(`STAGE=${stage} npx cdk synth`, {
    stdio: ['ignore', 'ignore', 'ignore'],
  });
  stackGraphs[stage] = parseManifest();
});

const data = JSON.stringify(stackGraphs, undefined, 2);
fs.writeFileSync(path.join(__dirname, '..', 'generated', 'graph.json'), data);

Our stack graph:

{
  "demo": {
    "stacks": [
      {
        "id": "Secrets-demo",
        "name": "Secrets-demo",
        "region": "us-east-1",
        "dependencies": []
      },
      {
        "id": "Datastore-demo",
        "name": "Datastore-demo",
        "region": "us-east-1",
        "dependencies": []
      },
      {
        "id": "AsyncJobs-demo",
        "name": "AsyncJobs-demo",
        "region": "us-east-1",
        "dependencies": [
          "Datastore-demo",
          "Secrets-demo"
        ]
      },
      {
        "id": "Api-demo",
        "name": "Api-demo",
        "region": "us-east-1",
        "dependencies": [
          "Datastore-demo",
          "Secrets-demo"
        ]
      },
      {
        "id": "Dashboards-demo",
        "name": "Dashboards-demo",
        "region": "us-east-1",
        "dependencies": [
          "AsyncJobs-demo",
          "Api-demo"
        ]
      }
    ]
  }
}

The workflow below is easy to define programmatically from the stack graph, which allows GitHub Actions to do all the heavy lifting of orchestrating the jobs for us.

Due to network latency and variance in the GitHub runner setup, this change sometimes causes our fastest deployments to slow down. However, the median deployment performs 1 minute faster. Most importantly, our p99 deployment times always perform 12 minutes faster than before: 18 minutes!

At last we achieved our goal! With everything sped up and working smoothly, we are able to add it to our team’s projen setup and make this available to all of our other services.

How to get started

To see how this works, you can find a full working sample on GitHub.

The sample repo provides an un-opinionated example with just one stage to deploy. You could build on this in a few ways, such as specifying stage orders, implementing integration tests or whatever else is needed in your stack.

Feb 24, 2022

Products

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

Instead of writing conditions inside of your mapping for simple data conversions, you can now create a lookup table to automatically replace a value that your system (or trading partner) does not recognize with one that they do.

When building B2B integrations, it is expected that your trading partner will send you data in a format that is incompatible with your system. Or they may require you to send the data in a format that your system doesn’t produce.

Lookup tables make it easier to deal with those scenarios; they are designed for developers that want to write and maintain as little code as possible.

Where lookup tables fit in

Let’s examine the following lookup table, which can be used to replace an internal code, like 313630, with a human-readable code your trading partner requires, like FedEx Express.

When you create a lookup table inside of a mapping, you can find and use a matching value for any of these columns. In the example above, you look up any value by internalID, by SCAC code, or by shippingMethod.

Lookup tables are generic and can be used as part of any data mapping exercise, regardless of where in your pipeline you are using Mappings.

Say you have a trading partner that is sending you CSV files, you can:

  • Use Functions to convert the CSV into JSON

  • Use Mappings to transform that JSON into a shape that you need, and

  • Use a lookup table to transform the values of that JSON to what your system needs

In another example, if you are generating EDI and need to change the values that come from your custom JSON API to something that is required on the EDI document, you can:

  • Use Mappings to transform that JSON into a JEDI file

  • Use a lookup table to change the values from your system to the values your trading partner requires

  • Send JEDI to the EDI Core translate API to get EDI back

Lookup tables in action

Let’s assume that you are ingesting an invoice, and you want to map the following source JSON…

{
  "product": {
    "id": "QL-5490S",
    "country": "USA",
    "price": "500"
  }
}

…to the following target JSON:

{
  "product_number": "QL-5490S",
  "price": {
    "currency": "USD",
    "amount": 500
  }
}

Once you upload your target and source to the mappings editor, you will see this:

Your trading partner sends you USA as the country but does not include a currency field, which is required by your system. Additionally, you work internationally so you will also receive invoices from trading partners operating out of Germany (DE) and Australia (AU). It is standard in your industry for trading partners to send invoices in their local currency, so when you need to populate your currency key, your mapping needs to look up what country the invoice is from.

USA is not a valid currency code, so you need to convert USA to USD, and eventually DE to EUR and AU to AUD. To solve this, you need a lookup table. To create one, open the fullscreen view next to currency, click “Lookup tables”, and “Add new”.

When creating a new lookup table, you can enter the table values manually or upload a CSV file with all values that you’d like to populate in your lookup table.

Let’s create a table named Currency_Codes with two columns, Country and Currency, and populate the rows with the relevant values.

On the upper right side of your screen, you will see that the UI provides you with the JSONata snippet you need to use in your mapping. Simply copy the snippet, and click Confirm. Paste your JSONata snippet into the expression editor:

The $lookupTable JSONata function takes three arguments:

  • The name of the lookup table

  • The name of the column we will lookup data by

  • The path to a value

Simply replace <path.to.key> with product.country, and you will see the Output preview shows USD.

Hit the Test button and swap the input value to see how it works!

Lookup tables are flexible. They can be as simple as two columns or be expanded to include multiple columns. In the example above, you could also look up a country by its currency (or by any other column in the table).

Complex transforms with lookup tables

Lookup tables can be used for more complicated transformations, like if you need to use one source value to populate multiple destination fields.

Let’s say a trading partner only sends a location code that represents where your product needs to be shipped to. Your current fulfillment software requires a full address for validation and rate shopping. You can create a lookup table with multiple columns so that each required data point is satisfied.

Let's create the following lookup table:

The example mapping for Name field would be:

To get a mapping Address, change the column at the end of the expression:

Now that this lookup table and logic are complete, any data coming in with just an address code will expand to a full address to meet your data requirements.

Build your first lookup table

You can get started with lookup tables by reading the overview in our documentation, the API Reference, and the $lookupTable custom JSONata function.

There is no additional charge to create or reference lookup tables when using Mappings; you only pay for the requests to the Mappings API.

Feb 3, 2022

Products

This post mentions Stedi’s EDI Core API. Converting EDI into JSON remains a key Stedi offering, however EDI Core API has been superseded by Stedi Core, an event-driven EDI system that allows you to configure integrations without being an EDI or development expert.

Developers come to Stedi to build EDI systems of their own. And at the center of any EDI system is data translation: a way of turning EDI – an arcane file format – into a more approachable format, like JSON.

That's why we launched EDI Core – a basic building block that developers can use to build flexible, scalable EDI applications.

Introducing EDI Core

EDI Core is a collection of APIs that make working with EDI simple. Users can translate between EDI and Stedi’s JEDI (JSON EDI) format, which is a lossless and user-friendly representation of EDI. EDI Core is built to be the foundation of any scalable and reliable EDI system, and the JEDI format makes programmatic manipulation and validation easy.

There are two ways to interact with EDI Core:

  • In the browser using the free Inspector

  • Programmatically using the EDI Core API (Note: This API has been deprecated since this blog post was published. It has been replaced by our EDI platform APIs.)

Visualize EDI with Inspector

Inspector is a free tool built on top of the EDI Core API. You can use it to easily understand the contents of an EDI file, to debug errors, and to share the file with others.

Let’s take an example Amazon Direct Fulfillment Purchase Order from a code editor…

Code editor

…and load it into the free Inspector tool.

Inspector view with raw EDI on the left and a rich view on the right

On the right is the Rich view – a human-readable version of the EDI file. This will help you understand the contents and the structure of the file. Hover over the elements in the Rich view to see where they show up in the original EDI file on the left; click on the elements to gain a deeper understanding of the codes and their definitions.

A gif showing how the inspector highlights data in the raw EDI when the user hovers over fields in the rich view

If you toggle to the JSON view, you will see the file in Stedi’s JEDI format.

Working with imperfect EDI files

It’s fairly common to receive a less-than-perfect EDI file. Trying to debug a malformed EDI file can be a frustrating and time-consuming process. If you find yourself in a situation like this, the Inspector has several features that can help make the process less painful:

  • When an EDI document is malformed, we’ll do our best to parse it and display actionable error messages to help troubleshoot

  • If the EDI file is not formatted correctly, you can make it easier to read by clicking the ‘Format EDI’ button

  • If you want to share the file via the Inspector with others, simply copy the URL in your browser and share it, or click the download button to save the EDI file.

Translate EDI programmatically using EDI Core API

Where EDI Core API fits into your system

There are two scenarios: you’re either working to receive EDI files from your trading partner – that is, getting data into your system, or you need to send EDI files, that is, move data out of your system. Perhaps you need to do both.

Regardless, EDI Core can be the foundation of this system.

EDI Core Diagram showing requests and data flow

When you’re ingesting EDI, you will want to translate EDI to JEDI and then map JEDI into your JSON format (Stedi’s Mappings can help with this!).

When you’re generating EDI, you will want to create JEDI (Mappings works here, too!) and then translate JEDI to EDI.

In both cases, you will receive validation errors if the JEDI file does not conform to the EDI release or Guide you configured on request.

Additionally, when generating EDI, these API options will come in handy, you can:

  • Override EDI delimiters,

  • Generate control numbers, including setting control number values,

  • Remove empty segments

EDI Core is now Generally Available

EDI Core translates EDI files to JEDI (JSON EDI) and vice versa, which allows you to treat EDI just like anything else: a problem to solve using your familiar tools.

If you want to build an end-to-end EDI system that utilizes EDI Core, there is some assembly required. You’ll need to handle the actual transmission of EDI files with your trading partners (via SFTP or AS2), orchestrate API calls, handle retries and errors, and more – though we do have developer-focused products in each of these categories coming soon.

Get started today.

Jan 26, 2022

Products

Business integrations are anything but straightforward – they are typically composed of many steps that need to be executed sequentially to process a single business transaction.

At a high level, each step can be divided into one of two categories: transporting data and transforming data. In order to enable developers to implement all manner of data transformations with ease, we've built Mappings.

Introducing Mappings

Mappings is a robust JSON-to-JSON data transformation engine that enables developers to define field mappings in an intuitive UI – and then invoke those mappings programmatically via an API.

Mappings is composed of two parts:

  • a browser-based tool that allows users to create and test JSON-to-JSON data transformations

  • an API that can be used to programmatically invoke defined mappings at web scale.

Here's a look at the UI...

Image an overview of the Mappings UI with a partially created mapping

...and the corresponding API call:



Using Mappings, developers can transform JSON payloads without the need to write, provision, maintain, or monitor any infrastructure. Mappings is powered by an open-source query and transformation language called JSONata, which comes with a large number of built-in functions and expressions.

Mappings allows developers to solve common tasks like retrieving data from nested structures, manipulating strings, writing conditional expressions, parsing/converting dates, transforming data (with filter, map, reduce functions) and much more.

Using Mappings

As an example, let's build a mapping between an 850 Purchase Order and a QuickBooks Online Estimate. The Purchase Order is in the X12 EDI format; since Mappings works with JSON as its input and output formats, we need to translate it to JSON first. You can read how to do that here.

Now that we have a JSON source document to start with, we can create a new mapping by navigating to Mappings in Stedi. We'll input the JSON as the source in the first step of the Mapping wizard…

Image of an EDI 850 purchase order displayed in JEDI (JSON EDI) format

…and we'll input the target JSON in the next step:

Image of a QuickBooks Online Estimate API shape displayed in JSON

With our source and target selected, we can start building a transformation between them by writing simple functions and expressions:

Image of the Mappings fullscreen view showing a complex JSONata transformation

The Mappings UI has autocomplete functionality and built-in documentation. Together, these facilitate a seamless experience for building complex data transformations:

Image of the Mappings Editor with several mapping transformations

Each mapping can be tested in the UI…

Image of the Test Mapping modal

…and executed at web scale using the API.

Mappings is now Generally Available

Mappings allows you to create your own data transformation API - without the need to write, provision, maintain or monitor any code; you focus on building your business logic and we’ll do the rest for you.

Get started today.

Jan 5, 2022

EDI

EDI – Electronic Data Interchange – is an umbrella term for many different “standardized” frameworks for exchanging business-to-business transactions. It dates back to the 1960s and remains a pain point in every commercial industry from supply chain and logistics to healthcare and finance. What makes it so hard? Why is it still an unsolved problem despite many decades of immense usage?

These are the questions we get most often from developers – both developers who want to become Stedi customers and developers who want to join us to help build Stedi. And these are our favorite questions to answer, too – the world of EDI is complex, opaque, and arcane, but it’s also enormously powerful, underpinning huge swaths of the world economy.

The problem is that there just aren’t any good, developer-focused resources out there that can help make sense of EDI from the ground up. The result is that this wonderfully rich ecosystem has been locked away from the sweeping changes that are happening in the broader world of technology.

We’ve had the good fortune of ramping up a number of people from zero to a solid working knowledge of how the whole EDI picture fits together. It helps to start at the highest level and get more and more specific as you go; this piece you’re reading now is the first one that our engineers read when they join our team – a sort of orientation to the wide, wide world of EDI – and we’ve decided to post it publicly to help others ramp up, too.

Trade

At the most basic level, there are many thousands of businesses on Earth; those businesses provide a wide variety of goods and services to end consumers. Since few businesses have the resources or the desire to be completely self-sufficient, they must exchange goods and services between one another in order to deliver finished products. This mechanism is known as trade.

When two businesses wish to conduct trade, they must exchange certain business documents, or transactions, which contain information necessary to conduct the business at hand. There are hundreds of conceivable transaction types for all manner of situations; some are common across many industries, like Purchase Orders and Invoices, and some are specific to a class of business, such as Load Tenders or Bills of Lading, which pertain only to logistics.

Businesses used to exchange paper transactions and record those transactions into a hand-written book called a ledger (which is why we refer to accounting as “the books,” and people who work with accounting systems as “bookkeepers”), but modern businesses use one or many software applications, called business systems, to facilitate operations. There are many types of business systems, ranging from generic software suites like Oracle, SAP, and NetSuite to vertical-specific products that serve some particular industry, like purpose-built systems for healthcare, agriculture, or education.

Each business system uses a different internal transaction format, which makes it impossible to directly import a transaction from one business system into another; even if both businesses were using the exact same version of SAP – say, SAP ERP 6.0 – the litany of configuration and customization options (each of which affects the system’s transaction format) renders the likelihood of direct interoperability extraordinarily improbable. These circumstances necessitate the conversion of data from one format to another prior to ingestion into a new system.

Data conversion

The most popular method of data conversion is human data entry. Customer A creates Purchase Order n in NetSuite (its business system) and emails a PDF of Purchase Order n to Vendor B. A clerical worker employed by Vendor B enters the data from Purchase Order n into SAP (its business system). The clerk “maps” data from one format to another as necessary – for example, if Customer A’s PDF includes a field called “PO No.”, and Vendor B’s business system requires a field called “Purchase Order #”.

People are smart – you can think of a person sort of like AI, except that it actually works – and are able to handle these sort of mappings on-the-fly with little training. But manual data entry is costly, error-prone, and impractical at high volumes, so businesses often choose to pursue some method of transaction automation, or trading partner integration, in order to programmatically ingest transactions and avoid manual data entry.

Since each business has multiple trading partners, and each of its trading partner operates on different business systems, “point to point” integration of these transactions (that is, mapping Walmart’s internal database format directly to the QuickBooks JSON API) would require the recipient to have detailed knowledge of many different transaction formats – for example, a company selling to three customers running on NetSuite, SAP, and QuickBooks, respectively, would need to understand NetSuite XML, SAP IDoc, and QuickBooks JSON. Maintaining familiarity with so many systems isn’t practical; to avoid this explosion of complexity, businesses instead use commonly-accepted intermediary formats, which are broadly known as Electronic Data Interchange – EDI.

EDI

EDI is an umbrella term for many different “standardized” frameworks for exchanging business-to-business transactions, but it is often used synonymously with two of the most popular standards – X12, used primarily in North America, and EDIFACT, which is prevalent throughout Europe. It’s important to note that EDI isn’t designed to solve all of the problems of B2B transaction exchange; rather, it is designed to eliminate only the unrealistic requirement that a trading partner be able to understand each of its trading partner’s internal syntax and vocabulary.

Instead of businesses having to work with many different syntaxes (e.g., JSON, XML, CSV) and vocabularies (e.g., PO No. and Purchase Order #), frameworks like X12 and EDIFACT provide highly structured, opinionated alternatives intended to reduce the surface area of knowledge required to successfully integrate with trading partners. All documents conforming to a given standard follow that standard’s syntax, allowing an adoptee of the standard to work with just one syntax for all trading partners who have also adopted that syntax.

Further, standards work to reduce variation in vocabulary and the placement of fields, where possible. The X12 standard, for example, has an element type 92 which enumerates Purchase Order Type Codes; the enumerated value Dropship reflects X12’s opinion that POs that might be colloquially referred to as Drop Ship, Drop Shipment, or Dropship will all be referenced as Dropship, which limits the vocabulary for which an adoptee might have to account. Similarly, X12 has designated the 850 Purchase Order’s BEG03 element – that is, the value provided in the third position of the BEG segment in the 850 Purchase Order transaction set – as the proper location for specifying the Purchase Order number. This reduces some of the burden of mapping a transaction into or out of an adoptee’s business system; only one value for drop shipping and one location for PO number must be mapped.

Of course, the vast majority of fields cannot be standardized to this degree. Take, for example, the product identifier of a line item on a Purchase Order – while X12 specifies that the 850 Purchase Order’s PO107 element should be used to specify the product identifier value, the standard cannot possibly mandate which type of product identifier should be used. Some companies use SKUs (Stock Keeping Units), while others use Part Numbers, UPCs (Universal Product Codes), or GTINs (Global Trade Item Numbers); all in all, the X12 standard specifies a dictionary of 544 different possible product identifier values that can be populated in the PO106 element.

The problem with standards

What we’re seeing here is that while a standard can be opinionated about the structure of a document and the naming of fields, it cannot be opinionated about the contents of a business transaction – the contents of a business transaction are dictated by the idiosyncrasies of the business itself. If a standard doesn’t provide enough flexibility to account for the particulars of a given business, businesses would choose not to opt into the standard. A standard like X12, then, does not provide an opinionated definition of transactions – it provides a structured superset of all the possible values of commerce.

Intermediary formats – that is, EDI – make life somewhat easier by limiting the number of different formats that a business must understand in order to work with a wide array of trading partners; a US-based brand selling to dozens of US-based retailers likely only needs to work with the X12 format. However, the brand still needs to account for the different ways that each retailer uses X12. Again, X12 is just a dictionary of possible values – since Walmart and Amazon run their businesses in different ways (and on different business systems), their implementation of an X12 intermediary format will differ, too.

A simple example may be that Walmart allows customers to include a gift message at the order level (“Happy Birthday – and Merry Christmas!”), whereas Amazon allows its customers to specify gift messages at the line item level (“Happy Birthday!” at line item #1, “and Merry Christmas!” at line item #2). This difference in implementation of gift message means that a brand selling to both Amazon and Walmart would need to account for these differences when ‘mapping’ the respective fields to its business system.

Such per-trading-partner nuances cannot be avoided – because different businesses operate in different ways, a single, canonical, ultra-opinionated representation of, say, a Purchase Order, is unlikely to ever exist. In other words, the per-trading-partner setup process is driven by inherent complexity – that is, complexity necessitated by the unavoidable circumstances of the problem at hand. And because field mappings such as these affect real-world transactions, they cannot be done with a probabilistic machine learning approach; for example, mapping “Shipper Address” to “Shipping Address” would result in orders being shipped to the shipper’s own warehouse, rather than the customers’ respective addresses. While there are many ways to build business-to-business integrations, any solution must account for a setup process that involves per-trading-partner, human-driven field mappings.

There are other areas of inherent complexity in EDI, too. Because businesses change over time, the configurations of the businesses’ respective business systems must change, too; an example might be a retailer adding DHL as a shipping option, whereas it previously only offered FedEx. Those changes must be communicated to trading partners so that field mappings can be updated appropriately; because such communications and updates involve ‘best efforts’ from humans, some percentage of them will be missed or completed incorrectly, leading to integration failures on subsequent transactions. Even without inter-business changes, errors happen – for example, a business system’s API keys might expire, or the system might experience intermittent downtime. Such errors will need to be reviewed, retried, and resolved. Just as every solution’s setup process will always require per-trading-partner, human-driven field mappings, every solution must also provide functionality for managing configuration changes on the control plane and intermittent errors on the data plane.

Business physics

The 'laws of physics' of the business universe, then, are as follows:

  1. There are many businesses, and those businesses must conduct business-to-business trade in order to deliver end products and services;

  2. Those businesses run on a large but bounded number of business systems;

  3. Due to the heterogeneity of business practices, those business systems offer configuration options that result in an unbounded number of different configurations;

  4. The heterogeneity of configurations makes it impossible for a single unified format, therefore necessitating a per-trading-partner setup process;

  5. The business impact of incorrect setup requires that a human be involved in setup;

  6. Businesses are not static, so configuration will change over time, again necessitating human input;

  7. Business systems are not perfectly reliable;

  8. Because neither human input not business systems are perfectly reliable, intermittent errors will occur on an ongoing basis;

  9. Errors must be resolved in order to maintain a reliable flow of business.

Any generalized business integration system must account for all these constraints. The so-called Law of Sufficient Variety summarizes this nicely: “in order to ensure stability, a control system must be able to, at a minimum, represent all possible states of the system it controls.” Failing that, it must limit scope in some way – say, by only handling a subset of transaction types, industries, business systems, or use cases. But since limiting scope definitionally means limiting market size, any sufficiently-ambitious effort to develop a business integration system must not limit scope at all: it must provide mechanisms to work with any configuration of any business system, in any industry, across 300+ business-to-business transaction types, in any format, as well as provision for any evolutions that might develop in the future.

Such is the challenge of developing a generalized business integration system.

A familiar problem

The good news is that such circumstances (an unbounded set of heterogeneous, complex, mission-critical, web-scale use cases) are not unique to business integrations – they mirror the circumstances found in the broader world of software development.

If, instead of setting out to create a software application for developing business integrations, one were to set out to create a software application for developing software applications, one would encounter the same challenges – to continue with the Law of Sufficient Variety, a system for developing software would need to be able to represent all possible states of the software it wishes to develop.

This, of course, isn’t reasonable – it isn’t feasible to design a single software application for developing software applications. Instead of a single application, successful platforms like Amazon Web Services provide a series of many small, relatively simple building blocks – primitives – that can be composed to represent virtually any larger application. AWS, then, can be thought of as a catalog of software applications for developing software applications. Using AWS’s array of offerings, software developers today can build web-scale applications with considerably less code and less knowledge of underlying concepts.

Stedi’s approach

Whereas AWS is a catalog of developer-focused software applications for developing software applications, Stedi is a catalog of developer-focused software applications for developing business integrations.

Stedi serves three types of customers:

  • Customers that need to build integrations to support their own business (e.g. an ecommerce retailer that wants to exchange EDI files with its suppliers);

  • Customers that need to build EDI functionality into their own software offerings (e.g. a digital freight forwarder, transportation management system, or fulfillment provider that wants to embed self-service EDI integration into their platform);

  • EDI providers or VANs that need to modernize their technology stack (e.g. a retail-specific EDI software provider that wants to replace legacy infrastructure or tools).

With Stedi, you can build massively scalable and reliable integrations on your own without being an EDI or development expert.

Learn more about building on Stedi.

Get updates on what’s new at Stedi

Get updates on what’s new at Stedi

Get updates on what’s new at Stedi

Get updates on what’s new at Stedi

Backed by

Stedi is a registered trademark of Stedi, Inc. All names, logos, and brands of third parties listed on our site are trademarks of their respective owners (including “X12”, which is a trademark of X12 Incorporated). Stedi, Inc. and its products and services are not endorsed by, sponsored by, or affiliated with these third parties. Our use of these names, logos, and brands is for identification purposes only, and does not imply any such endorsement, sponsorship, or affiliation.

Get updates on what’s new at Stedi

Backed by

Stedi is a registered trademark of Stedi, Inc. All names, logos, and brands of third parties listed on our site are trademarks of their respective owners (including “X12”, which is a trademark of X12 Incorporated). Stedi, Inc. and its products and services are not endorsed by, sponsored by, or affiliated with these third parties. Our use of these names, logos, and brands is for identification purposes only, and does not imply any such endorsement, sponsorship, or affiliation.

Get updates on what’s new at Stedi

Backed by

Stedi is a registered trademark of Stedi, Inc. All names, logos, and brands of third parties listed on our site are trademarks of their respective owners (including “X12”, which is a trademark of X12 Incorporated). Stedi, Inc. and its products and services are not endorsed by, sponsored by, or affiliated with these third parties. Our use of these names, logos, and brands is for identification purposes only, and does not imply any such endorsement, sponsorship, or affiliation.