# LeadArc Inbox API v1 A read-and-act HTTP API over the LeadArc Master Inbox (cold email replies). This file is the entire API surface in one fetch. Docs: https://docs.leadarc.io Every docs page is also available as markdown at https://docs.leadarc.io/.md Generated from the API source, so it matches the running server. BASE URL: https://api.leadarc.io/v1 AUTH: Authorization: Bearer lak_live_<8-char-handle>_<32-char-secret> FORMAT: JSON in, JSON out. Content-Type: application/json on any request with a body. CORS: none. Server to server only. ## Conventions - Pagination: cursor only. ?limit=N (default 50, max 200) & ?cursor=. List envelope: { "object":"list", "data":[...], "has_more":bool, "next_cursor":string|null }. No offsets, no totals. Page until has_more is false. - Sorting: ?order=asc|desc flips the endpoint's one fixed sort key. There is no sort_by. - Unknown query parameters and unknown body fields are 400 errors, never ignored. - Ids are opaque encrypted strings (lead_, rep_, msg_, tsk_, mtg_, note_, tpl_, evt_, seq_, ast_, cmp_, usr_). Never parse, sort or construct one. A workspace id is its slug (e.g. "airpay"). - Timestamps: ISO 8601 UTC with Z. Calendar days: YYYY-MM-DD. Wall-clock times: "HH:MM". - Rates are ratios in [0,1], never percentages. - Read validators must tolerate unknown enum values; status columns are not database enums. - Every write response includes an "effects" object naming everything the call changed. ## Rate limits (per key) - read: 120/minute - write: 60/minute - upstream: 10/minute - concurrency: 4 requests in flight - Headers on every response: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset. - 429 carries retry-after in whole seconds. ## Start here GET /v1/me -> which workspaces and scopes this key can use RIGHT NOW (no scope needed) GET /v1/capabilities -> live enums, limits, filters, error catalog (no scope needed) ## Dangerous writes (read before acting autonomously) - POST /v1/workspaces/{ws}/leads/{lead}/status with not_interested or unsubscribed soft-completes EVERY open task on that lead, across all of its replies. So do /booked and /disqualified. - POST /v1/workspaces/{ws}/leads/{lead}/responded is TEAM WIDE: it clears the lead from every rep's daily worklist, not just yours. - POST .../notes and POST .../tasks are NOT idempotent: a retry creates a second row. Every other v1 write is idempotent. - unread, has_open_task and next_due_at are resolved for the KEY OWNER, not the team. ## Scopes (17) - workspaces:read: List and read the workspaces this key can reach - members:read: List the people who can work a workspace - leads:read: List and read leads, their replies, and who they are assigned to - leads:write: Set a lead's tag, mark it read, unread, responded, booked or disqualified, and set assignees - threads:read: Read the full text of every message in a conversation - notes:read: Read notes on a lead - notes:write: Add a note to a lead - tasks:read: Read tasks - tasks:write: Create, edit, complete and delete tasks - meetings:read: Read scheduled meetings - meetings:write: Set a meeting's outcome, and read its internal notes - event_types:read: Read event types and their free slots - templates:read: Read reply templates and render one against a lead - assets:read: Read a workspace's assets - sequences:read: Read follow-up sequences and their steps - campaigns:read: Read the campaign roster and its tags - insights:read: Read funnel, campaign and daily numbers ## Errors Shape: { "error": { "code": string, "message": string, "param"?: string, "required_scope"?: string, "retry_after"?: number } } Branch on code, never on message. - 400 invalid_request - 400 invalid_cursor - 400 invalid_id - 400 unsupported_filter - 400 unsupported_sort - 401 missing_authorization - 401 malformed_key - 401 invalid_key - 401 key_revoked - 401 key_expired - 401 owner_deactivated - 403 insufficient_scope - 403 workspace_forbidden - 403 test_keys_unsupported - 404 not_found - 409 conflict - 422 unprocessable - 429 rate_limited - 429 too_many_concurrent - 500 internal_error - 502 upstream_error ## Endpoints (41) ### GET /v1/capabilities purpose: Every enum value, limit, and filter this version accepts scope: none | rate bucket: read ### GET /v1/me purpose: Who this key is, what it may do, which workspaces it currently reaches scope: none | rate bucket: read ### GET /v1/workspaces purpose: List the workspaces this key reaches scope: workspaces:read | rate bucket: read returns: list of workspace ### GET /v1/workspaces/{ws} purpose: One workspace scope: workspaces:read | rate bucket: read returns: workspace query: include errors: invalid_request, unsupported_filter ### GET /v1/workspaces/{ws}/assets purpose: Looms, docs, links, snippets scope: assets:read | rate bucket: read returns: list of asset query: cursor, kind, limit, order errors: invalid_cursor ### GET /v1/workspaces/{ws}/campaign_tags purpose: Campaign tags, the match key for rules and notes scope: campaigns:read | rate bucket: upstream returns: list of campaign_tag query: cursor, limit errors: invalid_request, upstream_error ### GET /v1/workspaces/{ws}/campaigns purpose: The EmailBison campaign roster scope: campaigns:read | rate bucket: upstream returns: list of campaign query: cursor, limit errors: invalid_request, upstream_error note: proxied live from EmailBison; can be stale or fail with 502 ### GET /v1/workspaces/{ws}/event_types purpose: Bookable event types scope: event_types:read | rate bucket: read returns: list of event_type query: limit, cursor, order, active, public (starting_after is accepted as an alias for cursor) errors: invalid_cursor, invalid_request, unsupported_filter ### GET /v1/workspaces/{ws}/event_types/{id} purpose: One event type with hosts and form fields scope: event_types:read | rate bucket: read returns: event_type errors: invalid_request, not_found ### GET /v1/workspaces/{ws}/event_types/{id}/slots purpose: Free slots in a window scope: event_types:read | rate bucket: upstream returns: availability query: from, to, timezone errors: invalid_request, not_found, unsupported_filter, upstream_error ### GET /v1/workspaces/{ws}/insights/campaigns purpose: Per-campaign and per-step metrics scope: insights:read | rate bucket: upstream returns: list of campaign_insight query: campaign_id, cursor, from, limit, to errors: invalid_request, not_found, upstream_error ### GET /v1/workspaces/{ws}/insights/daily purpose: Dense daily series, max 120 days scope: insights:read | rate bucket: upstream returns: list of daily_point query: cursor, from, limit, metric, to errors: invalid_request, unsupported_filter, upstream_error ### GET /v1/workspaces/{ws}/insights/summary purpose: Funnel, rates, speed to lead, per-rep scope: insights:read | rate bucket: read returns: insights_summary query: from, member_id, to errors: invalid_request ### GET /v1/workspaces/{ws}/leads purpose: Filtered, paginated lead feed, one row per lead scope: leads:read | rate bucket: read returns: list of lead query: limit, cursor, order, expand, status, campaign_id, assignee_id, email, has_open_task, task_due_before, meeting_outcome, direction, updated_since, updated_before, unread, responded errors: invalid_cursor, invalid_request, not_found, unsupported_filter ### GET /v1/workspaces/{ws}/leads/{lead} purpose: One lead with rollups scope: leads:read | rate bucket: read returns: lead query: expand, include errors: invalid_request, not_found, unsupported_filter ### GET /v1/workspaces/{ws}/leads/{lead}/assignees purpose: Who owns this lead scope: leads:read | rate bucket: read returns: list of member errors: invalid_request, not_found, unprocessable, unsupported_filter ### PUT /v1/workspaces/{ws}/leads/{lead}/assignees purpose: Replace the assignee set scope: leads:write | rate bucket: write returns: list of member body: member_ids errors: invalid_request, not_found, unprocessable, unsupported_filter ### POST /v1/workspaces/{ws}/leads/{lead}/booked purpose: Mark booked, local only scope: leads:write | rate bucket: write returns: lead body: create_meeting note: soft-completes the lead's open tasks; local only, EmailBison is untouched ### POST /v1/workspaces/{ws}/leads/{lead}/disqualified purpose: Mark disqualified, local only scope: leads:write | rate bucket: write returns: lead body: reengage_approved note: soft-completes the lead's open tasks; local only, EmailBison is untouched ### GET /v1/workspaces/{ws}/leads/{lead}/messages purpose: Full merged conversation, inbound and outbound scope: threads:read | rate bucket: read returns: list of message query: limit, order errors: not_found note: most expensive read in the API; single lead only, never expandable on a list ### POST /v1/workspaces/{ws}/leads/{lead}/read purpose: Mark read for the key's owner scope: leads:write | rate bucket: write returns: lead ### GET /v1/workspaces/{ws}/leads/{lead}/replies purpose: Every indexed inbound reply from this lead scope: leads:read | rate bucket: read returns: list of reply query: limit, cursor, order errors: invalid_cursor, not_found, unsupported_filter ### POST /v1/workspaces/{ws}/leads/{lead}/responded purpose: Team-wide "handled", drops it from every rep's Daily scope: leads:write | rate bucket: write returns: lead note: team-wide marker: clears the lead from EVERY rep's worklist ### POST /v1/workspaces/{ws}/leads/{lead}/status purpose: Set the SDR tag scope: leads:write | rate bucket: write returns: lead body: status errors: invalid_request, not_found, unsupported_filter note: not_interested and unsubscribed soft-complete every open task on the lead ### POST /v1/workspaces/{ws}/leads/{lead}/unread purpose: Clear the read receipt scope: leads:write | rate bucket: write returns: lead ### GET /v1/workspaces/{ws}/meetings purpose: Paginated meeting feed scope: meetings:read | rate bucket: upstream returns: list of meeting query: limit, cursor, order, outcome, source, host_id, event_type_id, booked_since, booked_before, starts_after, starts_before (starting_after is accepted as an alias for cursor) errors: invalid_cursor, invalid_request, unsupported_filter note: times can lag a host-side calendar change by up to 15 minutes ### GET /v1/workspaces/{ws}/meetings/{id} purpose: One meeting scope: meetings:read | rate bucket: read returns: meeting errors: invalid_request, not_found ### POST /v1/workspaces/{ws}/meetings/{id}/outcome purpose: Showed, no-show, cancelled, rescheduled scope: meetings:write | rate bucket: write returns: meeting body: outcome, force errors: conflict, invalid_request, not_found ### GET /v1/workspaces/{ws}/members purpose: People who can work this workspace scope: members:read | rate bucket: read returns: list of member query: active, assignable, cursor, limit errors: invalid_request, unsupported_filter ### GET /v1/workspaces/{ws}/notes purpose: Notes, filterable by lead scope: notes:read | rate bucket: read returns: list of note query: author_id, created_since, cursor, lead_id, limit, order errors: invalid_cursor, invalid_request, not_found, unprocessable ### POST /v1/workspaces/{ws}/notes purpose: Add a note to a lead scope: notes:write | rate bucket: write returns: note body: lead_id, body errors: invalid_cursor, invalid_request, not_found, unprocessable note: not idempotent: a retry creates a second note ### GET /v1/workspaces/{ws}/sequences purpose: Follow-up sequences with steps scope: sequences:read | rate bucket: read returns: list of sequence query: active, cursor, expand, limit, order, visibility errors: invalid_cursor, invalid_request ### GET /v1/workspaces/{ws}/tasks purpose: Tasks, filterable by lead, assignee, due date, status scope: tasks:read | rate bucket: read returns: list of task query: assignee_id, cursor, due_after, due_before, lead_id, limit, order, status, task_type errors: invalid_cursor, invalid_request, not_found, unprocessable ### POST /v1/workspaces/{ws}/tasks purpose: Create a task on a lead scope: tasks:write | rate bucket: write returns: task body: lead_id, due_date, due_time, notes, assignee_id errors: invalid_cursor, invalid_request, not_found, unprocessable note: not idempotent: a retry creates a second task ### DELETE /v1/workspaces/{ws}/tasks/{id} purpose: Soft-delete a task scope: tasks:write | rate bucket: write returns: task errors: invalid_request, not_found note: soft delete: sets a deleted timestamp, the row survives ### PATCH /v1/workspaces/{ws}/tasks/{id} purpose: Edit due date, time, notes, assignee scope: tasks:write | rate bucket: write returns: task body: due_date, due_time, notes, assignee_id errors: invalid_request, not_found ### POST /v1/workspaces/{ws}/tasks/{id}/complete purpose: Complete a task scope: tasks:write | rate bucket: write returns: task body: completed errors: invalid_request ### GET /v1/workspaces/{ws}/template_folders purpose: Folders with counts scope: templates:read | rate bucket: read returns: list of template_folder query: cursor, limit, order errors: invalid_cursor ### GET /v1/workspaces/{ws}/templates purpose: Reply templates scope: templates:read | rate bucket: read returns: list of template query: cursor, folder_id, limit, order, visibility errors: invalid_cursor, not_found ### GET /v1/workspaces/{ws}/templates/{id} purpose: One template scope: templates:read | rate bucket: read returns: template errors: not_found ### POST /v1/workspaces/{ws}/templates/{id}/render purpose: Fill the variables for a lead. Read this before sending anything. scope: templates:read | rate bucket: read body: lead_id, resolve_ai_variables, overrides errors: invalid_request, not_found, unprocessable note: call this before sending any template body: raw bodies still contain {{tokens}} ## Objects ### workspace - object: "workspace" - id: string - name: string - domain: string | null - logo_url: string | null - active: boolean - features: Record - granted_scopes: string[] - synced_at: string | null - counts?: WorkspaceCounts - sdr_notes?: string | null ### member - object: "member" - id: string - name: string | null - email: string - avatar_url: string | null - role: "admin" | "user" - timezone: string | null - active: boolean - assignable: boolean - created_at: string | null - assignment_source?: "auto" | "manual" - assigned_at?: string | null ### lead - object: "lead" - id: string - workspace: string - email: string - first_name: string | null - last_name: string | null - company_name: string | null - title: string | null - status: LeadStatus - classification: string | null - state: LeadState - subject: string | null - body_preview: string | null - latest_reply_id: string - last_message_at: string | null - last_message_direction: string - last_action_at: string | null - booked_at: string | null - disqualified_at: string | null - meeting_outcome: string | null - meeting_outcome_at: string | null - responded: boolean - unread: boolean - campaign_id: string | null - campaign_name: string | null - assignee_ids: string[] - has_open_task: boolean - next_due_at: string | null - indexed_at: string | null - assignees?: SerializedMember[] # Expansions, each present only when requested. - campaign?: SerializedCampaign | null - open_tasks?: SerializedTask[] - notes?: SerializedNote[] - meetings?: SerializedMeeting[] - sdr_notes?: string | null ### reply - object: "reply" - id: string - lead_id: string - workspace: string - email: string - subject: string | null - body_preview: string | null - status: string | null - classification: string | null - direction: string - received_at: string | null - campaign_id: string | null - campaign_name: string | null ### attachment - object: "attachment" - id: string - filename: string ### message - object: "message" - id: string - lead_id: string - kind: "reply" | "campaign_send" - direction: "inbound" | "outbound" - reply_id: string | null - subject: string | null - from_name: string | null - from_email: string | null - to_email: string | null - body_html: string | null - body_text: string | null - sent_at: string | null - attachments: SerializedAttachment[] ### note - object: "note" - id: string - workspace: string - lead_id: string - reply_id: string | null - body: string | null - author_id: string | null - created_at: string | null - updated_at: string | null ### task_sequence - id: string - name: string | null - step_index: number | null ### task - object: "task" - id: string - workspace: string - lead_id: string - reply_id: string - title: string | null - notes: string | null - task_type: string | null - due_date: string | null - due_time: string | null - status: "open" | "completed" - creator_id: string - assignee_id: string - completed_at: string | null - created_at: string | null - sequence: SerializedTaskSequence | null ### meeting - object: "meeting" - id: string - workspace: string - lead_id: string | null - event_type_id: string | null - source: string - booked_at: string | null - booked_by_id: string | null - host_id: string | null - starts_at: string | null - ends_at: string | null - timezone: string | null - invitee: { name: string | null; email: string | null; phone: string | null } - email: string | null - name: string | null - company_name: string | null - domain: string | null - guests: Array<{ name: string; email: string }> - custom_fields: Record - utm_params: Record - channel: string | null - confirmation_status: string - location: { type: string | null; phone: string | null } - outcome: string | null - outcome_at: string | null - outcome_by_id: string | null - locked: boolean - created_at: string | null - internal_notes?: string | null ### event_type - object: "event_type" - id: string - workspace: string - name: string - slug: string - description: string | null - event_name_template: string | null - duration_minutes: number - buffer_before_minutes: number - buffer_after_minutes: number - scheduling_type: string - public: boolean - public_url: string | null - public_booking_window_days: number - minimum_notice_minutes: number - business_emails_only: boolean - redirect_url: string | null - workspace_wide: boolean - active: boolean - requires_confirmation: boolean - type: string | null - phone: string | null - description: string | null - order_index: number - use_event_type_availability: boolean - default_field_config: DefaultFieldConfig - booking_provider: string - created_by_id: string | null - created_at: string | null - updated_at: string | null - hosts?: SerializedEventTypeHost[] - form_fields?: SerializedEventTypeFormField[] ### event_type_host - object: "event_type_host" - id: string - event_type_id: string - member_id: string - weight: number - priority: number - is_host: boolean - active: boolean ### event_type_form_field - object: "event_type_form_field" - id: string - event_type_id: string - field_key: string - label: string - type: string - options: string[] - required: boolean - order_index: number - active: boolean ### slot - object: "slot" - start: string | null - end: string | null ### availability - object: "availability" - event_type_id: string - duration_minutes: number - timezone: string | null - window: { from: string | null; to: string | null } - slots: SerializedSlot[] - reason: string | null ### template - object: "template" - id: string - workspace: string | null - scope: "workspace" | "global" - name: string - subject: string | null - body: string - is_html: boolean - visibility: string - folder_id: string | null - order_index: number - variables: string[] - ai_variables: Array<{ name: string; read_website: boolean }> - created_by_id: string | null - created_at: string | null ### template_folder - object: "template_folder" - id: string - workspace: string - name: string - sort_order: number - template_count: number - created_at: string | null ### asset - object: "asset" - id: string - workspace: string - kind: string - title: string - url: string | null - body: string | null - visibility: string - order_index: number - created_by_id: string | null - created_at: string | null ### sequence - object: "sequence" - id: string - workspace: string - name: string - active: boolean - visibility: string - send_days: string[] - send_days_mask: number - created_by_id: string | null - created_at: string | null - steps?: SerializedSequenceStep[] ### sequence_step - object: "sequence_step" - id: string - order_index: number - type: string - title: string | null - delay_value: number - delay_unit: string - wait_for_previous: boolean - template_id: string | null - email_body: string | null - linkedin_text: string | null ### enrollment - object: "enrollment" - id: string - workspace: string - sequence_id: string | null - lead_id: string - reply_id: string - status: string - next_step_index: number - enrolled_by_id: string | null - created_at: string | null ### campaign_tag - object: "campaign_tag" - id: string - workspace: string - name: string - campaign_count: number ### campaign - object: "campaign" - id: string - workspace: string - name: string - status: string - tags: Array<{ id: string; name: string }> ### rep_stat - member_id: string - name: string | null - meetings_booked: number - showed_meetings: number - meetings_with_outcome: number - show_rate: number - interested_leads: number - booked_from_interested: number - booking_rate: number - first_response_avg_sec: number - first_response_count: number - followup_avg_sec: number - followup_count: number ### speed_bucket - label: string - n: number - booked: number - showed: number - booking_rate: number - show_rate: number ### insights_summary - object: "insights_summary" - workspace: string - window: { from: string | null; to: string | null } - all_responses: number - positive_responses: number - booked_meetings: number - showed_meetings: number - total_meetings_booked: number - meetings_with_outcome: number - show_rate: number - total_interested_leads: number - booked_interested_leads: number - booking_rate: number - first_response_avg_sec: number - first_response_count: number - followup_avg_sec: number - followup_count: number - per_rep: SerializedRepStat[] - speed_buckets: SerializedSpeedBucket[] - generated_at: string | null ### campaign_step - object: "campaign_step" - step_id: number - label: string - subject: string - sent: number - contacts: number - replies: number - interested: number ### campaign_insight - object: "campaign_insight" - campaign_id: string - campaign_name: string - status: string - emails_sent: number - contacts: number - replies: number - interested: number - bounced: number - booked: number - showed: number - tags: Array<{ id: string; name: string }> - steps: SerializedCampaignStep[] ### daily_point - object: "daily_point" - date: string - emails_sent: number - responses: number - positive_responses: number - booked: number ## Never returned These columns are withheld from every response, expansion and error message: - clients: id, eb_token, eb_team_id, docuseal_api_key, airtable_record_id, slack_internal_channel_id, slack_client_channel_id, gdrive_link, cal_com_link, no_calling, public_booking_enabled, feature_crm, feature_calling - users: password_hash, password_set_at, fathom_api_key, google_sub, signature, twilio_phone_number, twilio_phone_sid, twilio_numbers, crm_enabled, inbox_enabled, calling_enabled, email_notify_enabled, onboarded_at, install_prompt_dismissed_at, deactivated_at - responses_index: notified_at, last_read_at, disqualified_by, booked_by - lead_notes: client_id, response_id - tasks: deleted_at, email_body, linkedin_text, email_template_id, enrollment_id, sequence_step_id, company_id, opportunity_id - scheduled_meetings: meet_link, google_event_id, google_calendar_id, google_event_html_link, host_notified_at, response_id, eb_lead_id, company_id, contact_id, opportunity_id, internal_notes - event_types: external_id, exclude_from_insights, created_by_user_id - event_type_email_settings: * - reply_templates: ai_vars.instructions, group - followup_sequences: deleted_at - followup_enrollments: client_id - thread_cache: payload, updated_at - reply_attachments: download_url, uuid - api_keys: secret_hash, key_id, created_by_id, revoked_by_id, last_used_ip_prefix, request_count - enrichment_events: phone