---
title: OAuth API Endpoints
description: Technical reference for Midday OAuth 2.0 endpoints.
section: developer
order: 4
---

Complete technical reference for Midday's OAuth 2.0 implementation. These endpoints follow the [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749) and [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636) specifications.

## Base URLs

| Environment | URL |
|-------------|-----|
| Authorization | `https://app.midday.ai/oauth` |
| Token & API | `https://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

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `response_type` | string | Yes | Must be `code` |
| `client_id` | string | Yes | Your application's client ID |
| `redirect_uri` | string | Yes | URI to redirect after authorization (must be registered) |
| `scope` | string | Yes | Space-separated list of scopes |
| `state` | string | Recommended | Opaque value for CSRF protection |
| `code_challenge` | string | PKCE | Base64-URL-encoded SHA-256 hash of code verifier |
| `code_challenge_method` | string | PKCE | Must 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:

| Parameter | Description |
|-----------|-------------|
| `code` | Authorization code (valid for 10 minutes) |
| `state` | Same value you sent (verify this!) |

```
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=xyz789
```

### Error response

Redirects to your `redirect_uri` with:

| Parameter | Description |
|-----------|-------------|
| `error` | Error code |
| `error_description` | Human-readable description |
| `state` | Same value you sent |

```
https://yourapp.com/callback?error=access_denied&error_description=User%20denied%20access&state=xyz789
```

### Error codes

| Code | Description |
|------|-------------|
| `invalid_request` | Missing or invalid parameter |
| `unauthorized_client` | Client not authorized for this grant type |
| `access_denied` | User denied authorization |
| `invalid_scope` | Invalid or unknown scope |
| `server_error` | Internal 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

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `grant_type` | string | Yes | Must be `authorization_code` |
| `code` | string | Yes | Authorization code from callback |
| `redirect_uri` | string | Yes | Same URI used in authorization |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your client secret |
| `code_verifier` | string | PKCE | Original 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"
}
```

| Field | Description |
|-------|-------------|
| `access_token` | Token for API requests |
| `token_type` | Always `Bearer` |
| `expires_in` | Seconds until expiration (3600 = 1 hour) |
| `refresh_token` | Token to get new access tokens |
| `scope` | Granted scopes (space-separated) |

### Refresh token grant

Get a new access token using a refresh token.

#### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `grant_type` | string | Yes | Must be `refresh_token` |
| `refresh_token` | string | Yes | Current refresh token |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your client secret |
| `scope` | string | No | Request 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"
}
```

| Error | Description |
|-------|-------------|
| `invalid_request` | Missing required parameter |
| `invalid_client` | Invalid client credentials |
| `invalid_grant` | Invalid, expired, or used code/token |
| `unauthorized_client` | Client not authorized for grant type |
| `unsupported_grant_type` | Grant type not supported |

---

## Revocation endpoint

Revoke an access token or refresh token.

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

### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | string | Yes | Token to revoke |
| `client_id` | string | Yes | Your application's client ID |
| `client_secret` | string | Confidential clients | Your 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:

| Endpoint | Limit |
|----------|-------|
| `/oauth/authorize` | 20 requests per 15 minutes per IP |
| `/oauth/token` | 20 requests per 15 minutes per IP |
| `/oauth/revoke` | 20 requests per 15 minutes per IP |

Exceeding limits returns `429 Too Many Requests`.

---

## Token lifetimes

| Token type | Lifetime | Notes |
|------------|----------|-------|
| Authorization code | 10 minutes | Single use |
| Access token | 1 hour | Use refresh token to renew |
| Refresh token | 30 days | Rotates 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](https://github.com/midday-ai/midday-ts) for all available methods.

---

## Related

- [Build an OAuth App](/docs/build-oauth-app) — Getting started guide
- [OAuth Scopes Reference](/docs/oauth-scopes) — Available permissions
- [App Review Process](/docs/app-review-process) — Get your app verified
- [API Reference](/docs/api-reference) — Full API documentation
