Midday / apps /website /src /app /docs /content /oauth-api-endpoints.mdx
Jules
Final deployment with all fixes and verified content
c09f67c
---
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