Back to Midday

OAuth API Endpoints

apps/website/src/app/docs/content/oauth-api-endpoints.mdx

latest11.7 KB
Original Source

Complete technical reference for Midday's OAuth 2.0 implementation. These endpoints follow the OAuth 2.0 RFC 6749 and PKCE RFC 7636 specifications.

Base URLs

EnvironmentURL
Authorizationhttps://app.midday.ai/oauth
Token & APIhttps://api.midday.ai/v1

Authorization endpoint

Initiates the OAuth flow by redirecting users to log in and authorize your app.

GET https://app.midday.ai/oauth/authorize

Request parameters

ParameterTypeRequiredDescription
response_typestringYesMust be code
client_idstringYesYour application's client ID
redirect_uristringYesURI to redirect after authorization (must be registered)
scopestringYesSpace-separated list of scopes
statestringRecommendedOpaque value for CSRF protection
code_challengestringPKCEBase64-URL-encoded SHA-256 hash of code verifier
code_challenge_methodstringPKCEMust be S256

Example request

https://app.midday.ai/oauth/authorize?
  response_type=code&
  client_id=mid_client_abc123&
  redirect_uri=https://yourapp.com/callback&
  scope=transactions.read%20invoices.read&
  state=xyz789&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256

Success response

Redirects to your redirect_uri with:

ParameterDescription
codeAuthorization code (valid for 10 minutes)
stateSame value you sent (verify this!)
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789

Error response

Redirects to your redirect_uri with:

ParameterDescription
errorError code
error_descriptionHuman-readable description
stateSame value you sent
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789

Error codes

CodeDescription
invalid_requestMissing or invalid parameter
unauthorized_clientClient not authorized for this grant type
access_deniedUser denied authorization
invalid_scopeInvalid or unknown scope
server_errorInternal server error

Token endpoint

Exchange authorization codes for access tokens, or refresh existing tokens.

POST https://api.midday.ai/v1/oauth/token

Content types

Accepts both:

  • application/json
  • application/x-www-form-urlencoded

Authorization code grant

Exchange an authorization code for tokens.

Request body

ParameterTypeRequiredDescription
grant_typestringYesMust be authorization_code
codestringYesAuthorization code from callback
redirect_uristringYesSame URI used in authorization
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour client secret
code_verifierstringPKCEOriginal code verifier

Example request (confidential client)

bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'

Example request (public client with PKCE)

bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "mid_client_abc123",
    "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
  }'

Success response

json
{
  "access_token": "mid_at_xxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mid_rt_xxxxxxxxxxxxx",
  "scope": "transactions.read invoices.read"
}
FieldDescription
access_tokenToken for API requests
token_typeAlways Bearer
expires_inSeconds until expiration (3600 = 1 hour)
refresh_tokenToken to get new access tokens
scopeGranted scopes (space-separated)

Refresh token grant

Get a new access token using a refresh token.

Request body

ParameterTypeRequiredDescription
grant_typestringYesMust be refresh_token
refresh_tokenstringYesCurrent refresh token
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour client secret
scopestringNoRequest subset of original scopes

Example request

bash
curl -X POST https://api.midday.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "mid_rt_xxxxxxxxxxxxx",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'

Response

Same format as authorization code grant. The refresh token may rotate (new token returned).

Token endpoint errors

json
{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired"
}
ErrorDescription
invalid_requestMissing required parameter
invalid_clientInvalid client credentials
invalid_grantInvalid, expired, or used code/token
unauthorized_clientClient not authorized for grant type
unsupported_grant_typeGrant type not supported

Revocation endpoint

Revoke an access token or refresh token.

POST https://api.midday.ai/v1/oauth/revoke

Request body

ParameterTypeRequiredDescription
tokenstringYesToken to revoke
client_idstringYesYour application's client ID
client_secretstringConfidential clientsYour client secret

Example request

bash
curl -X POST https://api.midday.ai/v1/oauth/revoke \
  -H "Content-Type: application/json" \
  -d '{
    "token": "mid_at_xxxxxxxxxxxxx",
    "client_id": "mid_client_abc123",
    "client_secret": "mid_secret_xyz789"
  }'

Response

Always returns success, even if token was already invalid:

json
{
  "success": true
}

Rate limits

OAuth endpoints have specific rate limits to prevent abuse:

EndpointLimit
/oauth/authorize20 requests per 15 minutes per IP
/oauth/token20 requests per 15 minutes per IP
/oauth/revoke20 requests per 15 minutes per IP

Exceeding limits returns 429 Too Many Requests.


Token lifetimes

Token typeLifetimeNotes
Authorization code10 minutesSingle use
Access token1 hourUse refresh token to renew
Refresh token30 daysRotates on use

PKCE implementation

PKCE adds security for public clients (mobile apps, SPAs).

1. Generate code verifier

Create a random string (43-128 characters, URL-safe):

typescript
function base64UrlEncode(buffer: Uint8Array): string {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

function generateCodeVerifier(): string {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

2. Create code challenge

SHA-256 hash of the verifier, base64-URL encoded:

typescript
async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest("SHA-256", data);
  return base64UrlEncode(new Uint8Array(hash));
}

3. Use in flow

  1. Store code_verifier securely (session storage)
  2. Send code_challenge in authorization request
  3. Send code_verifier in token exchange

Security considerations

State parameter

Always use and validate the state parameter:

typescript
// Generate
const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);

// Validate on callback
const storedState = sessionStorage.getItem("oauth_state");
if (callbackState !== storedState) {
  throw new Error("State mismatch - possible CSRF attack");
}

Redirect URI validation

  • Register all redirect URIs in your app settings
  • Use exact match validation (no wildcards)
  • Always use HTTPS in production

Token storage

  • Store tokens securely (encrypted, server-side preferred)
  • Never expose tokens in URLs or logs
  • Clear tokens on logout

Client secret protection

  • Never include client secrets in client-side code
  • Use environment variables on servers
  • Rotate secrets if compromised

Error handling examples

Handle authorization errors

typescript
app.get("/callback", (req, res) => {
  const { error, error_description, code, state } = req.query;
  
  if (error) {
    console.error(`OAuth error: ${error} - ${error_description}`);
    return res.redirect("/connect?error=" + encodeURIComponent(error as string));
  }
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).send("Invalid state");
  }
  
  // Exchange code for tokens
  // ...
});

Handle token errors

typescript
async function exchangeCode(code: string) {
  const response = await fetch("https://api.midday.ai/v1/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token error: ${error.error} - ${error.error_description}`);
  }
  
  return response.json();
}

Handle refresh failures

typescript
async function refreshTokens(refreshToken: string) {
  try {
    const response = await fetch("https://api.midday.ai/v1/oauth/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "refresh_token",
        refresh_token: refreshToken,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
      }),
    });
    
    const tokens = await response.json();
    
    if (tokens.error) {
      // Refresh token expired or revoked
      // Redirect user to re-authorize
      return null;
    }
    
    return tokens;
  } catch (error) {
    console.error("Refresh failed:", error);
    return null;
  }
}

Using the SDK with OAuth tokens

Once you have an access token, use the Midday SDK for API requests:

typescript
import { Midday } from "@midday-ai/sdk";

const midday = new Midday({
  token: accessToken, // OAuth access token
});

// List transactions
const transactions = await midday.transactions.list({
  pageSize: 50,
});

// Get invoices
const invoices = await midday.invoices.list({
  statuses: ["unpaid", "overdue"],
});

// Get financial metrics
const profit = await midday.metrics.profit({
  from: "2024-01-01",
  to: "2024-12-31",
});

See the SDK documentation for all available methods.