# PumpGTM Core X API

> Public integration guide and endpoint reference. Base URL: https://app.pumpgtm.com/api/v1

Human guide: https://app.pumpgtm.com/docs/api
OpenAPI 3.1: https://app.pumpgtm.com/openapi.json
Interactive reference: https://app.pumpgtm.com/docs/api/reference

## One workspace key. REST or MCP.

Use the workspace key from the Connect MCP page as Authorization: Bearer <key>. Store it in your server's secret manager. It is the same credential used for MCP; never put it in browser code, URLs, logs, prompts or vendor requests.

Call GET /api/v1/workspaces without X-Eve-Client first. A workspace key uses its own workspace by default. A Team owner key can select a returned workspace with X-Eve-Client: <workspace UUID>. A selector cannot grant access to another tenant. Use the selected workspace consistently for discovery, checks, imports and webhooks.

All API calls require a key. This guide, the OpenAPI schema and Markdown reference are public and require no session. Keep numeric X user IDs as strings to avoid loss of precision.

## From an existing lead to an outreach option

1. Discover the workspace and connected sender with GET /workspaces and GET /x/accounts. Read GET /x/readiness for saved account, billing and Energy state. This snapshot does not contact X or prove delivery is possible.

2. If your lead has a known X handle, call POST /x/eligibility directly. If you also need the numeric ID for import, call POST /x/users/resolve. Email-only leads require your own verified email-to-X enrichment provider; Pump does not currently offer complete email-to-X matching.

3. Inspect followsAccount: true means the lead follows your selected account; false is a verified negative from a direct handle relationship check; null means unknown. Missing IDs in a public follower snapshot remain unknown. Check checkedAt for freshness. A lead following you is different from your account following them.

4. Offer X through Pump only when your workflow's channel policy allows it. The eligibility call never enrolls or sends. A positive follower result is not a guarantee that the recipient can receive a DM.

5. When you intend outreach, discover an existing Play with GET /x/plays, check its reviewApiEnabled flag, and look up candidates. Import any missing numeric identities, then fetch current Play/sequence revisions and explicitly enroll selected candidate IDs. Enrollment into an active eligible sequence may send on its next engine run.

6. Track lead status and activity, receive signed webhooks, and stop the lead when needed. Reply handling already freezes automated follow-ups. X reply sending and sequence/Play authoring are outside this core release; use the product and X to manage them.

## Import safely, then enroll deliberately

POST /plays/{id}/import stages 1–100 unique numeric X identities as review candidates. requestId is required (8–120 letters, digits, periods, underscores, colons or hyphens; first character alphanumeric). Scope it to one logical import. Retry an identical request after a timeout; changing its payload or revision with the same requestId returns 409. The transaction commits the whole import or nothing.

Use the returned candidate IDs, or POST /plays/{id}/candidates/lookup, to avoid duplicates. Existing candidates keep their review state. Import names are caller-provided labels; import never asserts that a lead follows you. Resolve a public handle first if you do not have the numeric ID.

Read GET /plays/{id}/candidates to get the latest Play revision and selected sequence revision. POST /plays/{id}/enroll requires both plus candidateIds. Repeated completed good-fit reviews return already_reviewed. Existing enrolled, stopped or replied lead records are preserved; they are not moved to another sequence or reactivated.

Only Plays explicitly enabled for the review API support these operations. Check reviewApiEnabled on /x/plays. Imports require manual review, not autopilot. Sequence state and approvals stay unchanged. A paused sequence stays paused; enrollment alone does not guarantee a send.

## Events your agent can act on

Subscribe with POST /webhooks. For X workflows, use reply.received, lead.messaged and x.followed. x.followed represents an inbound follow of your connected account. Successful X DMs emit lead.messaged with channel: x. X replies have no generated draft; encrypted message text can be unavailable.

Save the signing secret from the creation response; it is shown once. Each delivery contains { id, event, createdAt, data }. Validate X-PumpGTM-Signature using HMAC-SHA256 of timestamp + '.' + the exact raw request body. Reject signatures older than five minutes and deduplicate id. X-PumpGTM-Delivery also carries this event ID.

Delivery is best-effort: up to three attempts for network errors, HTTP 429 or 5xx. Other non-2xx responses end delivery; redirects are not followed. There is no durable replay queue or event history endpoint in this release. Poll lead state and activity to reconcile missed events. A test returning 202 means scheduled, not delivered.

Receivers must use HTTPS on port 443 and a public IPv4 DNS address. Private or mixed public/private addresses, URL credentials, fragments, IP literals and redirects are rejected. Return 2xx quickly and do long-running work in your own queue. Registration is not idempotent; list subscriptions before retrying an uncertain creation.

## Handle uncertainty and retries

401: check or rotate the workspace key. 403: check workspace access or X channel availability. 404: the resource is not accessible in this workspace, or the Play has not enabled review APIs. 409: fetch current state, inspect the conflict, then decide whether a new request is appropriate. Do not blindly change the request ID and repeat a mutation.

On 429, respect Retry-After when provided. Retry read-only calls on transient 5xx with exponential backoff and jitter. Follower checks can return HTTP 200 with individual null results when a provider is unavailable: always inspect each result. Identity resolution returns 503 and status unknown when it cannot verify a profile.

Pagination uses nextCursor: pass it as after, then stop when null. Candidate and enrolled-lead pages support at most 200 rows. Eligibility supports at most 100 inputs, but small batches avoid the bounded execution deadline. The API does not offer a bulk asynchronous job in this release.

POST /x/leads/{id}/stop halts future automation across this lead's sequence lanes. It preserves history, does not permanently suppress the person, and cannot recall actions already dispatched. automationStopped on status means the sequence is done or stopped; it does not identify the reason.

## Usage and costs

The key itself is not a separate X credential. Customer costs follow your PumpGTM plan and Energy rules. Read /x/readiness for the current balance and billing state; it is not a quote or reservation for future work.

Known identities supplied through this import API are customer-provided leads, like uploads, and do not count as paid audience discovery. Public identity and relationship reads use Pump's TwitterAPI.io integration and are metered in the provider ledger. This reference does not promise those upstream reads are free or set a new per-request retail price; confirm your commercial allowance with Pump.

Sending still uses the existing X execution engine, its connected account, Energy checks and dispatch limits. Eligibility does not reserve Energy or activate a campaign. No official-X read fallback is used when the public-data provider is unavailable.

## TypeScript quickstart

```typescript
const base = "https://app.pumpgtm.com/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.PUMP_API_KEY}`,
  "Content-Type": "application/json",
  "X-Eve-Client": process.env.PUMP_WORKSPACE_ID!,
};

const response = await fetch(base + "/x/eligibility", {
  method: "POST",
  headers,
  body: JSON.stringify({
    account: "mastra",
    leads: [{ reference: "crm-123", xUsername: "example_handle" }],
  }),
});
if (!response.ok) throw new Error(`Pump HTTP ${response.status}`);
const { results } = await response.json();
const lead = results[0];
if (lead.followsAccount === true) {
  // Offer X through Pump in your channel selector.
  // Enroll separately only when outreach is intended.
} else if (lead.followsAccount === null) {
  // Unknown: retain other channels; retry later if useful.
}
```

## Verify a webhook

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody must be the exact bytes received, BEFORE JSON parsing.
export function verify(rawBody: Buffer, header: string, secret: string) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header);
  if (!match) return false;
  const timestamp = Number(match[1]);
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(match[1] + ".").update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(match[2], "hex"));
}
// Verify X-PumpGTM-Signature, then parse the JSON.
// Persist event.id to deduplicate; enqueue work and return 2xx quickly.
```

## GET /api/v1/workspaces

List accessible workspaces

Use the MCP key without X-Eve-Client. Returns only its workspace and permitted Team workspaces. Select a returned ID on subsequent requests; a header never grants access.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "workspaces": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "enum": [
              "owner",
              "admin",
              "member"
            ]
          }
        },
        "required": [
          "id"
        ]
      }
    }
  },
  "required": [
    "workspaces"
  ]
}
```

## GET /api/v1/x/accounts

List connected X identities

Read saved account identities. No provider health check or OAuth refresh.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "accounts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          },
          "handle": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "handle"
        ]
      }
    }
  },
  "required": [
    "accounts"
  ]
}
```

## GET /api/v1/x/readiness

Read account, billing and Energy state

Read-only snapshot. Check each section's state/issues; data may be null. Provider health is unverified. A connection or positive follower result does not guarantee delivery.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "workspaceId": {
      "type": "string",
      "format": "uuid"
    },
    "asOf": {
      "type": "string"
    },
    "accounts": {
      "type": "object",
      "properties": {
        "state": {
          "enum": [
            "complete",
            "partial",
            "unavailable"
          ]
        },
        "observedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "data": {
          "anyOf": [
            {
              "type": "object",
              "additionalProperties": true
            },
            {
              "type": "null"
            }
          ]
        },
        "issues": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "retryable": {
                "type": "boolean"
              }
            },
            "required": [
              "code",
              "message",
              "retryable"
            ]
          }
        }
      },
      "required": [
        "state",
        "observedAt",
        "data",
        "issues"
      ]
    },
    "billing": {
      "type": "object",
      "properties": {
        "state": {
          "enum": [
            "complete",
            "partial",
            "unavailable"
          ]
        },
        "observedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "data": {
          "anyOf": [
            {
              "type": "object",
              "additionalProperties": true
            },
            {
              "type": "null"
            }
          ]
        },
        "issues": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "retryable": {
                "type": "boolean"
              }
            },
            "required": [
              "code",
              "message",
              "retryable"
            ]
          }
        }
      },
      "required": [
        "state",
        "observedAt",
        "data",
        "issues"
      ]
    },
    "energy": {
      "type": "object",
      "properties": {
        "state": {
          "enum": [
            "complete",
            "partial",
            "unavailable"
          ]
        },
        "observedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "data": {
          "anyOf": [
            {
              "type": "object",
              "additionalProperties": true
            },
            {
              "type": "null"
            }
          ]
        },
        "issues": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "retryable": {
                "type": "boolean"
              }
            },
            "required": [
              "code",
              "message",
              "retryable"
            ]
          }
        }
      },
      "required": [
        "state",
        "observedAt",
        "data",
        "issues"
      ]
    },
    "providerHealthChecked": {
      "const": false
    },
    "deliveryGuaranteed": {
      "const": false
    },
    "note": {
      "type": "string"
    }
  },
  "required": [
    "workspaceId",
    "asOf",
    "accounts",
    "billing",
    "energy",
    "providerHealthChecked",
    "deliveryGuaranteed",
    "note"
  ]
}
```

## POST /api/v1/x/users/resolve

Resolve a handle to a numeric X ID

One exact public handle lookup via TwitterAPI.io. No email-to-X enrichment or fuzzy identity matching. An unverified or unavailable profile returns HTTP 503 with status unknown and user null. No official X fallback.

Request example:
```json
{
  "username": "example_handle"
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "pattern": "^@?[A-Za-z0-9_]{1,15}$",
      "description": "Public X handle, with optional @. Case-insensitive."
    }
  },
  "required": [
    "username"
  ],
  "additionalProperties": false
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "status": {
      "const": "resolved"
    },
    "user": {
      "type": "object",
      "properties": {
        "xUserId": {
          "type": "string",
          "pattern": "^[1-9][0-9]{0,19}$",
          "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
        },
        "username": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "checkedAt": {
          "type": "string"
        },
        "evidence": {
          "const": "public_profile"
        }
      },
      "required": [
        "xUserId",
        "username",
        "name",
        "checkedAt",
        "evidence"
      ]
    },
    "provider": {
      "const": "twitterapi"
    },
    "sendsMessages": {
      "const": false
    }
  },
  "required": [
    "status",
    "user",
    "provider",
    "sendsMessages"
  ]
}
```

## POST /api/v1/x/eligibility

Check whether leads follow your account

Direction: the lead follows the connected account. Supply exactly one X identity per lead. Handles use direct relationship evidence. Numeric IDs use a bounded public follower snapshot: presence is true; absence is null, never a verified false. null means unknown, not eligible or ineligible. Provider errors can produce HTTP 200 with unknown individual results. Up to 100 leads; sequential work is time bounded, so small batches are preferable. Per-instance short cache may be reused; checkedAt is evidence time. No messages are sent.

Request example:
```json
{
  "account": "mastra",
  "leads": [
    {
      "reference": "crm-123",
      "xUsername": "example_handle"
    }
  ]
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "account": {
      "type": "string",
      "pattern": "^@?[A-Za-z0-9_]{1,15}$",
      "description": "Public X handle, with optional @. Case-insensitive."
    },
    "leads": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "maxLength": 200
          },
          "xUsername": {
            "type": "string",
            "pattern": "^@?[A-Za-z0-9_]{1,15}$",
            "description": "Public X handle, with optional @. Case-insensitive."
          },
          "xUserId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          }
        },
        "required": [],
        "additionalProperties": false,
        "oneOf": [
          {
            "required": [
              "xUsername"
            ]
          },
          {
            "required": [
              "xUserId"
            ]
          }
        ]
      },
      "minItems": 1,
      "maxItems": 100
    }
  },
  "required": [
    "account",
    "leads"
  ],
  "additionalProperties": false
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "account": {
      "type": "string"
    },
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string"
          },
          "xUsername": {
            "type": "string"
          },
          "xUserId": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^[1-9][0-9]{0,19}$",
                "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
              },
              {
                "type": "null"
              }
            ]
          },
          "followsAccount": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ]
          },
          "checkedAt": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "evidence": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "xUserId",
          "followsAccount",
          "checkedAt",
          "reason",
          "evidence"
        ]
      }
    },
    "provider": {
      "const": "twitterapi"
    },
    "sendsMessages": {
      "const": false
    }
  },
  "required": [
    "account",
    "results",
    "provider",
    "sendsMessages"
  ]
}
```

## GET /api/v1/x/plays

Discover X Plays and their revisions

Use reviewApiEnabled to identify Plays supporting candidate review, import, lookup and enrollment. Plays must be explicitly enabled for review APIs. Autopilot Plays cannot use manual import/enrollment.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "plays": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "enum": [
              "active",
              "paused",
              "archived"
            ]
          },
          "revision": {
            "type": "integer",
            "minimum": 1,
            "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
          },
          "sequenceId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "accountUserId": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^[1-9][0-9]{0,19}$",
                "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
              },
              {
                "type": "null"
              }
            ]
          },
          "reviewApiEnabled": {
            "type": "boolean"
          },
          "autopilot": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "revision",
          "sequenceId",
          "accountUserId",
          "reviewApiEnabled",
          "autopilot"
        ]
      }
    }
  },
  "required": [
    "plays"
  ]
}
```

## GET /api/v1/plays/{id}/candidates

Page through review candidates

Saved review candidates, not evidence of a current follow relationship. Returns Play and sequence revisions for guarded enrollment. Cursor paging covers all batches.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "counts": {
      "type": "object",
      "properties": {
        "total": {
          "type": "integer"
        },
        "unreviewed": {
          "type": "integer"
        },
        "goodFit": {
          "type": "integer"
        },
        "notAGoodFit": {
          "type": "integer"
        }
      },
      "required": [
        "total",
        "unreviewed",
        "goodFit",
        "notAGoodFit"
      ]
    },
    "candidates": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "xUserId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          },
          "username": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "enum": [
              "proposed",
              "approved",
              "imported",
              "rejected"
            ]
          },
          "review": {
            "anyOf": [
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "xUserId",
          "username",
          "name",
          "status",
          "review"
        ]
      }
    },
    "nextCursor": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ]
    },
    "play": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "name": {
          "type": "string"
        },
        "revision": {
          "type": "integer",
          "minimum": 1,
          "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
        },
        "status": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "name",
        "revision",
        "status"
      ]
    },
    "sequence": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "format": "uuid"
            },
            "name": {
              "type": "string"
            },
            "revision": {
              "type": "integer",
              "minimum": 1,
              "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
            },
            "status": {
              "type": "string"
            },
            "approved": {
              "type": "boolean"
            }
          },
          "required": [
            "id",
            "name",
            "revision",
            "status",
            "approved"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "reviewUrl": {
      "type": "string"
    }
  },
  "required": [
    "counts",
    "candidates",
    "nextCursor",
    "play",
    "sequence",
    "reviewUrl"
  ]
}
```

## POST /api/v1/plays/{id}/candidates/lookup

Find existing candidates by X identity

Exact identity lookup across all saved Play batches. Each identity requires xUserId OR username. Ambiguous matches return 409; use numeric IDs or the candidate list. This endpoint does not check a live follower relationship.

Request example:
```json
{
  "identities": [
    {
      "xUserId": "123456789"
    }
  ]
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "identities": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "xUserId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          },
          "username": {
            "type": "string",
            "pattern": "^@?[A-Za-z0-9_]{1,15}$",
            "description": "Public X handle, with optional @. Case-insensitive."
          }
        },
        "required": [],
        "additionalProperties": false,
        "oneOf": [
          {
            "required": [
              "xUserId"
            ]
          },
          {
            "required": [
              "username"
            ]
          }
        ]
      },
      "minItems": 1,
      "maxItems": 200
    }
  },
  "required": [
    "identities"
  ],
  "additionalProperties": false
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "playId": {
      "type": "string",
      "format": "uuid"
    },
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "exists": {
            "type": "boolean"
          },
          "candidate": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "xUserId": {
                    "type": "string",
                    "pattern": "^[1-9][0-9]{0,19}$",
                    "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
                  },
                  "username": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "name": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "status": {
                    "enum": [
                      "proposed",
                      "approved",
                      "imported",
                      "rejected"
                    ]
                  },
                  "review": {
                    "anyOf": [
                      {
                        "type": "object"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "id",
                  "xUserId",
                  "username",
                  "name",
                  "status",
                  "review"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "enrolledInPlay": {
            "type": "boolean"
          },
          "currentFollowerStatus": {
            "const": "unknown"
          },
          "existingLeads": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "sequenceId": {
                  "anyOf": [
                    {
                      "type": "string",
                      "format": "uuid"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "playId": {
                  "anyOf": [
                    {
                      "type": "string",
                      "format": "uuid"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "stage": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "sequenceId",
                "playId",
                "stage"
              ]
            }
          }
        }
      }
    }
  },
  "required": [
    "playId",
    "results"
  ]
}
```

## POST /api/v1/plays/{id}/import

Import external leads for review

Requires an enabled, non-archived, manual-review Play. Resolve known handles first, then submit numeric IDs. Names are caller-provided display labels, not verified identity evidence. The operation is atomic: 1–100 unique IDs. requestId is scoped to workspace + Play; retry the exact same normalized payload for the stored result. Reusing it with changed inputs returns 409. Existing candidates retain their review state. Import does not verify followers, enroll, change sequence status or send. Duplicate candidate records produce 409.

Request example:
```json
{
  "requestId": "crm-import-20261003-001",
  "expectedRevision": 1,
  "leads": [
    {
      "xUserId": "123456789",
      "reference": "crm-123"
    }
  ]
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "requestId": {
      "type": "string",
      "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{7,119}$"
    },
    "expectedRevision": {
      "type": "integer",
      "minimum": 1,
      "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
    },
    "leads": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "xUserId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "reference": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "xUserId"
        ],
        "additionalProperties": false
      },
      "minItems": 1,
      "maxItems": 100
    }
  },
  "required": [
    "requestId",
    "expectedRevision",
    "leads"
  ],
  "additionalProperties": false
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "playId": {
      "type": "string",
      "format": "uuid"
    },
    "batchId": {
      "type": "string",
      "format": "uuid"
    },
    "requestId": {
      "type": "string"
    },
    "replayed": {
      "type": "boolean"
    },
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "xUserId": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,19}$",
            "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
          },
          "reference": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "candidateId": {
            "type": "string",
            "format": "uuid"
          },
          "result": {
            "enum": [
              "imported",
              "existing_candidate"
            ]
          }
        },
        "required": [
          "xUserId",
          "reference",
          "candidateId",
          "result"
        ]
      }
    },
    "enrolled": {
      "const": false
    },
    "sendsMessages": {
      "const": false
    }
  },
  "required": [
    "playId",
    "batchId",
    "requestId",
    "replayed",
    "results",
    "enrolled",
    "sendsMessages"
  ]
}
```

## POST /api/v1/plays/{id}/enroll

Approve candidates and enroll through the existing engine

Mutating action: marks selected candidates good fit and queues newly enrolled leads. Requires exact Play and sequence revisions and the Play's own connected sender/sequence. An active eligible sequence can send on its next engine run. Does not activate, approve or change the sequence itself. Existing leads (including stopped/replied leads) keep their original state and sequence. Repeating a completed good-fit review returns already_reviewed; conflicting review decisions return 409. POST /plays/{id}/leads is an alias.

Request example:
```json
{
  "expectedRevision": 1,
  "sequenceId": "00000000-0000-4000-8000-000000000001",
  "sequenceRevision": 1,
  "candidateIds": [
    "00000000-0000-4000-8000-000000000002"
  ]
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "expectedRevision": {
      "type": "integer",
      "minimum": 1,
      "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
    },
    "sequenceId": {
      "type": "string",
      "format": "uuid"
    },
    "sequenceRevision": {
      "type": "integer",
      "minimum": 1,
      "description": "Current saved revision. Fetch again after a 409; do not silently retry a changed plan."
    },
    "candidateIds": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "uuid"
      },
      "minItems": 1,
      "maxItems": 200,
      "uniqueItems": true
    }
  },
  "required": [
    "expectedRevision",
    "sequenceId",
    "sequenceRevision",
    "candidateIds"
  ],
  "additionalProperties": false
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "candidateId": {
            "type": "string",
            "format": "uuid"
          },
          "result": {
            "enum": [
              "enrolled",
              "existing_lead",
              "already_reviewed",
              "rejected"
            ]
          },
          "leadId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "candidateId",
          "result",
          "leadId"
        ]
      }
    },
    "sequenceStatus": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "sequenceStatusChanged": {
      "const": false
    },
    "reviewUrl": {
      "type": "string"
    }
  },
  "required": [
    "results",
    "sequenceStatus",
    "sequenceStatusChanged",
    "reviewUrl"
  ]
}
```

## GET /api/v1/plays/{id}/leads

List enrolled leads

Enrolled records for the selected Play, with stable cursor pagination. Use each lead ID to read detailed state or stop its automation.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "playId": {
      "type": "string",
      "format": "uuid"
    },
    "leads": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "xUserId": {
            "anyOf": [
              {
                "type": "string",
                "pattern": "^[1-9][0-9]{0,19}$",
                "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
              },
              {
                "type": "null"
              }
            ]
          },
          "username": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "sequenceId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "stage": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "xUserId",
          "username",
          "sequenceId",
          "stage",
          "createdAt"
        ]
      }
    },
    "nextCursor": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid"
        },
        {
          "type": "null"
        }
      ]
    },
    "hasMore": {
      "type": "boolean"
    }
  },
  "required": [
    "playId",
    "leads",
    "nextCursor",
    "hasMore"
  ]
}
```

## GET /api/v1/x/leads/{id}

Read one lead's saved progress

Tenant-owned X lead only. Stage/milestones are saved observations, not a live provider probe. automationStopped means the sequence is done/stopped; it does not distinguish normal completion from a manual stop.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "lead": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "xUserId": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^[1-9][0-9]{0,19}$",
              "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
            },
            {
              "type": "null"
            }
          ]
        },
        "username": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "accountUserId": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^[1-9][0-9]{0,19}$",
              "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
            },
            {
              "type": "null"
            }
          ]
        },
        "playId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ]
        },
        "sequenceId": {
          "anyOf": [
            {
              "type": "string",
              "format": "uuid"
            },
            {
              "type": "null"
            }
          ]
        },
        "stage": {
          "type": "string"
        },
        "automationStopped": {
          "type": "boolean"
        },
        "currentStep": {
          "type": "integer"
        },
        "nextActionAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "followedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "followedBackAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "messagedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "repliedAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "optedOutAt": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "deliveryGuaranteed": {
          "const": false
        }
      },
      "required": [
        "id",
        "xUserId",
        "username",
        "accountUserId",
        "playId",
        "sequenceId",
        "stage",
        "automationStopped",
        "currentStep",
        "nextActionAt",
        "followedAt",
        "followedBackAt",
        "messagedAt",
        "repliedAt",
        "optedOutAt",
        "deliveryGuaranteed"
      ]
    }
  },
  "required": [
    "lead"
  ]
}
```

## POST /api/v1/x/leads/{id}/stop

Stop this lead's future automation

Idempotent stop using the existing removal controls. Stops all sequence lanes for this lead, preserving contacted history; uncontacted queued leads become skipped. Available even after X is disabled. Does not recall dispatched actions, permanently suppress the identity, or delete history.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "ok": {
      "const": true
    },
    "lead": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "format": "uuid"
            },
            "xUserId": {
              "anyOf": [
                {
                  "type": "string",
                  "pattern": "^[1-9][0-9]{0,19}$",
                  "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
                },
                {
                  "type": "null"
                }
              ]
            },
            "username": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "accountUserId": {
              "anyOf": [
                {
                  "type": "string",
                  "pattern": "^[1-9][0-9]{0,19}$",
                  "description": "Numeric X user ID as a string. Never convert it to a JavaScript number."
                },
                {
                  "type": "null"
                }
              ]
            },
            "playId": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "null"
                }
              ]
            },
            "sequenceId": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "null"
                }
              ]
            },
            "stage": {
              "type": "string"
            },
            "automationStopped": {
              "type": "boolean"
            },
            "currentStep": {
              "type": "integer"
            },
            "nextActionAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "followedAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "followedBackAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "messagedAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "repliedAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "optedOutAt": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ]
            },
            "deliveryGuaranteed": {
              "const": false
            }
          },
          "required": [
            "id",
            "xUserId",
            "username",
            "accountUserId",
            "playId",
            "sequenceId",
            "stage",
            "automationStopped",
            "currentStep",
            "nextActionAt",
            "followedAt",
            "followedBackAt",
            "messagedAt",
            "repliedAt",
            "optedOutAt",
            "deliveryGuaranteed"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "scope": {
      "const": "lead_automation"
    },
    "permanentSuppression": {
      "const": false
    },
    "note": {
      "type": "string"
    }
  },
  "required": [
    "ok",
    "lead",
    "scope",
    "permanentSuppression",
    "note"
  ]
}
```

## GET /api/v1/sequences

List existing sequences

Read tenant-owned sequences, ordered steps and counts. Create/edit sequences in the product for this release; X reply sending and sequence/Play authoring APIs are outside the core release.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "sequences": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": true
      }
    }
  },
  "required": [
    "sequences"
  ]
}
```

## PATCH /api/v1/sequences/{id}/status

Pause or resume an existing sequence

Affects every lead in this sequence. active may enable sending, subject to existing approval, billing, budget and provider controls. paused halts future sequence work; already dispatched actions cannot be recalled. Does not edit or approve content.

Request example:
```json
{
  "status": "paused"
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "status": {
      "enum": [
        "active",
        "paused",
        "draft"
      ]
    }
  },
  "required": [
    "status"
  ]
}
```

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "ok": {
      "type": "boolean"
    }
  },
  "required": [
    "ok"
  ]
}
```

## GET /api/v1/replies

Read pending replies

Includes X and LinkedIn replies. X rows have channel x, no draft and no allowedDecisions; answer in X. Encrypted text can be unavailable. This is a bounded pending queue, not a complete conversation archive.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "replies": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": true
      }
    }
  },
  "required": [
    "replies"
  ]
}
```

## GET /api/v1/activity

Read the activity ledger

Use action=x_dm and outcome=ok to inspect confirmed X sends. Other actions may be skipped/failed. Bounded recent ledger; polling supplements best-effort webhook delivery.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "additionalProperties": true
}
```

## GET /api/v1/webhooks

List enabled subscriptions

Lists workspace webhook URLs and events; never returns signing secrets.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "webhooks": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "url",
          "events",
          "createdAt"
        ]
      }
    },
    "events": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": [
    "webhooks",
    "events"
  ]
}
```

## POST /api/v1/webhooks

Subscribe to signed events

Maximum 10 enabled hooks. Save the returned secret securely; it is shown only at creation. HTTPS port 443 and publicly resolving IPv4 DNS required. Private/mixed IPs, URL credentials, fragments and redirects are rejected. Registration is not idempotent: list before retrying after a timeout. Delivery is best-effort with up to three attempts, not a durable replay queue. Verify signatures on the exact raw body and deduplicate event IDs. x.followed means an inbound follow of your connected account. lead.messaged includes successful x_dm with channel x; reply.received can include unreadable encrypted content.

Request example:
```json
{
  "url": "https://hooks.example.com/pump",
  "events": [
    "reply.received",
    "lead.messaged",
    "x.followed"
  ]
}
```

Request schema:
```json
{
  "type": "object",
  "properties": {
    "url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2000
    },
    "events": {
      "type": "array",
      "items": {
        "enum": [
          "reply.received",
          "lead.invited",
          "lead.accepted",
          "lead.messaged",
          "lead.emailed",
          "lead.meeting_booked",
          "x.followed",
          "ping"
        ]
      },
      "minItems": 1,
      "maxItems": 8
    }
  },
  "required": [
    "url",
    "events"
  ]
}
```

Response schema (HTTP 201):
```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "url": {
      "type": "string"
    },
    "events": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "secret": {
      "type": "string"
    }
  },
  "required": [
    "id",
    "url",
    "events",
    "secret"
  ]
}
```

## POST /api/v1/webhooks/{id}/test

Schedule a signed ping

202 means the ping was scheduled, not that the receiver accepted it. Verify the event at your receiver.

Response schema (HTTP 202):
```json
{
  "type": "object",
  "properties": {
    "ok": {
      "type": "boolean"
    },
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "event": {
      "const": "ping"
    }
  },
  "required": [
    "ok",
    "id",
    "event"
  ]
}
```

## DELETE /api/v1/webhooks/{id}

Disable a subscription

Only an enabled hook belonging to the selected workspace can be disabled. A subsequent delete returns 404. Already queued delivery may still complete.

Response schema (HTTP 200):
```json
{
  "type": "object",
  "properties": {
    "ok": {
      "type": "boolean"
    },
    "id": {
      "type": "string",
      "format": "uuid"
    }
  },
  "required": [
    "ok",
    "id"
  ]
}
```