---
title: Build an app on mailfloss
slug: build-an-app
docTags: 
createdAt: 2026-10-20T03:01:00.000Z
---

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](quickstart) 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

1. Sign in to the [mailfloss dashboard](https://app.mailfloss.com) as an owner or
   admin of your account.
2. Open **Developers**, then the **Your apps** tab, and click **Create app**.
3. 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](docId\:I1zMFVhQwiYxF8k4lqzzF) your app may ask for. Ask only for
     what it needs.
4. 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:

```javascript
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=S256
```

| Parameter                                 | 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](docId\:I1zMFVhQwiYxF8k4lqzzF), 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:

```javascript
https://yourapp.example/oauth/callback?code=…&state=…&iss=https://api.mailfloss.com
```

If 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.

```bash
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`.

```json
{
  "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](docId\:I1zMFVhQwiYxF8k4lqzzF)). 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:

```bash
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](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:

```bash
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):

```bash
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:

1. **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.
2. **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.
3. **Permissions:** one sentence on why your app needs each scope. Scopes that
   change data (`write:*`) need one.
4. **Company:** legal name, contact email and country.
5. **Proof it works:** at least one successful connection. Your own account
   counts.
6. **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.                                                                                                                                              |
