# Marketo REST API Quirks (Airtable Context)

This reference covers Marketo-specific behaviors that affect Airtable automation scripts. These are empirically confirmed behaviors, not theoretical possibilities.

---

## Authentication

**Endpoint:** `GET /identity/oauth/token?grant_type=client_credentials&client_id={id}&client_secret={secret}`

**Critical:** Marketo returns HTTP `200 OK` even on auth failures. The error is in the JSON response body:

```javascript
// Success: { access_token: "...", token_type: "bearer", expires_in: 3600, scope: "..." }
// Failure: { error: "invalid_client", error_description: "Bad client credentials" }
```

Always check `result.error` or `result.access_token`, not the HTTP status.

Token lifetime is typically 3600 seconds (1 hour). For long-running pipelines, refresh proactively at ~80% of the lifetime.

---

## Error Codes

When `success: false` in the API response, the error code determines the retry strategy:

| Code | Meaning | Action |
|---|---|---|
| 601 | Access token invalid | Refresh token, retry once |
| 602 | Access token expired | Refresh token, retry once |
| 605 | Request method not supported | Do not retry. Check if endpoint requires POST vs GET |
| 606 | Max rate limit exceeded | Wait with jitter, retry. Shared across all API users on the instance |
| 607 | Daily quota exhausted | Stop all scripts. Resume next day |
| 611 | System error | Retry with backoff |
| 615 | Concurrent access limit | Wait with jitter, retry. Max 10 concurrent API calls per instance |

**Rate limits are per Marketo instance, not per token or per automation.** If 20 automations run simultaneously, they all share the same 100 calls per 20 seconds burst limit and 10 concurrent call limit.

---

## Timestamp Handling

### Reading from Marketo

Marketo returns timestamps in a non-standard format:
```
"2023-09-08T18:41:00Z+0000"
```

The `Z+0000` suffix is redundant and breaks `new Date()`. Always normalize:

```javascript
function cleanMarketoDate(dateStr) {
  if (!dateStr) return null;
  return String(dateStr).replace(/Z\+0000$/, "Z");
}
```

### Sending to Marketo

Datetime parameters (paging tokens, filters) require the format:
```
YYYY-MM-DDTHH:MM:SS+0000
```

Not `...Z` (Marketo doesn't accept the Z shorthand in parameters).

---

## Activity API: Global Cursor Behavior

The Get Lead Activities endpoint (`/rest/v1/activities.json`) uses a global activity stream cursor.

**How it works:**
1. Request a paging token for a datetime window: `GET /rest/v1/activities/pagingtoken.json?sinceDatetime=...`
2. Fetch activities with that token + a `leadIds` filter
3. Marketo returns up to 300 activities per page from the global stream, filtered to your lead(s)
4. `moreResult: true` means there are more positions in the global stream, not necessarily more activities for your lead

**The bug everyone hits:** If your lead has 0 activities on page 1 but `moreResult` is true, the activities may appear on page 5, 10, or 20. The cursor is walking through everyone's activities, not just yours. Stopping on the first empty page is incorrect and will cause missing data.

**Practical impact:** On a busy Marketo instance, a single day can have thousands of pages in the global stream. Your lead's activities are scattered throughout. You may need 15+ pages to find them all.

---

## Asset API: Token Operations

### Reading tokens
```
GET /rest/asset/v1/folder/{folderId}/tokens.json?folderType=Program
```

Response nests at `result[0].tokens`, not directly in `result`:
```javascript
var tokens = data.result[0].tokens;  // array of { name, type, value, ... }
```

### Writing tokens
```
POST /rest/asset/v1/folder/{folderId}/tokens.json
Content-Type: application/x-www-form-urlencoded

name=TokenName&value=NewValue&type=text&folderType=Program
```

**Must use `application/x-www-form-urlencoded`**, not JSON. The `type` parameter is required and must match the token's existing type. Always read the current type first with a GET call.

---

## Asset API: Pagination

Asset endpoints (forms, emails, templates, programs, smart lists, smart campaigns) use offset-based pagination:

```
GET /rest/asset/v1/forms.json?maxReturn=200&offset=0
```

- `maxReturn` max is 200
- Walk offset by the count of results returned
- Stop when results array is empty or shorter than `maxReturn`

---

## Bulk Export API

Used for large-scale data extraction (activity history, leads, program members).

**Daily quota:** Default is 500 MB/day, shared across all export types. Can be increased by request.

**Key behaviors:**
- `fileSize` only appears in the status response after processing completes
- Processing is what consumes the quota, not the download
- There is no preview-before-consumption mechanism
- `numberOfRows` returns 0 even for completed jobs; row count only comes from parsing the file
- Activity type 13 (Change Data Value) typically dominates at ~80% of volume
- A 6-hour export window can produce gigabytes of data on an active instance

Check remaining quota: Admin > Web Services > Bulk Export API Information.

---

## Smart Lists

The REST API returns only metadata for smart lists (id, name, folder, dates), not filter rules. Filter rules are only visible in the Marketo UI. The endpoint `GET /rest/asset/v1/smartList/{id}/rules.json` returns error 605 ("Request method not supported") because rules are not exposed via API.

To capture rules, a human-in-the-loop workflow is required: screenshot the rules in Marketo UI, upload to Airtable, and parse with AI.

---

## Smart Campaigns

Smart Campaigns contain an embedded smart list (the trigger/filter criteria). The `smartListId` field on a campaign record maps to a Smart List record. This relationship is an Airtable-side join, not an API call.

Key fields from `GET /rest/asset/v1/smartCampaigns.json`:
- `id`, `name`, `type` (batch, trigger, default)
- `status` (active, inactive, etc.)
- `isActive` (boolean)
- `smartListId` (the embedded smart list's ID)
- `flowId`, `parentProgramId`
