groundmail-dev OAuth API

OAuth 2.0 Device Authorization Grant (RFC 8628) for the CLI and agents, the authorization code grant with PKCE for the hosted MCP server, dynamic client registration (RFC 7591), token refresh with rotating refresh tokens, revocation (RFC 7009), and a UserInfo endpoint for the `openid` and `email` scopes. These endpoints send CORS headers and answer OPTIONS preflights. Server metadata: https://app.groundmail.dev/.well-known/oauth-authorization-server

Operations 6

POST /oauth/register Register an OAuth client (RFC 7591) #
POST /oauth/device_authorization Start a device sign-in (RFC 8628) #
POST /oauth/token Exchange a device code, authorization code or refresh token for tokens #
POST /oauth/revoke Revoke a token (RFC 7009) #
GET /oauth/userinfo Signed-in user (OpenID Connect UserInfo) #
POST /oauth/userinfo Signed-in user (OpenID Connect UserInfo) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/groundmail-dev:groundmail-dev-oauth-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

groundmail-dev-oauth-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Groundmail OAuth API
  version: 0.16.0
  description: 'Transactional email API for developers.

    Auth: `Authorization: Bearer gm_…` with an API key, or `Bearer gm_oat_…` with an OAuth

    access token from the device flow (`groundmail login`). Dashboard session cookies work

    for integrations.


    Azure managed identity (keyless): an Azure workload can send a Microsoft Entra access

    token (a JWT) as the Bearer credential instead of a key. Register the identity''s tenant

    ID, object ID and token audience in the dashboard (API keys → Azure managed identity);

    it then has the same access options as an API key (scopes, domain limit, tenant).

    App-only tokens only. Any authentication failure is the same 401 `unauthorized`.

    See https://docs.groundmail.dev/authentication/azure-managed-identity.


    Test mode: a test key (`gm_test_…`, `POST /api-keys` with `test: true`) behaves

    like a live key, but its sends are never delivered: they are stored with

    `test: true`, get simulated events and webhooks (`data.test: true`), and do not

    count toward plan limits, the sending ramp, stats or billing. Simulator

    addresses (`delivered@`, `bounce@`, `block@`, `complaint@`, `suppressed@`,

    `delayed@simulator.groundmail.dev`, plus addressing allowed) produce one outcome

    each with any key. See https://docs.groundmail.dev/testing.


    Tenants: a platform that sends email for its own customers creates one tenant per

    customer (`/tenants`), each with its own domains, suppression list, stats, sending

    ramp and pause switch. Name the tenant with `tenant` on sends, domains and

    suppressions, or send from a domain the tenant owns. API keys and webhook

    endpoints can be limited to tenants. Included on pay as you go and the Dedicated plan;

    elsewhere a request that names a tenant gets 403 `tenants_not_enabled`. See

    https://docs.groundmail.dev/tenants.


    Data retention: sent emails (content and events) and received emails are kept for

    your plan''s window, then deleted: 7 days on Free, 30 on Pro, 90 on Scale, up to 365

    on Dedicated. Past it, `GET /emails/{id}` and `GET /received-emails/{id}` return

    404 `not_found` and lists no longer include them. Aggregate stats

    (`GET /deliverability`) are kept. Store webhook events to keep history longer. See

    https://docs.groundmail.dev/emails#data-retention.


    Scopes: every credential carries scopes (`full`, `emails:send`, `emails:read`,

    `domains:read`, `domains:write`, `webhooks:write`, `templates:write`,

    `suppressions:write`, `account:read`). Each operation lists the scope it needs under

    `x-required-scope` (space-separated: all of them; `a | b`: either); a credential

    without it gets 403 `insufficient_scope` with `required_scope`. `full` grants everything; `domains:write` includes `domains:read`.

    Legacy `sending` API keys have `emails:send` + `emails:read`.

    Production base URL: https://api.groundmail.dev'
servers:
- url: https://api.groundmail.dev
  description: Production
tags:
- name: OAuth
  description: 'OAuth 2.0 Device Authorization Grant (RFC 8628) for the CLI and agents, the authorization code grant with PKCE for the hosted MCP server, dynamic client registration (RFC 7591), token refresh with rotating refresh tokens, revocation (RFC 7009), and a UserInfo endpoint for the `openid` and `email` scopes. These endpoints send CORS headers and answer OPTIONS preflights. Server metadata: https://app.groundmail.dev/.well-known/oauth-authorization-server'
paths:
  /oauth/register:
    post:
      operationId: registerOAuthClient
      summary: Register an OAuth client (RFC 7591)
      tags:
      - OAuth
      security: []
      description: 'Dynamic client registration for MCP clients and other apps. Redirect URIs must be `https://`, loopback `http://` (`localhost`, `127.0.0.1`, `[::1]`, any port), or a private-use scheme for a native app (RFC 8252), such as `cursor://anysphere.cursor-mcp/oauth/callback`. Fragments and schemes a browser handles itself (`javascript:`, `data:`, `file:`, …) are refused. With `token_endpoint_auth_method: client_secret_post` the response includes a `client_secret`, shown once, that the client must send to `/oauth/token`.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthClientRegistrationRequest'
      responses:
        '201':
          description: Client registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthClientRegistrationResponse'
        '400':
          description: '`invalid_client_metadata`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
  /oauth/device_authorization:
    post:
      operationId: startDeviceAuthorization
      summary: Start a device sign-in (RFC 8628)
      tags:
      - OAuth
      security: []
      description: Public and rate limited. Returns a secret `device_code` to poll `/oauth/token` with, and a short `user_code` the person approves at `verification_uri_complete`. The approval page shows the client, the device name and the scopes asked for, and the person picks the workspace. Form-encoded (RFC 8628) or JSON bodies are accepted.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/DeviceAuthorizationRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceAuthorizationRequest'
      responses:
        '200':
          description: Device sign-in started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceAuthorizationResponse'
        '400':
          description: '`invalid_request`, `invalid_scope` or `unauthorized_client`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '401':
          description: '`invalid_client`: unknown client_id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '429':
          description: Rate limited (`Retry-After`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
  /oauth/token:
    post:
      operationId: oauthToken
      summary: Exchange a device code, authorization code or refresh token for tokens
      tags:
      - OAuth
      security: []
      description: '`grant_type=urn:ietf:params:oauth:grant-type:device_code` polls a device sign-in: `authorization_pending` until approved, `slow_down` when polled faster than `interval` (add 5 seconds), `access_denied` when canceled, `expired_token` after ten minutes. `grant_type=refresh_token` rotates the refresh token: each one works once, and presenting a used one revokes the whole sign-in. Pass `scope` to narrow the new access token. Access tokens last 3600 seconds; refresh tokens expire after 90 days without use. `authorization_code` (PKCE S256 required) serves the hosted MCP server; the tokens carry the scopes approved on the consent page, and a code presented twice revokes what it was exchanged for. Clients registered with a secret authenticate with `client_secret` (body or HTTP Basic); public clients send only `client_id`. A `resource` (RFC 8707) must be the one that was authorized.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuthTokenRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthTokenRequest'
      responses:
        '200':
          description: Tokens
          headers:
            Cache-Control:
              schema:
                type: string
                enum:
                - no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthTokenResponse'
        '400':
          description: '`authorization_pending`, `slow_down`, `access_denied`, `expired_token`, `invalid_grant`, `invalid_scope`, `invalid_target`, `invalid_request` or `unsupported_grant_type`

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '401':
          description: '`invalid_client`: unknown client, or a confidential client without a valid `client_secret`

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '429':
          description: Rate limited (`Retry-After`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
  /oauth/revoke:
    post:
      operationId: oauthRevoke
      summary: Revoke a token (RFC 7009)
      tags:
      - OAuth
      security: []
      description: 'Revoking a refresh token ends the sign-in: it and every access token it issued stop working. Revoking an access token revokes only that token. Always 200, whether or not the token was known.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuthRevokeRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthRevokeRequest'
      responses:
        '200':
          description: Revoked (or unknown)
        '429':
          description: Rate limited (`Retry-After`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
  /oauth/userinfo:
    get:
      operationId: oauthUserInfo
      summary: Signed-in user (OpenID Connect UserInfo)
      tags:
      - OAuth
      security:
      - bearerAuth: []
      description: 'Who the access token''s user is. Send an OAuth access token (`gm_oat_…`) that was granted the `openid` and/or `email` scope; API keys and tokens without an identity scope are refused. `sub` is the stable user id. `email` and `email_verified` are returned with the `email` scope. Sign-in requires a verified email address, so `email_verified` is `true` for every account. The identity scopes grant no other access: a token with only `openid` and `email` is refused by every other endpoint.'
      responses:
        '200':
          description: Claims for the signed-in user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthUserInfo'
        '401':
          description: 'Missing, invalid or expired access token (`WWW-Authenticate: Bearer`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '403':
          description: The token has neither the `openid` nor the `email` scope (`insufficient_scope`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
    post:
      operationId: oauthUserInfoPost
      summary: Signed-in user (OpenID Connect UserInfo)
      tags:
      - OAuth
      security:
      - bearerAuth: []
      description: 'Who the access token''s user is. Send an OAuth access token (`gm_oat_…`) that was granted the `openid` and/or `email` scope; API keys and tokens without an identity scope are refused. `sub` is the stable user id. `email` and `email_verified` are returned with the `email` scope. Sign-in requires a verified email address, so `email_verified` is `true` for every account. The identity scopes grant no other access: a token with only `openid` and `email` is refused by every other endpoint.'
      responses:
        '200':
          description: Claims for the signed-in user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthUserInfo'
        '401':
          description: 'Missing, invalid or expired access token (`WWW-Authenticate: Bearer`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
        '403':
          description: The token has neither the `openid` nor the `email` scope (`insufficient_scope`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthErrorResponse'
components:
  schemas:
    DeviceAuthorizationResponse:
      type: object
      required:
      - device_code
      - user_code
      - verification_uri
      - verification_uri_complete
      - expires_in
      - interval
      properties:
        device_code:
          type: string
        user_code:
          type: string
          example: BCDF-GHJK
        verification_uri:
          type: string
        verification_uri_complete:
          type: string
        expires_in:
          type: integer
          example: 600
        interval:
          type: integer
          description: Minimum seconds between polls
    OAuthUserInfo:
      type: object
      required:
      - sub
      properties:
        sub:
          type: string
          description: Stable user id
        email:
          type: string
          format: email
          description: With the `email` scope
        email_verified:
          type: boolean
          description: With the `email` scope
          example: true
    OAuthClientRegistrationResponse:
      type: object
      required:
      - client_id
      - client_id_issued_at
      - client_name
      - redirect_uris
      - token_endpoint_auth_method
      - grant_types
      properties:
        client_id:
          type: string
          example: gm_oauth_0123456789abcdef0123456789abcdef
        client_secret:
          type: string
          description: Only for `client_secret_post` clients, shown once
        client_id_issued_at:
          type: integer
        client_name:
          type: string
        redirect_uris:
          type: array
          items:
            type: string
        token_endpoint_auth_method:
          type: string
          enum:
          - none
          - client_secret_post
        grant_types:
          type: array
          items:
            type: string
        response_types:
          type: array
          items:
            type: string
    DeviceAuthorizationRequest:
      type: object
      required:
      - client_id
      properties:
        client_id:
          type: string
          description: '`groundmail-cli`, or a client registered with the device_code grant type'
        scope:
          type: string
          description: Space-separated scopes; defaults to `full`
          example: emails:send domains:read
        device_name:
          type: string
          description: Shown on the approval page and in Settings → CLIs & agents (e.g. the hostname)
        workspace:
          type: string
          description: Workspace name or id to preselect on the approval page (a hint, not a grant)
    OAuthErrorResponse:
      type: object
      required:
      - error
      properties:
        error:
          type: string
          example: authorization_pending
        error_description:
          type: string
    OAuthRevokeRequest:
      type: object
      required:
      - token
      properties:
        token:
          type: string
        token_type_hint:
          type: string
          enum:
          - access_token
          - refresh_token
        client_id:
          type: string
    OAuthTokenRequest:
      type: object
      required:
      - grant_type
      properties:
        grant_type:
          type: string
          enum:
          - urn:ietf:params:oauth:grant-type:device_code
          - refresh_token
          - authorization_code
        client_id:
          type: string
        device_code:
          type: string
        refresh_token:
          type: string
        client_secret:
          type: string
          description: Confidential clients only (`client_secret_post`); or send HTTP Basic
        scope:
          type: string
          description: With refresh_token, narrows the new access token (never widens it)
        resource:
          type: string
          description: 'RFC 8707 resource indicator. Optional; when sent it must be the resource that was authorized (`https://mcp.groundmail.dev/mcp`, or its origin), else `invalid_target`

            '
        code:
          type: string
        redirect_uri:
          type: string
        code_verifier:
          type: string
    OAuthTokenResponse:
      type: object
      required:
      - access_token
      - token_type
      - expires_in
      - refresh_token
      - scope
      properties:
        access_token:
          type: string
          description: Bearer token for the API (`gm_oat_…`)
        token_type:
          type: string
          enum:
          - Bearer
        expires_in:
          type: integer
          example: 3600
        refresh_token:
          type: string
          description: Single-use; store the new one each time
        scope:
          type: string
        workspace:
          type: object
          description: Device grant only
          properties:
            id:
              type: string
            name:
              type: string
        user:
          type: object
          description: Device grant only
          properties:
            id:
              type: string
            email:
              type: string
    OAuthClientRegistrationRequest:
      type: object
      required:
      - client_name
      properties:
        client_name:
          type: string
          example: Cursor
        redirect_uris:
          type: array
          description: Required with the authorization_code grant (the default)
          items:
            type: string
          example:
          - cursor://anysphere.cursor-mcp/oauth/callback
        token_endpoint_auth_method:
          type: string
          enum:
          - none
          - client_secret_post
          default: none
        grant_types:
          type: array
          description: Default `authorization_code` and `refresh_token`
          items:
            type: string
            enum:
            - authorization_code
            - refresh_token
            - urn:ietf:params:oauth:grant-type:device_code
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Groundmail API key (`gm_…`), OAuth access token (`gm_oat_…`), or a Microsoft Entra access token from an Azure managed identity registered in the dashboard (keyless; see https://docs.groundmail.dev/authentication/azure-managed-identity). All carry scopes; see the API description.

        '