@signaliz/sdk 1.0.59 → 1.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,10 @@ const verified = await signaliz.verifyEmail(found.email!, {
44
44
  skipCache: true,
45
45
  });
46
46
 
47
+ // Syntax failures are terminal local results, with no HTTP or provider call.
48
+ const malformed = await signaliz.verifyEmail('badformat@@gmail..com');
49
+ console.log(malformed.success, malformed.isMalformed, malformed.verificationVerdict);
50
+
47
51
  // The SDK waits for long checks by default. For an agent handoff, return early
48
52
  // and resume the exact provider run later without duplicate spend.
49
53
  const queuedVerification = await signaliz.verifyEmail('jane@example.com', {
@@ -69,6 +73,12 @@ const signalDiscovery = await signaliz.discoverSignals({
69
73
  limit: 100,
70
74
  sources: ['web', 'news'],
71
75
  });
76
+ const painSignalDiscovery = await signaliz.discoverSignals({
77
+ query: 'companies showing signals they need account-signal automation',
78
+ icpContext: 'B2B SaaS companies with outbound sales teams and manual account research',
79
+ limit: 100,
80
+ sources: ['web', 'news'],
81
+ });
72
82
  const completedDiscovery = signalDiscovery.signalSearchRunId
73
83
  ? await signaliz.discoverSignals({ signalSearchRunId: signalDiscovery.signalSearchRunId })
74
84
  : signalDiscovery;
@@ -150,12 +160,15 @@ Signal to Copy also shares company research across copy recipients.
150
160
  Signals Everything is query-first rather than a row batch. It defaults to 100
151
161
  distinct companies and supports up to 1,000. Every returned signal contains a
152
162
  company name, company domain, dated evidence, and a source URL. The typed
153
- `costGuardrail` reports both projected and observed source acquisition cost;
163
+ `costGuardrail` reports projected and observed source acquisition cost plus
164
+ identity-resolution cost, receipt source, and fallback-provider request count;
154
165
  Signaliz returns signals only when cost stays strictly below $0.01 per qualified
155
166
  company. A request is a ceiling, not a promise: it can return fewer results
156
167
  when public evidence cannot verify both a date and company identity. It is
157
168
  included on Team and Agency plans and can be resumed with
158
- `signalSearchRunId` without duplicate source acquisition.
169
+ `signalSearchRunId` without duplicate source acquisition. Pass `icpContext` to
170
+ ground an open-ended request such as “companies showing signals they need our
171
+ product” in the user's target market.
159
172
 
160
173
  Company Signal Enrichment defaults `online` and `enableDeepSearch` to `true`
161
174
  unless the caller explicitly disables them. Its `searchMode` defaults to
@@ -166,6 +179,13 @@ complete, evidence-backed email variations when supported signals are available.
166
179
  Signal to Copy is cache-first and defaults `enableDeepSearch` to `false` so it
167
180
  normally finishes within an agent turn; set `skipCache` or `enableDeepSearch`
168
181
  only when fresh or deeper research is required.
182
+ Every core request uses the authenticated workspace. Find Email and Verify Email
183
+ honor that workspace's configured cache window; eligible hits cost 0 credits,
184
+ while fresh discovery or verification is charged on every plan. Team, Agency,
185
+ and Pay-As-You-Go include fresh Company Signals and Signal to Copy. Those plans
186
+ still check eligible workspace signal data first even when Company Signals is
187
+ set to No cache. `lookbackDays` remains a hard evidence-age bound for both
188
+ cached and fresh Company Signals and Signal to Copy results.
169
189
  Unclassified evidence is returned by signal enrichment but is not used to
170
190
  generate outreach copy.
171
191
  Each normalized signal keeps `date` separate from crawl-time `detectedAt` and
@@ -9,17 +9,21 @@ var SignalizError = class extends Error {
9
9
  this.details = data.details;
10
10
  }
11
11
  get isRetryable() {
12
+ if (typeof this.details?.retry_eligible === "boolean") {
13
+ return this.details.retry_eligible;
14
+ }
12
15
  return this.errorType === "rate_limited" || this.errorType === "timeout" || this.errorType === "provider_error";
13
16
  }
14
17
  };
15
18
  function parseError(status, body) {
16
19
  if (body?.error && typeof body.error === "object") {
20
+ const nestedDetails = body.error.details && typeof body.error.details === "object" ? body.error.details : {};
17
21
  return new SignalizError({
18
22
  code: body.error.code || `HTTP_${status}`,
19
23
  message: body.error.message || `Request failed with status ${status}`,
20
24
  errorType: mapErrorType(body.error.code, status),
21
25
  retryAfter: retryAfterSeconds(body.error),
22
- details: body.error.details
26
+ details: mergePublicErrorDetails({ ...body, ...body.error }, nestedDetails)
23
27
  });
24
28
  }
25
29
  const code = typeof body?.error_code === "string" && body.error_code.trim() ? body.error_code.trim() : `HTTP_${status}`;
@@ -28,7 +32,7 @@ function parseError(status, body) {
28
32
  message: body?.message || body?.error || `Request failed with status ${status}`,
29
33
  errorType: mapErrorType(code, status),
30
34
  retryAfter: retryAfterSeconds(body),
31
- details: body?.details || publicRetryDetails(body)
35
+ details: mergePublicErrorDetails(body, body?.details)
32
36
  });
33
37
  }
34
38
  function retryAfterSeconds(body) {
@@ -40,13 +44,26 @@ function publicRetryDetails(body) {
40
44
  ...body?.retry_eligible !== void 0 ? { retry_eligible: body.retry_eligible } : {},
41
45
  ...body?.retry_after !== void 0 ? { retry_after: body.retry_after } : {},
42
46
  ...body?.retry_after_seconds !== void 0 ? { retry_after_seconds: body.retry_after_seconds } : {},
47
+ ...body?.retry_strategy !== void 0 ? { retry_strategy: body.retry_strategy } : {},
48
+ ...body?.credits_used !== void 0 ? { credits_used: body.credits_used } : {},
49
+ ...body?.credits_charged !== void 0 ? { credits_charged: body.credits_charged } : {},
50
+ ...body?.job_id !== void 0 ? { job_id: body.job_id } : {},
51
+ ...body?.verification_run_id !== void 0 ? { verification_run_id: body.verification_run_id } : {},
43
52
  ...body?.run_id !== void 0 ? { run_id: body.run_id } : {},
53
+ ...body?.suggested_action !== void 0 ? { suggested_action: body.suggested_action } : {},
44
54
  ...body?.signal_run_id !== void 0 ? { signal_run_id: body.signal_run_id } : {},
45
55
  ...body?.company_signal_run_id !== void 0 ? { company_signal_run_id: body.company_signal_run_id } : {},
46
56
  ...body?.resume_context_persisted !== void 0 ? { resume_context_persisted: body.resume_context_persisted } : {}
47
57
  };
48
58
  return Object.keys(details).length > 0 ? details : void 0;
49
59
  }
60
+ function mergePublicErrorDetails(body, explicitDetails) {
61
+ const merged = { ...publicRetryDetails(body) || {} };
62
+ if (explicitDetails && typeof explicitDetails === "object" && !Array.isArray(explicitDetails)) {
63
+ Object.assign(merged, explicitDetails);
64
+ }
65
+ return Object.keys(merged).length > 0 ? merged : void 0;
66
+ }
50
67
  function mapErrorType(code, status) {
51
68
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
52
69
  if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
@@ -108,7 +125,9 @@ var HttpClient = class {
108
125
  const errBody = await res.json().catch(() => ({}));
109
126
  const err = parseError(res.status, errBody);
110
127
  if (err.isRetryable && attempt < this.maxRetries) {
111
- const wait = err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
128
+ const retryWithNewKey = err.details?.retry_strategy === "new_idempotency_key" && err.details?.credits_used === 0 && err.details?.credits_charged === 0;
129
+ if (retryWithNewKey) throw err;
130
+ const wait = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
112
131
  await sleep(wait);
113
132
  lastError = err;
114
133
  continue;
@@ -408,6 +427,29 @@ var Signaliz = class {
408
427
  );
409
428
  }
410
429
  async verifyEmail(email, options) {
430
+ const normalizedEmail = typeof email === "string" ? email.trim() : "";
431
+ if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
432
+ const error = `Invalid email format: "${normalizedEmail}"`;
433
+ return normalizeVerifyEmailResult(normalizedEmail, {
434
+ success: false,
435
+ email: normalizedEmail,
436
+ status: "completed",
437
+ is_valid: false,
438
+ is_deliverable: false,
439
+ email_verified: false,
440
+ verified_for_sending: false,
441
+ is_malformed: true,
442
+ verification_status: "malformed",
443
+ verification_verdict: "malformed",
444
+ verification_source: "input_validation",
445
+ provider_status: "rejected_before_provider",
446
+ error,
447
+ error_code: "INPUT_001",
448
+ retry_eligible: false,
449
+ credits_used: 0,
450
+ live_provider_called: false
451
+ });
452
+ }
411
453
  const request = compact({
412
454
  email: email || void 0,
413
455
  verification_run_id: options?.verificationRunId,
@@ -742,7 +784,10 @@ var Signaliz = class {
742
784
  );
743
785
  const uniqueTasks = deduplicated.uniqueItems;
744
786
  const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
745
- if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
787
+ const containsFindRecoveryReads = path === "api/v1/find-email" && uniqueTasks.some(
788
+ (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
789
+ );
790
+ if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
746
791
  const rows = await this.runRecoverableCoreBatchJob(
747
792
  path,
748
793
  uniqueTasks.map((task) => task.request),
@@ -868,13 +913,27 @@ function batchItemFailure(index, error, raw) {
868
913
  raw?.retry_after_seconds ?? raw?.retry_after ?? raw?.retryAfterSeconds ?? (Number.isFinite(Number(raw?.retry_after_ms)) ? Number(raw?.retry_after_ms) / 1e3 : void 0)
869
914
  );
870
915
  const retryEligible = raw?.retry_eligible ?? raw?.retryEligible;
916
+ const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
917
+ const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
918
+ const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
919
+ const runId = raw?.run_id ?? raw?.runId;
920
+ const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
921
+ const jobId = raw?.job_id ?? raw?.jobId;
922
+ const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
871
923
  return {
872
924
  index,
873
925
  success: false,
874
926
  error,
875
927
  ...errorCode ? { errorCode } : {},
876
928
  ...typeof retryEligible === "boolean" ? { retryEligible } : {},
877
- ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {}
929
+ ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {},
930
+ ...retryStrategy === "new_idempotency_key" ? { retryStrategy } : {},
931
+ ...creditsUsed === 0 ? { creditsUsed: 0 } : {},
932
+ ...creditsCharged === 0 ? { creditsCharged: 0 } : {},
933
+ ...typeof runId === "string" && runId.trim() ? { runId: runId.trim() } : {},
934
+ ...typeof verificationRunId === "string" && verificationRunId.trim() ? { verificationRunId: verificationRunId.trim() } : {},
935
+ ...typeof jobId === "string" && jobId.trim() ? { jobId: jobId.trim() } : {},
936
+ ...typeof suggestedAction === "string" && suggestedAction.trim() ? { suggestedAction: suggestedAction.trim() } : {}
878
937
  };
879
938
  }
880
939
  function recoverableJobRowFailed(row) {
@@ -889,6 +948,7 @@ function compact(input) {
889
948
  }
890
949
  function findEmailRequestBody(params) {
891
950
  return compact({
951
+ run_id: params.runId,
892
952
  company_domain: params.companyDomain,
893
953
  full_name: params.fullName,
894
954
  first_name: params.firstName,
@@ -928,6 +988,7 @@ function normalizeCoreProductDryRunResult(data) {
928
988
  };
929
989
  }
930
990
  function normalizeFindEmailResult(data) {
991
+ const processing = String(data.status || "").toLowerCase() === "processing";
931
992
  const failed = coreProductResponseFailed(data);
932
993
  const rawEmail = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
933
994
  const email = failed ? null : rawEmail;
@@ -940,7 +1001,7 @@ function normalizeFindEmailResult(data) {
940
1001
  const verified = !explicitlyNotSendSafe && (data.is_valid === true || data.isValid === true || data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
941
1002
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
942
1003
  return {
943
- success: !failed && found,
1004
+ success: processing || !failed && found,
944
1005
  found,
945
1006
  email,
946
1007
  firstName: data.first_name,
@@ -967,8 +1028,12 @@ function normalizeFindEmailResult(data) {
967
1028
  liveProviderCalled: data.live_provider_called,
968
1029
  openrouterCalled: data.openrouter_called,
969
1030
  byoOpenrouterUsed: data.byo_openrouter_used,
970
- error: found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
971
- errorCode: found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1031
+ error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1032
+ errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1033
+ status: processing ? "processing" : "completed",
1034
+ runId: data.run_id,
1035
+ nextPollAfterSeconds: data.next_poll_after_seconds,
1036
+ suggestedAction: data.suggested_action,
972
1037
  raw: data
973
1038
  };
974
1039
  }
@@ -1104,6 +1169,12 @@ function validateSignalDiscoveryParams(params) {
1104
1169
  if (query && (query.length < 2 || query.length > 2e3)) {
1105
1170
  throw new RangeError("query must contain between 2 and 2000 characters");
1106
1171
  }
1172
+ if (params.icpContext !== void 0) {
1173
+ const icpContext = typeof params.icpContext === "string" ? params.icpContext.trim() : "";
1174
+ if (icpContext.length < 2 || icpContext.length > 4e3) {
1175
+ throw new RangeError("icpContext must contain between 2 and 4000 characters");
1176
+ }
1177
+ }
1107
1178
  if (params.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 1e3)) {
1108
1179
  throw new RangeError("limit must be an integer between 1 and 1000");
1109
1180
  }
@@ -1128,6 +1199,7 @@ function signalDiscoveryRequestBody(params) {
1128
1199
  }
1129
1200
  return compact({
1130
1201
  query: params.query?.trim(),
1202
+ icp_context: params.icpContext?.trim(),
1131
1203
  limit: params.limit ?? 100,
1132
1204
  sources: params.sources,
1133
1205
  lookback_days: params.lookbackDays,
@@ -1153,6 +1225,8 @@ function normalizeSignalDiscoveryResult(params, data) {
1153
1225
  availableCompanySignalCount: Number.isFinite(Number(data.available_company_signal_count)) ? Number(data.available_company_signal_count) : void 0,
1154
1226
  sourceLanes: Array.isArray(data.source_lanes) ? data.source_lanes.filter((lane) => typeof lane === "string") : void 0,
1155
1227
  evidenceValidation: data.evidence_validation && typeof data.evidence_validation === "object" ? data.evidence_validation : void 0,
1228
+ discoveryDiagnostics: data.discovery_diagnostics && typeof data.discovery_diagnostics === "object" ? data.discovery_diagnostics : void 0,
1229
+ identityResolution: data.identity_resolution && typeof data.identity_resolution === "object" ? data.identity_resolution : void 0,
1156
1230
  costGuardrail: normalizeSignalDiscoveryCostGuardrail(data.cost_guardrail),
1157
1231
  warnings: Array.isArray(data.warnings) ? data.warnings.filter((warning) => typeof warning === "string") : void 0,
1158
1232
  raw: data
@@ -1181,7 +1255,15 @@ function normalizeSignalDiscoveryCostGuardrail(value) {
1181
1255
  guardrail.projected_cost_per_requested_company_signal_usd
1182
1256
  ),
1183
1257
  sourceCostUsd: numberOrNullable(guardrail.source_cost_usd),
1184
- costSource: ["observed", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1258
+ costSource: ["observed", "catalog_preflight", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1259
+ identityResolutionCostUsd: numberOrNullable(guardrail.identity_resolution_cost_usd),
1260
+ identityCostSource: [
1261
+ "observed_zero_credit_receipts",
1262
+ "contractual_zero_cost",
1263
+ "not_used",
1264
+ "unavailable"
1265
+ ].includes(String(guardrail.identity_cost_source)) ? String(guardrail.identity_cost_source) : void 0,
1266
+ identityProviderRequests: numberOrUndefined(guardrail.identity_provider_requests),
1185
1267
  qualifiedCompanySignalCount: numberOrUndefined(guardrail.qualified_company_signal_count),
1186
1268
  returnedCompanySignalCount: numberOrUndefined(guardrail.returned_company_signal_count),
1187
1269
  availableCompanySignalCount: numberOrUndefined(guardrail.available_company_signal_count),
@@ -1285,6 +1367,7 @@ function normalizedFindEmailBatchName(request) {
1285
1367
  function coreProductBatchRequestKey(path, request) {
1286
1368
  if (path === "api/v1/find-email") {
1287
1369
  return JSON.stringify([
1370
+ normalizedBatchText(request.run_id),
1288
1371
  normalizedFindEmailBatchName(request),
1289
1372
  normalizedBatchDomain(request.company_domain),
1290
1373
  normalizedBatchLinkedIn(request.linkedin_url),
@@ -1390,6 +1473,7 @@ function signalToCopyRequestBody(params) {
1390
1473
  title: params.title,
1391
1474
  campaign_offer: params.campaignOffer,
1392
1475
  research_prompt: params.researchPrompt,
1476
+ lookback_days: params.lookbackDays,
1393
1477
  signal_run_id: params.signalRunId,
1394
1478
  skip_cache: params.skipCache,
1395
1479
  enable_deep_search: params.enableDeepSearch,
@@ -1400,6 +1484,9 @@ function signalToCopyRequestBody(params) {
1400
1484
  }
1401
1485
  function validateSignalToCopyParams(params) {
1402
1486
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1487
+ if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
1488
+ throw new RangeError("lookbackDays must be an integer between 1 and 365");
1489
+ }
1403
1490
  if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1404
1491
  const missing = [
1405
1492
  ["companyDomain", params.companyDomain],
package/dist/index.d.mts CHANGED
@@ -27,6 +27,20 @@ interface BatchItemResult<T> {
27
27
  retryEligible?: boolean;
28
28
  /** Server-provided delay before retrying the failed row. */
29
29
  retryAfterSeconds?: number;
30
+ /** Explicit recovery strategy. A fresh key can repeat provider work and is never automatic. */
31
+ retryStrategy?: 'new_idempotency_key';
32
+ /** Present only when the server proves the failed row consumed zero Signaliz credits. */
33
+ creditsUsed?: 0;
34
+ /** Present only when the server proves the failed row charged zero Signaliz credits. */
35
+ creditsCharged?: 0;
36
+ /** Existing provider run that can be resumed without dispatching new work. */
37
+ runId?: string;
38
+ /** Existing Verify Email run that can be resumed without dispatching new work. */
39
+ verificationRunId?: string;
40
+ /** Existing recoverable batch job. */
41
+ jobId?: string;
42
+ /** Machine-readable recovery action returned by Signaliz. */
43
+ suggestedAction?: string;
30
44
  /** Zero-based input index containing the full result for this exact duplicate. */
31
45
  duplicateOf?: number;
32
46
  }
@@ -66,8 +80,7 @@ interface CoreProductDryRunResult {
66
80
  sideEffects: unknown[];
67
81
  raw: Record<string, unknown>;
68
82
  }
69
- interface FindEmailParams {
70
- companyDomain: string;
83
+ interface FindEmailCommonParams {
71
84
  fullName?: string;
72
85
  firstName?: string;
73
86
  lastName?: string;
@@ -80,6 +93,15 @@ interface FindEmailParams {
80
93
  /** Stable retry key for recovering the same request after an ambiguous response. */
81
94
  idempotencyKey?: string;
82
95
  }
96
+ type FindEmailParams = FindEmailCommonParams & ({
97
+ companyDomain: string;
98
+ /** Omit for a new lookup. */
99
+ runId?: undefined;
100
+ } | {
101
+ /** Resume one existing Find Email run without dispatching a new finder. */
102
+ runId: string;
103
+ companyDomain?: string;
104
+ });
83
105
  interface FindEmailResult {
84
106
  success: boolean;
85
107
  found: boolean;
@@ -112,6 +134,12 @@ interface FindEmailResult {
112
134
  byoOpenrouterUsed?: boolean;
113
135
  error?: string;
114
136
  errorCode?: string;
137
+ /** Processing while an existing provider run is still active; otherwise completed. */
138
+ status?: 'processing' | 'completed';
139
+ /** Durable Find Email run ID for polling without duplicate spend. */
140
+ runId?: string;
141
+ nextPollAfterSeconds?: number;
142
+ suggestedAction?: string;
115
143
  raw: Record<string, unknown>;
116
144
  }
117
145
  interface VerifyEmailResult {
@@ -250,8 +278,10 @@ interface CompanySignalEnrichmentResult {
250
278
  type SignalDiscoverySource = 'web' | 'news';
251
279
  type SignalDiscoveryStatus = 'planned' | 'queued' | 'processing' | 'completed' | 'completed_with_warnings' | 'failed';
252
280
  interface SignalDiscoveryParams {
253
- /** Natural-language description of the signals to find. Required for a new search. */
281
+ /** Any natural-language signal question. Required for a new search. */
254
282
  query?: string;
283
+ /** Optional natural-language ICP, product, or pain context that returned signals must support. */
284
+ icpContext?: string;
255
285
  /** Number of qualified company signals requested. Defaults to 100; allowed range is 1-1,000. */
256
286
  limit?: number;
257
287
  /** Restrict discovery to selected source families. */
@@ -300,7 +330,13 @@ interface SignalDiscoveryCostGuardrail {
300
330
  maxSourceCostUsd?: number | null;
301
331
  projectedCostPerRequestedCompanySignalUsd?: number | null;
302
332
  sourceCostUsd?: number | null;
303
- costSource?: 'observed' | 'catalog_bounded' | 'mixed' | 'unavailable';
333
+ costSource?: 'observed' | 'catalog_preflight' | 'catalog_bounded' | 'mixed' | 'unavailable';
334
+ /** Separately reported company identity-resolution cost; null when receipts are incomplete. */
335
+ identityResolutionCostUsd?: number | null;
336
+ /** Evidence used to account for company identity-resolution cost. */
337
+ identityCostSource?: 'observed_zero_credit_receipts' | 'contractual_zero_cost' | 'not_used' | 'unavailable';
338
+ /** Zero-cost fallback identity-provider requests covered by identityResolutionCostUsd. */
339
+ identityProviderRequests?: number;
304
340
  qualifiedCompanySignalCount?: number;
305
341
  returnedCompanySignalCount?: number;
306
342
  /** Qualified signals retained on the same search run for later expansion. */
@@ -326,6 +362,10 @@ interface SignalDiscoveryResult {
326
362
  availableCompanySignalCount?: number;
327
363
  sourceLanes?: string[];
328
364
  evidenceValidation?: Record<string, unknown>;
365
+ /** Search, reachability, date, classifier, and rejection counts for diagnosis. */
366
+ discoveryDiagnostics?: Record<string, unknown>;
367
+ /** Company-domain verification counts for the retained cohort. */
368
+ identityResolution?: Record<string, unknown>;
329
369
  costGuardrail?: SignalDiscoveryCostGuardrail;
330
370
  warnings?: string[];
331
371
  raw: Record<string, unknown>;
@@ -336,6 +376,8 @@ interface SignalToCopyParams {
336
376
  title?: string;
337
377
  campaignOffer?: string;
338
378
  researchPrompt?: string;
379
+ /** Maximum signal age used for cache reuse and fresh research. Defaults to 90 days. */
380
+ lookbackDays?: number;
339
381
  signalRunId?: string;
340
382
  /** Bypass cached company signals and start fresh research. */
341
383
  skipCache?: boolean;
package/dist/index.d.ts CHANGED
@@ -27,6 +27,20 @@ interface BatchItemResult<T> {
27
27
  retryEligible?: boolean;
28
28
  /** Server-provided delay before retrying the failed row. */
29
29
  retryAfterSeconds?: number;
30
+ /** Explicit recovery strategy. A fresh key can repeat provider work and is never automatic. */
31
+ retryStrategy?: 'new_idempotency_key';
32
+ /** Present only when the server proves the failed row consumed zero Signaliz credits. */
33
+ creditsUsed?: 0;
34
+ /** Present only when the server proves the failed row charged zero Signaliz credits. */
35
+ creditsCharged?: 0;
36
+ /** Existing provider run that can be resumed without dispatching new work. */
37
+ runId?: string;
38
+ /** Existing Verify Email run that can be resumed without dispatching new work. */
39
+ verificationRunId?: string;
40
+ /** Existing recoverable batch job. */
41
+ jobId?: string;
42
+ /** Machine-readable recovery action returned by Signaliz. */
43
+ suggestedAction?: string;
30
44
  /** Zero-based input index containing the full result for this exact duplicate. */
31
45
  duplicateOf?: number;
32
46
  }
@@ -66,8 +80,7 @@ interface CoreProductDryRunResult {
66
80
  sideEffects: unknown[];
67
81
  raw: Record<string, unknown>;
68
82
  }
69
- interface FindEmailParams {
70
- companyDomain: string;
83
+ interface FindEmailCommonParams {
71
84
  fullName?: string;
72
85
  firstName?: string;
73
86
  lastName?: string;
@@ -80,6 +93,15 @@ interface FindEmailParams {
80
93
  /** Stable retry key for recovering the same request after an ambiguous response. */
81
94
  idempotencyKey?: string;
82
95
  }
96
+ type FindEmailParams = FindEmailCommonParams & ({
97
+ companyDomain: string;
98
+ /** Omit for a new lookup. */
99
+ runId?: undefined;
100
+ } | {
101
+ /** Resume one existing Find Email run without dispatching a new finder. */
102
+ runId: string;
103
+ companyDomain?: string;
104
+ });
83
105
  interface FindEmailResult {
84
106
  success: boolean;
85
107
  found: boolean;
@@ -112,6 +134,12 @@ interface FindEmailResult {
112
134
  byoOpenrouterUsed?: boolean;
113
135
  error?: string;
114
136
  errorCode?: string;
137
+ /** Processing while an existing provider run is still active; otherwise completed. */
138
+ status?: 'processing' | 'completed';
139
+ /** Durable Find Email run ID for polling without duplicate spend. */
140
+ runId?: string;
141
+ nextPollAfterSeconds?: number;
142
+ suggestedAction?: string;
115
143
  raw: Record<string, unknown>;
116
144
  }
117
145
  interface VerifyEmailResult {
@@ -250,8 +278,10 @@ interface CompanySignalEnrichmentResult {
250
278
  type SignalDiscoverySource = 'web' | 'news';
251
279
  type SignalDiscoveryStatus = 'planned' | 'queued' | 'processing' | 'completed' | 'completed_with_warnings' | 'failed';
252
280
  interface SignalDiscoveryParams {
253
- /** Natural-language description of the signals to find. Required for a new search. */
281
+ /** Any natural-language signal question. Required for a new search. */
254
282
  query?: string;
283
+ /** Optional natural-language ICP, product, or pain context that returned signals must support. */
284
+ icpContext?: string;
255
285
  /** Number of qualified company signals requested. Defaults to 100; allowed range is 1-1,000. */
256
286
  limit?: number;
257
287
  /** Restrict discovery to selected source families. */
@@ -300,7 +330,13 @@ interface SignalDiscoveryCostGuardrail {
300
330
  maxSourceCostUsd?: number | null;
301
331
  projectedCostPerRequestedCompanySignalUsd?: number | null;
302
332
  sourceCostUsd?: number | null;
303
- costSource?: 'observed' | 'catalog_bounded' | 'mixed' | 'unavailable';
333
+ costSource?: 'observed' | 'catalog_preflight' | 'catalog_bounded' | 'mixed' | 'unavailable';
334
+ /** Separately reported company identity-resolution cost; null when receipts are incomplete. */
335
+ identityResolutionCostUsd?: number | null;
336
+ /** Evidence used to account for company identity-resolution cost. */
337
+ identityCostSource?: 'observed_zero_credit_receipts' | 'contractual_zero_cost' | 'not_used' | 'unavailable';
338
+ /** Zero-cost fallback identity-provider requests covered by identityResolutionCostUsd. */
339
+ identityProviderRequests?: number;
304
340
  qualifiedCompanySignalCount?: number;
305
341
  returnedCompanySignalCount?: number;
306
342
  /** Qualified signals retained on the same search run for later expansion. */
@@ -326,6 +362,10 @@ interface SignalDiscoveryResult {
326
362
  availableCompanySignalCount?: number;
327
363
  sourceLanes?: string[];
328
364
  evidenceValidation?: Record<string, unknown>;
365
+ /** Search, reachability, date, classifier, and rejection counts for diagnosis. */
366
+ discoveryDiagnostics?: Record<string, unknown>;
367
+ /** Company-domain verification counts for the retained cohort. */
368
+ identityResolution?: Record<string, unknown>;
329
369
  costGuardrail?: SignalDiscoveryCostGuardrail;
330
370
  warnings?: string[];
331
371
  raw: Record<string, unknown>;
@@ -336,6 +376,8 @@ interface SignalToCopyParams {
336
376
  title?: string;
337
377
  campaignOffer?: string;
338
378
  researchPrompt?: string;
379
+ /** Maximum signal age used for cache reuse and fresh research. Defaults to 90 days. */
380
+ lookbackDays?: number;
339
381
  signalRunId?: string;
340
382
  /** Bypass cached company signals and start fresh research. */
341
383
  skipCache?: boolean;
package/dist/index.js CHANGED
@@ -36,17 +36,21 @@ var SignalizError = class extends Error {
36
36
  this.details = data.details;
37
37
  }
38
38
  get isRetryable() {
39
+ if (typeof this.details?.retry_eligible === "boolean") {
40
+ return this.details.retry_eligible;
41
+ }
39
42
  return this.errorType === "rate_limited" || this.errorType === "timeout" || this.errorType === "provider_error";
40
43
  }
41
44
  };
42
45
  function parseError(status, body) {
43
46
  if (body?.error && typeof body.error === "object") {
47
+ const nestedDetails = body.error.details && typeof body.error.details === "object" ? body.error.details : {};
44
48
  return new SignalizError({
45
49
  code: body.error.code || `HTTP_${status}`,
46
50
  message: body.error.message || `Request failed with status ${status}`,
47
51
  errorType: mapErrorType(body.error.code, status),
48
52
  retryAfter: retryAfterSeconds(body.error),
49
- details: body.error.details
53
+ details: mergePublicErrorDetails({ ...body, ...body.error }, nestedDetails)
50
54
  });
51
55
  }
52
56
  const code = typeof body?.error_code === "string" && body.error_code.trim() ? body.error_code.trim() : `HTTP_${status}`;
@@ -55,7 +59,7 @@ function parseError(status, body) {
55
59
  message: body?.message || body?.error || `Request failed with status ${status}`,
56
60
  errorType: mapErrorType(code, status),
57
61
  retryAfter: retryAfterSeconds(body),
58
- details: body?.details || publicRetryDetails(body)
62
+ details: mergePublicErrorDetails(body, body?.details)
59
63
  });
60
64
  }
61
65
  function retryAfterSeconds(body) {
@@ -67,13 +71,26 @@ function publicRetryDetails(body) {
67
71
  ...body?.retry_eligible !== void 0 ? { retry_eligible: body.retry_eligible } : {},
68
72
  ...body?.retry_after !== void 0 ? { retry_after: body.retry_after } : {},
69
73
  ...body?.retry_after_seconds !== void 0 ? { retry_after_seconds: body.retry_after_seconds } : {},
74
+ ...body?.retry_strategy !== void 0 ? { retry_strategy: body.retry_strategy } : {},
75
+ ...body?.credits_used !== void 0 ? { credits_used: body.credits_used } : {},
76
+ ...body?.credits_charged !== void 0 ? { credits_charged: body.credits_charged } : {},
77
+ ...body?.job_id !== void 0 ? { job_id: body.job_id } : {},
78
+ ...body?.verification_run_id !== void 0 ? { verification_run_id: body.verification_run_id } : {},
70
79
  ...body?.run_id !== void 0 ? { run_id: body.run_id } : {},
80
+ ...body?.suggested_action !== void 0 ? { suggested_action: body.suggested_action } : {},
71
81
  ...body?.signal_run_id !== void 0 ? { signal_run_id: body.signal_run_id } : {},
72
82
  ...body?.company_signal_run_id !== void 0 ? { company_signal_run_id: body.company_signal_run_id } : {},
73
83
  ...body?.resume_context_persisted !== void 0 ? { resume_context_persisted: body.resume_context_persisted } : {}
74
84
  };
75
85
  return Object.keys(details).length > 0 ? details : void 0;
76
86
  }
87
+ function mergePublicErrorDetails(body, explicitDetails) {
88
+ const merged = { ...publicRetryDetails(body) || {} };
89
+ if (explicitDetails && typeof explicitDetails === "object" && !Array.isArray(explicitDetails)) {
90
+ Object.assign(merged, explicitDetails);
91
+ }
92
+ return Object.keys(merged).length > 0 ? merged : void 0;
93
+ }
77
94
  function mapErrorType(code, status) {
78
95
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
79
96
  if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
@@ -135,7 +152,9 @@ var HttpClient = class {
135
152
  const errBody = await res.json().catch(() => ({}));
136
153
  const err = parseError(res.status, errBody);
137
154
  if (err.isRetryable && attempt < this.maxRetries) {
138
- const wait = err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
155
+ const retryWithNewKey = err.details?.retry_strategy === "new_idempotency_key" && err.details?.credits_used === 0 && err.details?.credits_charged === 0;
156
+ if (retryWithNewKey) throw err;
157
+ const wait = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
139
158
  await sleep(wait);
140
159
  lastError = err;
141
160
  continue;
@@ -435,6 +454,29 @@ var Signaliz = class {
435
454
  );
436
455
  }
437
456
  async verifyEmail(email, options) {
457
+ const normalizedEmail = typeof email === "string" ? email.trim() : "";
458
+ if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
459
+ const error = `Invalid email format: "${normalizedEmail}"`;
460
+ return normalizeVerifyEmailResult(normalizedEmail, {
461
+ success: false,
462
+ email: normalizedEmail,
463
+ status: "completed",
464
+ is_valid: false,
465
+ is_deliverable: false,
466
+ email_verified: false,
467
+ verified_for_sending: false,
468
+ is_malformed: true,
469
+ verification_status: "malformed",
470
+ verification_verdict: "malformed",
471
+ verification_source: "input_validation",
472
+ provider_status: "rejected_before_provider",
473
+ error,
474
+ error_code: "INPUT_001",
475
+ retry_eligible: false,
476
+ credits_used: 0,
477
+ live_provider_called: false
478
+ });
479
+ }
438
480
  const request = compact({
439
481
  email: email || void 0,
440
482
  verification_run_id: options?.verificationRunId,
@@ -769,7 +811,10 @@ var Signaliz = class {
769
811
  );
770
812
  const uniqueTasks = deduplicated.uniqueItems;
771
813
  const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
772
- if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
814
+ const containsFindRecoveryReads = path === "api/v1/find-email" && uniqueTasks.some(
815
+ (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
816
+ );
817
+ if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
773
818
  const rows = await this.runRecoverableCoreBatchJob(
774
819
  path,
775
820
  uniqueTasks.map((task) => task.request),
@@ -895,13 +940,27 @@ function batchItemFailure(index, error, raw) {
895
940
  raw?.retry_after_seconds ?? raw?.retry_after ?? raw?.retryAfterSeconds ?? (Number.isFinite(Number(raw?.retry_after_ms)) ? Number(raw?.retry_after_ms) / 1e3 : void 0)
896
941
  );
897
942
  const retryEligible = raw?.retry_eligible ?? raw?.retryEligible;
943
+ const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
944
+ const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
945
+ const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
946
+ const runId = raw?.run_id ?? raw?.runId;
947
+ const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
948
+ const jobId = raw?.job_id ?? raw?.jobId;
949
+ const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
898
950
  return {
899
951
  index,
900
952
  success: false,
901
953
  error,
902
954
  ...errorCode ? { errorCode } : {},
903
955
  ...typeof retryEligible === "boolean" ? { retryEligible } : {},
904
- ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {}
956
+ ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {},
957
+ ...retryStrategy === "new_idempotency_key" ? { retryStrategy } : {},
958
+ ...creditsUsed === 0 ? { creditsUsed: 0 } : {},
959
+ ...creditsCharged === 0 ? { creditsCharged: 0 } : {},
960
+ ...typeof runId === "string" && runId.trim() ? { runId: runId.trim() } : {},
961
+ ...typeof verificationRunId === "string" && verificationRunId.trim() ? { verificationRunId: verificationRunId.trim() } : {},
962
+ ...typeof jobId === "string" && jobId.trim() ? { jobId: jobId.trim() } : {},
963
+ ...typeof suggestedAction === "string" && suggestedAction.trim() ? { suggestedAction: suggestedAction.trim() } : {}
905
964
  };
906
965
  }
907
966
  function recoverableJobRowFailed(row) {
@@ -916,6 +975,7 @@ function compact(input) {
916
975
  }
917
976
  function findEmailRequestBody(params) {
918
977
  return compact({
978
+ run_id: params.runId,
919
979
  company_domain: params.companyDomain,
920
980
  full_name: params.fullName,
921
981
  first_name: params.firstName,
@@ -955,6 +1015,7 @@ function normalizeCoreProductDryRunResult(data) {
955
1015
  };
956
1016
  }
957
1017
  function normalizeFindEmailResult(data) {
1018
+ const processing = String(data.status || "").toLowerCase() === "processing";
958
1019
  const failed = coreProductResponseFailed(data);
959
1020
  const rawEmail = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
960
1021
  const email = failed ? null : rawEmail;
@@ -967,7 +1028,7 @@ function normalizeFindEmailResult(data) {
967
1028
  const verified = !explicitlyNotSendSafe && (data.is_valid === true || data.isValid === true || data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
968
1029
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
969
1030
  return {
970
- success: !failed && found,
1031
+ success: processing || !failed && found,
971
1032
  found,
972
1033
  email,
973
1034
  firstName: data.first_name,
@@ -994,8 +1055,12 @@ function normalizeFindEmailResult(data) {
994
1055
  liveProviderCalled: data.live_provider_called,
995
1056
  openrouterCalled: data.openrouter_called,
996
1057
  byoOpenrouterUsed: data.byo_openrouter_used,
997
- error: found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
998
- errorCode: found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1058
+ error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1059
+ errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1060
+ status: processing ? "processing" : "completed",
1061
+ runId: data.run_id,
1062
+ nextPollAfterSeconds: data.next_poll_after_seconds,
1063
+ suggestedAction: data.suggested_action,
999
1064
  raw: data
1000
1065
  };
1001
1066
  }
@@ -1131,6 +1196,12 @@ function validateSignalDiscoveryParams(params) {
1131
1196
  if (query && (query.length < 2 || query.length > 2e3)) {
1132
1197
  throw new RangeError("query must contain between 2 and 2000 characters");
1133
1198
  }
1199
+ if (params.icpContext !== void 0) {
1200
+ const icpContext = typeof params.icpContext === "string" ? params.icpContext.trim() : "";
1201
+ if (icpContext.length < 2 || icpContext.length > 4e3) {
1202
+ throw new RangeError("icpContext must contain between 2 and 4000 characters");
1203
+ }
1204
+ }
1134
1205
  if (params.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 1e3)) {
1135
1206
  throw new RangeError("limit must be an integer between 1 and 1000");
1136
1207
  }
@@ -1155,6 +1226,7 @@ function signalDiscoveryRequestBody(params) {
1155
1226
  }
1156
1227
  return compact({
1157
1228
  query: params.query?.trim(),
1229
+ icp_context: params.icpContext?.trim(),
1158
1230
  limit: params.limit ?? 100,
1159
1231
  sources: params.sources,
1160
1232
  lookback_days: params.lookbackDays,
@@ -1180,6 +1252,8 @@ function normalizeSignalDiscoveryResult(params, data) {
1180
1252
  availableCompanySignalCount: Number.isFinite(Number(data.available_company_signal_count)) ? Number(data.available_company_signal_count) : void 0,
1181
1253
  sourceLanes: Array.isArray(data.source_lanes) ? data.source_lanes.filter((lane) => typeof lane === "string") : void 0,
1182
1254
  evidenceValidation: data.evidence_validation && typeof data.evidence_validation === "object" ? data.evidence_validation : void 0,
1255
+ discoveryDiagnostics: data.discovery_diagnostics && typeof data.discovery_diagnostics === "object" ? data.discovery_diagnostics : void 0,
1256
+ identityResolution: data.identity_resolution && typeof data.identity_resolution === "object" ? data.identity_resolution : void 0,
1183
1257
  costGuardrail: normalizeSignalDiscoveryCostGuardrail(data.cost_guardrail),
1184
1258
  warnings: Array.isArray(data.warnings) ? data.warnings.filter((warning) => typeof warning === "string") : void 0,
1185
1259
  raw: data
@@ -1208,7 +1282,15 @@ function normalizeSignalDiscoveryCostGuardrail(value) {
1208
1282
  guardrail.projected_cost_per_requested_company_signal_usd
1209
1283
  ),
1210
1284
  sourceCostUsd: numberOrNullable(guardrail.source_cost_usd),
1211
- costSource: ["observed", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1285
+ costSource: ["observed", "catalog_preflight", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1286
+ identityResolutionCostUsd: numberOrNullable(guardrail.identity_resolution_cost_usd),
1287
+ identityCostSource: [
1288
+ "observed_zero_credit_receipts",
1289
+ "contractual_zero_cost",
1290
+ "not_used",
1291
+ "unavailable"
1292
+ ].includes(String(guardrail.identity_cost_source)) ? String(guardrail.identity_cost_source) : void 0,
1293
+ identityProviderRequests: numberOrUndefined(guardrail.identity_provider_requests),
1212
1294
  qualifiedCompanySignalCount: numberOrUndefined(guardrail.qualified_company_signal_count),
1213
1295
  returnedCompanySignalCount: numberOrUndefined(guardrail.returned_company_signal_count),
1214
1296
  availableCompanySignalCount: numberOrUndefined(guardrail.available_company_signal_count),
@@ -1312,6 +1394,7 @@ function normalizedFindEmailBatchName(request) {
1312
1394
  function coreProductBatchRequestKey(path, request) {
1313
1395
  if (path === "api/v1/find-email") {
1314
1396
  return JSON.stringify([
1397
+ normalizedBatchText(request.run_id),
1315
1398
  normalizedFindEmailBatchName(request),
1316
1399
  normalizedBatchDomain(request.company_domain),
1317
1400
  normalizedBatchLinkedIn(request.linkedin_url),
@@ -1417,6 +1500,7 @@ function signalToCopyRequestBody(params) {
1417
1500
  title: params.title,
1418
1501
  campaign_offer: params.campaignOffer,
1419
1502
  research_prompt: params.researchPrompt,
1503
+ lookback_days: params.lookbackDays,
1420
1504
  signal_run_id: params.signalRunId,
1421
1505
  skip_cache: params.skipCache,
1422
1506
  enable_deep_search: params.enableDeepSearch,
@@ -1427,6 +1511,9 @@ function signalToCopyRequestBody(params) {
1427
1511
  }
1428
1512
  function validateSignalToCopyParams(params) {
1429
1513
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1514
+ if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
1515
+ throw new RangeError("lookbackDays must be an integer between 1 and 365");
1516
+ }
1430
1517
  if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1431
1518
  const missing = [
1432
1519
  ["companyDomain", params.companyDomain],
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Signaliz,
3
3
  SignalizError
4
- } from "./chunk-VKBUQTDS.mjs";
4
+ } from "./chunk-ENA7AVFB.mjs";
5
5
  export {
6
6
  Signaliz,
7
7
  SignalizError
@@ -40,17 +40,21 @@ var SignalizError = class extends Error {
40
40
  this.details = data.details;
41
41
  }
42
42
  get isRetryable() {
43
+ if (typeof this.details?.retry_eligible === "boolean") {
44
+ return this.details.retry_eligible;
45
+ }
43
46
  return this.errorType === "rate_limited" || this.errorType === "timeout" || this.errorType === "provider_error";
44
47
  }
45
48
  };
46
49
  function parseError(status, body) {
47
50
  if (body?.error && typeof body.error === "object") {
51
+ const nestedDetails = body.error.details && typeof body.error.details === "object" ? body.error.details : {};
48
52
  return new SignalizError({
49
53
  code: body.error.code || `HTTP_${status}`,
50
54
  message: body.error.message || `Request failed with status ${status}`,
51
55
  errorType: mapErrorType(body.error.code, status),
52
56
  retryAfter: retryAfterSeconds(body.error),
53
- details: body.error.details
57
+ details: mergePublicErrorDetails({ ...body, ...body.error }, nestedDetails)
54
58
  });
55
59
  }
56
60
  const code = typeof body?.error_code === "string" && body.error_code.trim() ? body.error_code.trim() : `HTTP_${status}`;
@@ -59,7 +63,7 @@ function parseError(status, body) {
59
63
  message: body?.message || body?.error || `Request failed with status ${status}`,
60
64
  errorType: mapErrorType(code, status),
61
65
  retryAfter: retryAfterSeconds(body),
62
- details: body?.details || publicRetryDetails(body)
66
+ details: mergePublicErrorDetails(body, body?.details)
63
67
  });
64
68
  }
65
69
  function retryAfterSeconds(body) {
@@ -71,13 +75,26 @@ function publicRetryDetails(body) {
71
75
  ...body?.retry_eligible !== void 0 ? { retry_eligible: body.retry_eligible } : {},
72
76
  ...body?.retry_after !== void 0 ? { retry_after: body.retry_after } : {},
73
77
  ...body?.retry_after_seconds !== void 0 ? { retry_after_seconds: body.retry_after_seconds } : {},
78
+ ...body?.retry_strategy !== void 0 ? { retry_strategy: body.retry_strategy } : {},
79
+ ...body?.credits_used !== void 0 ? { credits_used: body.credits_used } : {},
80
+ ...body?.credits_charged !== void 0 ? { credits_charged: body.credits_charged } : {},
81
+ ...body?.job_id !== void 0 ? { job_id: body.job_id } : {},
82
+ ...body?.verification_run_id !== void 0 ? { verification_run_id: body.verification_run_id } : {},
74
83
  ...body?.run_id !== void 0 ? { run_id: body.run_id } : {},
84
+ ...body?.suggested_action !== void 0 ? { suggested_action: body.suggested_action } : {},
75
85
  ...body?.signal_run_id !== void 0 ? { signal_run_id: body.signal_run_id } : {},
76
86
  ...body?.company_signal_run_id !== void 0 ? { company_signal_run_id: body.company_signal_run_id } : {},
77
87
  ...body?.resume_context_persisted !== void 0 ? { resume_context_persisted: body.resume_context_persisted } : {}
78
88
  };
79
89
  return Object.keys(details).length > 0 ? details : void 0;
80
90
  }
91
+ function mergePublicErrorDetails(body, explicitDetails) {
92
+ const merged = { ...publicRetryDetails(body) || {} };
93
+ if (explicitDetails && typeof explicitDetails === "object" && !Array.isArray(explicitDetails)) {
94
+ Object.assign(merged, explicitDetails);
95
+ }
96
+ return Object.keys(merged).length > 0 ? merged : void 0;
97
+ }
81
98
  function mapErrorType(code, status) {
82
99
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
83
100
  if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
@@ -139,7 +156,9 @@ var HttpClient = class {
139
156
  const errBody = await res.json().catch(() => ({}));
140
157
  const err = parseError(res.status, errBody);
141
158
  if (err.isRetryable && attempt < this.maxRetries) {
142
- const wait = err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
159
+ const retryWithNewKey = err.details?.retry_strategy === "new_idempotency_key" && err.details?.credits_used === 0 && err.details?.credits_charged === 0;
160
+ if (retryWithNewKey) throw err;
161
+ const wait = err.retryAfter !== void 0 ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
143
162
  await sleep(wait);
144
163
  lastError = err;
145
164
  continue;
@@ -439,6 +458,29 @@ var Signaliz = class {
439
458
  );
440
459
  }
441
460
  async verifyEmail(email, options) {
461
+ const normalizedEmail = typeof email === "string" ? email.trim() : "";
462
+ if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
463
+ const error = `Invalid email format: "${normalizedEmail}"`;
464
+ return normalizeVerifyEmailResult(normalizedEmail, {
465
+ success: false,
466
+ email: normalizedEmail,
467
+ status: "completed",
468
+ is_valid: false,
469
+ is_deliverable: false,
470
+ email_verified: false,
471
+ verified_for_sending: false,
472
+ is_malformed: true,
473
+ verification_status: "malformed",
474
+ verification_verdict: "malformed",
475
+ verification_source: "input_validation",
476
+ provider_status: "rejected_before_provider",
477
+ error,
478
+ error_code: "INPUT_001",
479
+ retry_eligible: false,
480
+ credits_used: 0,
481
+ live_provider_called: false
482
+ });
483
+ }
442
484
  const request = compact({
443
485
  email: email || void 0,
444
486
  verification_run_id: options?.verificationRunId,
@@ -773,7 +815,10 @@ var Signaliz = class {
773
815
  );
774
816
  const uniqueTasks = deduplicated.uniqueItems;
775
817
  const recoverableBatch = path2 === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path2 === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
776
- if (path2 !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
818
+ const containsFindRecoveryReads = path2 === "api/v1/find-email" && uniqueTasks.some(
819
+ (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
820
+ );
821
+ if (path2 !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
777
822
  const rows = await this.runRecoverableCoreBatchJob(
778
823
  path2,
779
824
  uniqueTasks.map((task) => task.request),
@@ -899,13 +944,27 @@ function batchItemFailure(index, error, raw) {
899
944
  raw?.retry_after_seconds ?? raw?.retry_after ?? raw?.retryAfterSeconds ?? (Number.isFinite(Number(raw?.retry_after_ms)) ? Number(raw?.retry_after_ms) / 1e3 : void 0)
900
945
  );
901
946
  const retryEligible = raw?.retry_eligible ?? raw?.retryEligible;
947
+ const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
948
+ const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
949
+ const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
950
+ const runId = raw?.run_id ?? raw?.runId;
951
+ const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
952
+ const jobId = raw?.job_id ?? raw?.jobId;
953
+ const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
902
954
  return {
903
955
  index,
904
956
  success: false,
905
957
  error,
906
958
  ...errorCode ? { errorCode } : {},
907
959
  ...typeof retryEligible === "boolean" ? { retryEligible } : {},
908
- ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {}
960
+ ...Number.isFinite(retryAfterSeconds2) && retryAfterSeconds2 >= 0 ? { retryAfterSeconds: retryAfterSeconds2 } : {},
961
+ ...retryStrategy === "new_idempotency_key" ? { retryStrategy } : {},
962
+ ...creditsUsed === 0 ? { creditsUsed: 0 } : {},
963
+ ...creditsCharged === 0 ? { creditsCharged: 0 } : {},
964
+ ...typeof runId === "string" && runId.trim() ? { runId: runId.trim() } : {},
965
+ ...typeof verificationRunId === "string" && verificationRunId.trim() ? { verificationRunId: verificationRunId.trim() } : {},
966
+ ...typeof jobId === "string" && jobId.trim() ? { jobId: jobId.trim() } : {},
967
+ ...typeof suggestedAction === "string" && suggestedAction.trim() ? { suggestedAction: suggestedAction.trim() } : {}
909
968
  };
910
969
  }
911
970
  function recoverableJobRowFailed(row) {
@@ -920,6 +979,7 @@ function compact(input) {
920
979
  }
921
980
  function findEmailRequestBody(params) {
922
981
  return compact({
982
+ run_id: params.runId,
923
983
  company_domain: params.companyDomain,
924
984
  full_name: params.fullName,
925
985
  first_name: params.firstName,
@@ -959,6 +1019,7 @@ function normalizeCoreProductDryRunResult(data) {
959
1019
  };
960
1020
  }
961
1021
  function normalizeFindEmailResult(data) {
1022
+ const processing = String(data.status || "").toLowerCase() === "processing";
962
1023
  const failed = coreProductResponseFailed(data);
963
1024
  const rawEmail = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
964
1025
  const email = failed ? null : rawEmail;
@@ -971,7 +1032,7 @@ function normalizeFindEmailResult(data) {
971
1032
  const verified = !explicitlyNotSendSafe && (data.is_valid === true || data.isValid === true || data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
972
1033
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
973
1034
  return {
974
- success: !failed && found,
1035
+ success: processing || !failed && found,
975
1036
  found,
976
1037
  email,
977
1038
  firstName: data.first_name,
@@ -998,8 +1059,12 @@ function normalizeFindEmailResult(data) {
998
1059
  liveProviderCalled: data.live_provider_called,
999
1060
  openrouterCalled: data.openrouter_called,
1000
1061
  byoOpenrouterUsed: data.byo_openrouter_used,
1001
- error: found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1002
- errorCode: found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1062
+ error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1063
+ errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1064
+ status: processing ? "processing" : "completed",
1065
+ runId: data.run_id,
1066
+ nextPollAfterSeconds: data.next_poll_after_seconds,
1067
+ suggestedAction: data.suggested_action,
1003
1068
  raw: data
1004
1069
  };
1005
1070
  }
@@ -1135,6 +1200,12 @@ function validateSignalDiscoveryParams(params) {
1135
1200
  if (query && (query.length < 2 || query.length > 2e3)) {
1136
1201
  throw new RangeError("query must contain between 2 and 2000 characters");
1137
1202
  }
1203
+ if (params.icpContext !== void 0) {
1204
+ const icpContext = typeof params.icpContext === "string" ? params.icpContext.trim() : "";
1205
+ if (icpContext.length < 2 || icpContext.length > 4e3) {
1206
+ throw new RangeError("icpContext must contain between 2 and 4000 characters");
1207
+ }
1208
+ }
1138
1209
  if (params.limit !== void 0 && (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 1e3)) {
1139
1210
  throw new RangeError("limit must be an integer between 1 and 1000");
1140
1211
  }
@@ -1159,6 +1230,7 @@ function signalDiscoveryRequestBody(params) {
1159
1230
  }
1160
1231
  return compact({
1161
1232
  query: params.query?.trim(),
1233
+ icp_context: params.icpContext?.trim(),
1162
1234
  limit: params.limit ?? 100,
1163
1235
  sources: params.sources,
1164
1236
  lookback_days: params.lookbackDays,
@@ -1184,6 +1256,8 @@ function normalizeSignalDiscoveryResult(params, data) {
1184
1256
  availableCompanySignalCount: Number.isFinite(Number(data.available_company_signal_count)) ? Number(data.available_company_signal_count) : void 0,
1185
1257
  sourceLanes: Array.isArray(data.source_lanes) ? data.source_lanes.filter((lane) => typeof lane === "string") : void 0,
1186
1258
  evidenceValidation: data.evidence_validation && typeof data.evidence_validation === "object" ? data.evidence_validation : void 0,
1259
+ discoveryDiagnostics: data.discovery_diagnostics && typeof data.discovery_diagnostics === "object" ? data.discovery_diagnostics : void 0,
1260
+ identityResolution: data.identity_resolution && typeof data.identity_resolution === "object" ? data.identity_resolution : void 0,
1187
1261
  costGuardrail: normalizeSignalDiscoveryCostGuardrail(data.cost_guardrail),
1188
1262
  warnings: Array.isArray(data.warnings) ? data.warnings.filter((warning) => typeof warning === "string") : void 0,
1189
1263
  raw: data
@@ -1212,7 +1286,15 @@ function normalizeSignalDiscoveryCostGuardrail(value) {
1212
1286
  guardrail.projected_cost_per_requested_company_signal_usd
1213
1287
  ),
1214
1288
  sourceCostUsd: numberOrNullable(guardrail.source_cost_usd),
1215
- costSource: ["observed", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1289
+ costSource: ["observed", "catalog_preflight", "catalog_bounded", "mixed", "unavailable"].includes(String(guardrail.cost_source)) ? String(guardrail.cost_source) : void 0,
1290
+ identityResolutionCostUsd: numberOrNullable(guardrail.identity_resolution_cost_usd),
1291
+ identityCostSource: [
1292
+ "observed_zero_credit_receipts",
1293
+ "contractual_zero_cost",
1294
+ "not_used",
1295
+ "unavailable"
1296
+ ].includes(String(guardrail.identity_cost_source)) ? String(guardrail.identity_cost_source) : void 0,
1297
+ identityProviderRequests: numberOrUndefined(guardrail.identity_provider_requests),
1216
1298
  qualifiedCompanySignalCount: numberOrUndefined(guardrail.qualified_company_signal_count),
1217
1299
  returnedCompanySignalCount: numberOrUndefined(guardrail.returned_company_signal_count),
1218
1300
  availableCompanySignalCount: numberOrUndefined(guardrail.available_company_signal_count),
@@ -1316,6 +1398,7 @@ function normalizedFindEmailBatchName(request) {
1316
1398
  function coreProductBatchRequestKey(path2, request) {
1317
1399
  if (path2 === "api/v1/find-email") {
1318
1400
  return JSON.stringify([
1401
+ normalizedBatchText(request.run_id),
1319
1402
  normalizedFindEmailBatchName(request),
1320
1403
  normalizedBatchDomain(request.company_domain),
1321
1404
  normalizedBatchLinkedIn(request.linkedin_url),
@@ -1421,6 +1504,7 @@ function signalToCopyRequestBody(params) {
1421
1504
  title: params.title,
1422
1505
  campaign_offer: params.campaignOffer,
1423
1506
  research_prompt: params.researchPrompt,
1507
+ lookback_days: params.lookbackDays,
1424
1508
  signal_run_id: params.signalRunId,
1425
1509
  skip_cache: params.skipCache,
1426
1510
  enable_deep_search: params.enableDeepSearch,
@@ -1431,6 +1515,9 @@ function signalToCopyRequestBody(params) {
1431
1515
  }
1432
1516
  function validateSignalToCopyParams(params) {
1433
1517
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1518
+ if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
1519
+ throw new RangeError("lookbackDays must be an integer between 1 and 365");
1520
+ }
1434
1521
  if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1435
1522
  const missing = [
1436
1523
  ["companyDomain", params.companyDomain],
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Signaliz
4
- } from "./chunk-VKBUQTDS.mjs";
4
+ } from "./chunk-ENA7AVFB.mjs";
5
5
 
6
6
  // src/mcp-config.ts
7
7
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaliz/sdk",
3
- "version": "1.0.59",
3
+ "version": "1.0.61",
4
4
  "description": "Signaliz SDK for Signals Everything, Find Email, Verify Email, Company Signal Enrichment, and Signal to Copy AI.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",