Account linking
The Account linking endpoints.
Auth: Public · Base URL:
https://api.mailkite.dev
Endpoints
Each row expands to its request and response schema. The address bar follows along, so any endpoint can be linked to directly.
POST /oauth/register Register an OAuth client for this installation (RFC 7591 dynamic client registration) — step 1 of linking an existing MailKite account to your app. No pre-shared secret and no manual app review: you get a client_id back immediately and prove yourself with PKCE. Register once per install and keep the client_id.
Register an OAuth client for this installation (RFC 7591 dynamic client registration) — step 1 of linking an existing MailKite account to your app. No pre-shared secret and no manual app review: you get a client_id back immediately and prove yourself with PKCE. Register once per install and keep the client_id.
Request body
oauth-client-request.jsonPOST /oauth/register — RFC 7591 dynamic client registration. Register once per installation (a plugin registers itself per site) and keep the returned client_id; there is no secret to store.
| Field | Type | Notes |
|---|---|---|
| redirect_uris req | string[] | Where the authorization code is delivered. Every entry must be an absolute https URL — http is accepted only on loopback, for local development. A javascript: or data: URI is rejected. |
| client_name | string | Human-readable name shown to the user on the consent screen. Include the site or install it belongs to so someone with several connections can tell them apart. |
The smallest body that makes this call — every other field is optional.
{
"client_name": "MailKite for myapp.ai",
"redirect_uris": [
"https://myapp.ai/settings/mailkite/callback"
]
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/oauth-client-request.json",
"title": "OAuth client registration request body",
"description": "POST /oauth/register — RFC 7591 dynamic client registration. Register once per installation (a plugin registers itself per site) and keep the returned client_id; there is no secret to store.",
"type": "object",
"required": [
"redirect_uris"
],
"properties": {
"redirect_uris": {
"type": "array",
"description": "Where the authorization code is delivered. Every entry must be an absolute https URL — http is accepted only on loopback, for local development. A javascript: or data: URI is rejected.",
"minItems": 1,
"items": {
"type": "string"
},
"examples": [
[
"https://myapp.ai/settings/mailkite/callback"
]
]
},
"client_name": {
"type": "string",
"description": "Human-readable name shown to the user on the consent screen. Include the site or install it belongs to so someone with several connections can tell them apart.",
"examples": [
"MailKite for myapp.ai"
]
}
}
} Response body
oauth-client-response.jsonPOST /oauth/register — the registered client. Public client: there is no client_secret, and PKCE takes its place.
| Field | Type | Notes |
|---|---|---|
| client_id req | string | Pass this to /oauth/authorize and /oauth/token. |
| client_id_issued_at req | integer | Unix seconds when the client was registered. |
| client_name | string | Echoed back when supplied. |
| redirect_uris req | string[] | The registered redirect URIs. The one passed to /oauth/authorize must match an entry exactly. |
| grant_types req | string[] | |
| response_types req | string[] | |
| token_endpoint_auth_method req | string | Always `none` — the client is public and proves itself with PKCE. |
An actual response, recorded from the conformance suite.
{
"client_id": "mkcli_8Kq2Vn4TwXr7Bm3d",
"client_id_issued_at": 1769731200,
"client_name": "MailKite for myapp.ai",
"redirect_uris": [
"https://myapp.ai/settings/mailkite/callback"
],
"grant_types": [
"authorization_code",
"refresh_token"
],
"response_types": [
"code"
],
"token_endpoint_auth_method": "none"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/oauth-client-response.json",
"title": "OAuth client registration response",
"description": "POST /oauth/register — the registered client. Public client: there is no client_secret, and PKCE takes its place.",
"type": "object",
"required": [
"client_id",
"client_id_issued_at",
"redirect_uris",
"grant_types",
"response_types",
"token_endpoint_auth_method"
],
"properties": {
"client_id": {
"type": "string",
"description": "Pass this to /oauth/authorize and /oauth/token.",
"examples": [
"mkcli_8Kq2Vn4TwXr7Bm3d"
]
},
"client_id_issued_at": {
"type": "integer",
"description": "Unix seconds when the client was registered.",
"examples": [
1769731200
]
},
"client_name": {
"type": "string",
"description": "Echoed back when supplied.",
"examples": [
"MailKite for myapp.ai"
]
},
"redirect_uris": {
"type": "array",
"description": "The registered redirect URIs. The one passed to /oauth/authorize must match an entry exactly.",
"items": {
"type": "string"
},
"examples": [
[
"https://myapp.ai/settings/mailkite/callback"
]
]
},
"grant_types": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"authorization_code",
"refresh_token"
]
]
},
"response_types": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"code"
]
]
},
"token_endpoint_auth_method": {
"type": "string",
"description": "Always `none` — the client is public and proves itself with PKCE.",
"examples": [
"none"
]
}
}
} POST /oauth/token Exchange an authorization code for an access token (or rotate a refresh token) — step 3 of linking. Between steps you send the user's browser to /oauth/authorize with your client_id, redirect_uri, state, and an S256 code_challenge; they sign in with whatever method they already use and approve, and the code comes back to your redirect_uri. Then call getApiKey with the access token and store the key.
Exchange an authorization code for an access token (or rotate a refresh token) — step 3 of linking. Between steps you send the user's browser to /oauth/authorize with your client_id, redirect_uri, state, and an S256 code_challenge; they sign in with whatever method they already use and approve, and the code comes back to your redirect_uri. Then call getApiKey with the access token and store the key.
Request body
oauth-token-request.jsonPOST /oauth/token — exchange an authorization code for tokens, or rotate a refresh token. Accepts application/x-www-form-urlencoded (the OAuth norm) or application/json. Which fields are required depends on grant_type.
| Field | Type | Notes |
|---|---|---|
| grant_type req | "authorization_code" · "refresh_token" | Which exchange to perform. |
| code | string | authorization_code grant: the single-use code from the redirect. Expires quickly and is consumed on first use — a replay returns invalid_grant. |
| redirect_uri | string | authorization_code grant: must match the redirect_uri used at /oauth/authorize exactly. |
| client_id | string | authorization_code grant: required. refresh_token grant: optional, but checked against the token's client when supplied. |
| code_verifier | string | authorization_code grant: the PKCE verifier whose S256 hash you sent as code_challenge. Required — there is no non-PKCE path. |
| refresh_token | string | refresh_token grant: the token to rotate. Single-use — the old one is revoked and a new one returned alongside the access token. |
The smallest body that makes this call — every other field is optional.
{
"grant_type": "authorization_code",
"code": "mkc_4Tn8Wq2Vx7Rb",
"redirect_uri": "https://myapp.ai/settings/mailkite/callback",
"client_id": "mkcli_8Kq2Vn4TwXr7Bm3d",
"code_verifier": "dBjftJeZ4CVPmB92K27uhbUJU1p1r0wsUPvhBLIm3ZY"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/oauth-token-request.json",
"title": "OAuth token request body",
"description": "POST /oauth/token — exchange an authorization code for tokens, or rotate a refresh token. Accepts application/x-www-form-urlencoded (the OAuth norm) or application/json. Which fields are required depends on grant_type.",
"type": "object",
"required": [
"grant_type"
],
"properties": {
"grant_type": {
"type": "string",
"enum": [
"authorization_code",
"refresh_token"
],
"description": "Which exchange to perform."
},
"code": {
"type": "string",
"description": "authorization_code grant: the single-use code from the redirect. Expires quickly and is consumed on first use — a replay returns invalid_grant.",
"examples": [
"mkc_4Tn8Wq2Vx7Rb"
]
},
"redirect_uri": {
"type": "string",
"description": "authorization_code grant: must match the redirect_uri used at /oauth/authorize exactly.",
"examples": [
"https://myapp.ai/settings/mailkite/callback"
]
},
"client_id": {
"type": "string",
"description": "authorization_code grant: required. refresh_token grant: optional, but checked against the token's client when supplied.",
"examples": [
"mkcli_8Kq2Vn4TwXr7Bm3d"
]
},
"code_verifier": {
"type": "string",
"description": "authorization_code grant: the PKCE verifier whose S256 hash you sent as code_challenge. Required — there is no non-PKCE path.",
"examples": [
"dBjftJeZ4CVPmB92K27uhbUJU1p1r0wsUPvhBLIm3ZY"
]
},
"refresh_token": {
"type": "string",
"description": "refresh_token grant: the token to rotate. Single-use — the old one is revoked and a new one returned alongside the access token.",
"examples": [
"mkr_9Pw3Kv6Nz2Qy8Lt4"
]
}
}
} Response body
oauth-token-response.jsonPOST /oauth/token — an access token for the linked account. To finish linking, call getApiKey with this access token as the Bearer credential and store the API key it returns; then you can discard the OAuth tokens.
| Field | Type | Notes |
|---|---|---|
| access_token req | string | Bearer credential for the account that authorized you. Short-lived — see expires_in. |
| token_type req | string | Always `Bearer`. |
| expires_in req | integer | Access-token lifetime in seconds. |
| refresh_token req | string | Single-use refresh token. Rotating it revokes this one and issues a replacement. |
| scope req | string | The granted scope. Account-wide today, which is why the recommended pattern is to exchange it for an API key immediately rather than keeping it. |
An actual response, recorded from the conformance suite.
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfN0ZqM01uUXcifQ.sT4x",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mkr_9Pw3Kv6Nz2Qy8Lt4",
"scope": "mcp"
} {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.mailkite.dev/v1/schemas/oauth-token-response.json",
"title": "OAuth token response",
"description": "POST /oauth/token — an access token for the linked account. To finish linking, call getApiKey with this access token as the Bearer credential and store the API key it returns; then you can discard the OAuth tokens.",
"type": "object",
"required": [
"access_token",
"token_type",
"expires_in",
"refresh_token",
"scope"
],
"properties": {
"access_token": {
"type": "string",
"description": "Bearer credential for the account that authorized you. Short-lived — see expires_in.",
"examples": [
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfN0ZqM01uUXcifQ.sT4x"
]
},
"token_type": {
"type": "string",
"description": "Always `Bearer`.",
"examples": [
"Bearer"
]
},
"expires_in": {
"type": "integer",
"description": "Access-token lifetime in seconds.",
"examples": [
3600
]
},
"refresh_token": {
"type": "string",
"description": "Single-use refresh token. Rotating it revokes this one and issues a replacement.",
"examples": [
"mkr_9Pw3Kv6Nz2Qy8Lt4"
]
},
"scope": {
"type": "string",
"description": "The granted scope. Account-wide today, which is why the recommended pattern is to exchange it for an API key immediately rather than keeping it.",
"examples": [
"mcp"
]
}
}
}