LKDN Autopilot Attiva il servizio

API e server MCP di LKDN Autopilot

Con le API di LKDN Autopilot i tuoi programmi, il tuo CRM, le tue automazioni (n8n, Make, Zapier) e i tuoi assistenti AI (Claude, ChatGPT) usano il tuo account LinkedIn collegato, attraverso di noi: leggono le conversazioni e i collegamenti, cercano persone, inviano messaggi e richieste di collegamento, pubblicano e commentano.

Tre cose da sapere prima di tutto:

  1. Un solo account, il tuo. La chiave è legata al tuo account LKDN Autopilot e al tuo account LinkedIn. Non si collegano altri account e non si sceglie un account: la chiave sa già qual è.
  2. Un solo budget, condiviso con il pilota automatico. Ogni visita, richiesta di collegamento, messaggio, commento, reazione e post fatto dalle API consuma lo stesso budget giornaliero del pilota automatico, con gli stessi limiti e le stesse pause tra un'azione e l'altra. Se le API usano 5 richieste di collegamento, il pilota automatico ne avrà 5 in meno quel giorno. È così che il tuo account resta al sicuro.
  3. Le conversazioni non le conserviamo. Quando leggi una chat dalle API, la leggiamo in quel momento dal tuo account e te la passiamo. Non la salviamo nei nostri archivi né nei nostri log: se ti serve conservarla, la conserva il tuo CRM.

L'accesso alle API si attiva su richiesta, per account: scrivici a info@lkdnautopilot.com.

Indice #

Iniziare in cinque minuti #

  1. Nell'app vai su API e integrazioni (in alto nella pagina iniziale).
  2. Crea una chiave: un nome (per esempio "il mio CRM") e cosa può fare, solo lettura oppure lettura e scrittura. La chiave compare una volta sola: copiala e conservala in un posto sicuro.
  3. Prova:
curl https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/public-api/v1/me \
  -H "Authorization: Bearer lkdn_sk_LA_TUA_CHIAVE"

La risposta è il tuo profilo, lo stato del collegamento e il piano LinkedIn rilevato:

{
  "data": {
    "id": "per_3mQ8...",
    "name": "Giulia Demo",
    "headline": "Consulente, Demo Consulting Srl",
    "public_identifier": "giulia-demo",
    "linkedin_status": "connected",
    "linkedin_plan": "premium",
    "inmail_credits": 5,
    "simulated": false
  }
}

L'indirizzo base di tutte le chiamate è:

https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/public-api/v1

La specifica OpenAPI completa è in openapi.json: importala in Postman, Insomnia o in un generatore di client.

Chiavi e autenticazione #

Ogni richiesta porta la chiave nell'intestazione:

Authorization: Bearer lkdn_sk_...

Limiti e budget condiviso #

Il tuo account LinkedIn ha un budget giornaliero, uguale per il pilota automatico e per le API. Lo decidiamo in un solo punto, con le stesse regole: il piano LinkedIn rilevato, il periodo di riscaldamento di un account nuovo, i limiti che hai scelto tu e quelli che possiamo abbassare noi se l'account è a rischio. Le API non possono superarlo, in nessun caso.

AzioneValore predefinito al giornoEndpoint
Visite con notifica25POST /v1/people/{id}/visits
Richieste di collegamento20 (circa 5 su un account LinkedIn gratuito), 100 in 7 giorniPOST /v1/invitations
Messaggi25POST /v1/conversations, POST /v1/conversations/{id}/messages
InMail3POST /v1/conversations con inmail: true
Commenti10POST /v1/posts/{id}/comments
Reazioni20POST /v1/posts/{id}/reactions
Post2POST /v1/posts
Ritiro di richieste10DELETE /v1/invitations/{id}
Totale delle azioni qui sopra60
Profili letti (senza notifica)40GET /v1/people/{id}
Risultati di ricerca150POST /v1/searches

Sono i valori predefiniti: quelli del tuo account possono essere più bassi (riscaldamento, piano gratuito, limiti scelti da te). Quelli veri, di oggi, sono sempre in GET /v1/usage, insieme a quanto hai già usato e quanto hanno usato le API.

Altre regole che proteggono l'account:

Ogni 429 ha l'intestazione Retry-After: dopo quanti secondi riprovare. Quando il budget del giorno è finito, Retry-After punta all'apertura del prossimo orario di attività. Un agente AI che riceve un 429 deve fermarsi e dirlo, non riprovare in ciclo.

Formato delle risposte #

Le risposte sono JSON, con nomi in inglese e sempre gli stessi:

Ogni risposta ha l'intestazione X-Request-Id: citala se ci scrivi per un problema.

Gli errori seguono lo standard RFC 9457 (Content-Type: application/problem+json):

{
  "type": "https://lkdnautopilot.com/api-doc/#errori-daily_budget_exhausted",
  "title": "The daily budget for this action is used up, by the autopilot and the API together.",
  "status": 429,
  "code": "daily_budget_exhausted",
  "request_id": "req_9f2c...",
  "retry_after": 52310
}

Il campo da leggere nel codice è code: è stabile e non cambia mai significato. L'elenco completo è in Errori.

Idempotency-Key #

Ogni richiesta che agisce (POST e DELETE, tranne POST /v1/searches, che è una lettura) richiede l'intestazione Idempotency-Key: un valore unico scelto da te, da 8 a 200 caratteri, per esempio un UUID.

curl -X POST https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/public-api/v1/invitations \
  -H "Authorization: Bearer lkdn_sk_..." \
  -H "Idempotency-Key: 6b1f0e5e-2f4a-4c39-9a0e-4d8f2f0a1c11" \
  -H "Content-Type: application/json" \
  -d '{"person_id": "per_3mQ8...", "message": "Ciao Marco, ho letto il tuo post sulla logistica e mi è piaciuto."}'

502 action_outcome_unknown vuol dire che LinkedIn non ci ha dato una risposta leggibile e l'azione potrebbe essere partita oppure no. Non riprovare con una chiave nuova: controlla prima (per un messaggio, leggi la conversazione).

Modalità simulata #

Finché il tuo account è in preparazione, le API rispondono con dati simulati (persone di fantasia) e nessuna azione parte davvero: le azioni passano comunque dai limiti e vengono registrate. Le risposte simulate hanno l'intestazione LKDN-Mode: simulated e, per le azioni, "simulated": true. È il modo giusto per costruire e provare un'integrazione senza toccare nessuno.

Endpoint #

Tutti i percorsi sono relativi all'indirizzo base. Lettura: permesso read. Azione: permesso write e Idempotency-Key.

Il tuo account #

GET /v1/me (lettura). Il tuo profilo, linkedin_status (connected, reconnect_required, not_connected), linkedin_plan (free, premium, sales_navigator), i crediti InMail.

GET /v1/usage (lettura). Il budget di oggi, condiviso con il pilota automatico:

{
  "data": {
    "date": "2026-10-25",
    "timezone": "Europe/Rome",
    "linkedin_plan": "premium",
    "invitation_note_available": true,
    "requests": { "today": 41, "per_day": 2000, "per_minute": 60 },
    "budget": {
      "invitations": { "used": 7, "limit": 20, "used_by_api": 2, "weekly_limit": 100 },
      "messages": { "used": 3, "limit": 25, "used_by_api": 3 },
      "total_actions": { "used": 18, "limit": 60 }
    },
    "activity_window": { "open_now": true, "start": "09:00", "end": "18:00", "working_day": true },
    "shared_with_autopilot": true
  }
}

Persone #

GET /v1/people/{id} (lettura). Un profilo: id è un per_... oppure il public_identifier. La persona non viene avvisata. Consuma un "profilo letto". Non restituisce email né telefoni di altre persone.

POST /v1/people/{id}/visits (azione). Visita il profilo: la persona vede che l'hai guardata. Consuma una visita.

GET /v1/people/{id}/posts (lettura). I post recenti della persona. limit fino a 50, cursor.

GET /v1/connections (lettura). I tuoi collegamenti di primo grado, dal più recente. q filtra per nome, limit fino a 200, cursor.

Ricerca #

GET /v1/search/filters?type=location&q=verona (lettura). Trasforma un luogo (type=location) o un settore (type=industry) in un filtro flt_... da usare nella ricerca. Usalo sempre: la ricerca rifiuta un filtro che non viene da qui (400 invalid_filter), perché LinkedIn ignorerebbe in silenzio un filtro sconosciuto e ti darebbe risultati di una ricerca diversa da quella che volevi.

POST /v1/searches (lettura). Cerca persone:

{
  "keywords": "logistica",
  "titles": ["CFO", "direttore finanziario"],
  "locations": ["flt_..."],
  "industries": ["flt_..."],
  "degree": [2, 3],
  "limit": 10
}

Almeno uno tra keywords, titles, locations, industries. limit fino a 10 (fino a 25 con Sales Navigator); ogni risultato consuma un "risultato di ricerca". Per la pagina successiva ripeti la stessa ricerca con cursor. La risposta ha anche page.total_estimate.

Conversazioni #

GET /v1/conversations (lettura). Le tue conversazioni, dalla più recente: type (direct o group), participant_id, kind (message, inmail, sponsored, offer), inbox, unread_count, last_activity_at. unread=true per le sole non lette, limit fino a 100, cursor.

GET /v1/conversations/{id} (lettura). Una conversazione.

GET /v1/conversations/{id}/messages (lettura). I messaggi, dal più recente: text, from_me, sender_id, sent_at, kind, seen, attachments (tipo e nome, senza link). Il testo lo leggiamo dal tuo account in quel momento e non lo conserviamo.

POST /v1/conversations/{id}/messages (azione). Rispondi in una conversazione uno a uno: {"text": "..."} (fino a 8000 caratteri). Le conversazioni di gruppo non sono supportate (422 group_conversation_not_supported).

POST /v1/conversations (azione). Scrivi a una persona: {"person_id": "per_...", "text": "..."}. Funziona con i collegamenti di primo grado (altrimenti 422 not_first_degree). Per un'InMail a chi non è collegato: {"person_id": "per_...", "text": "...", "inmail": true, "subject": "..."}; consuma un'InMail del budget e un credito InMail del tuo piano.

Richieste di collegamento #

GET /v1/invitations?direction=sent (lettura). Le richieste che hai inviato e sono in attesa; direction=received per quelle ricevute.

POST /v1/invitations (azione). {"person_id": "per_...", "message": "..."}. La nota (message, fino a 300 caratteri) non è disponibile su un account LinkedIn gratuito (403 invitation_note_not_available): su quel piano la richiesta parte senza nota. Già collegati: 409 already_connected; richiesta già in attesa: 409 already_invited.

DELETE /v1/invitations/{id} (azione). Ritira una richiesta inviata e ancora in attesa.

Post #

GET /v1/posts/{id} (lettura). Un post: id è un pst_..., un URN di LinkedIn (urn:li:activity:...) o il numero dell'attività.

GET /v1/posts/{id}/comments, GET /v1/posts/{id}/reactions (lettura). Commenti e reazioni.

POST /v1/posts (azione). Pubblica un post sul tuo profilo: {"text": "..."} (fino a 3000 caratteri).

POST /v1/posts/{id}/comments (azione). {"text": "...", "reply_to": "cmt_..."}; reply_to per rispondere a un commento. L'autore del post riceve una notifica.

POST /v1/posts/{id}/reactions (azione). {"type": "like"}; type è uno tra like, celebrate, support, love, insightful, funny.

Aziende #

GET /v1/companies/{id} (lettura). Una pagina aziendale: id è un cmp_... oppure il nome pubblico dopo linkedin.com/company/.

Webhook #

GET /v1/webhooks (lettura), POST /v1/webhooks (azione), DELETE /v1/webhooks/{id} (azione). Vedi Webhook.

Webhook #

Invece di chiedere ogni minuto "ci sono novità?", ti chiamiamo noi. Crea un webhook dall'app (API e integrazioni) oppure con:

curl -X POST https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/public-api/v1/webhooks \
  -H "Authorization: Bearer lkdn_sk_..." -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  -d '{"url": "https://crm.example.com/hooks/lkdn", "events": ["message.received", "connection.accepted"]}'

La risposta contiene il segreto di firma (whsec_...) una volta sola.

EventoQuandodata
message.receiveduna persona ti scriveconversation_id, message_id, sender_id, sent_at
connection.accepteduna richiesta di collegamento viene accettata (può arrivare fino a 8 ore dopo)person_id, public_identifier, accepted_at
invitation.sentparte una richiesta di collegamento, dal pilota automatico o dalle APIinvitation_id, person_id, source (autopilot o api), sent_at
account.status_changedil collegamento del tuo account cambia statostatus, previous_status, changed_at; stati: connected, connecting, reconnect_required, disconnected, error

Il corpo della chiamata:

{
  "id": "evt_0b6f1f40000040008000000000000001",
  "type": "message.received",
  "created_at": "2026-10-25T10:00:03.000Z",
  "data": {
    "conversation_id": "cnv_...",
    "message_id": "msg_...",
    "sender_id": "per_...",
    "sent_at": "2026-10-25T10:00:00.000Z"
  }
}

Il testo del messaggio non viaggia nel webhook: leggilo con GET /v1/conversations/{conversation_id}/messages. Così nessun testo resta nelle nostre code di invio.

Ogni chiamata ha le intestazioni LKDN-Event-Id, LKDN-Event-Type e LKDN-Signature: t=<secondi unix>,v1=<firma>. La firma è l'HMAC-SHA256, in esadecimale, della stringa <t>.<corpo esatto> con il tuo segreto. Verificala sempre, e rifiuta una chiamata più vecchia di 5 minuti:

// Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, rawBody, header, now = Math.floor(Date.now() / 1000)) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
  if (!m || Math.abs(now - Number(m[1])) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
# Python
import hashlib, hmac, re, time

def verify(secret: str, raw_body: str, header: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not m or abs(time.time() - int(m.group(1))) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{m.group(1)}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, m.group(2))

Rispondi con un codice 2xx entro 10 secondi. Altrimenti riproviamo dopo 1, 5 e 30 minuti, poi 2, 6, 12 e 24 ore; dopo 8 tentativi l'evento è perso, e dopo 20 eventi persi di fila il webhook si spegne (lo riaccendi dall'app). Un 410 Gone lo spegne subito per quell'evento. Lo stesso evento può arrivare due volte: usa id per riconoscerlo. Accettiamo solo indirizzi https sulla porta 443, mai indirizzi interni o privati, e non seguiamo i redirect.

Server MCP: Claude, ChatGPT, n8n #

Il server MCP espone le stesse funzioni come strumenti per un assistente AI, con la stessa chiave e gli stessi limiti: non esiste un percorso che li salti. Gli strumenti che agiscono lo dicono nella loro descrizione, e chiedono all'assistente di mostrarti il testo esatto e aspettare il tuo sì.

Indirizzo:

https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/mcp

Autenticazione: Authorization: Bearer lkdn_sk_.... Per i programmi che non permettono di impostare un'intestazione esiste la forma con la chiave nell'indirizzo, .../functions/v1/mcp/lkdn_sk_...: trattalo come una password, perché chi ha l'indirizzo ha la chiave. Usa per questo una chiave dedicata, e di sola lettura se all'assistente basta leggere.

Strumenti: get_my_profile, get_usage, get_person, visit_profile, list_person_posts, list_connections, find_search_filters, search_people, list_conversations, get_conversation_messages, send_message, start_conversation, list_invitations, send_invitation, withdraw_invitation, get_post, list_post_comments, list_post_reactions, publish_post, comment_on_post, react_to_post, get_company.

Claude Code #

claude mcp add --transport http lkdn-autopilot \
  https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/mcp \
  --header "Authorization: Bearer lkdn_sk_..."

Claude Desktop #

In Impostazioni, Sviluppatore, Modifica configurazione (claude_desktop_config.json), con il ponte mcp-remote (serve Node.js):

{
  "mcpServers": {
    "lkdn-autopilot": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/mcp",
               "--header", "Authorization:Bearer ${LKDN_API_KEY}"],
      "env": { "LKDN_API_KEY": "lkdn_sk_..." }
    }
  }
}

Riavvia Claude Desktop: gli strumenti compaiono nel menu degli strumenti.

Claude.ai (connettore personalizzato) #

In Impostazioni, Connettori, Aggiungi connettore personalizzato: come URL usa la forma con la chiave nell'indirizzo, https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/mcp/lkdn_sk_..., e lascia vuoti i campi OAuth. Il connettore personalizzato è disponibile nei piani di Claude che lo prevedono.

ChatGPT #

In Impostazioni, App e connettori, con la modalità sviluppatore attiva: crea un connettore con l'URL nella forma con la chiave nell'indirizzo e autenticazione "nessuna". Gli strumenti che agiscono chiedono conferma prima di partire.

n8n #

Nel nodo MCP Client Tool di un AI Agent: Endpoint https://ayuhpqrbmizgsjstmcrt.supabase.co/functions/v1/mcp, trasporto HTTP Streamable, autenticazione Bearer con la tua chiave. Per un flusso senza AI usa il nodo HTTP Request sulle API REST, con una credenziale "Header Auth" (Authorization = Bearer lkdn_sk_...), e un nodo Webhook come destinazione dei nostri webhook.

Prompt pronti #

Da incollare in Claude o ChatGPT con il server MCP collegato.

Le risposte di oggi

Guarda le mie conversazioni non lette di LinkedIn. Per ognuna dimmi chi è la persona (nome, ruolo, azienda),
cosa mi ha scritto in una riga e se secondo te serve una risposta oggi. Non rispondere a nessuno: proponimi
una bozza di risposta solo per quelle che contano, e aspetta il mio sì su ogni testo.

Preparare un incontro

Leggi il profilo LinkedIn di <nome o link> e i suoi ultimi post. Preparami cinque righe: chi è, di cosa si
occupa adesso, cosa ha scritto di recente, un punto in comune con me e una domanda intelligente da fargli.
Non visitare il profilo con notifica e non scrivergli.

Trovare persone per un segmento

Cerca su LinkedIn titolari e direttori di studi dentistici in Veneto (usa find_search_filters per il luogo).
Mostrami i primi 10 in tabella con nome, ruolo, città e grado di collegamento. Prima di cercare dimmi
quanti risultati di ricerca mi restano oggi (get_usage). Non mandare richieste di collegamento.

Richieste di collegamento con una nota vera

Per ognuna di queste persone <elenco di link>: leggi il profilo, scrivimi una nota di collegamento di massimo
250 caratteri, in italiano, che parli di qualcosa di vero del suo profilo, senza vendere nulla e senza link.
Mostrami tutte le note. Invia solo quelle che approvo, una alla volta, e fermati se il budget del giorno finisce.

Rispondere ai commenti sui miei post

Prendi il mio ultimo post (cercalo tra i post del mio profilo) e leggi i commenti. Per quelli a cui non ho
ancora risposto proponimi una risposta breve e cordiale. Pubblica solo le risposte che approvo.

Il riepilogo della settimana

Dimmi quante richieste di collegamento ho in attesa da più di due settimane e chi sono. Proponimi quali
ritirare. Non ritirarne nessuna senza il mio ok.

Regole che valgono per ogni prompt: l'assistente non deve inventare fatti sulla persona, deve mostrarti ogni testo prima di inviarlo, e quando riceve action_paced, daily_budget_exhausted o outside_activity_window deve fermarsi e dirtelo.

Errori #

Ogni errore ha un code stabile: è un contratto, non cambia mai significato. Leggi il code, non il testo.

StatocodeCosa vuol dire, cosa fare
400invalid_requestun campo manca o non è valido (field, detail dicono quale)
400invalid_cursorcursore malformato, scaduto o di un altro elenco: riparti dalla prima pagina
400invalid_filterun filtro di ricerca non viene da GET /v1/search/filters
400idempotency_key_requiredmanca Idempotency-Key su una richiesta che agisce
401invalid_api_keychiave mancante, sconosciuta o revocata
403insufficient_scopela chiave non ha il permesso write
403api_access_not_enabledle API non sono attive per il tuo account
403subscription_inactivel'abbonamento non è attivo
403person_not_contactablequesta persona non va contattata da questo account
403invitation_note_not_availablesul tuo piano LinkedIn la richiesta non può avere una nota
403linkedin_feature_missingserve un prodotto LinkedIn che l'account non ha
404not_foundla risorsa non esiste o non è del tuo account
404route_not_foundl'endpoint non esiste
405method_not_allowedl'endpoint esiste ma non con questo metodo
409linkedin_not_connectednessun account LinkedIn collegato: collegalo dall'app
409linkedin_reconnect_requiredl'account va ricollegato dall'app
409linkedin_account_unavailablel'account è sospeso per ora
409account_pausedil nostro team ha messo in pausa le azioni di questo account
409idempotency_in_progressla richiesta con questa Idempotency-Key è ancora in corso o non è terminata
409already_connectedsiete già collegati
409already_invitedc'è già una richiesta in attesa verso questa persona
422idempotency_key_reusedla stessa Idempotency-Key con una richiesta diversa
422invitation_not_allowedLinkedIn non permette un'altra richiesta a questa persona: definitivo
422recipient_unreachablela persona non è raggiungibile
422not_first_degreeper un messaggio serve un collegamento di primo grado: invia una richiesta o un'InMail
422inmail_not_alloweda questa persona non si può mandare un'InMail
422insufficient_inmail_creditscrediti InMail finiti
422comments_disabledi commenti sono chiusi su questo post
422group_conversation_not_supportedle conversazioni di gruppo non sono supportate
422action_refusedLinkedIn ha rifiutato l'azione
422invalid_webhookil webhook non si può creare (reason dice perché)
429rate_limitedtroppe richieste per questa chiave: aspetta Retry-After
429daily_request_limitrichieste del giorno finite per questo account
429daily_budget_exhaustedil budget del giorno per questa azione è finito, pilota automatico e API insieme
429weekly_budget_exhaustedil budget settimanale di richieste di collegamento è finito
429action_pacedazioni distanziate per proteggere l'account: aspetta Retry-After
429outside_activity_windowfuori dal tuo orario di attività
429linkedin_rate_limitedLinkedIn sta limitando l'account in questo momento: riprova più tardi
500internal_errorun errore nostro
502linkedin_errorLinkedIn ha dato una risposta che non sappiamo leggere
502action_outcome_unknownl'azione potrebbe essere partita oppure no: controlla prima di riprovare
503api_unavailablele API sono spente in questo momento
503service_not_configuredil servizio non è ancora configurato
503linkedin_unavailableLinkedIn non ha risposto in tempo: riprova più tardi

Privacy e responsabilità #

Cosa non fa #