# Crispy — Full API Reference for AI Agents > LinkedIn MCP server. 105 tools. Remote MCP + REST API. > Summary: https://crispy.sh/llms.txt ## Connection ```json { "mcpServers": { "crispy": { "url": "https://crispy.sh/api/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` API keys start with `crispy_`. Generate from https://crispy.sh/dashboard. Claude.ai connects via OAuth — no manual key needed. --- ## Authentication Modes **API Key** — use for Claude Desktop, Cursor, Windsurf, Cline, n8n, Make, custom agents. Header: `Authorization: Bearer crispy_...` **OAuth 2.1** — use for Claude.ai. Discovery at `/.well-known/oauth-authorization-server`. --- ## Plans & Limits | Plan | Price | Profiles | API calls | |------|-------|----------|-----------| | Standard | $49/seat/mo (or $49/seat/mo annual) | Per seat | Unlimited | Volume pricing available for 10+ seats (contact sales). Rate limit headers on every response: - `X-RateLimit-Limit` — daily limit - `X-RateLimit-Remaining` — remaining today - `X-RateLimit-Reset` — Unix timestamp, resets midnight UTC --- ## Permission Scopes Each API key operates under one of four scopes set at the account level: - `full_access` — all 105 tools (default) - `outbound` — messaging, invitations, search, analytics, campaigns, leads - `content` — posts, comments, reactions, analytics - `read_only` — read-only: profiles, search, read messages, analytics Check current scope via `crispy://account/info` resource or `get_profile_capacity`. --- ## Safety Limits (per account, per day, reset midnight UTC) | Action | Limit | |--------|-------| | Search queries | 300 | | Profile views | 250 | | Messages sent | 150 | | Engagement (likes, comments) | 100 | | Content polling | 50 | | Connection requests | 15 | | Posts published | 10 | Limits auto-enforce — tools return an error if the daily cap is reached. New accounts should run `start_warm_up` to ramp up limits safely over 14 days. --- ## MCP Resources These are readable before calling any tool: - `crispy://account/info` — account_id, scope, plan, Sales Navigator status, workspace_id - `crispy://account/usage` — calls used today, limits per category, warm-up status - `crispy://tools/list` — tools available under current permission scope - `crispy://account/writing-style` — tone, sentence length, emoji usage, example phrases --- ## Tools ### PROFILE **get_my_profile** Returns your full LinkedIn profile. Params: none Returns: name, headline, about, location, connections, followers, profile_url, provider_id --- **get_profile** View any LinkedIn profile. Sales Navigator (auto-enabled) may add emails, phones, and shared connections when available. Parameters: - `identifier` (string, required): LinkedIn provider_id (e.g. "ACoAA..."), public slug (e.g. "john-doe-123"), or full profile URL. Provider_id is most reliable. - `linkedin_api` (string, optional): API to use. Valid values: "classic" | "sales_navigator". Auto-defaults to "sales_navigator" when available. - `notify` (boolean, optional): Whether to count this as a profile view notification to the person. Default: false. - `sections` (array of strings, optional): Request specific sections for a faster, lighter response. Omit to get all. Valid values: "experience" | "education" | "languages" | "skills" | "certifications" | "about" Returns: full profile object with requested sections Example: get_profile(identifier="john-doe-123", sections=["experience", "education"], notify=false) Notes: - provider_id (starts with ACoAA or ACwAA) is the most reliable identifier — use it when available from search results. - Counts against the profile_views daily limit (250/day) only when notify=true. --- **get_profile_viewers** See who viewed your profile recently. Uses cached data when available, falls back to live API. Parameters: - `limit` (number, optional): Max viewers to return. Default: 20, max: 50. Returns: list of viewers with name, headline, profile_url, provider_id, viewed_at Notes: - Enable viewer polling via update_account_settings(tracking_mode="all_activity") for continuous data collection. - LinkedIn Premium or Sales Navigator may be required for full viewer details. --- ### MESSAGING **list_conversations** Parameters: - `limit` (number, optional): Default 20, max 100. - `cursor` (string, optional): Pagination cursor from previous response. Returns: conversations with last_message, unread count, participant list --- **get_messages** Parameters: - `chat_id` (string, required): From list_conversations. - `limit` (number, optional): Default 20, max 100. - `cursor` (string, optional): Pagination cursor. Returns: messages with sender, text, timestamp, attachments --- **send_message** Send a message in an existing conversation (150 messages/day limit). Parameters: - `chat_id` (string, required): The conversation ID. - `text` (string, required): The message text. - `attachments` (array, optional): Files to attach. Each item: `{url: string, filename: string}`. Max 10MB per file. - `campaign_id` (string, optional): Track this message under a campaign. Returns: {success, message_id} Example: send_message(chat_id="abc123", text="Great to connect!", attachments=[{url: "https://example.com/deck.pdf", filename: "deck.pdf"}]) --- **start_conversation** Start a new 1:1 or group conversation. Recipients must be 1st-degree connections (use send_inmail for non-connections). Parameters: - `attendee_ids` (array of strings, required): LinkedIn profile URL, slug, or provider_id for each recipient. - `text` (string, required): The opening message. - `attachments` (array, optional): Files to attach. Each item: `{url: string, filename: string}`. Max 10MB per file. - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success, chat_id} Example: start_conversation(attendee_ids=["john-doe-123"], text="Hi John, loved your post on AI agents!") Notes: - Counts against the 150 messages/day limit. - For non-connections, use send_inmail instead. --- **send_inmail** Send an InMail to someone outside your network. Requires Sales Navigator (consumes InMail credits). Parameters: - `recipient_id` (string, required): LinkedIn provider_id, slug, or full profile URL. - `text` (string, required): Message body. - `subject` (string, optional): Subject line for the InMail. - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success} Notes: - Requires an active Sales Navigator subscription. - Check can_send_inmail from get_profile for eligibility before sending. - Counts against the 150 messages/day limit. --- **react_to_message** Add an emoji reaction to a message. Parameters: - `message_id` (string, required): ID of the message to react to. - `reaction` (string, required): Emoji character. E.g. "👍", "❤️", "😂", "🎉". Returns: {success} --- **delete_message** Delete a sent message. LinkedIn only allows deletion within 60 minutes of sending. Parameters: - `message_id` (string, required): ID of the message to delete. Returns: {success} --- **delete_chat** Delete an entire conversation thread. Irreversible. Parameters: - `chat_id` (string, required): The conversation ID. Returns: {success} --- ### NETWORK **get_connections** List your 1st-degree connections. Parameters: - `keyword` (string, optional): Filter by name or headline (case-insensitive substring match). - `limit` (number, optional): Default 20, max 100. - `cursor` (string, optional): Pagination cursor. Returns: list of connections with profile info --- **send_invitation** Send a LinkedIn connection request. Parameters: - `profile_url` (string, required): LinkedIn provider_id (e.g. "ACoAA..."), public slug, or full profile URL. - `message` (string, optional): Personal note to include (max 300 chars). Increases acceptance rates. Not available on all account types. - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success} Example: send_invitation(profile_url="john-doe-123", message="Hi John, I'd love to connect — I saw your talk on AI agents.") Notes: - Subject to the 15 invitations/day limit. - Use get_profile_capacity to check remaining quota before bulk sends. --- **list_pending_invitations** Returns sent invitations not yet accepted. Parameters: - `limit` (number, optional): Default 20. - `cursor` (string, optional): Pagination cursor. --- **cancel_invitation** Parameters: - `invitation_id` (string, required): From list_pending_invitations. Returns: {success} --- **list_incoming_invitations** Returns received connection requests pending your response. Parameters: - `limit` (number, optional): Default 20. - `start` (number, optional): Offset for pagination (default 0). Use instead of cursor. Returns: list with invitation_urn and shared_secret needed for accept/decline. --- **accept_invitation** Accept a received connection request. Parameters: - `invitation_urn` (string, required): The invitation URN from list_incoming_invitations. - `shared_secret` (string, required): The shared secret from list_incoming_invitations. Returns: {accepted: true} Notes: - Both invitation_urn and shared_secret come from the list_incoming_invitations response — do not guess these values. --- **decline_invitation** Decline a received connection request. Parameters: - `invitation_urn` (string, required): The invitation URN from list_incoming_invitations. Returns: {declined: true} --- **get_company_profile** Parameters: - `identifier` (string, required): Company name or LinkedIn public ID / page URL slug (e.g. "microsoft"). Returns: name, about, employees, industry, headquarters, website, followers --- **list_accepted_invitations** See which of your sent invitations (tracked via Crispy) were accepted. Only tracks invitations sent via send_invitation. Parameters: - `days` (number, optional): Look back this many days. Default: 30. - `status` (string, optional): Filter by status. Valid values: "accepted" | "pending" | "declined" | "withdrawn". Default: "accepted". - `campaign_id` (string, optional): Filter by campaign. - `limit` (number, optional): Max results. Default: 50. Returns: {invitations: [...], count} Example: list_accepted_invitations(days=7, status="accepted") --- **add_to_blacklist** Prevents future outreach to this profile. All outreach tools check the blacklist automatically. Parameters: - `linkedin_url` (string, required): LinkedIn profile URL (e.g. "linkedin.com/in/john-doe"). - `reason` (string, optional): Note for why this profile is blocked. Returns: {success} --- **remove_from_blacklist** Parameters: - `linkedin_url` (string, required): LinkedIn profile URL to unblock. Returns: {success} --- **check_blacklist** Check if a profile is blacklisted, or list all blocked profiles. Parameters: - `linkedin_url` (string, optional): Profile to check. Omit to list all blacklisted profiles. - `limit` (number, optional): Max entries when listing all. Default: 100. Returns: When checking a profile: {blacklisted: boolean, entry}. When listing all: {entries: [...], count}. --- **set_auto_withdraw** Automatically withdraw pending invitations after N days. Parameters: - `days` (number, required): 0 to disable; 7–90 to enable. Values outside this range are rejected. Returns: {success, auto_withdraw_days, enabled} --- ### CONTENT **create_post** Publish a LinkedIn post. Always confirm content with the user before publishing. Parameters: - `text` (string, required): Post content. Max ~3000 characters. - `attachments` (array, optional): Images or documents to attach. Each item: `{url: string, filename: string}`. Max 10MB per file. Filename must include extension (e.g. "chart.png", "report.pdf"). - `schedule_at` (string, optional): ISO 8601 datetime to schedule future publishing (e.g. "2026-04-01T09:00:00Z"). Must be at least 1 minute in the future. Omit to publish immediately. - `campaign_id` (string, optional): Campaign tracking ID. Returns: If published immediately: {success, post_id, post_url}. If scheduled: {scheduled: true, id, scheduled_at, text_preview}. Example: create_post(text="Excited to share our new product launch!", schedule_at="2026-04-01T09:00:00Z") Notes: - Subject to the 10 posts/day limit. - Use list_scheduled_posts to see pending scheduled posts. - Use cancel_scheduled_post to cancel before it publishes. --- **get_post** Parameters: - `post_id` (string, required): The post ID. Returns: full post with text, author, engagement metrics, timestamp --- **list_posts** Parameters: - `identifier` (string, required): LinkedIn provider_id, public slug, or profile URL. For companies, use the company page slug. - `limit` (number, optional): Default 20, max 100. - `is_company` (boolean, optional): Set true when the identifier is a company page. - `cursor` (string, optional): Pagination cursor. --- **react_to_post** Parameters: - `post_id` (string, required): The post ID. - `reaction_type` (string, optional): Default "like". Valid values: "like" | "celebrate" | "support" | "love" | "insightful" | "funny" - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success} Notes: - Counts against the 100 engagement actions/day limit (shared with comments and reposts). --- **comment_on_post** Parameters: - `post_id` (string, required): The post ID. - `text` (string, required): Comment text. - `parent_comment_id` (string, optional): Reply to a specific comment. Get comment IDs from list_post_comments. - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success, comment_id} Notes: - Counts against the 100 engagement actions/day limit (shared with reactions). --- **repost_post** Reshare a post to your feed. Counts toward daily engagement limits. Parameters: - `post_id` (string, required): Post ID or URN (e.g. "urn:li:activity:123456789"). - `text` (string, optional): Optional commentary to add when reposting. Returns: {success, reposted: true} --- **list_post_comments** Parameters: - `post_id` (string, required): Post ID, activity URN (urn:li:activity:123456789), or feed URN. Activity ID is extracted automatically from any URN format. - `limit` (number, optional): Default 20, max 100. - `cursor` (string, optional): Pagination cursor. - `sort_by` (string, optional): Valid values: "MOST_RECENT" | "MOST_RELEVANT" (default). --- **list_post_reactions** Parameters: - `post_id` (string, required): Post ID, activity URN, or feed URN. - `limit` (number, optional): Default 20. - `cursor` (string, optional): Pagination cursor. --- **get_feed** Parameters: - `limit` (number, optional): Default 20, max 100. - `start` (number, optional): Offset for pagination (default 0). Use to page through results. - `exclude_sponsored` (boolean, optional): Filter out sponsored/promoted posts. Default: false. Returns: list of posts with post_id, author info, engagement counts, is_sponsored flag --- **engage_with_latest_post** React to and optionally comment on someone's most recent post. Combines list_posts + react_to_post + comment_on_post in one call. Parameters: - `identifier` (string, required): Profile URL, slug, or provider_id. - `reaction_type` (string, optional): Default "like". Valid values: "like" | "celebrate" | "support" | "love" | "insightful" | "funny" - `comment_text` (string, optional): Comment to leave on the post. - `campaign_id` (string, optional): Campaign tracking ID. Returns: {success, post_id, post_text, reacted, reaction_type, commented} --- **list_scheduled_posts** Parameters: - `status` (string, optional): Filter by status. Valid values: "pending" | "published" | "failed" | "cancelled". Omit to return all. - `limit` (number, optional): Default 20. Returns: list of scheduled posts with text preview, scheduled_at, status --- **cancel_scheduled_post** Parameters: - `post_id` (string, required): The scheduled post ID (from list_scheduled_posts). Returns: {success} Notes: - Only posts with status "pending" can be cancelled. --- ### SEARCH **search_people** Search for people on LinkedIn. Auto-uses Sales Navigator when available for richer filters and boolean search. Parameters: - `keyword` (string, optional): Free-text search. With Sales Navigator, supports boolean operators: AND, OR, NOT, quotes for exact phrases ("VP Engineering"), parentheses for grouping ((CTO OR CIO) AND AI). - `title` (string, optional): Filter by job title. - `company` (string, optional): Filter by current company. Numeric company ID gives exact match; text name is fuzzy-matched in SN mode. - `location` (string, optional): Filter by geography. Accepts place names (e.g. "Netherlands", "San Francisco") — auto-resolved to LinkedIn IDs. Comma-separate multiple IDs. - `industry` (string, optional): Filter by industry. Accepts names (e.g. "Software", "Financial Services") — auto-resolved. Use get_search_parameters(type="industry") or get_search_parameters(type="sales_industry") for valid IDs. - `seniority_level` (array of strings, optional, SN only): Valid values: "OWNER" | "CXO" | "VP" | "DIRECTOR" | "MANAGER" | "SENIOR" | "ENTRY" | "TRAINING" - `company_headcount` (string, optional, SN only): Employee count range. Valid values: "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+" - `function` (array of strings, optional, SN only): Job function IDs (numeric strings). E.g. ["8"] for Engineering. Use get_search_parameters(type="function") to look up IDs. - `years_of_experience` (object, optional, SN only): Total years of experience range. Shape: `{min: number, max: number}`. E.g. {min: 5, max: 10}. - `years_in_current_position` (object, optional, SN only): Years in current role. Shape: `{min: number, max: number}`. E.g. {min: 1, max: 3}. - `years_in_current_company` (object, optional, SN only): Years at current employer. Shape: `{min: number, max: number}`. E.g. {min: 1, max: 5}. - `changed_jobs` (boolean, optional, SN only): Only show people who recently changed jobs. - `posted_on_linkedin` (boolean, optional, SN only): Only show people who recently posted on LinkedIn. - `school` (array of strings, optional, SN only): School IDs (numeric strings). Use get_search_parameters(type="school", query="MIT") to find IDs. - `limit` (number, optional): Max results. Default: 10, max: 25. - `cursor` (string, optional): Pagination cursor. - `linkedin_api` (string, optional): Override API. Valid values: "classic" | "sales_navigator". Auto-defaults to "sales_navigator" when available. Returns: list of profile summaries with profile_url, provider_id, name, headline Example: search_people(keyword="VP Engineering", location="Netherlands", seniority_level=["VP", "DIRECTOR"], company_headcount="51-200", posted_on_linkedin=true) Notes: - SN-only filters require an active Sales Navigator subscription. They are silently ignored in classic mode. - Use get_search_parameters to look up valid IDs for location, industry, function, and school filters. - provider_id (starts with ACoAA) from results is the most reliable identifier for subsequent tools. --- **search_companies** Search for companies on LinkedIn. Parameters: - `keyword` (string, optional): Company name or keyword. - `industry` (string, optional): Filter by industry. Accepts name (auto-resolved) or numeric ID. Comma-separated for multiple IDs in SN mode. - `location` (string, optional): Filter by geography. Accepts name (auto-resolved) or numeric ID. - `headcount` (string, optional): Filter by employee count. Valid values: "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+" - `limit` (number, optional): Default 10, max 25. - `cursor` (string, optional): Pagination cursor. - `linkedin_api` (string, optional): "classic" | "sales_navigator". Auto-defaults to "sales_navigator" when available. Returns: company summaries. Use get_company_profile for full details. Example: search_companies(keyword="fintech", location="United Kingdom", headcount="51-200") --- **search_posts** Search posts by keyword. Parameters: - `keyword` (string, required): Keyword to search for in posts. - `limit` (number, optional): Default 10, max 25. - `cursor` (string, optional): Pagination cursor. Returns: post content, engagement metrics, author info --- **get_search_parameters** Look up LinkedIn filter IDs by name for use in search_people/search_companies. Call this before searching if you're unsure of valid filter values. Parameters: - `type` (string, required): Parameter type. Valid values: "location" | "industry" | "sales_industry" | "function" | "school" | "seniority" | "department" | "technologies" | "persona" | "region" | "postal_code" | "company" | "job_title" | "account_lists" | "lead_lists". Uppercase aliases (e.g. "LOCATION", "JOB_FUNCTION") also work. SN-only types: "technologies", "persona", "account_lists", "lead_lists". - `query` (string, optional): Keyword to filter results (e.g. "Netherlands", "Software", "MIT"). Case-insensitive substring match. Returns: list of {id, name} items. Use id values as filter parameters in search_people/search_companies. Example: get_search_parameters(type="location", query="Netherlands") get_search_parameters(type="seniority") --- **save_search** Save search parameters for repeated use. Location/industry names are auto-resolved to stable IDs at save time. Parameters: - `name` (string, required): Descriptive label (e.g. "VP Engineering in Netherlands"). - `search_type` (string, required): Valid values: "people" | "companies" | "posts". - `parameters` (object, required): The search parameters object — same shape as search_people / search_companies / search_posts. Returns: {success, search: {id, name, search_type, parameters, created_at}} Example: save_search(name="VP Eng Netherlands", search_type="people", parameters={keyword: "engineering", location: "Netherlands", seniority_level: ["VP", "DIRECTOR"]}) --- **list_saved_searches** Returns all saved searches with parameters and last_run_at timestamp. Params: none --- **run_saved_search** Execute a saved search by ID. Supports cursor-based pagination to iterate through all results across multiple calls. Parameters: - `search_id` (string, required): ID of the saved search to run. - `from_last_cursor` (boolean, optional): If true, resume from where the last run left off. Default: false (start fresh). - `new_results_only` (boolean, optional): If true, return only results new since the last run. Useful for lead discovery workflows. Returns: search results + next_cursor. Cursor is stored automatically for next from_last_cursor run. Example: run_saved_search(search_id="abc123", from_last_cursor=true) --- **delete_saved_search** Parameters: - `search_id` (string, required): ID to delete. --- ### ANALYTICS **get_post_analytics** Live fetch from LinkedIn. Slower but always current. Parameters: - `post_id` (string, required): The post ID. Returns: impressions, reactions, comments, reposts, clicks, engagement_rate --- **get_cached_post_analytics** Get post metrics from cache. Faster — use for dashboards. Falls back to live data if cache is empty. Parameters: - `sort_by` (string, optional): Sort posts by this metric. Valid values: "impressions" | "reactions" | "comments" | "reposts" | "engagement_rate". Default: "impressions". - `limit` (number, optional): Max posts to return. Default: 20, max: 50. - `content_type` (string, optional): Filter by content type. Valid values: "text" | "image" | "video" | "document" | "poll" | "article". Returns: list of posts with metrics sorted by the chosen metric Notes: - This returns your post library sorted by performance — not a single post lookup. Use get_post_analytics for a single post. --- **get_top_posts** Get your top performing posts ranked by a chosen metric. Uses cache, falls back to live API. Parameters: - `metric` (string, optional): Rank by this metric. Valid values: "impressions" | "reactions" | "comments" | "reposts" | "engagement_rate". Default: "impressions". - `limit` (number, optional): How many posts to return. Default: 10, max: 25. - `days` (number, optional): Look back this many days. Default: 30. Returns: posts sorted by chosen metric with full metrics --- **get_content_summary** Aggregate content performance over a time window. Parameters: - `days` (number, optional): Summarize over this many days. Default: 30. Returns: total posts, total impressions, total reactions, total comments, total reposts, avg_engagement_rate, breakdown by content type --- **get_profile_analytics** Daily profile view counts and follower data. Uses cached data from the content poller. Parameters: - `days` (number, optional): How many days of data to return. Default: 30. Returns: daily time series of profile views and follower counts --- **get_activity_analytics** Daily activity breakdown: invitations sent, messages sent, posts published. Parameters: - `days` (number, optional): How many days of data to return. Default: 7. Returns: {data_source, days: [...daily breakdowns...]} Notes: - Only Crispy-initiated actions are counted by default. Use update_account_settings(tracking_mode="all_activity") for full coverage including manual LinkedIn actions. --- **purge_analytics_cache** Force-refresh cached analytics for your account. Data is re-populated on the next automated poll cycle (every few hours). Params: none --- **get_writing_style** Returns AI-generated style profile: tone, sentence length, emoji usage, example phrases, dos/don'ts. Params: none Returns: {status, messaging_style, posting_style, has_style_examples, generated_at} Status values: "ready" | "processing" | "needs_examples" | "error" | "not_found" --- **set_style_examples** Provide 1–3 LinkedIn profile URLs of people whose posting style you want to match. Their recent posts are analyzed to extract tone, structure, and patterns. Parameters: - `profile_urls` (array of strings, required): 1–3 LinkedIn profile URLs (e.g. ["https://linkedin.com/in/garyvee"]). Minimum 5 public posts required across all profiles. Returns: {success, posting_style, profiles_analyzed} Notes: - This takes profile URLs of people to emulate — not example post texts. - Requires ~5+ publicly visible posts across the provided profiles. --- **refresh_writing_style** Re-trigger writing style analysis. Re-fetches your messages and posts to regenerate style guides. Params: none Returns: {success, message} --- **get_comment_reach** Measure how much engagement your commenting activity drives. Parameters: - `days` (number, optional): How many days of data to analyze. Default: 30. Returns: comments made, estimated reach, daily breakdown, top performing comments --- **daily_recap** Personalized daily briefing: new connections, unread messages, pending invitations, today's usage, and suggested next actions. Params: none Returns: {period, new_connections, invitations_accepted, unread_messages, pending_invitations, today (usage), suggested_actions} Tip: Use suggested_actions to decide what to do next. Run the matching prompt template. --- ### CAMPAIGNS **create_campaign** Create a new campaign for tracking outbound actions. Parameters: - `id` (string, required): Campaign slug — 3–50 chars, lowercase alphanumeric + hyphens. E.g. "q1-fintech-outreach". - `name` (string, required): Human-readable display name. - `description` (string, optional): Campaign description. Returns: {success, campaign} Example: create_campaign(id="q1-fintech-outreach", name="Q1 Fintech Outreach", description="VP-level prospects in fintech") --- **list_campaigns** Parameters: - `include_archived` (boolean, optional): Include archived campaigns. Default: false. Returns: all campaigns with status and action count summary --- **archive_campaign** Parameters: - `campaign_id` (string, required): Campaign ID to archive. Notes: - Archived campaigns stop accepting new actions but historical data is preserved. Use delete_campaign for permanent deletion. --- **update_campaign** Parameters: - `campaign_id` (string, required): Campaign ID to update. - `name` (string, optional): New name. - `description` (string, optional): New description. - `archived` (boolean, optional): Set to false to unarchive a campaign. --- **delete_campaign** Permanently deletes campaign and unlinks all tracked activity. Cannot be undone. Parameters: - `campaign_id` (string, required): Campaign ID to delete. Notes: - Use archive_campaign if you want to preserve historical data. --- **get_campaign_stats** Parameters: - `campaign_id` (string, required): Campaign ID. - `days` (number, optional): Look back this many days. Default: 30. Returns: action counts by tool, daily breakdown, per-account breakdown, acceptance_rate, reply_rate --- ### ADMIN **update_account_settings** Parameters: - `tracking_mode` (string, required): Valid values: "mcp_only" (default — only Crispy-initiated actions logged) | "all_activity" (all LinkedIn activity tracked, including manual actions). Returns: {success} --- **get_profile_capacity** Get status and remaining daily action limits for all connected accounts. Returns per-category limits (search, profile_views, messages, invitations, posts, engagement) with used/remaining counts. Params: none Health values: "healthy" | "warming_up" (first 14 days) | "rate_limited" (near capacity) | "session_stale" (needs verify_session) | "restricted" (not connected) Returns: {accounts: [...], total_remaining_messages, total_remaining_invitations} --- **contact_support** Parameters: - `subject` (string, required): Brief description of the issue. - `message` (string, required): Detailed description. - `priority` (string, optional): Valid values: "normal" (default) | "urgent". Use urgent only for blocking issues. - `include_diagnostics` (boolean, optional): Auto-attach account diagnostics. Default: true. Returns: {success, ticket_id} --- **verify_session** Check if the LinkedIn session is still active. Use when get_profile_capacity shows "session_stale" or before critical operations. Params: none Returns: {session_valid: boolean, name, provider_id, today (usage summary)} If invalid, the user needs to reconnect via the Chrome extension. --- **configure_storage** Control how Crispy stores your LinkedIn contact data. Parameters: - `mode` (string, required): Valid values: "managed" | "byos" | "off" - "managed" — Crispy stores data (default, powers lead CRM and campaigns) - "byos" — bring your own Supabase database; contacts sync there - "off" — fully stateless, nothing retained - `supabase_url` (string, required when mode="byos"): Your Supabase project URL. - `supabase_key` (string, required when mode="byos"): Your Supabase service role key. Returns: {success, storage_mode} --- **start_warm_up** Gradually increase daily limits over time to avoid LinkedIn restrictions. Recommended for new accounts. Parameters: - `mode` (string, required): Valid values: "auto" (14-day ramp, recommended) | "aggressive" (7-day ramp) | "custom" - `categories` (object, optional, required when mode="custom"): Per-category config. Keys: "invitation" | "messaging" | "profile_view" | "engagement" | "search" | "posting". Each value: `{start: number, target: number, ramp_days: number}`. E.g. `{invitation: {start: 3, target: 15, ramp_days: 14}}`. - `timezone` (string, optional): IANA timezone for day-boundary calculations (e.g. "America/New_York"). Default: UTC. Returns: {success, warm_up_id, mode, day_number, effective_limits} Notes: - Only one warm-up can be active at a time. Use stop_warm_up first to restart. --- **get_warm_up_status** Params: none Returns: {active, mode, status, day_number, started_at, effective_limits, config} --- **stop_warm_up** Params: none (stops warm-up for all categories, normal safety limits apply immediately) --- ### WEBHOOKS **list_webhooks** Returns all registered webhooks with URL, subscribed events, enabled status, and description. Params: none --- **create_webhook** Register a new HTTPS endpoint for real-time event notifications. Parameters: - `url` (string, required): HTTPS URL to receive POST requests. - `events` (array of strings, required): Event types to subscribe to. See Webhook Events section below for full list. At least one required. - `description` (string, optional): Label for this webhook (e.g. "n8n pipeline trigger"). Returns: {success, webhook_id, signing_secret} Example: create_webhook(url="https://hooks.example.com/crispy", events=["message.received", "invitation.accepted"], description="n8n pipeline") Notes: - The signing_secret is shown only once — save it. Payloads are signed with HMAC-SHA256 in the X-Crispy-Signature header. - Maximum 10 webhooks per account. --- **update_webhook** Parameters: - `webhook_id` (string, required): The webhook ID to update. - `url` (string, optional): New HTTPS URL. - `events` (array of strings, optional): Replace subscribed event list. - `enabled` (boolean, optional): Enable or disable the webhook. - `description` (string, optional): New description. --- **delete_webhook** Parameters: - `webhook_id` (string, required): The webhook ID to delete. --- **test_webhook** Send a test payload to verify a webhook endpoint is reachable and returning 2xx. Parameters: - `webhook_id` (string, required): The webhook ID to test. Returns: {success, status, ok} Notes: - Uses the first subscribed event type as the test event type. - Check get_webhook_deliveries afterward to see the delivery record. --- **get_webhook_deliveries** View recent delivery attempts for a webhook. Use this to debug why a webhook isn't receiving events. Parameters: - `webhook_id` (string, required): The webhook ID to inspect. - `status` (string, optional): Filter by delivery status. Valid values: "all" | "delivered" | "pending" | "failed". Default: "all". - `limit` (number, optional): Number of deliveries to return. Default: 20, max: 50. Returns: {deliveries: [...]} with event, status, attempts, status_code, delivered_at, next_retry_at --- ### LEAD MANAGEMENT **search_leads** Search your saved leads/contacts CRM. Parameters: - `query` (string, optional): Search by name (first, last, or full name — case-insensitive substring). - `status` (string, optional): Filter by lead status (e.g. "new", "contacted", "replied", "qualified"). - `tags` (array of strings, optional): Filter by tags (matches any). - `company` (string, optional): Filter by company name. - `limit` (number, optional): Max results. Default: 50. Returns: {leads: [...], count} Example: search_leads(status="replied", tags=["fintech"], limit=20) --- **add_lead** Manually add a lead/contact to your CRM. Requires storage_mode to be "managed". Parameters: - `linkedin_url` (string, required): LinkedIn profile URL. - `first_name` (string, optional): First name. - `last_name` (string, optional): Last name. - `email` (string, optional): Email address. - `company` (string, optional): Company name. - `title` (string, optional): Job title. - `tags` (array of strings, optional): Tags for categorization (e.g. ["fintech", "warm"]). Returns: {success, contact: {id, full_name, linkedin_url}} Notes: - Upserts by linkedin_url — calling with an existing URL updates the record. - Use configure_storage(mode="managed") first if not already set. --- **update_lead** Update a lead/contact's information. Parameters: - `contact_id` (string, optional): Contact ID (from add_lead or search_leads). Provide this or linkedin_url. - `linkedin_url` (string, optional): LinkedIn URL as alternative to contact_id. - `first_name` (string, optional): Updated first name. - `last_name` (string, optional): Updated last name. - `email` (string, optional): Updated email. - `company` (string, optional): Updated company. - `title` (string, optional): Updated job title. - `status` (string, optional): Lead status (e.g. "new", "contacted", "replied", "qualified", "converted"). - `tags` (array of strings, optional): Replace all tags. Returns: {success, contact: {id, full_name}} Example: update_lead(linkedin_url="linkedin.com/in/john-doe", status="replied", tags=["hot-lead"]) --- **get_outreach_history** Full timeline of interactions with a contact, or all recent activity if no contact specified. Parameters: - `linkedin_url` (string, optional): LinkedIn URL of the contact. Omit to see all recent activity. - `contact_id` (string, optional): Contact ID as alternative to linkedin_url. - `activity_type` (string, optional): Filter by action type (e.g. "send_invitation", "send_message", "profile_view"). - `limit` (number, optional): Max results. Default: 50. Returns: {activities: [...], count} --- **import_contacts** Bulk import contacts. Flexible field naming (camelCase, snake_case, Title Case — all accepted). Deduplicates by LinkedIn URL. Parameters: - `contacts` (array of objects, required): Each object must have a LinkedIn URL field (accepted names: linkedin_url, linkedinUrl, profileUrl, url, "LinkedIn URL", "LinkedIn"). Other auto-mapped fields: first_name/firstName, last_name/lastName, email, company/companyName, title/jobTitle, phone, industry, location, country, tags, status. Max 1000 contacts per call. - `source` (string, optional): Provenance label for tracking (e.g. "lemlist", "heyreach", "expandi", "csv"). Returns: {success, imported, skipped, total_provided, source} Example: import_contacts(contacts=[{linkedin_url: "linkedin.com/in/jane-doe", first_name: "Jane", company: "Acme"}], source="lemlist") --- **import_blacklist** Bulk import profiles to do-not-contact list. Parameters: - `entries` (array, required): Array of LinkedIn URL strings, or objects with `{linkedin_url, reason}`. Max 5000 entries per call. Returns: {success, imported, skipped, total_provided} Example: import_blacklist(entries=["linkedin.com/in/john-doe", {linkedin_url: "linkedin.com/in/jane-smith", reason: "competitor"}]) --- **create_list** Create a named contact list. Lists are reusable collections for organizing contacts before campaigns. - id (string, required) — list slug (3-50 chars, lowercase alphanumeric + hyphens) - name (string, required) — human-readable list name - description (string) — optional description Returns: {success, list} Example: create_list(id="senior-clinical-belgium", name="Senior Clinical Belgium") **list_lists** List your contact lists with member counts. - include_archived (boolean) — include archived lists (default: false) Returns: {lists: [{id, name, description, member_count, archived, created_at}]} **update_list** Update a list's name, description, or archived status. - list_id (string, required) — the list ID - name (string) — new name - description (string) — new description - archived (boolean) — archive or unarchive Returns: {success, list} **delete_list** Permanently delete a list. Contacts are preserved — only the grouping is removed. - list_id (string, required) — the list ID Returns: {success, deleted: true} **add_to_list** Add contacts to a list by ID or LinkedIn URL. Idempotent — duplicates ignored. - list_id (string, required) — the list ID - contact_ids (string[]) — contact UUIDs - linkedin_urls (string[]) — LinkedIn URLs (resolved to existing contacts) Returns: {success, added, already_in_list} Example: add_to_list(list_id="senior-clinical-belgium", linkedin_urls=["linkedin.com/in/jeroen-van-der-hilst-2b618464"]) **remove_from_list** Remove contacts from a list by ID or LinkedIn URL. - list_id (string, required) — the list ID - contact_ids (string[]) — contact UUIDs - linkedin_urls (string[]) — LinkedIn URLs Returns: {success, removed} **get_list_contacts** Get contacts in a list with profile data. Paginated. - list_id (string, required) — the list ID - limit (number) — max results (default 50, max 200) - offset (number) — skip results (default 0) Returns: {contacts: [...], total} --- ## Webhook Events (29) Configure subscriptions with `create_webhook`. All payloads are HMAC-SHA256 signed via the X-Crispy-Signature header. **Outreach (7)** - `invitation.sent` — you sent a connection request - `invitation.cancelled` — you withdrew an invitation - `invitation.accepted_by_us` — you accepted someone's request - `invitation.declined_by_us` — you declined someone's request - `invitation.received` — someone sent you a request - `invitation.accepted` — someone accepted your invitation - `invitation.auto_withdrawn` — auto-withdraw triggered **Messaging (3)** - `message.sent` — you sent a message - `inmail.sent` — you sent an InMail - `message.received` — you received a message **Content (5)** - `post.published` — you published a post - `post.reacted` — you reacted to a post - `post.commented` — you commented on a post - `post.milestone` — your post hit a milestone (1K impressions, etc.) - `post.trending` — your post is trending in your network **Account (6)** - `account.connected` — LinkedIn account connected - `account.disconnected` — session expired or disconnected - `profile.views_spike` — unusual spike in profile views - `profile.viewed_back` — someone you viewed also viewed you - `safety_limit.reached` — daily action limit hit for a category - `usage.daily_summary` — end-of-day usage summary **Billing (3)** - `billing.subscription_changed` — plan upgrade or downgrade - `billing.payment_failed` — payment attempt failed - `billing.payment_succeeded` — payment succeeded **Organization (2)** - `org.member_added` — member joined the organization - `org.member_removed` — member removed from organization **Workspace (3)** - `workspace.created` — new client workspace created (Agency) - `workspace.member_added` — member added to a workspace - `workspace.member_removed` — member removed from a workspace --- ## Errors | Status | Meaning | |--------|---------| | 401 | Missing or invalid API key / OAuth token | | 403 | Action not allowed under current permission scope | | 429 | Daily rate limit reached (check X-RateLimit-Remaining) or IP rate limit | | 503 | LinkedIn session expired — user must reconnect via Chrome extension | All errors return JSON: `{"error": "message", "code": "error_code"}` --- ## Common Patterns **Start every session with:** ``` Get daily_recap → check suggested_actions → proceed ``` **Before bulk outreach:** ``` get_profile_capacity → check limits → start_warm_up if new account ``` **Campaign tracking:** All write tools (send_invitation, send_message, start_conversation, create_post, react_to_post, comment_on_post, send_inmail) accept an optional `campaign_id` parameter. Pass it to attribute every action to a campaign and track performance via get_campaign_stats. **Pagination:** Most list tools return a `cursor` field. Pass it as the `cursor` parameter in the next call. Cursor is null when there are no more results. Exception: list_incoming_invitations uses `start` (integer offset) instead of cursor. **Sales Navigator:** send_inmail, extended search filters (seniority_level, company_headcount, function, years_of_experience, years_in_current_position, years_in_current_company, school, changed_jobs, posted_on_linkedin), and boolean search keywords require an active Sales Navigator subscription on the connected LinkedIn account. Crispy detects it automatically — check via `crispy://account/info`. **Identifier resolution:** All tools that accept `identifier`, `profile_url`, or `recipient_id` auto-resolve slugs and URLs to provider_ids. However, passing a provider_id directly (starts with ACoAA or ACwAA) is fastest and most reliable. **Blacklist and lead CRM:** Blacklist and CRM tools use `linkedin_url` (not `provider_id`) as the primary identifier. --- ## Links - Summary: https://crispy.sh/llms.txt - Docs: https://crispy.sh/docs - Dashboard: https://crispy.sh/dashboard - Sign up: https://crispy.sh/signup - Support: support@crispy.sh