Build an app on mailfloss
Build an app that other mailfloss users connect with one click. Your app sends people to mailfloss to sign in and approve, then calls the v1 REST API on their behalf with an OAuth 2.0 access token. It never sees their password or API key.
mailfloss is a standard OAuth 2.0 authorization server (authorization code grant, RFC 6749), so most OAuth client libraries work without changes.
1. Create your app
- Sign in to the mailfloss dashboard as an owner or admin of your account.
- Open Developers, then the Your apps tab, and click Create app.
- Fill in:
- App name. People see it when they connect your app.
- Redirect URIs, one per line. mailfloss sends people back only to these exact URLs. Use https://, or http://localhost, http://127.0.0.1 or http://[::1] (any port) while you develop.
- Permissions: the scopes your app may ask for. Ask only for what it needs.
- Copy the client ID (mfapp_…) and client secret (mfcs_…). The secret is shown once. Store it like a password, on your server only.
To add a logo, open the app from Your apps, click Upload logo on its Settings tab, then click Save changes. Use a square PNG of at least 512×512. The logo shows next to your app's name when people connect it.
An account can have up to 10 apps. To replace a leaked or lost secret, open the app and click Rotate next to the client secret. The old secret stops working at once, and existing connections keep working. Deleting an app disconnects every account connected to it.
Changing your app's permissions affects new connections only. Existing connections keep the scopes they approved until the user connects your app again or disconnects it. Rotating the secret or saving a change also cancels any connection that's in progress; that user starts again.
2. Send the user to mailfloss
Redirect the user's browser to the authorization endpoint:
https://api.mailfloss.com/oauth/authorize
?response_type=code
&client_id=mfapp_…
&redirect_uri=https://yourapp.example/oauth/callback
&scope=read:organization verify
&state=<random value you check later>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256Parameter | Required | Notes |
|---|---|---|
response_type | yes | Always code. |
client_id | yes | Your app's client ID. |
redirect_uri | yes, unless your app has exactly one | Must match a registered URI exactly. A loopback URI (localhost, 127.0.0.1 or [::1]) may use any port. |
scope | no | Space-separated scopes, within your app's permissions. Leave it out to ask for all of them. Unknown values are ignored. If nothing you ask for is in your app's permissions, the request fails with invalid_scope. |
state | recommended | Returned unchanged. Check it to stop cross-site request forgery. |
code_challenge, code_challenge_method | recommended | PKCE. Send both, with code_challenge_method=S256, or neither. Optional for apps, because your secret also protects the code. |
mailfloss signs the user in, shows what your app asks for, and redirects back:
https://yourapp.example/oauth/callback?code=…&state=…&iss=https://api.mailfloss.comIf the user declines, or the request fails, the redirect carries error and error_description instead of code, for example error=access_denied. If the client ID or the redirect URI is wrong, mailfloss shows an error page instead of redirecting.
3. Exchange the code for tokens
Exchange the code from your server within 5 minutes. The examples read your client ID and secret from the MAILFLOSS_CLIENT_ID and MAILFLOSS_CLIENT_SECRET environment variables.
curl https://api.mailfloss.com/oauth/token \
-u "$MAILFLOSS_CLIENT_ID:$MAILFLOSS_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=… \
-d redirect_uri=https://yourapp.example/oauth/callback \
-d code_verifier=…Authenticate with HTTP Basic (client_secret_basic), or send client_id and client_secret in the body (client_secret_post). The body may be form-urlencoded or JSON. Send code_verifier only if you sent a code_challenge. If you skipped PKCE, you must send redirect_uri.
{
"access_token": "mf_oauth_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mf_ort_…",
"scope": "read:organization verify",
"created_at": 1760774400
}scope is what the user actually granted, which can be less than you asked for (see Scopes). A code works once. Presenting it twice disconnects everything it issued.
4. Call the API
Send the access token as a Bearer token to the v1 REST API:
curl https://api.mailfloss.com/v1/organization \
-H "Authorization: Bearer mf_oauth_…"The token acts for the user's mailfloss account, like its API key: same data, credits, rate limits and errors. It works only on https://api.mailfloss.com/v1/…. A call outside the granted scopes returns 403 with the code forbidden_scope.
5. Refresh
Access tokens last up to one hour. Get a new pair with the refresh token:
curl https://api.mailfloss.com/oauth/token \
-u "$MAILFLOSS_CLIENT_ID:$MAILFLOSS_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token=mf_ort_…Every refresh returns a new access token and a new refresh token, and the previous access token stops working at once. Store both and drop the old ones. If several workers share a connection, let one of them refresh and share the new pair. Using an old refresh token again disconnects the user, because it means someone copied it. A refresh token unused for 30 days expires.
6. Disconnect
When a user uninstalls your app, revoke its token (RFC 7009):
curl https://api.mailfloss.com/oauth/revoke \
-u "$MAILFLOSS_CLIENT_ID:$MAILFLOSS_CLIENT_SECRET" \
-d token=mf_ort_…Send the access token or the refresh token; either one ends the whole connection. The endpoint answers 200 even for a token it doesn't know.
Users can also disconnect your app themselves under Account Settings → Connected apps. Your next call then gets 401, and a refresh gets invalid_grant.
When a call gets 401, refresh the token once. If the refresh fails with invalid_grant, the connection is gone; ask the user to connect your app again.
Scopes
Scope | Lets your app |
|---|---|
read:organization | See the account details, plan and credit balance |
read:users | See the team members |
read:integrations | See the connected email platforms and their settings |
read:jobs | See email verification jobs, their results and the account activity history |
read:reports | See reports |
verify | Verify email addresses and start or cancel verification jobs, which spends credits |
write:integrations | Connect and disconnect email platforms, change their settings and rules, and set up webhooks |
write:erasures | Permanently erase contacts (GDPR requests) |
write:account | Change the account and notification settings |
A token gets the scopes you asked for, limited by your app's permissions and by the approving user's role in their mailfloss account. A contributor's approval never includes write:erasures or write:account. mailfloss re-checks the role on every call, so a user who loses a role loses that access at once.
Before your app is verified
A new app works right away, with two limits:
- The consent page warns people that mailfloss hasn't verified your app.
- It can hold up to 100 connections at once; each approval counts as one. At the limit, new connections are refused with a message that the app has reached its limit until mailfloss verifies it. The token endpoint answers invalid_grant with the same message. Accounts already connected keep working.
Your apps shows how many accounts each app has connected.
Get verified
Verification removes the warning and the limit: people see a Verified by mailfloss badge when they connect your app. Open your app's Get verified tab in Your apps and fill in:
- Listing: name, a square PNG logo (at least 512×512), a summary (at most 90 characters), a description (at most 2,000), a category, and your website, support email or link, privacy policy and (optionally) terms.
- Show how it works: 2 to 8 screenshots (PNG or JPEG, up to 4000×4000 and 5 MB each), a Loom or YouTube demo video, and setup and testing steps. The video replaces a test account: we never ask for passwords.
- Permissions: one sentence on why your app needs each scope. Scopes that change data (write:*) need one.
- Company: legal name, contact email and country.
- Proof it works: at least one successful connection. Your own account counts.
- Agreement: accept the mailfloss Developer Terms and App review guidelines.
mailfloss reviews submissions within 5 business days and emails the contact address with the decision. Your app keeps working meanwhile, but you can't change it until the review is done. If we ask for changes, the email and the tab say what to fix. Edit the app and resubmit.
After verification, a new name or logo, a new redirect domain or a new scope goes to us for review from the app's Settings tab. People keep seeing the approved version until we approve the change. Removing a scope or a redirect URI takes effect at once for new connections.
A suspended app can't connect accounts, and its tokens stop working.
Endpoints
Endpoint | URL |
|---|---|
Authorization | https://api.mailfloss.com/oauth/authorize |
Token | https://api.mailfloss.com/oauth/token |
Revocation | https://api.mailfloss.com/oauth/revoke |
Server metadata (RFC 8414) | https://api.mailfloss.com/.well-known/oauth-authorization-server |
API resource metadata (RFC 9728) | https://api.mailfloss.com/.well-known/oauth-protected-resource/v1 |
Errors
OAuth endpoints answer with { "error": "…", "error_description": "…" }:
error | Status | Means |
|---|---|---|
invalid_client | 401 | Unknown client ID or wrong secret, for example after a rotation. |
invalid_grant | 400 | The code or refresh token is invalid, expired, used or revoked; the exchange doesn't match the authorization request (redirect_uri or PKCE); or the app has reached its connection limit. |
invalid_scope | redirect | None of the requested scopes is in your app's permissions. |
invalid_request | 400 or redirect | A required parameter is missing or malformed. |
invalid_target | 400 or redirect | The requested resource isn't one this app can use. |
unsupported_response_type | redirect | response_type isn't code. |
unsupported_grant_type | 400 | grant_type isn't authorization_code or refresh_token. |
access_denied | redirect | The user declined, their role includes none of the requested scopes, the app is at its connection limit, or their account can't connect apps right now. |
server_error | 500 | Something went wrong on our side. Retry later. |