Build an OAuth app
This guide explains how to register an OAuth app, test it in your Stedi production account, and go live. Visit OAuth apps for an overview of Stedi apps.
Requirements
To build an OAuth app, you need:
- A Stedi production account: You register your app from a production account. There's no separate sandbox for apps, so you also test by installing your app into that same account. Visit Create an account for instructions.
- An OAuth 2.1 library: Any standard library that supports Proof Key for Code Exchange (PKCE) works.
Optionally, install curl, jq, and openssl. The examples in this guide use these tools to show the exact HTTP request to send at each step, so you only need them to run the examples as written. You can send the same requests from your OAuth library instead.
Customers can only install your app in a new Stedi account.
Register your app
Registering an app creates its listing and issues the credentials your app uses to identify and authenticate itself. New apps start in development mode, so the listing stays hidden from the Stedi Apps page until Stedi approves it.
-
Go to the Published apps page in your developer settings.
-
Click Register new app. Stedi opens the app registration form.
-
Add the app's details:
- App name: The display name for your app on the Stedi Apps page.
- Slug: The URL path for your app's page.
- Description: The description that appears on your app's page.
- App logo: A PNG file with maximum dimensions of 1024 x 1024 pixels and a maximum size of 1 MB.
- Website URL: The website for your business.
- Support URL: Where app users can get support from you about the app.
-
Under OAuth Configuration, add the callback URLs where Stedi sends customers after they authorize your app. Most apps use one, but you can add more if you test locally or run separate environments. You don't need a working handler yet, only the URL you plan to use.
Callback URLs must use HTTPS, with one exception: loopback addresses can use plain HTTP, so
http://127.0.0.1:31415/callbackworks for local development. Stedi logs these URLs, so don't include any sensitive values. -
Under App access, open the dropdown to review the permissions your app has in customer accounts. Stedi marks each one read-only or read and write. Every app uses the same fixed permission set, so you can't change them. Visit Permissions for the full list.
-
Click Register app.
Stedi returns a Client ID, prefixed stedi_cid_, and a Client secret, prefixed stedi_cs_. Stedi shows the Client secret exactly once.
Store the Client secret in your secret or password manager immediately. Stedi keeps only a keyed hash and can't show it to you again. If you lose it, create a replacement and delete the old one. Visit Rotate your client secret for details.
Your app now appears on the Published apps page with an inactive approval status. Customers can't see it until Stedi approves it.
Generate install links and handle callbacks
Before a customer can install your app, you must generate a link that starts the install and write a handler that turns the resulting code into an access token.
Install link
Customers install your app by clicking an install link. The link takes them to Stedi, where they create a Stedi account, review the permissions your app requests, and approve the install.
After you register your app, go to the Published apps page and click your app to find its install link, along with your Client ID and callback URLs. The link contains YOUR_STATE and YOUR_CODE_CHALLENGE placeholders. Replace them with fresh values each time you start the flow, and make the state value unique and unguessable.
The install link contains the following values:
| Value | Description |
|---|---|
response_type | Always code. |
client_id | Your app's Client ID, prefixed stedi_cid_. |
redirect_uri | One of the callback URLs you registered, URL-encoded. It must match a registered URL exactly, character for character. If it doesn't match, Stedi doesn't return the customer to your app, and the customer sees a message that reads This connection request can't be completed. |
scope | Always app:operator, the permission set every app uses. |
state | A unique, unguessable value your app generates for each install. Stedi returns it unchanged on the callback, so you can confirm the response belongs to the install you started. |
code_challenge | The SHA-256 hash of the code_verifier, a secret your app generates for each install. |
code_challenge_method | Always S256. |
A finished install link looks like the following example, with each value on its own line for readability:
https://portal.stedi.com/auth/oauth/authorize
?response_type=code
&client_id=stedi_cid_b7e2c1d0-9f8a-4e3b-a6c5-1d2e3f4a5b6c
&redirect_uri=https%3A%2F%2Fexample.com%2Fstedi%2Fcallback
&scope=app:operator
&state=3a7f1c92e4b8065d2f6a9c1e83b45d70
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256Generate a link for each install
For each install, your app must:
- Generate a
code_verifier, a secret that proves your app started this install, and store it in the customer's session. Use a high-entropy random string of 43 to 128 characters, made up of letters, digits, and the characters-,.,_, and~. - Hash the
code_verifierwith SHA-256 and base64url-encode the result. This is thecode_challenge. - Generate a
statevalue and store it in the customer's session. Use a unique, unguessable string for each install. - Build the install link with the
code_challengeandstate, then send the customer to it. Never send thecode_verifierto the browser.
Your OAuth library handles most of this. The following commands do the same thing in a terminal, so you can build a link by hand and test the flow:
CLIENT_ID=<your client_id, prefixed stedi_cid_>
REDIRECT_URI=<one of the callback URLs you registered>
CODE_VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr -d '\n=' | tr '+/' '-_')
STATE=$(openssl rand -hex 16)
AUTH_URL="https://portal.stedi.com/auth/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$(jq -rn --arg u "$REDIRECT_URI" '$u|@uri')&scope=app:operator&state=$STATE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"
echo "$AUTH_URL"When the customer opens the link, Stedi handles the rest of the install. The customer creates a Stedi account, then approves the permissions on the consent screen. Stedi redirects them to your callback URL when they're done.
Get access and refresh tokens
Stedi redirects the customer to your callback URL with an authorization code: ?code=…&state=…. The code is single-use and expires in 60 seconds.
When your app receives the authorization code, it must exchange the code for access and refresh tokens:
-
Compare the
statein the callback against the value you stored for this session. If they don't match, stop and don't exchange the code. -
Find the
code_verifieryou stored alongside thatstate. -
Post the code to Stedi's token endpoint with the
code_verifier, your Client ID, your Client secret, and the sameredirect_uriyou used in the install link.CLIENT_SECRET=<your client_secret, prefixed stedi_cs_> CODE=<paste the code from the redirected URL> curl -sS -X POST https://oauth.us.stedi.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "code=$CODE" \ --data-urlencode "code_verifier=$CODE_VERIFIER" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" \ --data-urlencode "redirect_uri=$REDIRECT_URI" -
Retrieve the access token and refresh token from the response and store them for that customer.
A successful code exchange returns a 200 response containing the following information:
| Property | Description |
|---|---|
access_token | The token your app sends in the Authorization: Bearer <access_token> header on Stedi API calls. |
token_type | Always Bearer. |
expires_in | How long the access token lasts, in seconds. Always 3600, or 60 minutes. |
refresh_token | The token your app sends to get the next access token before the current one expires. Visit Refresh access tokens for how to use it. |
scope | Always app:operator, the permission set every app uses. |
stedi_account_id | The customer account this connection is bound to. |
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdGVkaV9jaWRfYjdlMmMxZDAiLCJleHAiOjE3OTAxMjM0NTZ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "stedi_rt_9f2c1d7e-4b8a-4305-9c61-2f6a9c1e83b4",
"scope": "app:operator",
"stedi_account_id": "7f3c1b2a-9d40-4e6f-8a11-5c2d9e7b4a03"
}Test the connection
Once you've registered your app and built the install flow, test the connection between your app and Stedi. This requires installing the app in your own production account and making a read-only API call.
There's no separate sandbox for apps, and OAuth apps can't use Test mode. Every call your app makes runs against production, in a real account, where it can access protected health information (PHI).
Install the app in your production account
Newly registered apps run in development mode, where they stay unlisted and support up to 5 active installs. Use one of those installs to test your app yourself:
- Open your install link in a browser.
- Sign in to your Stedi production account.
- Review the permissions on the consent screen.
- Click Authorize.
This is the same experience your customers have. Stedi returns you to your callback URL, where your handler gets access and refresh tokens.
Make a test API call
Once your handler exchanges the authorization code for an access token, send that token as Authorization: Bearer <access_token> in a test API call. We recommend a read-only call to the account where you installed the app. The following example calls the List Enrollments endpoint:
curl -sS https://enrollments.us.stedi.com/2024-09-01/enrollments \
-H "Authorization: Bearer $ACCESS_TOKEN"A 200 with an items array means your token works and your app can reach Stedi:
{ "items": [], "totalCount": 0 }Call Stedi APIs
Your app calls Stedi APIs to do work in your customer's account.
Access tokens expire after 60 minutes. Your app must refresh them before that.
Permissions
Your app operates under the app:operator role, the same permission set the customer approves on the consent screen. Every app uses this set, and you can't change it.
Read only
- Account data: Read transactions, enrollments, claims, and configuration.
- Account login: Temporarily log in to the customer's Stedi account with the same access granted to the app.
Read and write
- Eligibility checks: Submit real-time and batch eligibility checks, and read responses.
- Claims: Submit professional, institutional, and dental claims over API or SFTP.
- Enrollments: Create and manage transaction enrollments.
- Credentials: Create and manage FTP users and SFTP credentials.
- Account events: Create and manage event destinations, and receive events.
Example request
Every call takes the access token as a bearer token in the Authorization header, in place of an API key. Apart from that header, requests are identical to the ones in the API Reference.
The following request runs an eligibility check in the account that installed your app:
curl --request POST \
--url https://healthcare.us.stedi.com/2026-06-01/eligibility-check \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header 'Content-Type: application/json' \
--data '{
"payerId": "61101",
"subscriber": {
"dateOfBirth": "1975-05-05",
"memberId": "HUMANA123",
"name": {
"person": {
"firstName": "Jane",
"lastName": "Doe"
}
}
},
"provider": {
"name": {
"organization": "Provider Name"
},
"npi": "1999999984"
},
"encounter": {
"services": [
{
"value": "30",
"system": "STC"
}
]
}
}'Go live
Going live lifts the install limit on your app.
Make sure you've thoroughly tested your app before you go live. Once your app is live, you can't return it to development mode.
To go live:
- Open the Published apps page, click your app, and click Submit for approval. Stedi reviews your app and contacts you with any questions. You can keep testing your app during the review.
- Once Stedi approves your app, switch it to live mode.
After your app is live, you can choose to list it on the Stedi Apps page.
Manage your app
Your registered apps appear on the Published apps page. Click an app to open its details, where you can find its install link, edit its listing, see which customers installed it, and manage its client secrets.
Edit your app listing
To edit your app listing, open the Published apps page, click your app, and click Edit app.
You can't change your app's name or its Client ID. However, you can add a callback URL at any time, including after your app goes live. New URLs take effect immediately.
Log in to customer accounts
To help a customer troubleshoot, you can log in to their Stedi account from your app.
Click your app on the Published apps page, then click Installed accounts to see every customer account that installed your app, along with its account ID and install date. Click the ellipsis (...) next to an account, then click Login to this account.
You're signed in as the customer, with your app's permissions. Stedi badges the session as a support session, limits how long it lasts, and audits it.
Force uninstall your app
To remove your app from a customer's account, go to Installed accounts, click the ellipsis (...) next to the account, and click Force uninstall.
Stedi deletes the SFTP connection and SFTP users your app created. Stedi leaves other resources in the account until someone manually deletes them.
App credentials
Your app uses four credentials, each for a different job.
| Credential | Description |
|---|---|
Client ID (stedi_cid_) | Stedi gives you the Client ID when you register your app. It identifies which app made the request. It's public, so you can put it in an install link, and your app sends it with every request to the token endpoint. |
Client secret (stedi_cs_) | Stedi shows you the Client secret once, when you register your app. It proves the request comes from your servers, so treat it like a password: send it only as a form field in token endpoint requests, and never put it in an install link or anywhere a browser can read it. |
| Access token | Stedi returns the access token in the response from the token endpoint. Visit Get access and refresh tokens for how to request it. It proves a customer authorized your app to act in their account. Send it in the Authorization: Bearer <access_token> header on Stedi API calls. |
Refresh token (stedi_rt_) | Stedi returns the refresh token alongside the access token. Send it as a form field to the token endpoint to get a new access token when the current one expires. Visit Refresh access tokens for how to use it. |
The Client ID and Client secret belong to your app, stay the same across customers, and never expire on their own. Stedi issues an access token and a refresh token per customer when that customer installs your app.
Refresh access tokens
Your app must refresh access tokens to stay connected. These limits govern how long each credential lasts, and what breaks the connection:
- Access tokens are valid for 60 minutes (
expires_inreports it as 3600 seconds). - Refresh tokens last one year, and the clock resets every time you use one. An app that refreshes regularly stays connected indefinitely, with no periodic re-authorization.
- Each refresh token works once. Stedi treats a consumed refresh token presented again - including one restored from a backup or snapshot - as a stolen token, revokes the connection, and makes the customer authorize your app again. A duplicate refresh within about 30 seconds succeeds without revoking anything, which covers retries and concurrent app instances.
When the access token nears expiry, post the refresh token back to the token endpoint:
curl -sS -X POST https://oauth.us.stedi.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET"The response contains a fresh access_token and a new refresh_token.
Deactivate or delete secrets
You can deactivate or delete a client secret at any time.
- Deactivating blocks a secret at the token endpoint, so authorization and refresh requests that use it fail. Deactivating is reversible, and the secret still counts toward your app's limit of two secrets. Access tokens your app received before you deactivate keep working until they expire.
- Deleting removes a secret permanently and frees space for a new one. You can't undo it.
To deactivate or delete a secret:
- Go to the Published apps page and click your app.
- Under Client secrets, click the ellipsis (...) next to the secret.
- Click Deactivate client secret or Delete client secret.
Rotate your client secret
Your app can have up to two secrets at once, which lets you roll over with no downtime. You can rotate your client secret in accordance with your organization's security policies. Secrets don't expire on their own. You must deactivate or delete them yourself.
We recommend the following process for rotating secrets:
- Go to the Published apps page and click your app.
- Under Client secrets, click + Create another secret.
- Deploy the new secret everywhere and refresh your tokens.
- Watch the old secret's last used time stop advancing.
- Click the ellipsis (...) next to the old secret and click Deactivate client secret.
- Once you're certain nothing uses the old secret, click the ellipsis again and click Delete client secret.
Troubleshooting
These are common errors and how to fix them:
| Symptom | Cause and fix |
|---|---|
400 invalid_grant | The grant is no longer usable: an expired or already-used code, a redirect_uri or code_verifier that doesn't match the authorize request, an uninstalled app, or a revoked connection. You can't retry, so send the customer a new install link to start over. If this happens on a refresh, something reused the refresh token and Stedi revoked the connection - check for restored stale tokens or concurrent refresh requests. |
401 invalid_client | The app's credential is wrong: a mistyped, deactivated, or deleted client secret, or a deleted app, which revokes its OAuth client. Fix the credential and retry. If a refresh that used to work starts failing, check whether someone deactivated or deleted the secret. |
Customer sees This connection request can't be completed | The redirect_uri isn't registered on the app, or doesn't match a registered URL exactly. |
state doesn't match on the callback | Cross-site request forgery, or your session store lost the value. Don't exchange the code. |
401 from a Stedi API | The access token expired. Refresh it. |
403 from a Stedi API | The token is valid, but app:operator doesn't allow that operation. |
If you've worked through these and still can't resolve the issue, contact Stedi support.