Files
lead-scraper/lib/services/vayne.ts
Timo Uttenweiler facf8c9f69 Initial commit: LeadFlow lead generation platform
Full-stack Next.js 16 app with three scraping pipelines:
- AirScale CSV → Anymailfinder Bulk Decision Maker search
- LinkedIn Sales Navigator → Vayne → Anymailfinder email enrichment
- Apify Google SERP → domain extraction → Anymailfinder bulk enrichment

Includes Docker multi-stage build + docker-compose for Coolify deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:21:11 +01:00

154 lines
4.3 KiB
TypeScript

// Vayne API integration
// Docs: https://www.vayne.io (OpenAPI spec available at /api endpoint)
// Auth: Authorization: Bearer <api_token>
// Base URL: https://www.vayne.io
//
// Flow:
// 1. POST /api/orders with { url, limit, name, email_enrichment: false, export_format: "simple" }
// 2. Poll GET /api/orders/{id} until scraping_status === "finished" | "failed"
// 3. POST /api/orders/{id}/export with { export_format: "simple" }
// 4. Poll GET /api/orders/{id} until exports[0].status === "completed"
// 5. Download CSV from exports[0].file_url (S3 presigned URL)
import axios from "axios";
import Papa from "papaparse";
const BASE_URL = "https://www.vayne.io";
export interface VayneOrder {
id: number;
name: string;
order_type: string;
scraping_status: "initialization" | "pending" | "segmenting" | "scraping" | "finished" | "failed";
limit: number;
scraped: number;
created_at: string;
exports?: Array<{ status: "completed" | "pending" | "not_started"; file_url?: string }>;
}
export interface LeadProfile {
firstName: string;
lastName: string;
fullName: string;
title: string;
company: string;
companyDomain: string;
linkedinUrl: string;
location: string;
}
export async function createOrder(
salesNavUrl: string,
maxResults: number,
apiToken: string,
orderName?: string
): Promise<VayneOrder> {
const response = await axios.post(
`${BASE_URL}/api/orders`,
{
url: salesNavUrl,
limit: maxResults,
name: orderName || `LeadFlow-${Date.now()}`,
email_enrichment: false,
export_format: "simple",
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
timeout: 30000,
}
);
return response.data.order;
}
export async function getOrderStatus(
orderId: number,
apiToken: string
): Promise<VayneOrder> {
const response = await axios.get(`${BASE_URL}/api/orders/${orderId}`, {
headers: { Authorization: `Bearer ${apiToken}` },
timeout: 15000,
});
return response.data.order;
}
export async function triggerExport(
orderId: number,
apiToken: string
): Promise<VayneOrder> {
const response = await axios.post(
`${BASE_URL}/api/orders/${orderId}/export`,
{ export_format: "simple" },
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
timeout: 15000,
}
);
return response.data.order;
}
export async function downloadOrderCSV(
fileUrl: string
): Promise<LeadProfile[]> {
const response = await axios.get(fileUrl, {
timeout: 60000,
responseType: "text",
});
return parseVayneCSV(response.data);
}
function parseVayneCSV(csvContent: string): LeadProfile[] {
const { data } = Papa.parse<Record<string, string>>(csvContent, {
header: true,
skipEmptyLines: true,
});
return data.map((row) => {
// Vayne simple format columns (may vary; handle common variants)
const fullName = row["Name"] || row["Full Name"] || row["full_name"] || "";
const nameParts = fullName.trim().split(/\s+/);
const firstName = nameParts[0] || "";
const lastName = nameParts.slice(1).join(" ") || "";
// Extract domain from company URL or website column
const companyUrl = row["Company URL"] || row["company_url"] || row["Website"] || "";
let companyDomain = "";
if (companyUrl) {
try {
const u = new URL(companyUrl.startsWith("http") ? companyUrl : `https://${companyUrl}`);
companyDomain = u.hostname.replace(/^www\./i, "");
} catch {
companyDomain = companyUrl.replace(/^www\./i, "").split("/")[0];
}
}
return {
firstName,
lastName,
fullName,
title: row["Job Title"] || row["Title"] || row["title"] || "",
company: row["Company"] || row["Company Name"] || row["company"] || "",
companyDomain,
linkedinUrl: row["LinkedIn URL"] || row["linkedin_url"] || row["Profile URL"] || "",
location: row["Location"] || row["location"] || "",
};
});
}
export async function checkLinkedInAuth(apiToken: string): Promise<boolean> {
try {
const response = await axios.get(`${BASE_URL}/api/linkedin/status`, {
headers: { Authorization: `Bearer ${apiToken}` },
timeout: 10000,
});
return response.data?.connected === true;
} catch {
return false;
}
}