@saiprasad4/seamless-api-sdk
v0.1.1
Published
Unofficial, typed, zero-dependency TypeScript SDK for the Seamless.AI public API. Search contacts and companies, run async email research, and parse credit and rate-limit headers.
Maintainers
Readme
@saiprasad4/seamless-api-sdk
A small, typed, zero-dependency TypeScript SDK for the Seamless.AI public API.
Search contacts and companies, run the asynchronous email-research (reveal) flow, and read credit and rate-limit headers, without re-learning the API's undocumented quirks.
Unofficial & community-maintained. Not affiliated with, endorsed by, or sponsored by Seamless.AI, Inc. "Seamless.AI" is a trademark of its respective owner. This package only calls the documented public API on your behalf, using your own API key.
Why this exists
The Seamless public API has a few traps that cost real time to discover:
- It authenticates with a bare
Token:header, notAuthorization: Bearer. - Email reveal is two-phase and asynchronous: a free
searchreturnssearchResultIds, then a paidresearchcall returnsrequestIds you poll until the email resolves. - Responses mix camelCase and snake_case, and records arrive under different envelope keys (
contacts,results,data). - Credit balance and rate-limit state live in response headers (
X-PublicAPI-Credits,X-RateLimit-*), not the body.
The SDK wraps all of that behind a typed surface. When the vendor renames a field, every result still carries the untouched raw record, so nothing drops silently.
Install
npm install @saiprasad4/seamless-api-sdkRequires Node 18 or newer (uses the global fetch). Works in any modern runtime with fetch (Deno, Bun, edge); you can also inject your own via options.fetch.
Quick start
import { SeamlessClient } from '@saiprasad4/seamless-api-sdk';
const client = new SeamlessClient();
const apiKey = process.env.SEAMLESS_API_KEY!;
// 1) Free search: find marketing decision-makers at Indian D2C brands
const page = await client.searchContacts(apiKey, {
jobTitle: ['Head of Marketing', 'CMO', 'Marketing Manager'],
contactCountry: ['India'],
companyKeyword: ['D2C', 'consumer brand'],
limit: 25,
});
console.log(`Found ${page.data.results.length} contacts, ${page.creditsRemaining} credits left`);
// 2) Paid reveal: research and poll until emails resolve, in one call
const revealed = await client.researchAndWait(
apiKey,
page.data.results.map((c) => c.searchResultId),
{ maxWaitMs: 60_000 },
);
for (const r of revealed) {
if (r.status === 'completed') console.log(r.email, r.phone);
}Bring-your-own key pool (by design)
Every method takes the API key as the first argument instead of holding one. That's deliberate: it lets you own key rotation and credit metering without the SDK baking in any policy. The SeamlessCall envelope returns creditsRemaining and rateLimit on every response so your pool can decide when to rotate or back off:
class KeyPool {
constructor(private keys: string[]) {}
private i = 0;
next() { return this.keys[this.i++ % this.keys.length]; }
}
const pool = new KeyPool(loadKeysFromVault());
const res = await client.searchContacts(pool.next(), filters);
if ((res.creditsRemaining ?? Infinity) < 50) pool.retire(/* current key */);Use one key per Seamless account and stay within your plan's terms. This SDK ships no credit-multiplying behavior; rotation policy is yours to define responsibly.
Error handling
Every failure is a SeamlessError subclass, so you can branch precisely:
import {
SeamlessAuthError, // 401/403: key invalid or out of permission
SeamlessRateLimitError, // 429: has .retryAfterMs
SeamlessApiError, // other 4xx/5xx
SeamlessNetworkError, // network failure / timeout (no response)
} from '@saiprasad4/seamless-api-sdk';
try {
await client.searchContacts(key, filters);
} catch (e) {
if (e instanceof SeamlessAuthError) pool.retire(key);
else if (e instanceof SeamlessRateLimitError) await wait(e.retryAfterMs ?? 5000);
else throw e;
}Transient failures (429, 5xx, network/timeout) are retried automatically with linear backoff (maxRetries, default 3). Auth and other 4xx errors are terminal and never retried.
API
new SeamlessClient(options?: {
baseUrl?: string; // default https://api.seamless.ai/api/client/v1
timeoutMs?: number; // default 30000
maxRetries?: number; // default 3
retryDelayMs?: number; // default 1000 (linear: delay * attempt)
logger?: { warn(msg): void };
fetch?: typeof fetch; // inject for testing / proxies
})| Method | Cost | Returns |
| --- | --- | --- |
| searchContacts(key, filters) | free | SeamlessCall<SeamlessSearchPage<SeamlessContactResult>> |
| searchCompanies(key, filters) | free | SeamlessCall<SeamlessSearchPage<SeamlessCompanyResult>> |
| researchContacts(key, searchResultIds) | 1 credit/contact | SeamlessCall<{ requestIds: string[] }> |
| pollResearch(key, requestIds) | free | SeamlessCall<{ results: SeamlessResearchResult[] }> |
| researchAndWait(key, searchResultIds, opts?) | 1 credit/contact | SeamlessResearchResult[] |
All result objects include a raw: Record<string, unknown> with the untouched vendor record.
Status & contributing
v0.x. The search, research and poll core is covered by tests against a mocked transport. Field names follow the public docs. If you hit a response shape the mappers miss, the data is still in raw, and a PR adding the candidate key is welcome.
License
MIT © Saiprasad Shankar
