Skip to main content

2026-11-05 — Messaging leads added by profile URL

POST /v1/messages and POST /v1/messages/schedule returned 422 No conversation exists with this lead yet for leads you already had a conversation with, if they were added with a linkedin.com/in/name URL and had never been through a campaign. Botdog stored the URL but not the lead’s LinkedIn member id, and without one it never looked for the conversation.
  • Fixed when a lead has no member id, these endpoints now find it from the lead’s LinkedIn profile, save it, then find the conversation and send. You don’t need to re-add these leads. You still get 422 when there genuinely is no conversation; these endpoints never start one.
  • Fixed a lead added with a Sales Navigator id (ACwAA…) is resolved the same way for sending. The stored id does not change, so with the default inbox setting these conversations may still not appear in the Botdog inbox.
  • Changed this lookup is limited to 10 a minute and 100 a day per sending account, separately from your send limits. Going over returns 429 with retryAfter.
  • Changed if your integration treats this 422 as “not connected yet”, check it before this release reaches you: these calls can now send a real message into the existing conversation.
Blacklisted leads with no member id are not looked up and still return 422. POST /v1/conversations/{id}/reply still returns 404 for a lead with no member id until a send has found it. No field was removed or renamed.

2026-10-22 — Team campaigns: whose LinkedIn account did the work

  • Added senderUserId, senderUserEmail and senderUserLinkedinPublicUrl to every lead webhook event. In a team campaign they name the teammate whose LinkedIn account sent the invitation or message, or received the acceptance or reply, while botdogUser* stays the teammate who created the campaign. For every other campaign both name the same user. See Whose LinkedIn account did it.
  • Added senderUserId to each lead returned by GET /v1/leads, GET /v1/leads/{id} and the campaign leads endpoint, and to each event in a lead’s events. For a team-campaign lead it is the teammate the lead is assigned to (null until one picks it up); otherwise the lead’s owner.
  • Changed team campaign events now also reach the all-campaigns webhooks of the teammate whose account did the work, and the specific-campaigns webhooks any teammate created for that campaign. Each webhook still receives an event once. Other campaigns are routed exactly as before.
No field was removed or renamed.

2026-10-16 — Webhook for messages sent from the inbox, one reply per webhook

  • Added the INBOX_MESSAGE_SENT webhook event: sent when you send a message by hand from the Botdog inbox, to anyone, campaign leads included. Like NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED it is delivered only to webhooks with scope CONNECTED_ACCOUNT. See INBOX_MESSAGE_SENT.
  • Clarified LEAD_MESSAGE_SENT only fires for messages sent by a campaign step. Its behaviour has not changed; the two events never overlap.
  • Fixed a reply from someone who is a lead in several of your campaigns was delivered to each webhook once per campaign — the same reply two or three times, differing only in campaignId. Each webhook now receives it once: an all-campaigns webhook for the campaign whose conversation the reply arrived in, a specific-campaigns webhook for one of the campaigns it selected. A specific-campaigns webhook still always receives a campaign it subscribed to. The payload is unchanged, and replies from someone in a single campaign are delivered exactly as before. See LEAD_MESSAGE_REPLIED.
No field was removed or renamed.

2026-10-15 — Lead dates in the list response, per-endpoint rate limits

  • Added invitedAt, connectedAt, repliedAt, withdrawnAt and stoppedAt to each lead returned by GET /v1/leads. They hold the same values as GET /v1/leads/{id} and are null until the event happens, so you can follow a campaign’s progress from the list alone instead of reading each lead.
  • Changed rate limits are counted separately for each endpoint. Calls to one endpoint no longer count toward another’s limit; for example, GET /v1/leads/{id} calls no longer reduce what you can send to GET /v1/leads. The limits themselves are unchanged. See Rate limiting.
No field was removed or renamed.

2026-10-01 — Replying to leads who wrote first

POST /v1/messages and POST /v1/conversations/{id}/reply could refuse a conversation that the Botdog inbox showed: 422 No conversation exists with this lead yet and 404 Conversation not found. It happened whenever Botdog had not recorded the conversation against the lead — most often a lead who accepted your invitation and wrote to you before your campaign messaged them.
  • Fixed POST /v1/messages and POST /v1/messages/schedule: when Botdog has no conversation recorded for the lead, it now finds the lead’s LinkedIn conversation, records it and sends. You still get 422 when there genuinely is no conversation yet — these endpoints never start one.
  • Fixed POST /v1/conversations/{id}/reply and POST /v1/conversations/{id}/read: the conversation id from the Botdog inbox URL now works even if it was never recorded against the lead, as long as the conversation is with one of your team’s leads.
  • Fixed new replies and first campaign messages now record their conversation against the lead straight away.
  • Added chatId to LEAD_MESSAGE_REPLIED webhooks: the conversation the reply arrived in, for POST /v1/conversations/{id}/reply. That works for one-to-one LinkedIn, Sales Navigator and Recruiter conversations; InMail and group conversations cannot be replied to by id. See LEAD_MESSAGE_REPLIED.
  • Added chatId to GET /v1/leads/{id}: the lead’s standard LinkedIn conversation, null until Botdog has linked one.
  • Changed POST /v1/messages and POST /v1/messages/schedule return 409 (account not connected) rather than 422 when the lead has no recorded conversation and the sending LinkedIn account is disconnected, since Botdog now needs the account to look the conversation up.
  • Fixed the docs said API keys are prefixed bd_live_. They are not, and never were: a key is a 32-character hexadecimal string. Existing keys are unaffected and work for every endpoint, messaging included.
  • Changed the MCP tool get_api_setup_guide returns apiKeyFormat in place of apiKeyPrefix, which always held the wrong bd_live_ value.
Stopping a lead (DELETE /v1/leads/{id}) does not affect its conversation; you can still message a stopped lead. No REST endpoint or webhook field was removed or renamed.

2026-09-23 — Job title, location and 19 more webhook fields

Lead webhook events now carry everything Botdog holds about the person and about the lead row you imported, rather than just name, company, emails and phones. See Shared fields.
  • Added profile fields: contactJobTitle, contactHeadline, contactLocation, contactProfilePictureUrl, contactLinkedinPublicProfileUrl, contactConnectionDegree, contactFollowerCount, contactIsPremium, contactIsOpenToWork, contactIsCreator, contactWebsites.
  • Added enrichment fields: contactEnrichedEmail, contactEnrichedPhone. These are deliberately separate from contactEmails / contactPhones, which keep their original LinkedIn-only meaning.
  • Added lead fields: leadCustomField1..3, leadCustomAttributes (every extra column from your CSV, as a flat string map keyed by your own column names), leadListId, leadListName, leadInvitedAt, leadConnectedAt.
  • Applies to LEAD_PROFILE_VISITED, LEAD_INVITATION_SENT, LEAD_INVITATION_ACCEPTED, LEAD_MESSAGE_SENT and LEAD_MESSAGE_REPLIED. NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED is unchanged: the sender is not a lead, so these fields are absent rather than empty.
  • Strings are "" when unknown; numbers and booleans are null when unknown, so false and 0 stay meaningful.
  • Fixed test events sending eventName where real deliveries send humanReadableEventName. A mapping built from the Send test event button left that field blank on the first real delivery. Test events now send humanReadableEventName and keep eventName, and carry all the new fields too. If you built an integration from a test event before today, re-check it against a real delivery.
  • contactConnectionDegree is 1, 2, 3, or 4 for out of network, and null when Botdog has not observed the distance. It is never 0.
  • On LEAD_INVITATION_SENT only, leadInvitedAt is "" — the event fires just before Botdog records the invitation against the lead. Use sentAt on that event.
  • Fixed contactIsOpenToWork never being recorded at all. It is now stored when a Sales Navigator step fetches the profile, and stays null — meaning “not checked” — otherwise.
No field was removed or renamed.

2026-09-22 — Webhook contact identity fields

Every lead webhook event now identifies the person three ways, so integrations can match on what they imported and still see what LinkedIn shows. See Shared fields.
  • Added leadName and leadLinkedinUrl: the name and LinkedIn URL exactly as you imported them (via POST /v1/leads, CSV or URL import), never rewritten after import. Empty strings when no imported lead is behind the event — notably a NON_CAMPAIGN_LEAD_INBOX_MESSAGE_RECEIVED from someone you never imported.
  • Added contactLinkedinUrn: LinkedIn’s internal member id (ACoAAA…), the only identifier that never changes.
  • Changed contactLinkedinUrl: now prefers the public profile URL as last observed on LinkedIn, falling back to the imported slug and then the member-id form. Between 2026-09-20 and this release it preferred the imported slug; before that, three events sent the member-id form. If you matched on contactLinkedinUrl, switch to leadLinkedinUrl — it always equals your own record.
  • contactName now falls back to the imported name instead of "undefined undefined" when Botdog has not yet fetched the profile, and is the LinkedIn display name on every event (the inbox event for imported contacts previously sent the imported name there).
  • Fixed LEAD_INVITATION_ACCEPTED and LEAD_MESSAGE_REPLIED sending https://www.linkedin.com/in/ with an empty slug for leads imported by member id or Sales Navigator URL.
  • Test events from the dashboard now use the production field names (contactCompanyName and the three new fields).
No field was removed or renamed.

Unreleased — expanded API surface

Not yet released. Adds, on top of v1.0:
  • Campaigns: create and update (name + dates), pause/resume, pause/resume specific activity types, list paused activities, list a campaign’s leads, archive, and delete.
  • Leads: list with filters, full detail with event history, update metadata (notes / custom fields), find a lead by LinkedIn profile URL, and stop a sequence.
  • Lists & blacklist: create, list, get, and add/remove members.
  • Snippets: list, create, update, delete.
  • Accounts: list connected accounts, read daily limits + remaining quota, and update limits (caps clamped to safe maximums).
  • Webhooks: create, update, list, and get (delete is available via MCP).
  • Messaging: send a message or reply, schedule a future send, and mark a conversation read/unread — guarded by a dedicated per-account send rate limit.
  • MCP: expanded to 41 tools (23 read / 18 write). stop_lead_in_campaign now normalizes linkedin_url before matching, so a URL that differs only in spelling from the imported one no longer silently stops nothing.
  • New status codes in use: 202, 409, 502, 503 (see Errors).
  • Four new webhook event types are reserved but not yet delivered: LEAD_INVITATION_WITHDRAWN, LEAD_SKIPPED, ACCOUNT_DISCONNECTED, DAILY_LIMIT_REACHED.

v1.0 — Initial release

Released.
  • Initial public API under /v1.
  • Authentication via x-api-key.
  • Campaigns, leads, users, teams, analytics, and health endpoints as documented in the OpenAPI spec.
Update this page when you ship breaking or notable behavioral changes to the API.