| name | unipile-v2-posts-and-webhooks |
| description | Padrão v2 para ENGAJAMENTO em posts do LinkedIn (curtir, comentar, listar posts — sempre por social_id) e para RECEBER webhooks (novas mensagens, status de conta, tracking de e-mail, nova relação/convite aceito). Use ao implementar warming por post, reações/comentários, ou o handler de webhook (runtime/src/webhook.ts). Requer [[unipile-v2-foundations]]. |
Unipile v2 — Posts e Webhooks
Rotas de post são /v2/{account_id}/posts/.... Ver [[unipile-v2-foundations]].
Regra de ouro dos posts: use social_id, não id
Toda ação (reagir/comentar/listar) usa o social_id no formato urn:li:activity:...
(ou urn:li:ugcPost:... / urn:li:share:...). O id numérico é fonte de bug.
- Listar posts de um usuário/empresa:
GET /v2/{account_id}/users/{user_id}/posts
→ use o campo social_id de cada item. ✅ Nosso listRecentPosts prioriza social_id.
Reagir a um post — POST /v2/{account_id}/posts/{post_id}/reactions
({post_id} = social_id.) Body: { "reaction": "<tipo>" }. No LinkedIn o tipo é o
enum linkedin_like | linkedin_celebrate | linkedin_support | linkedin_love | linkedin_insightful | linkedin_funny. ✅ Nosso reactToPost normaliza like →
linkedin_like. (Reagir a um comentário: .../comments/{comment_id}/reactions.)
Comentar um post — POST /v2/{account_id}/posts/{post_id}/comments
Body: { "text": "…", "comment_as": "<user_id p/ comentar como org (opcional)>", "attachments": [...] }. Responder a um comentário: .../comments/{comment_id}.
Webhooks (recebimento)
Os payloads de webhook são o formato realtime da Unipile e não mudaram na v2 (são
independentes do versionamento das rotas REST). Gestão de endpoints:
POST /v2/webhooks/endpoints/. Regra operacional: responda 200 em < 30s;
webhooks são at-least-once → dedup por id externo (message_id, event_id…).
Novas mensagens — event: message_received
{ "account_id":"…", "account_type":"LINKEDIN",
"account_info": { "feature":"classic|sales_navigator|recruiter", "user_id":"<provider_id do dono>" },
"event":"message_received", "chat_id":"…", "message_id":"…", "message":"…",
"sender": { "attendee_provider_id":"…", "attendee_name":"…" },
"attendees":[…], "timestamp":"…" }
(Outros: message_reaction|message_read|message_edited|message_deleted|message_delivered.)
Enviadas também entram como message_received → compare account_info.user_id com
sender.attendee_provider_id p/ saber se fomos nós. Resposta de prospect = "mão
levantada" (use chat_id p/ responder).
Status de conta — payload aninhado sob AccountStatus
{ "AccountStatus": { "account_id":"…", "account_type":"LINKEDIN", "message":"CREDENTIALS" } }
Valores de message: OK, CREDENTIALS, ERROR, STOPPED, CONNECTING, DELETED,
CREATION_SUCCESS, RECONNECTED, SYNC_SUCCESS. ✅ Nosso handler pausa a conta só em
status não-saudável (fora de OK/CONNECTING/CREATION_SUCCESS/RECONNECTED/SYNC_SUCCESS).
Tracking de e-mail — event: mail_opened | mail_link_clicked
{ "event":"mail_link_clicked", "event_id":"…", "tracking_id":"…",
"email_id":"…", "account_id":"…", "url":"…", "label":"…", "date":"…" }
Correlacione por tracking_id (persistido no envio) → opened/clicked. Só chega se
o envio ligou tracking_options (ver [[unipile-v2-messaging]]).
Nova relação / convite aceito — event: new_relation
{ "event":"new_relation", "account_id":"…", "account_type":"LINKEDIN",
"user_provider_id":"…", "user_public_identifier":"…", "user_full_name":"…" }
Não é real-time (LinkedIn não suporta): a Unipile faz polling e pode atrasar. É como
detectamos aceite de convite p/ avançar a cadência (connection_accepted).
Novos e-mails — event: mail_received
Traz o remetente em from_attendee.identifier (canal=email), sem sender/account_type.
Polling (tier local, sem webhook público) — rotas confirmadas (spec 2.16.0)
O daemon local não recebe webhook; a reconciliação é por polling REST. Shapes
confirmados contra a spec real + conta viva:
- Mensagens: NÃO existe
GET /v2/{acc}/messages top-level. E no LinkedIn o
GET /v2/{acc}/chats global responde 501 ("Use List inbox Chats endpoint"):
os chats vivem em inboxes por produto — GET /v2/{acc}/inboxes lista
CLASSIC_{PRIMARY,ARCHIVED,SPAM,JOBS,INMAIL,STARRED} +
SALES_NAVIGATOR_{PRIMARY,UNREAD,SENT,...}; respostas de prospect chegam nos
*_PRIMARY/*_INMAIL. Caminho completo:
GET /chats?after (501 no LinkedIn → GET /inboxes → GET /inboxes/{id}/chats?after
dos *_PRIMARY/*_INMAIL) → GET /chats/{chat_id}/messages?after por chat.
No item de mensagem REST, o remetente é sender_id (o
sender.attendee_provider_id é shape de webhook, não do REST) e há
is_sender: boolean (fomos nós). ✅ listRecentMessages implementa isso.
- ⚠️ Espaços de ID por produto:
sender_id do chat classic vem no espaço
Classic (ACoAAA...), mas a captura do Sales Nav guarda ACwAAA... —
não casam diretamente. GET /v2/{acc}/users/{ACw...} devolve o provider_id
Classic (conversão confirmada na conta viva); normalize na captura antes de
correlacionar polling→prospect.
- Relations:
GET /v2/{acc}/users/me/relations (200 confirmado com
user_id="me"). ⚠️ No item, id é o ID da relação — o provider id do
usuário relacionado vem em user.id. ✅ listRelations mapeia user.id.
- Follow (contexto: warming):
POST /v2/{acc}/users/me/follow/{user_id} e
endorse: POST /v2/{acc}/linkedin/member/{id}/endorse-skill com
skill_id = endorsement_id do perfil (with_sections=linkedin_skills) —
detalhes em [[unipile-v2-messaging]].
Mapa código → endpoint/evento v2
| Nosso | v2 |
|---|
reactToPost / linkedin_like_recent_posts | POST /v2/{acc}/posts/{social_id}/reactions (reaction: linkedin_*) |
commentOnPost / linkedin_comment_last_post | POST /v2/{acc}/posts/{social_id}/comments |
listRecentPosts | GET /v2/{acc}/users/{id}/posts (usar social_id) |
listRecentMessages (polling local) | GET /chats?after (LinkedIn: 501 → GET /inboxes/{id}/chats?after dos *_PRIMARY/*_INMAIL) → GET /chats/{id}/messages?after (sender_id) |
listRelations (polling local) | GET /v2/{acc}/users/me/relations (provider id em user.id) |
handleWebhook (runtime) | payloads acima (dedup + AccountStatus aninhado + new_relation) |
Handler: runtime/src/webhook.ts. Fundamentos → [[unipile-v2-foundations]].