mirror of
https://github.com/superdesigndev/treg.git
synced 2026-10-02 03:24:35 +08:00
Merge pull request #566 from superdesigndev/codex/provider-fixes
fix(catalog): remove unsafe batch tools and route ContactOut
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -17,13 +17,13 @@ covers (frontmatter `sources:`). Regenerate this index with
|
||||
| Fragment | Status | Covers |
|
||||
|---|---|---|
|
||||
| [Google Ads conversion tracking — capture, outbox, upload](architecture/ads-conversions.md) | shipped | adsconv.py, signup.py, adtrack.js, gtag.js |
|
||||
| [AI Ark — bounded synchronous enrichment and BYOK async jobs](architecture/aiark.md) | shipped | aiark.yaml, aiark.companies.search.json, aiark.lists.upsert.json, aiark.people.email.find.bulk.submissions.json, … |
|
||||
| [AI Ark — bounded synchronous enrichment](architecture/aiark.md) | shipped | aiark.yaml, aiark.companies.search.json, aiark.lists.upsert.json, aiark.people.email.find.json, … |
|
||||
| [Archive - versioned history and cache admission](architecture/archive.md) | building | archive.py, hunter.yaml, results.py, 0031_archive_result_admission.py, … |
|
||||
| [Auth & secrets — injectors, encryption, OAuth freshness, health](architecture/auth-secrets.md) | shipped | injectors.py, ssrf.py, crypto.py, oauth.py, … |
|
||||
| [BounceBan — email verification, billing boundaries and capacity](architecture/bounceban.md) | shipped | bounceban.yaml, bounceban.people.email.verify.json, bounceban.people.email.verify.waterfall.json, bounceban.people.email.verify.status.json, … |
|
||||
| [Endpoint catalog — what you can DO with a connected key, and which provider should do it](architecture/catalog.md) | shipped | financialdatasets.yaml, financialdatasets.company.facts.json, financialdatasets.company.facts.ciks.json, financialdatasets.company.facts.tickers.json, … |
|
||||
| [Application composition and deployment roles](architecture/composition.md) | shipped | bootstrap.py, bootstrap_handlers.py, bootstrap_http.py, call_surface.py, … |
|
||||
| [ContactOut — LinkedIn enrichment, Starter billing and independent credit pools](architecture/contactout.md) | implemented; live connect and core surface verified, informational capacity monitoring | contactout.yaml, adapters.yaml, contactout.people.email.verify.json, test_routing.py, … |
|
||||
| [ContactOut — LinkedIn enrichment, Starter billing and independent credit pools](architecture/contactout.md) | implemented; live connect and core surface verified, informational capacity monitoring | contactout.yaml, adapters.yaml, contactout.people.contact.work.json, contactout.people.contact.phone.json, … |
|
||||
| [Data model — the registry tables, async DB, audit writer](architecture/data-model.md) | shipped | alembic.ini, env.py, 0001_baseline_current_schema.py, 0002_archive_tables.py, … |
|
||||
| [Dropleads — synchronous people and company enrichment](architecture/dropleads.md) | implemented; live upstream behavior verified | dropleads.yaml, dropleads.people.email.find.json, dropleads.people.phone.find.json, dropleads.people.email.verify.json, … |
|
||||
| [Feedback - private intake for problems and suggestions](architecture/feedback.md) | shipped | feedback_contract.py, __init__.py, reports.py, reviews.py, … |
|
||||
@@ -42,7 +42,7 @@ covers (frontmatter `sources:`). Regenerate this index with
|
||||
| [Openmart — metered synchronous data and BYOK lifecycle boundaries](architecture/openmart.md) | shipped | openmart.yaml, openmart.businesses.search.json, openmart.businesses.search.ids.json, openmart.businesses.lookup.openmart.json, … |
|
||||
| [Prospeo — people and company enrichment](architecture/prospeo.md) | shipped | prospeo.yaml, prospeo.people.email.find.json, prospeo.people.phone.find.json, prospeo.people.enrich.json, … |
|
||||
| [The proxy — faithful credential-injecting relay + tool resolution](architecture/proxy-model.md) | shipped | relay.py, ssrf.py, api.py, authorize.py, … |
|
||||
| [Scrubby — quick and deep email verification](architecture/scrubby.md) | implemented; live authentication, billing and asynchronous behavior verified | scrubby.yaml, scrubby.people.email.verify.json, scrubby.people.email.verify.bulk.json, scrubby.people.email.verify.bulk.results.json, … |
|
||||
| [Scrubby — single email verification](architecture/scrubby.md) | shipped | scrubby.yaml, scrubby.people.email.verify.json, adapters.yaml, fx.yaml, … |
|
||||
| [Sumble — account intelligence, subscription credits and BYOK](architecture/sumble.md) | shipped | sumble.yaml, sumble.extended.yaml, sumble.organizations.json, sumble.py, … |
|
||||
| [Super-admin — cross-tenant read + control](architecture/super-admin.md) | shipped | api.py, admin.py, access.py, config.py |
|
||||
| [Wiza — synchronous search and company enrichment, BYOK async jobs](architecture/wiza.md) | shipped | wiza.yaml, wiza.people.search.json, wiza.companies.search.json, wiza.companies.enrich.json, … |
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
---
|
||||
title: AI Ark — bounded synchronous enrichment and BYOK async jobs
|
||||
title: AI Ark — bounded synchronous enrichment
|
||||
status: shipped
|
||||
sources:
|
||||
- src/treg/catalog/aiark.yaml
|
||||
- src/treg/catalog/examples/aiark.companies.search.json
|
||||
- src/treg/catalog/examples/aiark.lists.upsert.json
|
||||
- src/treg/catalog/examples/aiark.people.email.find.bulk.submissions.json
|
||||
- src/treg/catalog/examples/aiark.people.email.find.json
|
||||
- src/treg/catalog/examples/aiark.people.enrich.json
|
||||
- src/treg/catalog/examples/aiark.people.export.submissions.json
|
||||
- src/treg/catalog/examples/aiark.people.personality.analyze.json
|
||||
- src/treg/catalog/examples/aiark.people.phone.find.json
|
||||
- src/treg/catalog/examples/aiark.people.preview.json
|
||||
@@ -49,7 +47,7 @@ first and treg never meters it.
|
||||
|
||||
## Catalog surface and boundary
|
||||
|
||||
`aiark.yaml` exposes 18 tools from 21 documented operations. It uses the V2 single-email and mobile
|
||||
`aiark.yaml` exposes eight bounded tools from 21 documented operations. It uses the V2 single-email and mobile
|
||||
routes and omits their duplicate V1 forms. The free credits route stays internal to connection
|
||||
verification and `collectors._aiark`, so catalog callers cannot inspect either their own or treg's
|
||||
account balance.
|
||||
@@ -62,11 +60,10 @@ maximum hold. Successful platform responses settle from the provider's exact `X-
|
||||
AI Ark reports debits as negative numbers, so `_CREDIT_HEADERS` declares an explicit -1 multiplier;
|
||||
positive, missing, nonnumeric and non-finite values remain untrusted and fall back to other evidence.
|
||||
|
||||
List mutation and ten async submission, result, status, history and webhook-resend tools are
|
||||
BYOK-only. AI Ark track ids are account-scoped, a search track id is single-use, and delivery failure
|
||||
can trigger a refund up to ten hours after submission. The current shared async contract does not
|
||||
persist caller ownership and terminal charge/refund evidence for that lifecycle. The integration
|
||||
therefore adds no AI Ark branch to the call, money, async, routing or Arena runtime.
|
||||
List mutation remains BYOK-only. Ten async submission, result, status, history and webhook-resend
|
||||
operations are omitted from the catalog: AI Ark track ids are account-scoped, a search track id is
|
||||
single-use, and delivery failure can trigger a refund up to ten hours after submission. They require
|
||||
a purpose-built workflow with explicit cost and lifecycle ownership before agents can call them.
|
||||
|
||||
## Pricing and settlement
|
||||
|
||||
@@ -89,8 +86,8 @@ the existing `get` expression to normalize it. A
|
||||
reverse-lookup miss returned HTTP 404 and used no credit even though the public reference describes
|
||||
a per-request charge, so treg conservatively settles only a successful profile.
|
||||
|
||||
The two async submission tools retain documented `per_result` prices as BYOK information. They do
|
||||
not advertise a platform estimate, reserve a team balance or imply that delayed refunds are handled.
|
||||
The omitted async submissions do not advertise a platform estimate, reserve a team balance or imply
|
||||
that delayed refunds are handled.
|
||||
|
||||
## Routing and Arena
|
||||
|
||||
@@ -101,7 +98,7 @@ provider's observed response shapes. Sanitized fixtures verify every adapter whe
|
||||
The tools become route and Enrich Arena candidates through the generic capability machinery.
|
||||
|
||||
Preview and personality analysis stay direct tools because the existing routed contracts do not
|
||||
describe their outputs. Stateful list and async operations never enter Arena. There is no
|
||||
describe their outputs. The stateful list operation never enters Arena. There is no
|
||||
AI Ark-specific provider choice, retry or response wrapping.
|
||||
|
||||
## Capacity
|
||||
|
||||
@@ -6,11 +6,6 @@ sources:
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.waterfall.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.status.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.status.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.emails.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.dump.json
|
||||
- src/treg/catalog/examples/bounceban.people.email.verify.bulk.export.json
|
||||
- src/treg/catalog/examples/bounceban.account.usage.json
|
||||
- src/treg/catalog/adapters.yaml
|
||||
- src/treg/catalog/fx.yaml
|
||||
@@ -44,11 +39,10 @@ BounceBan supplies email verification through two hosts. The standard API uses
|
||||
|
||||
## Catalog boundary
|
||||
|
||||
The catalog exposes nine safe operations: standard and waterfall single verification, single-task
|
||||
status, JSON bulk creation, four bulk read/export operations, and account usage. The documented
|
||||
multipart CSV upload is omitted because the catalog has no file-upload contract. Bulk destruction
|
||||
is omitted because it is destructive. `/v1/check` is omitted because it uses a separate Check Plan
|
||||
whose acquisition price and capacity were not supplied.
|
||||
The catalog exposes four operations: standard and waterfall single verification, single-task status,
|
||||
and account usage. JSON and multipart bulk creation, bulk reads/exports, bulk destruction, and
|
||||
`/v1/check` are omitted. Bulk work needs an explicit cost-confirmation and owned-job workflow;
|
||||
`/v1/check` uses a separate plan whose acquisition price and capacity were not supplied.
|
||||
|
||||
Only `bounceban.people.email.verify` is platform eligible. It uses the standard host and does not
|
||||
expose `disable_catchall_verify` or webhook inputs. Every accepted request therefore has the proven
|
||||
@@ -57,12 +51,10 @@ body says `status=verifying`; malformed input returned HTTP 400 with no reported
|
||||
the hold through the shared settlement rule. One credit costs $0.004 from the supplied $40 / 10,000
|
||||
purchase. No provider-specific money branch is added.
|
||||
|
||||
Waterfall and bulk operations are BYOK only. Waterfall same-address retries inside 30 minutes do
|
||||
not deduct a new credit, but the repeated response reports the original credit count. A waterfall
|
||||
catch-all skipped with `disable_catchall_verify=1` can also cost zero. Bulk submission reserves
|
||||
credits first and refunds unknown results only when the task finishes. Those delayed and ambiguous
|
||||
rules cannot be settled exactly on a shared key, and task identifiers belong to the connected
|
||||
account. The related status, result, dump, and export operations stay with that account.
|
||||
Waterfall remains BYOK-only. Same-address retries inside 30 minutes do not deduct a new credit, but
|
||||
the repeated response reports the original credit count; a catch-all skipped with
|
||||
`disable_catchall_verify=1` can also cost zero. Bulk operations are omitted because submission
|
||||
reserves many credits, refunds arrive only after completion, and task identifiers are account-owned.
|
||||
|
||||
## Routing and Arena
|
||||
|
||||
|
||||
@@ -184,20 +184,21 @@ related:
|
||||
|
||||
# Endpoint catalog — platform-grouped operations per provider
|
||||
|
||||
LimaData adds all 24 Basic v2 operations. Fifteen fixed, synchronous operations can use the shared
|
||||
key; variable, 404-billed, and account-scoped batch operations require a team's own key. Six
|
||||
LimaData exposes 21 of 24 Basic v2 operations. Fourteen fixed, synchronous operations can use the
|
||||
shared key; variable and 404-billed operations require a team's own key. Account-scoped batch
|
||||
submission and result operations are omitted. Six
|
||||
fixture-verified adapters join existing routing and Enrich Arena contracts. See
|
||||
[LimaData](limadata.md) for the full boundary and live evidence.
|
||||
|
||||
## BounceBan email verification (2026-09-16)
|
||||
|
||||
BounceBan adds nine tools across standard single verification, BYOK waterfall verification, BYOK
|
||||
single/bulk lifecycle reads, JSON bulk submission, and account usage. Only the standard single tool
|
||||
BounceBan adds four tools across standard single verification, BYOK waterfall verification, BYOK
|
||||
single-result polling, and account usage. Only the standard single tool
|
||||
is platform eligible. It has a fixed observed cost of one credit, priced at the supplied acquisition
|
||||
rate of $0.004, and uses `per_call` so an accepted `status=verifying` submission is charged while a
|
||||
rejected HTTP 400 request releases its hold. Waterfall retries, conditional zero-credit catch-all
|
||||
results, bulk refunds, and task ownership make the other lifecycle operations unsafe for a shared
|
||||
key, so they remain BYOK only.
|
||||
results make waterfall unsafe for a shared key. Bulk submission and lifecycle operations are
|
||||
omitted until an explicit cost-confirmation and owned-job workflow exists.
|
||||
|
||||
The verified adapter adds only the standard endpoint to `treg.people.email.verify`; routing and
|
||||
Arena discover it from that adapter. Multipart upload, destructive bulk deletion, and the separately
|
||||
@@ -214,15 +215,18 @@ showed that it needs the key in its JSON body, which the faithful relay does not
|
||||
state-changing, and ambiguous-price operations are also outside the safe first surface. See
|
||||
[ZeroBounce](zerobounce.md) for the inventory and evidence.
|
||||
|
||||
MoltSets adds 17 verified data tools: nine single-result shared-plan offers and eight BYOK-only
|
||||
variable, batch, or dual-meter tools. See [MoltSets](moltsets.md) for the boundary and evidence.
|
||||
MoltSets adds 12 verified data tools: nine single-result shared-plan offers and three BYOK-only
|
||||
variable-result searches. Five hybrid scalar/batch email and phone operations are omitted. See
|
||||
[MoltSets](moltsets.md) for the boundary and evidence.
|
||||
|
||||
Sumble adds the full v9 surface with verified platform operations and explicit BYOK restrictions. See [Sumble](sumble.md) for schemas, pricing rules, routing and live evidence.
|
||||
|
||||
GetLeads.io adds 12 direct contact-data tools. Every tool accepts BYOK or a $0 platform trial with
|
||||
GetLeads.io adds 11 direct contact-data tools. Every tool accepts BYOK or a $0 platform trial with
|
||||
five successful credit-using calls per team per day; its two free discovery tools do not consume
|
||||
that allowance. The caller controls provider-valid page limits and batch
|
||||
sizes; the allowance counts calls rather than returned records or upstream credits. Internal
|
||||
that allowance. Three enrichment tools keep the upstream `items` array but opt into `strict_body`
|
||||
with exactly one item. `_enforce_catalog_body` rejects invalid cardinality on every credential tier
|
||||
without rewriting an accepted request. The allowance counts calls rather than returned records or
|
||||
upstream credits. Internal
|
||||
account routes, stateful exports and monitoring are excluded. See [GetLeads.io](getleadsio.md) for
|
||||
the boundary and evidence.
|
||||
|
||||
@@ -2275,8 +2279,11 @@ before any example is committed, all learned the hard way:
|
||||
|
||||
1. **No named private individuals.** Contact-lookup routes (LinkedIn contact info, people-enrichment
|
||||
by email) return a real person's name, personal email and phone. Such an endpoint stays in the
|
||||
catalog — the route is real and useful — but it is marked `untestable:` with the reason, carries
|
||||
NO `test_request` (so a re-verify cannot silently re-capture it), and no example is stored.
|
||||
catalog — the route is real and useful — but it is marked `untestable:` with the reason and
|
||||
carries NO `test_request` (so a re-verify cannot silently re-capture it). No captured person
|
||||
response is stored. A routing adapter may use a hand-sanitized structural fixture only when its
|
||||
contact values use reserved fake domains/numbers, it cannot be refreshed by the verifier, and
|
||||
separate live evidence establishes the mapped response fields.
|
||||
2. **No third-party PII riding along.** Emails and phones turn up inside unrelated payloads — a
|
||||
YouTube description, a review body. Sweep every captured example for address-shaped strings and
|
||||
mask anything that isn't a business contact.
|
||||
@@ -2338,8 +2345,11 @@ and signature test files. The reusable setup is in `tests/conftest.py`.
|
||||
|
||||
`contactout.yaml` adds the core LinkedIn/contact surface with explicit work/personal selectors,
|
||||
on-hit Starter rates supplied by the account owner, free verification, and deferred batches.
|
||||
People lookup/search entries are `untestable:` without test requests or stored examples under the
|
||||
PII rule. Their routing adapters are omitted; company search/enrichment and email verification
|
||||
Contact reveals and availability checks live on the People shelf; profile identity tools remain
|
||||
on LinkedIn. People entries stay `untestable:` without test requests under the PII rule. Work-email
|
||||
and phone lookup use reserved-value structural fixtures and verified adapters for the existing
|
||||
People routes. Personal email remains direct-only because the shared email contract is work-only;
|
||||
people search/profile adapters remain omitted. Company search/enrichment and email verification
|
||||
retain verified adapters. Profile-only LinkedIn enrichment costs $0.02 when found.
|
||||
See [ContactOut](contactout.md) for request limitations, derived settlement and live evidence.
|
||||
|
||||
@@ -2380,19 +2390,19 @@ The single verified adapter is usable by Arena; the two-provider public routing
|
||||
|
||||
## Dropleads integration
|
||||
|
||||
`dropleads.yaml` adds twelve synchronous people and company tools. The balance check and export-cost
|
||||
`dropleads.yaml` adds ten synchronous people and company tools. The balance check and export-cost
|
||||
route stay outside the public catalog. Seven verified adapters add email finding, phone finding,
|
||||
email verification, people search and enrichment, and company search and enrichment to the existing
|
||||
routed tools and Enrich Arena. The count and synchronous bulk tools stay direct. The provider uses
|
||||
routed tools and Enrich Arena. Count tools stay direct; two ten-person bulk tools are omitted. The provider uses
|
||||
the existing `CatalogTarget` allow-list for its second API host; catalog data cannot send a
|
||||
credential to another host. See [Dropleads](dropleads.md) for the surface, prices and live evidence.
|
||||
|
||||
|
||||
## Prospeo integration
|
||||
|
||||
`prospeo.yaml` adds nine people and company tools on both own and platform keys. Six verified
|
||||
`prospeo.yaml` adds seven people and company tools on both own and platform keys. Six verified
|
||||
adapters add email finding, phone finding, person/company enrichment and person/company search to
|
||||
the routed tools and Enrich Arena; bulk enrichment and search suggestions stay direct-only. Search
|
||||
the routed tools and Enrich Arena; search suggestions stay direct-only and bulk enrichment is omitted. Search
|
||||
pages are fixed at 25 upstream, so adapters cannot forward the contract `limit`; they expose
|
||||
Prospeo's `pagination.total_count` while relaying the native result page. The account-information
|
||||
route remains internal for key verification and capacity. See [Prospeo](prospeo.md) for pricing,
|
||||
|
||||
@@ -4,6 +4,8 @@ status: implemented; live connect and core surface verified, informational capac
|
||||
sources:
|
||||
- src/treg/catalog/contactout.yaml
|
||||
- src/treg/catalog/adapters.yaml
|
||||
- src/treg/catalog/examples/contactout.people.contact.work.json
|
||||
- src/treg/catalog/examples/contactout.people.contact.phone.json
|
||||
- src/treg/catalog/examples/contactout.people.email.verify.json
|
||||
- tests/test_routing.py
|
||||
- src/treg/catalog/examples/contactout.companies.search.json
|
||||
@@ -37,13 +39,16 @@ serving. No credential is committed or copied into a platform Secret row.
|
||||
|
||||
## Surface and selectors
|
||||
|
||||
Each tool has one catalog home: eight LinkedIn-specific lookup/contact tools on `linkedin`,
|
||||
ten general people tools on `people`, and two company tools on `companies`. LinkedIn placement covers the three contact splits, three availability checkers,
|
||||
LinkedIn profile enrichment and email-to-LinkedIn lookup. Their existing `contactout.people.*`
|
||||
IDs remain stable for saved CLI/API calls; platform and capability metadata control browsing.
|
||||
Capability labels distinguish work/personal email lookup from availability checks. Global catalog
|
||||
search remains cross-platform. Email verification, company search and company enrichment
|
||||
participate in their existing routed contracts. People search and profile enrichment remain direct-only: the PII rule excludes their
|
||||
Each tool has one catalog home: two LinkedIn profile-identity tools on `linkedin`, sixteen general
|
||||
people/contact tools on `people`, and two company tools on `companies`. The three contact reveals
|
||||
and three availability checks live on the People shelf even though they accept a LinkedIn URL;
|
||||
LinkedIn profile enrichment and email-to-LinkedIn lookup remain on the LinkedIn shelf. Existing
|
||||
`contactout.people.*` IDs remain stable for saved CLI/API calls; platform and capability metadata
|
||||
control browsing. Capability labels distinguish work/personal email lookup from availability
|
||||
checks. Global catalog search remains cross-platform. Work-email lookup, phone lookup, email
|
||||
verification, company search and company enrichment participate in their existing routed
|
||||
contracts. Personal-email lookup remains direct-only because the shared email route promises work
|
||||
email. People search and profile enrichment also remain direct-only: the PII rule excludes their
|
||||
verification requests/examples, so their adapter registrations are omitted.
|
||||
|
||||
The catalog covers count, personal/work email and phone availability, single email verification,
|
||||
@@ -205,8 +210,9 @@ https://api.contactout.com/#errors (checked 2026-09-08).
|
||||
|
||||
### Renewal and rollout
|
||||
|
||||
People routes carry `untestable:` with no catalog test request or stored example under the PII
|
||||
rule. These entries cannot participate in automated catalog re-verification.
|
||||
People routes carry `untestable:` with no catalog test request under the PII rule, so they cannot
|
||||
participate in automated catalog re-verification. The two routed contact tools have only fixed,
|
||||
reserved-value structural fixtures; no captured person response is retained.
|
||||
`scripts/contactout_overflow_verify.py --budget-usd 10 --apply` discovers one profile
|
||||
at runtime, builds requests using that ephemeral URL and required catalog selectors, compares
|
||||
direct and aggregator shapes through the existing verifier, then syncs stamps.
|
||||
@@ -238,22 +244,27 @@ recorded in `examples/contactout.people.email.verify.json`; no credential is inc
|
||||
routing tests cover all five documented verdicts, query preservation, zero cost, provenance,
|
||||
and fallback on missing verdicts or embedded errors.
|
||||
|
||||
## Shared discovery and profile routing
|
||||
## Shared contact, discovery and profile routing
|
||||
|
||||
Company adapters join the existing contracts without changing capability labels. The three people
|
||||
adapter registrations are omitted after removal of PII-bearing catalog fixtures; direct calls
|
||||
remain available. Tests assert their absence from shared routing; direct platform tests cover
|
||||
profile-only billing and own-key exclusion. No verification gate is bypassed:
|
||||
Work-email and phone adapters join the existing People contracts, and company adapters join the
|
||||
existing company contracts. The contact endpoints remain `untestable:` with no `test_request`, so
|
||||
catalog re-verification cannot call them and recapture PII. Their adapter output maps are checked
|
||||
against hand-sanitized structural fixtures containing only reserved fake contact values; the live
|
||||
positive checks described above establish the real response fields. Personal-email, people-search
|
||||
and profile adapters remain omitted. Direct platform tests cover profile-only billing and own-key
|
||||
exclusion. No verification gate is bypassed:
|
||||
|
||||
| Routed tool | ContactOut child | Selected behavior |
|
||||
|---|---|---|
|
||||
| `treg.people.email.find` | `contactout.people.contact.work` | LinkedIn URL; `email_type=work`, `include_phone=false` |
|
||||
| `treg.people.phone.find` | `contactout.people.contact.phone` | LinkedIn URL; `email_type=none`, `include_phone=true` |
|
||||
| Ineligible: `treg.people.search` | `contactout.people.search` | `reveal_info=false`; domain/title/name/keyword identities; page size and location filters |
|
||||
| `treg.companies.search` | `contactout.companies.search` | Domain/name/industry/technology identities; vendor page size retained |
|
||||
| `treg.companies.enrich` | `contactout.companies.enrich` | One domain, sent as a one-element `domains` array |
|
||||
| Ineligible: `treg.people.enrich` | `contactout.people.enrich` | LinkedIn URL or email; `include=[]` prevents contact reveal |
|
||||
| Ineligible: `treg.linkedin.user.profile` | `contactout.people.linkedin.enrich` | LinkedIn URL or handle; `profile_only=true` |
|
||||
|
||||
These mappings do not route decision-maker search, personal-email splits, or combined-reveal
|
||||
These mappings do not route decision-maker search, personal-email lookup, or combined-reveal
|
||||
variants. The direct provider tools retain those capabilities. Company search does not document
|
||||
a page-size control: the adapter does not forward the contract's `limit` as an invented parameter.
|
||||
Its returned companies are still metered at $0.02 each. People search and domain enrichment use
|
||||
@@ -271,5 +282,6 @@ company search two companies, domain enrichment one company, and person/LinkedIn
|
||||
one profile each. The first sample LinkedIn URL missed, so the two successful profile captures
|
||||
used a profile from that search. All captured contact arrays were empty with the above selectors.
|
||||
The people captures and their catalog requests were removed after maintainer review under the
|
||||
catalog PII rule; only company and email-verification examples remain. People tools are marked
|
||||
`untestable:` to prevent re-capture. Other accepted company input variants were not separately live-verified.
|
||||
catalog PII rule. The work-email and phone adapters instead use fixed structural fixtures with
|
||||
reserved fake values; all People tools remain `untestable:` to prevent re-capture. Other accepted
|
||||
company input variants were not separately live-verified.
|
||||
|
||||
@@ -9,9 +9,7 @@ sources:
|
||||
- src/treg/catalog/examples/dropleads.people.search.json
|
||||
- src/treg/catalog/examples/dropleads.people.search.count.json
|
||||
- src/treg/catalog/examples/dropleads.people.enrich.verified.json
|
||||
- src/treg/catalog/examples/dropleads.people.enrich.verified.bulk.json
|
||||
- src/treg/catalog/examples/dropleads.people.enrich.json
|
||||
- src/treg/catalog/examples/dropleads.people.enrich.bulk.json
|
||||
- src/treg/catalog/examples/dropleads.companies.search.json
|
||||
- src/treg/catalog/examples/dropleads.companies.search.count.json
|
||||
- src/treg/catalog/examples/dropleads.companies.enrich.json
|
||||
@@ -48,11 +46,10 @@ because `CatalogTarget` explicitly allow-lists it. The platform credential is
|
||||
|
||||
## Public surface
|
||||
|
||||
The catalog exposes twelve synchronous tools: email finding, mobile finding, email verification,
|
||||
people search and count, verified person enrichment and its 10-item bulk form, simple person
|
||||
enrichment and its 10-item bulk form, and company search, count and enrichment. Company enrichment
|
||||
accepts at most 50 combined domains and company names. These are synchronous bulk requests, so they
|
||||
fit the ordinary relay; no asynchronous task model was added.
|
||||
The catalog exposes ten synchronous tools: email finding, mobile finding, email verification,
|
||||
people search and count, verified and simple single-person enrichment, and company search, count and
|
||||
enrichment. Company enrichment accepts at most 50 combined domains and company names. The two
|
||||
10-person bulk enrichment operations are omitted from the catalog.
|
||||
|
||||
The two single-person Try requests use a neutral name, organization and domain combination. They
|
||||
show a documented identity shape without storing a user's email or LinkedIn profile in the catalog.
|
||||
@@ -63,7 +60,7 @@ searchable or callable through the public catalog on BYOK or platform credential
|
||||
|
||||
Seven adapters join existing routed tools and therefore the corresponding Enrich Arena tasks:
|
||||
work-email finding, phone finding, email verification, people search, simple person enrichment,
|
||||
company search and company enrichment. Count and bulk endpoints remain direct tools. Verified
|
||||
company search and company enrichment. Count endpoints remain direct tools. Verified
|
||||
person enrichment has a distinct catalog capability rather than silently changing the ordinary
|
||||
person-enrichment contract. Company count reuses the existing `companies.search.count` capability,
|
||||
so it compares with other providers that count the same search result set.
|
||||
@@ -81,8 +78,8 @@ Dropleads settlement is provider-specific. `credits_charged` is read for finder/
|
||||
finite, nonnegative reported value—including zero—replaces the reservation estimate. Email Finder's
|
||||
documented `status: not_found` response omits its numeric field and settles at zero explicitly.
|
||||
Missing or malformed charge evidence falls back to the catalog estimate rather than inventing a
|
||||
free call. Request-time reservation counts `details` for 10-item people batches, combines
|
||||
`domains + companyNames` up to 50 for company enrichment, and caps company-search pagination at 50.
|
||||
free call. Request-time reservation combines `domains + companyNames` up to 50 for company
|
||||
enrichment and caps company-search pagination at 50.
|
||||
Numeric string limits use the same bound, so a request for `"50"` cannot reserve only the default
|
||||
20 rows. People search sends `filters.countries` as a list; company search sends the provider's
|
||||
different `{include: [...]}` country object. Live three-way count checks proved both shapes.
|
||||
@@ -104,8 +101,8 @@ The other paid rates remain documented until equivalent endpoint-specific billin
|
||||
retained; their response shapes still support exact settlement when they report a charge. Tested
|
||||
misses were free. People search and count agreed on totals. People search accepted a requested
|
||||
limit above 50 but returned at most 50.
|
||||
Both people bulk routes rejected 0 and 11 inputs; company enrichment rejected more than 50 combined
|
||||
identities. Prime balance and API-host consumption can reconcile with delay, so per-call settlement
|
||||
Historical discovery confirmed both omitted people-bulk routes rejected 0 and 11 inputs;
|
||||
company enrichment rejected more than 50 combined identities. Prime balance and API-host consumption can reconcile with delay, so per-call settlement
|
||||
uses response evidence rather than before/after wallet reads.
|
||||
|
||||
The same live checks confirmed that both count routes return root-level `{success, count}`. Their
|
||||
|
||||
@@ -7,7 +7,6 @@ sources:
|
||||
- src/treg/catalog/examples/getleadsio.people.enrich.from_linkedin.json
|
||||
- src/treg/catalog/examples/getleadsio.people.enrich.from_person.json
|
||||
- src/treg/catalog/examples/getleadsio.people.phone.lookup.json
|
||||
- src/treg/catalog/examples/getleadsio.people.phone.lookup_batch.json
|
||||
- src/treg/catalog/examples/getleadsio.people.colleagues.json
|
||||
- src/treg/catalog/examples/getleadsio.people.decision_makers.json
|
||||
- src/treg/catalog/examples/getleadsio.people.search.json
|
||||
@@ -44,16 +43,16 @@ a positive credit balance. A team's own key wins over treg's key and is never me
|
||||
|
||||
## Public surface
|
||||
|
||||
The catalog publishes 12 direct tools: email, LinkedIn, and person enrichment; single and batch
|
||||
phone lookup; colleague and decision-maker lookup; contact search and count; filter discovery; and
|
||||
funding and acquisition feeds. The fair-use and contacts-health routes remain internal. CSV upload,
|
||||
The catalog publishes 11 direct tools: three single-person enrichment operations, single phone
|
||||
lookup, colleague and decision-maker lookup, contact search and count, filter discovery, and funding
|
||||
and acquisition feeds. The phone batch operation is omitted. The fair-use and contacts-health routes remain internal. CSV upload,
|
||||
asynchronous search export, and profile-monitoring state are excluded because they introduce files,
|
||||
jobs, or account-owned resources that this integration does not yet model.
|
||||
|
||||
All 12 tools can use either a team's own key or treg's key. The catalog exposes only the original
|
||||
provider operations and does not add restricted `.trial` copies. Callers choose any value that the
|
||||
provider accepts for page limits, batch sizes, and other request fields. treg does not insert,
|
||||
reduce, or otherwise rewrite those values. No provider-specific relay branch is added.
|
||||
All 11 tools can use either a team's own key or treg's key. The catalog exposes only the original
|
||||
provider operations and does not add restricted `.trial` copies. The three enrichment tools preserve the upstream `items` array but require exactly one entry on every
|
||||
credential tier; invalid cardinality is rejected before relay and accepted requests are not rewritten.
|
||||
Other provider-native request fields remain a faithful relay.
|
||||
|
||||
No GetLeads.io tool joins routed capabilities or Enrich Arena. Live synthetic misses were not a
|
||||
reliable success signal: reserved invalid inputs on email, LinkedIn, and person enrichment returned
|
||||
@@ -91,7 +90,7 @@ contact values, or absolute balances.
|
||||
| Search count and filter values | 200 | Reported zero usage; balance unchanged |
|
||||
| Contact search, two one-row pages | 200 | Distinct rows; one reported and consumed credit per page |
|
||||
| Contact search, zero-row miss | 200 | Reported zero usage; balance unchanged |
|
||||
| Phone lookup and phone batch, synthetic miss | 200 | No match; balance unchanged |
|
||||
| Phone lookup, synthetic miss | 200 | No match; balance unchanged |
|
||||
| Colleagues and decision makers, synthetic miss | 200 | Zero rows and zero reported usage; balance unchanged |
|
||||
| Email, LinkedIn, and person enrichment, reserved invalid inputs | 200 | Each returned success and consumed one credit; motivates no adapter |
|
||||
| Funding feed, limit one | 200 | One row and one reported/consumed credit |
|
||||
|
||||
@@ -24,9 +24,6 @@ sources:
|
||||
- src/treg/catalog/examples/limadata.web.extract.json
|
||||
- src/treg/catalog/examples/limadata.web.research.json
|
||||
- src/treg/catalog/examples/limadata.google.serp.organic.json
|
||||
- src/treg/catalog/examples/limadata.people.audience.batch.start.json
|
||||
- src/treg/catalog/examples/limadata.people.email.verify.batch.start.json
|
||||
- src/treg/catalog/examples/limadata.people.batch.results.json
|
||||
- src/treg/catalog/adapters.yaml
|
||||
- src/treg/catalog/fx.yaml
|
||||
- src/treg/config.py
|
||||
@@ -64,13 +61,13 @@ allows `limadata`. A team's own key remains first and treg never meters it.
|
||||
|
||||
## Surface and shared-key boundary
|
||||
|
||||
`limadata.yaml` catalogs all 24 operations in the official Basic v2 OpenAPI document. Connected
|
||||
teams can call every operation. Fourteen synchronous operations can use the shared key because their
|
||||
`limadata.yaml` exposes 21 of the 24 operations in the official Basic v2 OpenAPI document. The two
|
||||
batch submissions and their shared result reader are omitted from the catalog. Fourteen synchronous operations can use the shared key because their
|
||||
successful charge is fixed and bounded: company enrichment, database autocomplete, company count,
|
||||
five contact and identity lookups, email verification, company LinkedIn lookup, phone lookup, AI
|
||||
research, and web search.
|
||||
|
||||
Ten operations stay BYOK-only:
|
||||
Seven exposed operations stay BYOK-only:
|
||||
|
||||
- Person enrichment varies from one to 15 credits based on the identifier and optional results.
|
||||
- People count requires People Database API access, which is not enabled on treg's shared account.
|
||||
@@ -80,10 +77,7 @@ Ten operations stay BYOK-only:
|
||||
- Identity resolution costs two credits even on HTTP 404. Generic settlement releases a hold on an
|
||||
upstream error, so shared service would undercharge.
|
||||
- URL extraction varies by URL count and JavaScript-rendering mode.
|
||||
- Both batch submissions and batch result reads use account-scoped job identifiers. Advertising
|
||||
batches refund misses only after completion.
|
||||
|
||||
The batch-results route is free. A live completed-job response carried `x-credits-cost: 1`, but the
|
||||
The omitted batch-results route is free. A live completed-job response carried `x-credits-cost: 1`, but the
|
||||
dashboard showed no polling activity or debit. That header describes task evidence and must not be
|
||||
used as the current request price. No LimaData branch is added to the shared settlement runtime.
|
||||
|
||||
|
||||
@@ -8,11 +8,6 @@ sources:
|
||||
- src/treg/catalog/examples/moltsets.linkedin.profile.search.json
|
||||
- src/treg/catalog/examples/moltsets.people.email.find.name.json
|
||||
- src/treg/catalog/examples/moltsets.people.enrich.name.json
|
||||
- src/treg/catalog/examples/moltsets.people.email.find.json
|
||||
- src/treg/catalog/examples/moltsets.people.email.find.business.json
|
||||
- src/treg/catalog/examples/moltsets.people.email.find.personal.json
|
||||
- src/treg/catalog/examples/moltsets.people.email.find.personal-best.json
|
||||
- src/treg/catalog/examples/moltsets.people.phone.find.json
|
||||
- src/treg/catalog/examples/moltsets.people.enrich.email.json
|
||||
- src/treg/catalog/examples/moltsets.people.enrich.linkedin.json
|
||||
- src/treg/catalog/examples/moltsets.people.audiences.maid.json
|
||||
@@ -55,7 +50,7 @@ caller value without adding provider logic to the relay. A team's own key wins a
|
||||
by treg. `platform_key_moltsets` supplies the optional shared credential, and the existing provider
|
||||
allow-list remains the production switch. This integration does not change that switch.
|
||||
|
||||
The catalog exposes all 17 documented data operations: people and company search, name/company
|
||||
The catalog exposes 12 of 17 documented data operations: people and company search, name/company
|
||||
lookups, four profile-to-email variants, phone lookup, two reverse enrichments, three audience/hash
|
||||
lookups, email-to-profile, and IP-to-company. Every operation was exercised with synthetic inputs.
|
||||
MoltSets returns HTTP 200 for both hits and misses; `status: ok` is a hit and
|
||||
@@ -76,20 +71,13 @@ Nine single-result tools are safe shared-key offers: business email and profile
|
||||
email and profile lookup, three audience/hash conversions, email-to-profile, and IP-to-company.
|
||||
They reserve one ordinary record and settle only when `status` is `ok`.
|
||||
|
||||
Eight tools are BYOK-only:
|
||||
Three exposed tools are BYOK-only:
|
||||
|
||||
- People search, company search, and profile search can return variable record counts; profile
|
||||
search also has a free `count_only` mode.
|
||||
- Four profile-to-email tools accept either one URL or batches of up to 100, whose entries can have
|
||||
mixed hit/miss outcomes.
|
||||
- Phone lookup accepts the same batch shape and has two meters on a hit: one ordinary record plus
|
||||
one phone token.
|
||||
|
||||
The current generic response contract cannot count these variable or per-item outcomes, and the
|
||||
request guard cannot safely promise single-only input for a body that documents both fields. They
|
||||
remain faithful, unmetered BYOK relays rather than gaining MoltSets-specific call or money logic.
|
||||
Phone pricing is displayed as the documented upper-bound replacement cost: $0.50 phone token plus
|
||||
the $0.01 ordinary record, or $0.51 per hit. Misses cost neither meter.
|
||||
The four hybrid profile-to-email operations and hybrid phone lookup are omitted because each accepts
|
||||
one URL or a batch of up to 100 with mixed hit/miss outcomes. They need a separately designed scalar
|
||||
or confirmed-batch contract before agents can call them.
|
||||
|
||||
## Routing and Arena
|
||||
|
||||
|
||||
@@ -9,13 +9,6 @@ sources:
|
||||
- src/treg/catalog/examples/openmart.businesses.lookup.google-place.json
|
||||
- src/treg/catalog/examples/openmart.companies.enrich.json
|
||||
- src/treg/catalog/examples/openmart.companies.search.json
|
||||
- src/treg/catalog/examples/openmart.people.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.technologies.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.companies.email.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.people.enrich.batch.json
|
||||
- src/treg/catalog/examples/openmart.tasks.batch.status.json
|
||||
- src/treg/catalog/examples/openmart.tasks.batch.ids.json
|
||||
- src/treg/catalog/examples/openmart.tasks.get.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.create.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.check.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.delete.json
|
||||
@@ -53,8 +46,8 @@ related:
|
||||
|
||||
Openmart is a pasted Bearer-key enrichment provider at `https://api.openmart.ai`. The free
|
||||
`GET /api/v2/credit-balance` operation verifies a connection and supplies the capacity collector.
|
||||
The catalog exposes 16 caller-facing operations: business and brand search, two ID lookups, company
|
||||
enrichment, four batch submission types, three task reads, and three deny-rule operations. The
|
||||
The catalog exposes nine caller-facing operations: business and brand search, two ID lookups,
|
||||
company enrichment, and three deny-rule operations. The
|
||||
balance route is deliberately not a tool: it is internal connection/capacity infrastructure.
|
||||
|
||||
## Shared-key boundary
|
||||
@@ -73,10 +66,10 @@ records hold eight credits. Own-key calls bypass the guard and retain the upstre
|
||||
catalog access check prices each endpoint's runnable example with this same Openmart-specific
|
||||
formula instead of the generic 20-row estimate.
|
||||
|
||||
The other 11 operations remain BYOK-only. Fast ID search has no proven fractional price. The four
|
||||
batch submissions and three task reads are one delayed, account-owned lifecycle whose charges
|
||||
cannot be assigned safely by a synchronous response. The three deny-rule operations read or mutate
|
||||
private account state and forbid caching. The shared balance route is absent from the catalog so no
|
||||
The other four exposed operations remain BYOK-only. Fast ID search has no proven fractional price,
|
||||
and the three deny-rule operations read or mutate private account state and forbid caching. Four
|
||||
batch submissions and three task reads are omitted because their delayed, account-owned lifecycle
|
||||
needs explicit cost confirmation and durable ownership. The shared balance route is absent from the catalog so no
|
||||
caller can inspect operational inventory.
|
||||
|
||||
The catalog records the active subscription conversion of $149 for 5,000 credits, or $0.0298 per
|
||||
@@ -93,8 +86,7 @@ Openmart tools are direct-call only: none has a routing adapter and none appears
|
||||
The direct platform-eligible tools remain callable with treg's key. A live nonsense business query
|
||||
returned an unrelated fallback row with `match_score: 0`, so direct callers must treat that score as
|
||||
a miss; the result is not safe for automatic selection. Company enrichment can return several
|
||||
location matches, so choosing the first would also change semantics. People operations are
|
||||
asynchronous and do not join the synchronous people routes or Arena tasks.
|
||||
location matches, so choosing the first would also change semantics. Asynchronous people operations are omitted and do not join the synchronous people routes or Arena tasks.
|
||||
|
||||
## Capacity and evidence
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ sources:
|
||||
- src/treg/catalog/examples/prospeo.people.email.find.json
|
||||
- src/treg/catalog/examples/prospeo.people.phone.find.json
|
||||
- src/treg/catalog/examples/prospeo.people.enrich.json
|
||||
- src/treg/catalog/examples/prospeo.people.enrich.bulk.json
|
||||
- src/treg/catalog/examples/prospeo.people.search.json
|
||||
- src/treg/catalog/examples/prospeo.companies.enrich.json
|
||||
- src/treg/catalog/examples/prospeo.companies.enrich.bulk.json
|
||||
- src/treg/catalog/examples/prospeo.companies.search.json
|
||||
- src/treg/catalog/examples/prospeo.search.suggestions.json
|
||||
- src/treg/catalog/adapters.yaml
|
||||
@@ -46,15 +44,14 @@ metered by treg.
|
||||
|
||||
## Public surface and routing
|
||||
|
||||
The public catalog contains nine tools: email finding, phone finding, person enrichment, 50-person
|
||||
bulk enrichment, person search, company enrichment, 50-company bulk enrichment, company search and
|
||||
search suggestions. `/account-information` remains internal to connection verification and capacity
|
||||
The public catalog contains seven tools: email finding, phone finding, person enrichment, person
|
||||
search, company enrichment, company search and search suggestions. `/account-information` remains internal to connection verification and capacity
|
||||
collection; callers cannot use the catalog to inspect either their own or treg's account balance.
|
||||
|
||||
Six fixture-verified adapters join routed capabilities: `people.email.find`, `people.phone.find`,
|
||||
`people.enrich`, `people.search`, `companies.enrich` and `companies.search`. The corresponding
|
||||
single-record tasks participate in Enrich Arena where that capability has a task. Bulk endpoints and
|
||||
suggestions remain direct tools because their contracts are not scalar enrichment results.
|
||||
`people.enrich`, `people.search`, `companies.enrich` and `companies.search`. The corresponding single-record tasks participate in Enrich Arena where that capability has a task.
|
||||
Suggestions remain a direct tool because its contract is not a scalar enrichment result. The two
|
||||
50-record bulk enrichment operations are omitted from the catalog.
|
||||
|
||||
The three single-person tools deliberately share `/enrich-person` with different fixed selectors.
|
||||
Email finding requires a verified email and disables mobile enrichment. Phone finding requires a
|
||||
@@ -66,24 +63,19 @@ still relays the caller's request without treg metering.
|
||||
|
||||
The platform conversion in `fx.yaml` is the public Starter list price: $49 / 2,000 credits, or
|
||||
$0.0245 per credit before the configured platform margin. Single email, person and company hits cost
|
||||
one credit. A non-free phone hit has a fixed ten-credit platform price. Search charges one credit for
|
||||
a non-empty page; suggestions are free. Bulk responses report the authoritative `total_cost`.
|
||||
one credit. A non-free phone hit has a fixed ten-credit platform price. Search charges one credit for a non-empty page; suggestions are free.
|
||||
|
||||
The phone response does not expose whether Prospeo internally charged nine incremental credits after
|
||||
a prior email reveal or ten credits for a fresh combined reveal. treg therefore advertises, reserves
|
||||
and settles a predictable ten-credit price for every non-free phone hit. `free_enrichment=true`
|
||||
still settles zero. Bulk mobile reservation reads the nine-credit rider from
|
||||
`cost.modifiers.enrich_mobile.add_credits_per_result`; `resolve._credit_modifiers` owns the generic
|
||||
arithmetic, so Python contains no Prospeo credit multiplier.
|
||||
|
||||
`settle._prospeo_cost_micro` requires endpoint-specific success evidence. Email finding requires a
|
||||
still settles zero. `settle._prospeo_cost_micro` requires endpoint-specific success evidence. Email finding requires a
|
||||
non-empty `person.email.email`; person enrichment charges one credit for a non-empty person object
|
||||
when `free_enrichment=false`, because email is optional in profile mode; phone finding requires a
|
||||
non-empty `person.mobile.mobile_international`; company enrichment requires a company object. A
|
||||
recognizable email- or phone-finder field-level miss settles zero even when an envelope contains
|
||||
`free_enrichment=false`. A malformed or wrongly typed single-enrichment object returns no
|
||||
observation and retains the frozen estimate for reconciliation. Explicit provider errors settle
|
||||
zero, searches use `free` plus the result list, and bulk calls use finite, nonnegative `total_cost`.
|
||||
zero, and searches use `free` plus the result list.
|
||||
|
||||
The non-free person-with-null-email shape was not observed in the live evidence pass. Treating
|
||||
`free_enrichment=false` plus a non-empty person as one charged credit is the conservative billing
|
||||
@@ -129,8 +121,6 @@ operator record.
|
||||
| `prospeo.people.enrich` | Public profile with unavailable email | 200 | `B → B` | person object and `free_enrichment=true` |
|
||||
| `prospeo.people.enrich` | Three fresh public profiles with unavailable email | 200 each | `B → B` each | non-empty person, null/absent email and `free_enrichment=true` in all three |
|
||||
| `prospeo.people.phone.find` | Prior email-reveal profile | 200 | `B → B-9` | mobile present, no numeric charge field; motivates fixed ten-credit platform price |
|
||||
| `prospeo.people.enrich.bulk` | One-record repeat | 200 | `B → B` | exact `total_cost=0` |
|
||||
| `prospeo.companies.enrich.bulk` | One-record repeat | 200 | `B → B` | exact `total_cost=0` |
|
||||
|
||||
A second end-to-end pass went through local treg rather than directly to Prospeo. Suggestions returned
|
||||
`X-Treg-Cost-Micro: 0`; a company enrichment reserved and settled 24,500 micro-USD; a deduplicated
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
title: Scrubby — quick and deep email verification
|
||||
status: implemented; live authentication, billing and asynchronous behavior verified
|
||||
title: Scrubby — single email verification
|
||||
status: shipped
|
||||
sources:
|
||||
- src/treg/catalog/scrubby.yaml
|
||||
- src/treg/catalog/examples/scrubby.people.email.verify.json
|
||||
- src/treg/catalog/examples/scrubby.people.email.verify.bulk.json
|
||||
- src/treg/catalog/examples/scrubby.people.email.verify.bulk.results.json
|
||||
- src/treg/catalog/examples/scrubby.people.email.verify.deep.json
|
||||
- src/treg/catalog/examples/scrubby.people.email.verify.deep.results.json
|
||||
- src/treg/catalog/adapters.yaml
|
||||
- src/treg/catalog/fx.yaml
|
||||
- src/treg/config.py
|
||||
@@ -41,17 +37,14 @@ Python urllib client profile. `TREG_PLATFORM_KEY_SCRUBBY` enables shared-key acc
|
||||
|
||||
## Surface and routing
|
||||
|
||||
The catalog exposes the complete five-operation OpenAPI surface: synchronous single verification,
|
||||
quick-bulk submit and fetch, and deep-bulk submit and fetch. The single tool adapts to the existing
|
||||
The catalog exposes only synchronous single verification. It adapts to the existing
|
||||
`people.email.verify` contract and therefore participates in normal routing and Enrich Arena.
|
||||
`Valid` maps to `valid=true`; `Invalid`, `Risky`, and `Unknown` are useful negative or uncertain
|
||||
answers rather than misses.
|
||||
|
||||
Only the single tool can use treg's shared key. The four bulk lifecycle tools are BYOK-only. Scrubby
|
||||
publishes no batch-size maximum, so treg cannot bound a shared-key reservation. Its poll identifier
|
||||
is also carried in a JSON body, while the existing shared-key ownership guard authorizes declared
|
||||
request parameters. The catalog does not invent a limit or weaken task ownership to add bulk
|
||||
platform access.
|
||||
The four bulk lifecycle operations are omitted from the catalog. Scrubby publishes no batch-size
|
||||
maximum, so one agent call can spend an unbounded number of credits; the delayed result lifecycle
|
||||
also needs explicit ownership and completion handling.
|
||||
|
||||
## Pricing and settlement
|
||||
|
||||
@@ -69,7 +62,7 @@ with `credits_used=0`. treg's real upstream timeout is 180 seconds: a fresh rese
|
||||
completed successfully after 108.5 seconds and reported one credit. The public OpenAPI statement
|
||||
that the server returns a refunded 408 after 15 seconds does not match this observed behavior.
|
||||
|
||||
## Async behavior and capacity
|
||||
## Excluded async behavior and capacity
|
||||
|
||||
Quick bulk returned 202, charged exactly the number of fresh inputs, and exposed an identifier plus
|
||||
a 30-second retry interval. Immediate polling returned processing; a later cached batch returned
|
||||
|
||||
@@ -216,6 +216,27 @@ def check_strict_query(ep: dict, where: str, errors: list[str]) -> None:
|
||||
fail(errors, where, f"strict query field {name} enum must contain strings")
|
||||
|
||||
|
||||
def check_strict_body(ep: dict, where: str, errors: list[str]) -> None:
|
||||
if "strict_body" not in ep:
|
||||
return
|
||||
if ep["strict_body"] is not True:
|
||||
fail(errors, where, "strict_body must be true when present")
|
||||
return
|
||||
fields = (ep.get("input") or {}).get("body")
|
||||
arrays = [
|
||||
spec for spec in (fields or {}).values()
|
||||
if isinstance(spec, dict) and str(spec.get("type") or "").startswith("array")
|
||||
]
|
||||
if ep.get("method") not in {"POST", "PUT", "PATCH"} or not arrays:
|
||||
fail(errors, where, "strict_body requires a body method with a declared array field")
|
||||
return
|
||||
for spec in arrays:
|
||||
minimum = spec.get("minItems", spec.get("min"))
|
||||
maximum = spec.get("maxItems", spec.get("max"))
|
||||
if not isinstance(minimum, int) or not isinstance(maximum, int) or minimum > maximum:
|
||||
fail(errors, where, "strict_body array fields require valid integer min/max bounds")
|
||||
|
||||
|
||||
def check_platform_request(rule: object, input_schema: object, where: str,
|
||||
errors: list[str]) -> None:
|
||||
"""Platform-only fixed body values; BYOK input remains an upstream contract."""
|
||||
@@ -921,6 +942,7 @@ def main(argv: list[str]) -> int:
|
||||
check_status_marker(ep, where, endpoint_status, errors)
|
||||
inp = ep.get("input") or {}
|
||||
check_strict_query(ep, where, errors)
|
||||
check_strict_body(ep, where, errors)
|
||||
check_platform_auth(ep, where, errors)
|
||||
if "platform_request" in ep:
|
||||
check_platform_request(ep["platform_request"], inp, where, errors)
|
||||
|
||||
@@ -658,14 +658,6 @@ def _marketplace_pricing(
|
||||
doc = _json_object(body)
|
||||
rate = float(cost.get("usd") or 0)
|
||||
unit = _usd_to_micro(rate)
|
||||
if endpoint_id in (
|
||||
"dropleads.people.enrich.verified.bulk",
|
||||
"dropleads.people.enrich.bulk",
|
||||
):
|
||||
details = doc.get("details")
|
||||
count = len(details) if isinstance(details, list) else 1
|
||||
# More than 10 is rejected before charging; reserve the maximum valid request.
|
||||
return _usd_to_micro(rate * max(1, min(count, 10))), unit
|
||||
if endpoint_id == "dropleads.companies.enrich":
|
||||
domains = doc.get("domains") if isinstance(doc.get("domains"), list) else []
|
||||
names = doc.get("companyNames") if isinstance(doc.get("companyNames"), list) else []
|
||||
@@ -727,14 +719,7 @@ def _marketplace_pricing(
|
||||
credits = -(-asked // 10) # ceil division: whole credits, minimum 1
|
||||
return _usd_to_micro(credits * rate), _usd_to_micro(rate)
|
||||
return estimate, unit
|
||||
record_count = None
|
||||
if provider == "prospeo" and endpoint_id in (
|
||||
"prospeo.people.enrich.bulk",
|
||||
"prospeo.companies.enrich.bulk",
|
||||
):
|
||||
records = _json_object(body).get("data")
|
||||
record_count = max(1, min(len(records) if isinstance(records, list) else 1, 50))
|
||||
if provider != "aviato" and not cost.get("modifiers") and record_count is None:
|
||||
if provider != "aviato" and not cost.get("modifiers"):
|
||||
return estimate, unit
|
||||
|
||||
# Credit-priced providers with a `cost.modifiers` block (Aviato, cloro): the request decides
|
||||
@@ -751,12 +736,6 @@ def _marketplace_pricing(
|
||||
return 0, 0
|
||||
credits = float(cost.get("value") or 0) + added
|
||||
settled_credits = float(cost.get("value") or 0) + settled_added
|
||||
if record_count is not None:
|
||||
# Prospeo's bulk routes price the base and optional mobile rider per submitted record.
|
||||
# The request shape selects the count; every credit number remains catalog-declared.
|
||||
return credit_micro(credits + per_result) * record_count, credit_micro(
|
||||
float(cost.get("value") or 0)
|
||||
)
|
||||
if endpoint_id in ("aviato.companies.enrich.bulk", "aviato.people.enrich.bulk"):
|
||||
lookups = doc.get("lookups") if isinstance(doc.get("lookups"), list) else []
|
||||
per_record = credit_micro(credits)
|
||||
@@ -1163,6 +1142,44 @@ def _enforce_catalog_query(ep: dict, query: QueryValues, has_body: bool) -> None
|
||||
)
|
||||
|
||||
|
||||
def _enforce_catalog_body(ep: dict, body: bytes) -> None:
|
||||
"""Enforce opt-in array cardinality without rewriting a catalog request.
|
||||
|
||||
Most catalog schemas describe the upstream API and deliberately leave BYOK requests as a
|
||||
faithful relay. ``strict_body`` is the narrow exception for a catalog tool whose advertised
|
||||
contract is intentionally smaller than the upstream surface. Array limits are read from the
|
||||
existing input declaration and applied on every credential tier.
|
||||
"""
|
||||
if not ep.get("strict_body"):
|
||||
return
|
||||
document = _strict_json_object(body, ep["id"])
|
||||
fields = (ep.get("input") or {}).get("body") or {}
|
||||
for name, spec in fields.items():
|
||||
if not isinstance(spec, dict) or not str(spec.get("type") or "").startswith("array"):
|
||||
continue
|
||||
value = document.get(name)
|
||||
minimum = spec.get("minItems", spec.get("min"))
|
||||
maximum = spec.get("maxItems", spec.get("max"))
|
||||
valid = isinstance(value, list)
|
||||
if valid and isinstance(minimum, int):
|
||||
valid = len(value) >= minimum
|
||||
if valid and isinstance(maximum, int):
|
||||
valid = len(value) <= maximum
|
||||
if not valid:
|
||||
expected = (
|
||||
f"between {minimum} and {maximum}" if minimum != maximum
|
||||
else f"exactly {minimum}"
|
||||
)
|
||||
raise ResolutionFailed(
|
||||
"catalog_parameter_invalid", status_code=400, detail={
|
||||
"error": "catalog_parameter_invalid",
|
||||
"endpoint_id": ep["id"],
|
||||
"parameter": f"body.{name}",
|
||||
"message": f"{ep['id']} requires {expected} item in body.{name}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _enforce_platform_request(ep: dict, body: bytes) -> None:
|
||||
"""Check explicit platform constraints and fixed pricing selectors before reserve/relay.
|
||||
|
||||
@@ -1543,6 +1560,7 @@ async def _resolve_marketplace_call(
|
||||
|
||||
upstream, consumed = _marketplace_upstream(ep, provider, query, chosen_method)
|
||||
body = await read_body() if has_body else b""
|
||||
_enforce_catalog_body(ep, body)
|
||||
phash = _params_hash(ep["id"], query.multi_items(), body)
|
||||
# The catalog's estimate travels on EVERY tier - informational on tiers 1/2 (the provider bills
|
||||
# the org's own account; Activity shows "estimated") and the reserve amount on tier 4 only
|
||||
|
||||
@@ -174,20 +174,11 @@ def _quickenrich_cost_micro(mk: MarketplaceCall, doc: dict) -> int | None:
|
||||
|
||||
|
||||
def _prospeo_cost_micro(mk: MarketplaceCall, doc: dict) -> int | None:
|
||||
"""Settle from Prospeo's dedupe flags, endpoint success field, and exact bulk charge."""
|
||||
"""Settle from Prospeo's dedupe flags and endpoint-specific success fields."""
|
||||
if doc.get("error") is True:
|
||||
return 0
|
||||
if doc.get("error") is not False:
|
||||
return None
|
||||
if mk.endpoint_id in (
|
||||
"prospeo.people.enrich.bulk",
|
||||
"prospeo.companies.enrich.bulk",
|
||||
):
|
||||
credits = doc.get("total_cost")
|
||||
if (isinstance(credits, (int, float)) and not isinstance(credits, bool)
|
||||
and math.isfinite(credits) and credits >= 0):
|
||||
return int(credits * mk.unit_micro + 0.5)
|
||||
return None
|
||||
if mk.endpoint_id in ("prospeo.people.search", "prospeo.companies.search"):
|
||||
if doc.get("free") is True:
|
||||
return 0
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
|
||||
adapters:
|
||||
# ---- people.email.find --------------------------------------------------------------------
|
||||
contactout.people.contact.work:
|
||||
accepts: [[linkedin_url]]
|
||||
in: {linkedin_url: queryParams.profile}
|
||||
const: {queryParams.email_type: work, queryParams.include_phone: false}
|
||||
test_identity: {linkedin_url: "https://www.linkedin.com/in/routing-fixture"}
|
||||
out: {email: "profile.work_email[0]"}
|
||||
miss: "at_least(status_code != 200, coalesce(profile.work_email) == null)"
|
||||
dropleads.people.email.find:
|
||||
accepts: [[first_name, last_name, domain]]
|
||||
in: {first_name: body.first_name, last_name: body.last_name, domain: body.company_domain}
|
||||
@@ -258,6 +265,13 @@ adapters:
|
||||
miss: "data.creator_info.unique_id == null"
|
||||
|
||||
# ---- people.phone.find --------------------------------------------------------------------
|
||||
contactout.people.contact.phone:
|
||||
accepts: [[linkedin_url]]
|
||||
in: {linkedin_url: queryParams.profile}
|
||||
const: {queryParams.email_type: none, queryParams.include_phone: true}
|
||||
test_identity: {linkedin_url: "https://www.linkedin.com/in/routing-fixture"}
|
||||
out: {phone: "profile.phone[0]", line_type: "'mobile'"}
|
||||
miss: "at_least(status_code != 200, coalesce(profile.phone) == null)"
|
||||
dropleads.people.phone.find:
|
||||
accepts: [[linkedin_url]]
|
||||
in: {linkedin_url: body.linkedin_url}
|
||||
@@ -2352,8 +2366,10 @@ adapters:
|
||||
|
||||
|
||||
|
||||
# ContactOut people search/profile adapters are omitted: the PII rule excludes
|
||||
# their verification fixtures. Direct tools remain available. Company discovery:
|
||||
# ContactOut work-email and phone lookups use sanitized structural fixtures above. People
|
||||
# search/profile adapters remain omitted because their PII-bearing responses have no safe
|
||||
# routing fixture. Personal email stays direct-only because the shared email contract is work-only.
|
||||
# Company discovery:
|
||||
contactout.companies.search:
|
||||
accepts: [[domain], [name], [industry], [technology]]
|
||||
in: {domain: body.domain, name: body.name, industry: body.industry, technology: body.technologies}
|
||||
|
||||
+1
-215
@@ -1,6 +1,6 @@
|
||||
# AI Ark documents 21 callable operations. treg exposes the V2 single-email and mobile routes and
|
||||
# omits their duplicate V1 forms. The free credit route is internal to connection verification and
|
||||
# capacity collection. Stateful lists and both asynchronous job families remain BYOK-only.
|
||||
# capacity collection. Stateful lists remain BYOK-only; both asynchronous job families are omitted.
|
||||
provider: aiark
|
||||
source:
|
||||
docs: https://docs.ai-ark.com/
|
||||
@@ -12,8 +12,6 @@ proposed_capabilities:
|
||||
people.preview: Preview masked people-search results for a flat page price
|
||||
people.personality.analyze: Generate DISC, OCEAN and outreach guidance for a profile
|
||||
people.lists.upsert: Create or update a temporary exclusion list
|
||||
people.export: Start and inspect an asynchronous people-and-email export
|
||||
people.email.find.bulk: Find emails for a prior people-search result set
|
||||
endpoints:
|
||||
- id: aiark.people.search
|
||||
capability: people.search
|
||||
@@ -258,215 +256,3 @@ endpoints:
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/aiark.lists.upsert.json
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.export.start
|
||||
capability: people.export
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /v1/people/export
|
||||
name: Start a people export with emails
|
||||
summary: Queue up to 10,000 filtered people with real-time verified emails.
|
||||
platform_blocked: Async ownership, delayed refunds and terminal settlement are not safe on a shared AI Ark account. Connect your own key.
|
||||
input:
|
||||
<<: *people_search_input
|
||||
body:
|
||||
account: {type: object, required: false}
|
||||
contact: {type: object, required: false}
|
||||
lists: {type: object, required: false}
|
||||
page: {type: integer, required: true, min: 0}
|
||||
size: {type: integer, required: true, min: 1, max: 10000}
|
||||
webhook: {type: string, required: true, format: uri}
|
||||
note: Poll aiark.people.export.statistics, then fetch aiark.people.export.results. Undelivered jobs can auto-refund after up to ten hours.
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://docs.ai-ark.com/reference/people-export-with-email
|
||||
checked: '2026-09-17'
|
||||
confidence: documented
|
||||
note: "BYOK only. Maximum one credit per exported person: 0.5 for the profile plus 0.5 when a valid email is found."
|
||||
docs_url: https://docs.ai-ark.com/reference/people-export-with-email
|
||||
untestable: A non-routable webhook was rejected before billing; successful async settlement and delayed refunds remain unproved.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.export.results
|
||||
capability: people.export
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: GET
|
||||
path: /v1/people/export/{trackId}/inquiries
|
||||
name: Fetch people export results
|
||||
summary: Fetch a page of completed people and email export results.
|
||||
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
|
||||
input: &result_page_input
|
||||
pathParams: {trackId: {type: string, required: true}}
|
||||
query:
|
||||
page: {type: integer, required: false, default: 0, min: 0}
|
||||
size: {type: integer, required: false, default: 10, min: 1, max: 100}
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/export-people-results-by-track-id
|
||||
untestable: Requires a track id created by the caller's own AI Ark account.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.export.statistics
|
||||
capability: people.export
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: GET
|
||||
path: /v1/people/export/{trackId}/statistics
|
||||
name: Get people export statistics
|
||||
summary: Poll export state and totals until the job is done or refunded.
|
||||
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
|
||||
input: {pathParams: {trackId: {type: string, required: true}}}
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/get-export-people-statistics-by-track-id
|
||||
untestable: Requires a track id created by the caller's own AI Ark account.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.export.submissions
|
||||
kind: utility
|
||||
capability: people.export
|
||||
platform: people
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /v1/people/export/submissions
|
||||
name: List people export submissions
|
||||
summary: List the connected account's export jobs and refund states.
|
||||
platform_blocked: Submission history belongs to the connected AI Ark account. Connect your own key.
|
||||
input: &submissions_input
|
||||
query:
|
||||
state: {type: string, required: false}
|
||||
fullyRefunded: {type: boolean, required: false}
|
||||
page: {type: integer, required: false, default: 0, min: 0}
|
||||
size: {type: integer, required: false, default: 10, min: 1, max: 100}
|
||||
sort: {type: string, required: false}
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/get-export-people-submissions
|
||||
test_request: {query: {page: 0, size: 1}}
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/aiark.people.export.submissions.json
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.export.webhook.resend
|
||||
kind: utility
|
||||
capability: people.export
|
||||
platform: people
|
||||
scope: own_account
|
||||
method: PATCH
|
||||
path: /v1/people/export/{trackId}/notify
|
||||
name: Resend a people export webhook
|
||||
summary: Ask AI Ark to send an export completion webhook again.
|
||||
platform_blocked: Track ids and webhook delivery belong to the connected AI Ark account. Connect your own key.
|
||||
input: &resend_input
|
||||
pathParams: {trackId: {type: string, required: true}}
|
||||
bodyType: json
|
||||
body: {webhook: {type: string, required: true, format: uri}}
|
||||
note: HTTP 200 can still contain delivered false. Inspect the response body.
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/resend-export-people-webhook-1
|
||||
untestable: Requires an export track id and sends data to a caller-selected external URL.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.email.find.bulk
|
||||
capability: people.email.find.bulk
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /v1/people/email-finder
|
||||
name: Start email finding for search results
|
||||
summary: Find verified emails for one prior people-search result set.
|
||||
platform_blocked: A track id is single-use, account-scoped and can refund later. Connect your own AI Ark key.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
trackId: {type: string, required: true}
|
||||
webhook: {type: string, required: true, format: uri}
|
||||
note: Use a trackId once within six hours of People Search. Poll the statistics tool and then fetch results.
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://docs.ai-ark.com/reference/people-email-finder-by-track-id
|
||||
checked: '2026-09-17'
|
||||
confidence: documented
|
||||
note: BYOK only. One credit per found valid email and zero for each miss. Undelivered jobs can auto-refund after up to ten hours.
|
||||
docs_url: https://docs.ai-ark.com/reference/people-email-finder-by-track-id
|
||||
untestable: A non-routable webhook was rejected before billing; successful async settlement and delayed refunds remain unproved.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.email.find.bulk.results
|
||||
capability: people.email.find.bulk
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: GET
|
||||
path: /v1/people/email-finder/{trackId}/inquiries
|
||||
name: Fetch email finder results
|
||||
summary: Fetch a page of email-finder inputs, states and verified outputs.
|
||||
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
|
||||
input: *result_page_input
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/get-email-finder-results-by-track-id
|
||||
untestable: Requires a track id created by the caller's own AI Ark account.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.email.find.bulk.statistics
|
||||
capability: people.email.find.bulk
|
||||
platform: people
|
||||
scope: own_account
|
||||
domain: contacts
|
||||
method: GET
|
||||
path: /v1/people/email-finder/{trackId}/statistics
|
||||
name: Get email finder statistics
|
||||
summary: Poll email-finder state and totals until the job is done or refunded.
|
||||
platform_blocked: Track ids are account-scoped and lack shared-key caller ownership. Connect your own AI Ark key.
|
||||
input: {pathParams: {trackId: {type: string, required: true}}}
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/get-email-finder-statistics-by-track-id
|
||||
untestable: Requires a track id created by the caller's own AI Ark account.
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.email.find.bulk.submissions
|
||||
kind: utility
|
||||
capability: people.email.find.bulk
|
||||
platform: people
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /v1/people/email-finder/submissions
|
||||
name: List email finder submissions
|
||||
summary: List the connected account's email-finder jobs and refund states.
|
||||
platform_blocked: Submission history belongs to the connected AI Ark account. Connect your own key.
|
||||
input: *submissions_input
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/get-email-finder-submissions
|
||||
test_request: {query: {page: 0, size: 1}}
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/aiark.people.email.find.bulk.submissions.json
|
||||
cache: forbidden
|
||||
|
||||
- id: aiark.people.email.find.bulk.webhook.resend
|
||||
kind: utility
|
||||
capability: people.email.find.bulk
|
||||
platform: people
|
||||
scope: own_account
|
||||
method: PATCH
|
||||
path: /v1/people/email-finder/{trackId}/notify
|
||||
name: Resend an email finder webhook
|
||||
summary: Ask AI Ark to send an email-finder completion webhook again.
|
||||
platform_blocked: Track ids and webhook delivery belong to the connected AI Ark account. Connect your own key.
|
||||
input: *resend_input
|
||||
cost: *free_cost
|
||||
docs_url: https://docs.ai-ark.com/reference/resend-email-finder-webhook-1
|
||||
untestable: Requires an email-finder track id and sends data to a caller-selected external URL.
|
||||
cache: forbidden
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# Complete documented surface: standard/waterfall single verification, single status, JSON and
|
||||
# CSV bulk submission, bulk status/emails/dump/export/destroy, Check API, and Account API.
|
||||
# Excluded from direct tools: multipart CSV upload (no catalog file-upload contract), destructive
|
||||
# bulk destroy, and /v1/check (a separate unfunded Check Plan with no supplied acquisition price).
|
||||
# Webhook parameters remain available on BYOK-only bulk and waterfall submissions. The platform
|
||||
# single tool omits them and omits disable_catchall_verify so every 2xx submission has the proven
|
||||
# fixed one-credit price.
|
||||
# Exposes standard/waterfall single verification, single status, and account usage. JSON and CSV
|
||||
# bulk lifecycles are omitted until they have an explicit cost-confirmation and owned-job workflow.
|
||||
# /v1/check is also omitted because it uses a separate unfunded Check Plan.
|
||||
provider: bounceban
|
||||
source:
|
||||
docs: https://bounceban.com/public/doc/api.html
|
||||
@@ -14,11 +10,6 @@ pricing_url: https://bounceban.com/pricing
|
||||
limits: "Live account limits on 2026-09-16: single and single-status 100/second, bulk create 5/second, bulk reads 25/second, account 5/second. Results remain available for 90 days."
|
||||
proposed_capabilities:
|
||||
people.email.verify.status: Poll a single email-verification task
|
||||
people.email.verify.bulk: Submit a list for asynchronous email verification
|
||||
people.email.verify.bulk.status: Read bulk email-verification progress and totals
|
||||
people.email.verify.bulk.emails: Read selected results from a bulk email-verification task
|
||||
people.email.verify.bulk.dump: List cursor-paginated bulk email-verification results
|
||||
people.email.verify.bulk.export: Create a CSV download link for bulk email-verification results
|
||||
|
||||
endpoints:
|
||||
- id: bounceban.people.email.verify
|
||||
@@ -106,134 +97,6 @@ endpoints:
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/bounceban.people.email.verify.status.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Single-Verification
|
||||
|
||||
- id: bounceban.people.email.verify.bulk
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /v1/verify/bulk
|
||||
name: Start bulk email verification
|
||||
summary: Submit a JSON list of emails for asynchronous verification
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
emails: {type: "array[string]", required: true, min: 1, max: 500000, example: ["dev@bounceban.com", "support@bounceban.com"]}
|
||||
name: {type: string, required: false}
|
||||
mode: {type: string, required: false, enum: [regular, deepverify], default: regular}
|
||||
greylisting_bypass: {type: string, required: false, enum: [auto, speed, robust], default: auto}
|
||||
disable_catchall_verify: {type: string, required: false, enum: ['0', '1'], default: '0'}
|
||||
url: {type: string, required: false, note: "BYOK webhook URL for each verified email"}
|
||||
url_finished: {type: string, required: false, note: "BYOK webhook URL for task completion"}
|
||||
note: "BYOK only. Unknown results are refunded only when the task finishes. Recommended maximum is 500,000 emails."
|
||||
test_request: {body: {emails: ["dev@bounceban.com"], name: "catalog-check"}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
checked: '2026-09-16'
|
||||
confidence: documented
|
||||
note: "One credit per final non-unknown result. Unknown results are refunded when the task finishes. BYOK is not metered by treg."
|
||||
platform_blocked: "Final-result refunds and shared-key task ownership require durable task settlement; connect your own BounceBan key."
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/bounceban.people.email.verify.bulk.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
|
||||
- id: bounceban.people.email.verify.bulk.status
|
||||
kind: utility
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk.status
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/verify/bulk/status
|
||||
name: Get bulk verification status
|
||||
summary: Read progress, result counts, and final credit usage for a bulk task
|
||||
input:
|
||||
queryParams:
|
||||
id: {type: string, required: true, example: "sanitized-bulk-id"}
|
||||
test_request: {queryParams: {id: "sanitized-bulk-id"}}
|
||||
cost: &free_bulk_read {type: free, value: 0, currency: USD, unit: call, confidence: verified, note: "Live bulk task reads used no credits"}
|
||||
platform_blocked: &bulk_ownership "Shared-key task ownership is not available; connect your own BounceBan key."
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/bounceban.people.email.verify.bulk.status.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
|
||||
- id: bounceban.people.email.verify.bulk.emails
|
||||
kind: utility
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk.emails
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /v1/verify/bulk/emails
|
||||
name: Get selected bulk verification results
|
||||
summary: Read results for up to 100 emails in a completed bulk task
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
id: {type: string, required: true, example: "sanitized-bulk-id"}
|
||||
emails: {type: "array[string]", required: true, min: 1, max: 100, example: ["dev@bounceban.com"]}
|
||||
test_request: {body: {id: "sanitized-bulk-id", emails: ["dev@bounceban.com"]}}
|
||||
cost: *free_bulk_read
|
||||
platform_blocked: *bulk_ownership
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/bounceban.people.email.verify.bulk.emails.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
|
||||
- id: bounceban.people.email.verify.bulk.dump
|
||||
kind: utility
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk.dump
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/verify/bulk/dump
|
||||
name: List bulk verification results
|
||||
summary: Read a cursor-paginated page of verified emails from a bulk task
|
||||
input:
|
||||
queryParams:
|
||||
id: {type: string, required: true, example: "sanitized-bulk-id"}
|
||||
state: {type: string, required: false, enum: [deliverable, risky, undeliverable, unknown]}
|
||||
cursor: {type: string, required: false, note: "opaque cursor from the previous response"}
|
||||
page_size: {type: integer, required: false, min: 100, max: 3000, default: 100}
|
||||
retrieve_all: {type: string, required: false, enum: ['0', '1'], default: '0', note: "1 returns all rows only for tasks with at most 20,000 emails"}
|
||||
note: "Request without cursor, then repeat with the returned cursor until it is null."
|
||||
test_request: {queryParams: {id: "sanitized-bulk-id", page_size: 100}}
|
||||
cost: *free_bulk_read
|
||||
platform_blocked: *bulk_ownership
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/bounceban.people.email.verify.bulk.dump.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
|
||||
- id: bounceban.people.email.verify.bulk.export
|
||||
kind: utility
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk.export
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /v1/verify/bulk/export
|
||||
name: Create a bulk-results download link
|
||||
summary: Create a four-hour CSV download link for a completed bulk task
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
id: {type: string, required: true, example: "sanitized-bulk-id"}
|
||||
keep_all_rows: {type: boolean, required: false, default: false}
|
||||
criteria: {type: object, required: false, note: "optional deliverable, risky, risky_score, undeliverable, unknown, free, role, accept_all, and disposable filters"}
|
||||
note: "Link creation can take time. Retry no more than 15 times per export in 15 minutes. The returned public link expires after four hours."
|
||||
test_request: {body: {id: "sanitized-bulk-id"}}
|
||||
cost: {type: free, value: 0, currency: USD, unit: call, confidence: documented, note: "The documented export-link operation does not consume verification credits"}
|
||||
platform_blocked: *bulk_ownership
|
||||
example_response: examples/bounceban.people.email.verify.bulk.export.json
|
||||
docs_url: https://bounceban.com/public/doc/api.html#tag/Bulk-Verification
|
||||
|
||||
- id: bounceban.account.usage
|
||||
kind: account
|
||||
domain: account
|
||||
|
||||
@@ -109,8 +109,8 @@ endpoints:
|
||||
docs_url: https://api.contactout.com/
|
||||
- id: contactout.people.personal_email.available
|
||||
domain: contacts
|
||||
capability: linkedin.email.personal.availability
|
||||
platform: linkedin
|
||||
capability: people.email.personal.availability
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin/personal_email_status
|
||||
@@ -137,8 +137,8 @@ endpoints:
|
||||
verification request or stored example under the catalog PII rule.
|
||||
- id: contactout.people.work_email.available
|
||||
domain: contacts
|
||||
capability: linkedin.email.work.availability
|
||||
platform: linkedin
|
||||
capability: people.email.work.availability
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin/work_email_status
|
||||
@@ -160,8 +160,8 @@ endpoints:
|
||||
verification request or stored example under the catalog PII rule.
|
||||
- id: contactout.people.phone.available
|
||||
domain: contacts
|
||||
capability: linkedin.phone.availability
|
||||
platform: linkedin
|
||||
capability: people.phone.availability
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin/phone_status
|
||||
@@ -645,8 +645,8 @@ endpoints:
|
||||
verification request or stored example under the catalog PII rule.
|
||||
- id: contactout.people.contact.work
|
||||
domain: contacts
|
||||
capability: linkedin.email.work.find
|
||||
platform: linkedin
|
||||
capability: people.email.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin
|
||||
@@ -686,11 +686,13 @@ endpoints:
|
||||
rates_micro: *id033
|
||||
docs_url: https://api.contactout.com/
|
||||
untestable: Requires a person lookup or returns named individuals; no automated
|
||||
verification request or stored example under the catalog PII rule.
|
||||
verification request is stored under the catalog PII rule. The example is a
|
||||
hand-sanitized structural routing fixture with reserved fake contact data.
|
||||
example_response: examples/contactout.people.contact.work.json
|
||||
- id: contactout.people.contact.personal
|
||||
domain: contacts
|
||||
capability: linkedin.email.personal.find
|
||||
platform: linkedin
|
||||
capability: people.email.personal.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin
|
||||
@@ -733,8 +735,8 @@ endpoints:
|
||||
verification request or stored example under the catalog PII rule.
|
||||
- id: contactout.people.contact.phone
|
||||
domain: contacts
|
||||
capability: linkedin.phone.find
|
||||
platform: linkedin
|
||||
capability: people.phone.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /v1/people/linkedin
|
||||
@@ -776,7 +778,9 @@ endpoints:
|
||||
rates_micro: *id033
|
||||
docs_url: https://api.contactout.com/
|
||||
untestable: Requires a person lookup or returns named individuals; no automated
|
||||
verification request or stored example under the catalog PII rule.
|
||||
verification request is stored under the catalog PII rule. The example is a
|
||||
hand-sanitized structural routing fixture with reserved fake contact data.
|
||||
example_response: examples/contactout.people.contact.phone.json
|
||||
- id: contactout.people.linkedin.enrich
|
||||
domain: contacts
|
||||
capability: linkedin.user.profile
|
||||
@@ -1033,13 +1037,10 @@ endpoints:
|
||||
verification request or stored example under the catalog PII rule.
|
||||
proposed_capabilities:
|
||||
people.count: Count profiles matching filters without returning profiles
|
||||
linkedin.email.personal.availability: Check personal email availability for a LinkedIn
|
||||
people.email.personal.availability: Check personal email availability for a LinkedIn
|
||||
profile
|
||||
linkedin.email.work.availability: Check work email availability for a LinkedIn profile
|
||||
linkedin.phone.availability: Check phone availability for a LinkedIn profile
|
||||
people.email.work.availability: Check work email availability for a LinkedIn profile
|
||||
people.phone.availability: Check phone availability for a LinkedIn profile
|
||||
linkedin.profile.from_email: Find LinkedIn profile URL from an email address
|
||||
linkedin.email.work.find: Find work email from a LinkedIn profile, optionally with
|
||||
phone
|
||||
linkedin.email.personal.find: Find personal email from a LinkedIn profile, optionally
|
||||
people.email.personal.find: Find personal email from a LinkedIn profile, optionally
|
||||
with phone
|
||||
linkedin.phone.find: Find phone number from a LinkedIn profile
|
||||
|
||||
@@ -10,8 +10,6 @@ pricing_url: https://dropleads.io/pricing
|
||||
proposed_capabilities:
|
||||
people.count: Count people matching search filters without returning records
|
||||
people.enrich.verified: Enrich one person and require verified email data
|
||||
people.enrich.verified.bulk: Synchronously enrich up to ten people with verified email data
|
||||
people.enrich.bulk: Synchronously enrich up to ten people
|
||||
companies.search.count: Count companies matching search filters without returning records
|
||||
endpoints:
|
||||
- id: dropleads.people.email.find
|
||||
@@ -221,40 +219,6 @@ endpoints:
|
||||
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-enrich
|
||||
verified: '2026-09-15'
|
||||
example_response: examples/dropleads.people.enrich.verified.json
|
||||
|
||||
- id: dropleads.people.enrich.verified.bulk
|
||||
capability: people.enrich.verified.bulk
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /api/v2/prime-db/leads/bulk-enrich
|
||||
name: Bulk enrich people with verified emails
|
||||
summary: Synchronously enrich up to 10 people with verified email data.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
details:
|
||||
type: array[object]
|
||||
required: true
|
||||
example: [{first_name: Jane, last_name: Doe, domain: example.com}]
|
||||
note: 1-10 person identity objects using the same identity fields as the single endpoint.
|
||||
email_verification_type:
|
||||
type: string
|
||||
required: false
|
||||
default: valid_and_catchall
|
||||
enum: [valid_only, valid_and_catchall]
|
||||
test_request:
|
||||
body: {details: [{id: treg-nonexistent-20260915}], email_verification_type: valid_and_catchall}
|
||||
cost:
|
||||
<<: *person_enrich_cost
|
||||
type: per_result
|
||||
unit: record
|
||||
note: 0.2 credit per returned verified lead, not per submitted item. credits_consumed is authoritative; 1-10 inputs.
|
||||
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-bulk-enrich
|
||||
verified: '2026-09-15'
|
||||
example_response: examples/dropleads.people.enrich.verified.bulk.json
|
||||
|
||||
- id: dropleads.people.enrich
|
||||
capability: people.enrich
|
||||
platform: people
|
||||
@@ -285,35 +249,6 @@ endpoints:
|
||||
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-simple-enrich
|
||||
verified: '2026-09-15'
|
||||
example_response: examples/dropleads.people.enrich.json
|
||||
|
||||
- id: dropleads.people.enrich.bulk
|
||||
capability: people.enrich.bulk
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /api/v2/prime-db/leads/simple-bulk-enrich
|
||||
name: Bulk enrich people
|
||||
summary: Synchronously enrich up to 10 people without requiring verified email results.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
details:
|
||||
type: array[object]
|
||||
required: true
|
||||
example: [{first_name: Jane, last_name: Doe, domain: example.com}]
|
||||
note: 1-10 person identity objects using the same fields as the single endpoint.
|
||||
test_request:
|
||||
body: {details: [{id: treg-nonexistent-20260915}]}
|
||||
cost:
|
||||
<<: *person_enrich_cost
|
||||
type: per_result
|
||||
unit: record
|
||||
note: 0.2 credit per returned person, not per submitted item. credits_consumed is authoritative; 1-10 inputs.
|
||||
docs_url: https://dropleads.readme.io/reference/post_api-v2-prime-db-leads-simple-bulk-enrich
|
||||
verified: '2026-09-15'
|
||||
example_response: examples/dropleads.people.enrich.bulk.json
|
||||
|
||||
- id: dropleads.companies.search
|
||||
capability: companies.search
|
||||
platform: companies
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"content":[],"totalElements":0,"totalPages":0,"number":0,"size":1,"numberOfElements":0,"first":true,"last":true,"empty":true}
|
||||
@@ -1 +0,0 @@
|
||||
{"content":[],"totalElements":0,"totalPages":0,"number":0,"size":1,"numberOfElements":0,"first":true,"last":true,"empty":true}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"result": "ok",
|
||||
"cursor": null,
|
||||
"items": [
|
||||
{
|
||||
"status": "success",
|
||||
"email": "dev@bounceban.com",
|
||||
"result": "deliverable",
|
||||
"score": 99,
|
||||
"is_disposable": false,
|
||||
"is_accept_all": false,
|
||||
"is_role": true,
|
||||
"is_free": false,
|
||||
"mx_records": ["aspmx.l.google.com"],
|
||||
"smtp_provider": "Google",
|
||||
"verify_at": "2026-09-16T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"result": "ok",
|
||||
"items": [
|
||||
{
|
||||
"status": "success",
|
||||
"email": "dev@bounceban.com",
|
||||
"result": "deliverable",
|
||||
"score": 99,
|
||||
"is_disposable": false,
|
||||
"is_accept_all": false,
|
||||
"is_role": true,
|
||||
"is_free": false,
|
||||
"mx_records": ["aspmx.l.google.com"],
|
||||
"smtp_provider": "Google",
|
||||
"verify_at": "2026-09-16T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"result": "ok",
|
||||
"download_url": "https://example.invalid/sanitized-bounceban-export.csv"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"id": "sanitized-bulk-id",
|
||||
"status": "importing",
|
||||
"credits_remaining": 9996
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "sanitized-bulk-id",
|
||||
"status": "finished",
|
||||
"total_count": 1,
|
||||
"deliverable_count": 1,
|
||||
"undeliverable_count": 0,
|
||||
"risky_count": 0,
|
||||
"unknown_count": 1,
|
||||
"catchall_count": 1,
|
||||
"credits_consumed": 1,
|
||||
"credits_remaining": 9998
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status_code": 200,
|
||||
"profile": {
|
||||
"phone": ["+10000000000"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status_code": 200,
|
||||
"profile": {
|
||||
"work_email": ["routing-fixture@example.invalid"]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"success":true,"total_requested_enrichments":1,"unique_enriched_records":1,"credits_consumed":0.2,"matches":[{"id":"example-person","first_name":"Jane","last_name":"Doe","name":"Jane Doe","email":"jane@example.com","organization_domain":"example.com"}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"success":true,"status":"success","total_requested_enrichments":1,"unique_enriched_records":1,"verified_records":1,"missing_records":0,"credits_consumed":0.2,"matches":[{"id":"example-person","first_name":"Jane","last_name":"Doe","name":"Jane Doe","email":"jane@example.com","organization_domain":"example.com"}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"ok":true,"results":[{"input":{"phone":"+1 202-555-0100"},"success":false,"data":null}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":12345,"name":"treg-test","type":"AdAudiences","status":"pending","requested_entity_count":1,"processed_entity_count":0,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12345","pagination":null,"items":null}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":12346,"name":"treg-test","type":"EmailVerification","status":"completed","requested_entity_count":1,"processed_entity_count":1,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12346","pagination":{"page":1,"page_size":100,"total_items":1,"total_pages":1,"has_next_page":false,"has_previous_page":false},"items":[{"input":"person@example.com","status":"completed","data":{"email":"person@example.com","result":"Risky"}}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":12346,"name":"treg-test","type":"EmailVerification","status":"pending","requested_entity_count":1,"processed_entity_count":0,"created":"2026-09-17T00:00:00Z","status_url":"/api/v2/batch/results?batch_id=12346","pagination":null,"items":null}
|
||||
@@ -1 +0,0 @@
|
||||
{"results":{"email":null,"risk_score":null},"status":"not_found","metadata":{}}
|
||||
@@ -1 +0,0 @@
|
||||
{"results":{"email":null,"risk_score":null,"type":null,"last_validated_at":null},"status":"not_found","metadata":{}}
|
||||
@@ -1 +0,0 @@
|
||||
{"results":{"email":null,"risk_score":null,"last_validated_at":null},"status":"not_found","metadata":{}}
|
||||
@@ -1 +0,0 @@
|
||||
{"results":{"email":null,"risk_score":null,"last_validated_at":null},"status":"not_found","metadata":{}}
|
||||
@@ -1 +0,0 @@
|
||||
{"results":{"mobile_phone":null,"last_validated_at":null},"status":"not_found","metadata":{},"phone_tokens_used":0,"phone_tokens_remaining":18}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000030","task_ids":["00000000-0000-4000-8000-000000000031"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000040","task_ids":["00000000-0000-4000-8000-000000000041"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000010","task_ids":["00000000-0000-4000-8000-000000000011"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"task_ids":["00000000-0000-4000-8000-000000000011"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000010","status_counts":{"COMPLETED":1,"FAILED":0,"PENDING":0,"PROCESSING":0}}
|
||||
@@ -1 +0,0 @@
|
||||
{"task_id":"00000000-0000-4000-8000-000000000011","status":"COMPLETED","result":{"people":[{"name":"Treg Example","emails":["person@example.com"],"phones":[]}]}}
|
||||
@@ -1 +0,0 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000020","task_ids":["00000000-0000-4000-8000-000000000021"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"error":false,"total_cost":0,"not_matched":[],"invalid_datapoints":[],"matched":[{"identifier":"public-example","company":{"company_id":"ccccpublicexample","name":"Intercom","website":"https://intercom.com","domain":"intercom.io","industry":"Software Development","employee_count":1117,"location":{"country":"United States","city":"San Francisco"}}}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"error":false,"total_cost":0,"not_matched":[],"invalid_datapoints":[],"matched":[{"identifier":"public-example","person":{"person_id":"aaaapublicexample","first_name":"Jane","last_name":"Example","full_name":"Jane Example","linkedin_url":"https://www.linkedin.com/in/public-example","current_job_title":"CEO","email":{"status":"VERIFIED","revealed":true,"email":"jane@example.com"},"mobile":{"status":"VERIFIED","revealed":false,"mobile":"+1 555-***-****","mobile_country_code":"US"}},"company":{"company_id":"ccccpublicexample","name":"Fin","domain":"fin.ai"}}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"status":"processing","identifier":"batch_sanitized_a1b2c3d4","fetch_result_endpoint":"/fetch_bulk_results","total":1,"credits_used":1,"remaining_credits":98,"retry_after_seconds":30}
|
||||
@@ -1 +0,0 @@
|
||||
{"status":"completed","identifier":"batch_sanitized_a1b2c3d4","retry_after_seconds":0,"results":{"verified@example.com":{"result":"Valid","status":"OK","quick_status":"OK"}}}
|
||||
@@ -1 +0,0 @@
|
||||
{"status":"processing","identifier":"deep_sanitized_a1b2c3d4","fetch_result_endpoint":"/fetch_bulk_results/deep","total":1,"credits_used":3,"remaining_credits":95,"retry_after_seconds":259200,"verification_type":"Deep"}
|
||||
@@ -1 +0,0 @@
|
||||
{"status":"processing","identifier":"deep_sanitized_a1b2c3d4","retry_after_seconds":259200,"results_24":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}},"results_48":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}},"results_72":{"verified@example.com":{"result":"pending","status":"pending","quick_status":"pending"}}}
|
||||
@@ -7,7 +7,7 @@ source:
|
||||
docs: https://www.getleads.io/docs/
|
||||
openapi: null
|
||||
curated: '2026-09-16'
|
||||
limits: '100 requests per minute by default across all endpoints. JSON enrichment batches accept up to 100 items. Search and company-contact lookups can return up to 5,000 records per request.'
|
||||
limits: '100 requests per minute by default across all endpoints. Catalog enrichment calls accept exactly one item. Search and company-contact lookups can return up to 5,000 records per request.'
|
||||
pricing_url: https://www.getleads.io/pricing/
|
||||
proposed_capabilities:
|
||||
people.count: Count people matching search filters without returning records
|
||||
@@ -23,18 +23,19 @@ endpoints:
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/enrich/from-email
|
||||
name: Enrich people from work emails
|
||||
summary: Enrich up to 100 people from known work email addresses.
|
||||
strict_body: true
|
||||
name: Enrich one person from a work email
|
||||
summary: Enrich one person from a known work email address.
|
||||
input:
|
||||
body:
|
||||
items:
|
||||
type: 'array[object]'
|
||||
required: true
|
||||
min: 1
|
||||
max: 100
|
||||
max: 1
|
||||
example: [{email: nobody-treg-20260916@example.invalid}]
|
||||
note: Each item needs an email string.
|
||||
note: One credit for each result where success is true. A 100-item batch can consume 100 credits.
|
||||
note: Exactly one item is accepted. One credit is charged when that result has success=true.
|
||||
test_request:
|
||||
body: {items: [{email: nobody-treg-20260916@example.invalid}]}
|
||||
cost: &one_credit_success
|
||||
@@ -59,19 +60,20 @@ endpoints:
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/enrich/from-linkedin
|
||||
name: Enrich people from LinkedIn URLs
|
||||
summary: Enrich up to 100 people from public LinkedIn profile URLs.
|
||||
strict_body: true
|
||||
name: Enrich one person from a LinkedIn URL
|
||||
summary: Enrich one person from a public LinkedIn profile URL.
|
||||
input:
|
||||
body:
|
||||
items:
|
||||
type: 'array[object]'
|
||||
required: true
|
||||
min: 1
|
||||
max: 100
|
||||
max: 1
|
||||
example: [{linkedin_url: https://www.linkedin.com/in/example}]
|
||||
note: Each item needs linkedin_url.
|
||||
limit_per_item: {type: integer, required: false, default: 1, min: 1, max: 10}
|
||||
note: One credit for each result where success is true.
|
||||
note: Exactly one item is accepted. One credit is charged when that result has success=true.
|
||||
test_request:
|
||||
body: {items: [{linkedin_url: https://www.linkedin.com/in/treg-nonexistent-20260916}], limit_per_item: 1}
|
||||
cost:
|
||||
@@ -88,18 +90,19 @@ endpoints:
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/enrich/from-person
|
||||
name: Enrich people from name and company
|
||||
summary: Enrich up to 100 people from names plus a company name or email domain.
|
||||
strict_body: true
|
||||
name: Enrich one person from name and company
|
||||
summary: Enrich one person from a name plus a company name or email domain.
|
||||
input:
|
||||
body:
|
||||
items:
|
||||
type: 'array[object]'
|
||||
required: true
|
||||
min: 1
|
||||
max: 100
|
||||
max: 1
|
||||
example: [{first_name: Jane, last_name: Doe, email_domain: example.com}]
|
||||
note: Each item needs first_name and last_name plus company_name or email_domain.
|
||||
note: One credit for each result where success is true.
|
||||
note: Exactly one item is accepted. One credit is charged when that result has success=true.
|
||||
test_request:
|
||||
body: {items: [{first_name: Treg, last_name: Nonexistent, email_domain: treg-nonexistent-20260916.invalid}]}
|
||||
cost:
|
||||
@@ -139,35 +142,6 @@ endpoints:
|
||||
docs_url: https://www.getleads.io/docs/#get-apiv1contactslookupphone
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/getleadsio.people.phone.lookup.json
|
||||
|
||||
- id: getleadsio.people.phone.lookup_batch
|
||||
capability: people.phone.find
|
||||
platform: people
|
||||
domain: contacts
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/contacts/from-phone
|
||||
name: Look up contacts by phone in a batch
|
||||
summary: Find contact records for a batch of phone numbers.
|
||||
input:
|
||||
body:
|
||||
items:
|
||||
type: 'array[object]'
|
||||
required: true
|
||||
min: 1
|
||||
max: 100
|
||||
example: [{phone: '+1 202-555-0100'}]
|
||||
note: Each item accepts phone or cellphone.
|
||||
limit_per_item: {type: integer, required: false, default: 1, min: 1, max: 10}
|
||||
test_request:
|
||||
body: {items: [{phone: '+1 202-555-0100'}], limit_per_item: 1}
|
||||
cost:
|
||||
<<: *one_credit_success
|
||||
source_url: https://www.getleads.io/docs/#post-apiv1contactsfrom-phone
|
||||
docs_url: https://www.getleads.io/docs/#post-apiv1contactsfrom-phone
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/getleadsio.people.phone.lookup_batch.json
|
||||
|
||||
- id: getleadsio.people.colleagues
|
||||
capability: people.organization_contacts
|
||||
platform: people
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# LimaData Basic v2. All documented operations are available with a connected customer key.
|
||||
# LimaData Basic v2. Batch submission and result operations are omitted from the catalog.
|
||||
# Shared platform access is limited to synchronous operations whose full cost the generic money
|
||||
# model can bound before the call. BYOK always wins and is never metered by treg.
|
||||
provider: limadata
|
||||
@@ -20,9 +20,6 @@ proposed_capabilities:
|
||||
people.identity.from_email: Resolve an email address to social profiles
|
||||
web.extract: Extract clean page content from URLs
|
||||
web.research: Search and synthesize web research for a question
|
||||
people.audience.batch.start: Start a batch advertising-audience job
|
||||
people.email.verify.batch.start: Start a batch email-verification job
|
||||
people.batch.results: Read a LimaData batch job and its results
|
||||
endpoints:
|
||||
- id: limadata.people.enrich
|
||||
capability: people.enrich
|
||||
@@ -572,100 +569,3 @@ endpoints:
|
||||
docs_url: https://api.limadata.com/docs/basic_v2
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/limadata.google.serp.organic.json
|
||||
|
||||
- id: limadata.people.audience.batch.start
|
||||
capability: people.audience.batch.start
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /api/v2/batch/ad_audiences
|
||||
name: Start a batch audience job
|
||||
summary: Start asynchronous advertising-audience resolution for up to 100,000 profiles.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
name: {type: string, required: false}
|
||||
urls: {type: array, required: true, minItems: 1, maxItems: 100000, items: {type: string}}
|
||||
target_network: {type: string, required: false, enum: [meta, google, x, reddit, tiktok, linkedin, snapchat, pinterest, amazon, universal]}
|
||||
notification_url: {type: string, required: false}
|
||||
test_request: {body: {name: treg-test, urls: ['https://www.linkedin.com/in/example'], target_network: universal}}
|
||||
cost:
|
||||
type: per_success
|
||||
value: 3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://api.limadata.com/docs/basic_v2
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Three credits are held per submitted URL and misses are refunded after completion.
|
||||
platform_blocked: Final-result refunds and shared-key task ownership require durable task settlement; connect your own LimaData key.
|
||||
docs_url: https://api.limadata.com/docs/basic_v2
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/limadata.people.audience.batch.start.json
|
||||
|
||||
- id: limadata.people.email.verify.batch.start
|
||||
capability: people.email.verify.batch.start
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /api/v2/batch/email_verify
|
||||
name: Start batch email verification
|
||||
summary: Start asynchronous deliverability checks for up to 100,000 email addresses.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
name: {type: string, required: false}
|
||||
emails: {type: array, required: true, minItems: 1, maxItems: 100000, items: {type: string}}
|
||||
notification_url: {type: string, required: false}
|
||||
test_request: {body: {name: treg-test, emails: [person@example.com]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: item
|
||||
source: docs
|
||||
source_url: https://api.limadata.com/docs/basic_v2
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: 0.3 credit per submitted email, charged when the job starts.
|
||||
platform_blocked: Shared-key task ownership is not available; connect your own LimaData key.
|
||||
docs_url: https://api.limadata.com/docs/basic_v2
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/limadata.people.email.verify.batch.start.json
|
||||
|
||||
- id: limadata.people.batch.results
|
||||
capability: people.batch.results
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
kind: utility
|
||||
method: GET
|
||||
path: /api/v2/batch/results
|
||||
name: Read batch results
|
||||
summary: Read status and paginated results for a LimaData batch job.
|
||||
input:
|
||||
query:
|
||||
batch_id: {type: integer, required: true, example: 12345}
|
||||
page: {type: integer, required: false, min: 1, default: 1}
|
||||
note: Results expire after 30 days. Poll no more often than once per 60 seconds.
|
||||
test_request: {query: {batch_id: 12345, page: 1}}
|
||||
cost:
|
||||
type: free
|
||||
value: 0
|
||||
currency: USD
|
||||
per: 1
|
||||
unit: call
|
||||
source: docs
|
||||
source_url: https://api.limadata.com/docs/basic_v2
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Polling is free. A live response carried a task-related credit header but created no usage row, so it is not a polling price.
|
||||
platform_blocked: Batch identifiers are account-scoped and shared-key task ownership is not available; connect your own LimaData key.
|
||||
docs_url: https://api.limadata.com/docs/basic_v2
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/limadata.people.batch.results.json
|
||||
|
||||
@@ -188,106 +188,6 @@ endpoints:
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.enrich.name.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/search-business-profile-by-name
|
||||
|
||||
- id: moltsets.people.email.find
|
||||
domain: contacts
|
||||
capability: people.email.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /linkedin_to_best_email
|
||||
name: Find best email from profile
|
||||
summary: Find the best business or personal email from a professional profile
|
||||
input: &linkedin_batch_input
|
||||
bodyType: json
|
||||
body:
|
||||
linkedin_url: {type: string}
|
||||
linkedin_urls: {type: array, items: {type: string}, maxItems: 100}
|
||||
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
|
||||
cost: *record_cost
|
||||
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.email.find.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-best-email
|
||||
|
||||
- id: moltsets.people.email.find.business
|
||||
domain: contacts
|
||||
capability: people.email.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /linkedin_to_business_email
|
||||
name: Find business email from profile
|
||||
summary: Find a business email from a professional profile
|
||||
input: *linkedin_batch_input
|
||||
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
|
||||
cost: *record_cost
|
||||
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.email.find.business.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-business-email
|
||||
|
||||
- id: moltsets.people.email.find.personal
|
||||
domain: contacts
|
||||
capability: people.email.find.personal
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /linkedin_to_personal_email
|
||||
name: Find personal email from profile
|
||||
summary: Find a personal email from a professional profile
|
||||
input: *linkedin_batch_input
|
||||
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
|
||||
cost: *record_cost
|
||||
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key. Personal email availability also depends on the upstream plan.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.email.find.personal.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-personal-email
|
||||
|
||||
- id: moltsets.people.email.find.personal-best
|
||||
domain: contacts
|
||||
capability: people.email.find.personal
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /linkedin_to_best_personal_email
|
||||
name: Find best personal email from profile
|
||||
summary: Find the best personal email from a professional profile
|
||||
input: *linkedin_batch_input
|
||||
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
|
||||
cost: *record_cost
|
||||
platform_blocked: The endpoint accepts batches of up to 100 and generic settlement cannot count per-item hits; connect your own MoltSets key. Personal email availability also depends on the upstream plan.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.email.find.personal-best.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-best-personal-email
|
||||
|
||||
- id: moltsets.people.phone.find
|
||||
domain: contacts
|
||||
capability: people.phone.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /linkedin_to_mobile_phone
|
||||
name: Find mobile phone from profile
|
||||
summary: Find a mobile phone from a professional profile
|
||||
input: *linkedin_batch_input
|
||||
test_request: {body: {linkedin_url: https://linkedin.com/in/treg-nonexistent-20260916}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.51
|
||||
currency: USD
|
||||
per: 1
|
||||
unit: result
|
||||
source: observed
|
||||
source_url: https://docs.moltsets.com/
|
||||
checked: '2026-09-16'
|
||||
confidence: documented
|
||||
note: Each hit consumes one ordinary record ($0.01 shared-plan rate) plus one phone token (documented $0.50 upper-bound replacement cost); misses and errors are free.
|
||||
platform_blocked: Phone hits use two upstream meters and batches can contain mixed outcomes; connect your own MoltSets key until the generic money contract can settle both safely.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/moltsets.people.phone.find.json
|
||||
docs_url: https://docs.moltsets.com/api-reference/linkedin-to-mobile-phone
|
||||
|
||||
- id: moltsets.people.enrich.email
|
||||
domain: contacts
|
||||
capability: people.enrich
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Synchronous data reads whose price and returned-record shape are proven can use either a team's
|
||||
# own key or treg's metered platform key. Openmart rounds 0.3-credit record prices up to whole
|
||||
# credits per operation; the call runtime applies that provider-specific rule and caps shared-key
|
||||
# requests at 25 records. Async lifecycles, account mutations, and unresolved prices remain
|
||||
# BYOK-only. The shared balance endpoint is internal capacity infrastructure, not a tool.
|
||||
# requests at 25 records. Async lifecycles are omitted; account mutations and unresolved prices
|
||||
# remain BYOK-only. The shared balance endpoint is internal capacity infrastructure, not a tool.
|
||||
provider: openmart
|
||||
source:
|
||||
docs: https://app.openmart.com/api-docs
|
||||
@@ -16,9 +16,6 @@ proposed_capabilities:
|
||||
companies.lookup: Retrieve company records by provider identifier.
|
||||
companies.technologies: Detect technologies used by a company.
|
||||
companies.email.find: Find company-level email addresses.
|
||||
companies.tasks.status: Read an asynchronous company-task batch status.
|
||||
companies.tasks.list: List task identifiers in an asynchronous company batch.
|
||||
companies.tasks.get: Read one asynchronous company-task result.
|
||||
companies.deny-rules.create: Add private company-account deny rules.
|
||||
companies.deny-rules.check: Check private company-account deny rules.
|
||||
companies.deny-rules.delete: Delete private company-account deny rules.
|
||||
@@ -193,175 +190,6 @@ endpoints:
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.companies.search.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.people.find.batch
|
||||
domain: contacts
|
||||
capability: people.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/find_people
|
||||
name: Find decision makers
|
||||
summary: Start up to 100 decision-maker tasks for businesses
|
||||
input: &task_batch_input
|
||||
bodyType: json
|
||||
body:
|
||||
tasks: {type: array, items: {type: object, additionalProperties: true}, minItems: 1, maxItems: 100}
|
||||
test_request: {body: {tasks: [{website: example.invalid, max_k: 1}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 11
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Email costs 3 credits and phone costs 8; requesting both can cost 11 per found person. A live both-fields task reserved 3 at submit and charged the remainder asynchronously.
|
||||
platform_blocked: Async fan-out, mixed outcomes, and delayed charges cannot be assigned safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.people.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.technologies.find.batch
|
||||
domain: companies
|
||||
capability: companies.technologies
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/find_tech
|
||||
name: Find technologies
|
||||
summary: Start up to 100 technology-detection tasks
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{website: example.invalid}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 2
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: observed
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Two credits per completed result, reconciled from the live batch and the published $6 per 100 rate.
|
||||
platform_blocked: Async batch ownership and delayed per-result charging cannot be assigned safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.technologies.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.companies.email.find.batch
|
||||
domain: contacts
|
||||
capability: companies.email.find
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/lookup_business_email
|
||||
name: Find company emails
|
||||
summary: Start up to 100 company-email lookup tasks
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{website: example.invalid}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: documented
|
||||
note: Dashboard price is 0.3 credit per returned company email. Integer balance deltas hide fractional usage and are not settlement evidence.
|
||||
platform_blocked: Async ownership and delayed charging make this unsafe for a shared key; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.companies.email.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.people.enrich.batch
|
||||
domain: contacts
|
||||
capability: people.enrich
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/lookup_people
|
||||
name: Enrich known people
|
||||
summary: Start email or phone enrichment for known people
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{people: [{name: Treg Example}], info_access: [EMAIL]}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 11
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Email costs 3 credits and phone costs 8; callers choose EMAIL, PHONE, or both.
|
||||
platform_blocked: Mixed requested fields and asynchronous per-person results cannot be settled safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.people.enrich.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.batch.status
|
||||
domain: async
|
||||
capability: companies.tasks.status
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/batch/{batch_id}/status
|
||||
name: Get batch status
|
||||
summary: Get status counts for an Openmart batch
|
||||
input: &batch_id_input
|
||||
pathParams:
|
||||
batch_id: {type: string}
|
||||
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}}
|
||||
cost: &free_task_read {type: free, value: 0, currency: USD, unit: call, source: observed, checked: '2026-09-17', confidence: verified, note: Live task reads used no credits.}
|
||||
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.batch.status.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.batch.ids
|
||||
domain: async
|
||||
capability: companies.tasks.list
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/batch/{batch_id}/task_ids
|
||||
name: List batch task IDs
|
||||
summary: List task IDs in a batch, optionally filtered by status
|
||||
input:
|
||||
pathParams: {batch_id: {type: string}}
|
||||
query:
|
||||
status: {type: string, enum: [PENDING, PROCESSING, COMPLETED, FAILED]}
|
||||
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}, query: {status: COMPLETED}}
|
||||
cost: *free_task_read
|
||||
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.batch.ids.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.get
|
||||
domain: async
|
||||
capability: companies.tasks.get
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/{task_id}
|
||||
name: Get task result
|
||||
summary: Get one asynchronous task and its result
|
||||
input:
|
||||
pathParams: {task_id: {type: string}}
|
||||
test_request: {pathParams: {task_id: 00000000-0000-4000-8000-000000000000}}
|
||||
cost: *free_task_read
|
||||
platform_blocked: Task ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.get.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.deny-rules.create
|
||||
domain: governance
|
||||
capability: companies.deny-rules.create
|
||||
|
||||
@@ -11,8 +11,6 @@ source:
|
||||
limits: 'Starter: enrich routes 5/second, 300/minute, 2,000/day; search routes 1/second, 30/minute, 1,000/day. Search suggestions: 15/second on every plan.'
|
||||
pricing_url: https://prospeo.io/pricing
|
||||
proposed_capabilities:
|
||||
people.enrich.bulk: Synchronously enrich up to 50 people
|
||||
companies.enrich.bulk: Synchronously enrich up to 50 companies
|
||||
people.search.suggestions: Resolve canonical Prospeo search-filter values
|
||||
endpoints:
|
||||
- id: prospeo.people.email.find
|
||||
@@ -127,48 +125,6 @@ endpoints:
|
||||
docs_url: https://prospeo.io/api-docs/enrich-person
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/prospeo.people.enrich.json
|
||||
|
||||
- id: prospeo.people.enrich.bulk
|
||||
capability: people.enrich.bulk
|
||||
platform: people
|
||||
scope: any_account
|
||||
domain: contacts
|
||||
method: POST
|
||||
path: /bulk-enrich-person
|
||||
name: Bulk enrich people
|
||||
summary: Synchronously enrich up to 50 people with profile, email and optional mobile data.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
only_verified_email: {type: boolean, required: false, default: false}
|
||||
enrich_mobile: {type: boolean, required: false, default: false}
|
||||
only_verified_mobile: {type: boolean, required: false, default: false}
|
||||
data:
|
||||
type: array[object]
|
||||
required: true
|
||||
min: 1
|
||||
max: 50
|
||||
example: [{identifier: public-example, linkedin_url: https://www.linkedin.com/in/eoghanmccabe}]
|
||||
note: Each record needs a unique identifier and one of the identity combinations accepted by the single-person endpoint.
|
||||
test_request:
|
||||
body: {only_verified_email: true, enrich_mobile: false, data: [{identifier: public-example, linkedin_url: https://www.linkedin.com/in/eoghanmccabe}]}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: observed
|
||||
source_url: https://prospeo.io/api-docs/bulk-enrich-person
|
||||
checked: '2026-09-16'
|
||||
confidence: verified
|
||||
note: One credit per charged email enrichment, or ten per charged mobile enrichment, up to 50 inputs. Response total_cost is authoritative, including zero.
|
||||
modifiers:
|
||||
enrich_mobile: {location: body, when: truthy, add_credits_per_result: 9}
|
||||
docs_url: https://prospeo.io/api-docs/bulk-enrich-person
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/prospeo.people.enrich.bulk.json
|
||||
|
||||
- id: prospeo.companies.enrich
|
||||
capability: companies.enrich
|
||||
platform: companies
|
||||
@@ -194,43 +150,6 @@ endpoints:
|
||||
docs_url: https://prospeo.io/api-docs/enrich-company
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/prospeo.companies.enrich.json
|
||||
|
||||
- id: prospeo.companies.enrich.bulk
|
||||
capability: companies.enrich.bulk
|
||||
platform: companies
|
||||
scope: any_account
|
||||
domain: companies
|
||||
method: POST
|
||||
path: /bulk-enrich-company
|
||||
name: Bulk company lookup and enrichment
|
||||
summary: Synchronously enrich up to 50 companies.
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
data:
|
||||
type: array[object]
|
||||
required: true
|
||||
min: 1
|
||||
max: 50
|
||||
example: [{identifier: public-example, company_website: intercom.com}]
|
||||
note: Each record needs a unique identifier and at least one company identity field accepted by the single endpoint.
|
||||
test_request:
|
||||
body: {data: [{identifier: public-example, company_website: intercom.com}]}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: observed
|
||||
source_url: https://prospeo.io/api-docs/bulk-enrich-company
|
||||
checked: '2026-09-16'
|
||||
confidence: verified
|
||||
note: One credit per matched company, up to 50 inputs. Response total_cost is authoritative, including zero.
|
||||
docs_url: https://prospeo.io/api-docs/bulk-enrich-company
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/prospeo.companies.enrich.bulk.json
|
||||
|
||||
- id: prospeo.people.search
|
||||
capability: people.search
|
||||
platform: people
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Scrubby documents five POST endpoints. Single verification is synchronous; quick and deep bulk
|
||||
# verification use separate submit/fetch pairs. Bulk submissions stay BYOK-only because Scrubby's
|
||||
# request field is a singular `email` array with no documented maximum; treg cannot bound a shared-
|
||||
# key reservation without rewriting the request or inventing a limit. Fetches are free and ownership-
|
||||
# checked. Single verification is safe for platform access and settles from `credits_used`.
|
||||
# Scrubby documents five POST endpoints. The catalog exposes only synchronous single verification;
|
||||
# quick and deep bulk submit/fetch pairs are omitted because they have no documented batch maximum
|
||||
# and require an owned asynchronous workflow. Single verification settles from `credits_used`.
|
||||
provider: scrubby
|
||||
source:
|
||||
docs: https://docs.scrubby.io/
|
||||
@@ -10,11 +8,6 @@ source:
|
||||
curated: '2026-09-16'
|
||||
pricing_url: https://scrubby.io/pricing/
|
||||
limits: 25 requests/second (1,500/minute) per API key. Quick bulk normally completes in 30-60 seconds; deep verification returns first results in 24 hours and final results in 72 hours.
|
||||
proposed_capabilities:
|
||||
people.email.verify.bulk: Submit quick bulk email verification
|
||||
people.email.verify.bulk.results: Fetch quick bulk verification results
|
||||
people.email.verify.deep: Submit 72-hour deep email verification
|
||||
people.email.verify.deep.results: Fetch deep verification results
|
||||
endpoints:
|
||||
- id: scrubby.people.email.verify
|
||||
domain: contacts
|
||||
@@ -45,107 +38,3 @@ endpoints:
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/scrubby.people.email.verify.json
|
||||
docs_url: https://docs.scrubby.io/
|
||||
|
||||
- id: scrubby.people.email.verify.bulk
|
||||
domain: contacts
|
||||
capability: people.email.verify.bulk
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /validate_bulk_emails
|
||||
name: Submit quick bulk email verification
|
||||
summary: Queue a list for quick verification and receive a batch identifier.
|
||||
platform_blocked: "Shared-key bulk reservation and body-carried task ownership are unavailable; connect your own Scrubby key."
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
email: {type: "array[string]", required: true, min: 1, example: [info@scrubby.io, support@scrubby.io]}
|
||||
note: Save identifier, wait retry_after_seconds, then call scrubby.people.email.verify.bulk.results. BYOK-only because the provider publishes no batch maximum.
|
||||
test_request: {body: {email: [info@scrubby.io]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 1
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: observed
|
||||
source_url: https://scrubby.io/pricing/
|
||||
checked: '2026-09-16'
|
||||
confidence: verified
|
||||
note: One credit per fresh address submitted; credits_used reports the exact batch charge, including cached zero-cost addresses.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/scrubby.people.email.verify.bulk.json
|
||||
docs_url: https://docs.scrubby.io/
|
||||
|
||||
- id: scrubby.people.email.verify.bulk.results
|
||||
kind: utility
|
||||
capability: people.email.verify.bulk.results
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /fetch_bulk_results
|
||||
name: Fetch quick bulk verification results
|
||||
summary: Poll a quick-verification batch until status is completed.
|
||||
platform_blocked: "Shared-key body-carried task ownership is unavailable; connect your own Scrubby key."
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
identifier: {type: string, required: true, example: batch_sample_a1b2c3d4}
|
||||
note: Poll after retry_after_seconds. Stop when status is completed.
|
||||
test_request: {body: {identifier: batch_sample_a1b2c3d4}}
|
||||
cost: {type: free, value: 0, currency: USD, unit: call, source: docs, source_url: https://docs.scrubby.io/, checked: '2026-09-16', confidence: documented, note: Scrubby prices verification when the batch is submitted; result polling reports no charge field.}
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/scrubby.people.email.verify.bulk.results.json
|
||||
docs_url: https://docs.scrubby.io/
|
||||
|
||||
- id: scrubby.people.email.verify.deep
|
||||
domain: contacts
|
||||
capability: people.email.verify.deep
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /validate_bulk_emails/deep/
|
||||
name: Submit deep email verification
|
||||
summary: Queue addresses for 72-hour verification based on observed delivery behavior.
|
||||
platform_blocked: "Shared-key bulk reservation and body-carried task ownership are unavailable; connect your own Scrubby key."
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
email: {type: "array[string]", required: true, min: 1, example: [info@scrubby.io]}
|
||||
note: First results arrive after about 24 hours and final results after 72 hours. BYOK-only because the provider publishes no batch maximum.
|
||||
test_request: {body: {email: [info@scrubby.io]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: observed
|
||||
source_url: https://scrubby.io/pricing/
|
||||
checked: '2026-09-16'
|
||||
confidence: verified
|
||||
note: Three credits per address. Live submission reported credits_used=3 for one input.
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/scrubby.people.email.verify.deep.json
|
||||
docs_url: https://docs.scrubby.io/
|
||||
|
||||
- id: scrubby.people.email.verify.deep.results
|
||||
kind: utility
|
||||
capability: people.email.verify.deep.results
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /fetch_bulk_results/deep
|
||||
name: Fetch deep verification results
|
||||
summary: Read the 24-, 48-, and 72-hour result windows for a deep batch.
|
||||
platform_blocked: "Shared-key body-carried task ownership is unavailable; connect your own Scrubby key."
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
identifier: {type: string, required: true, example: 2fbf182d25594091a2ab66a37bbe9f4d}
|
||||
note: Stop polling when status is completed. The live processing response requested a 259,200-second retry.
|
||||
test_request: {body: {identifier: 2fbf182d25594091a2ab66a37bbe9f4d}}
|
||||
cost: {type: free, value: 0, currency: USD, unit: call, source: docs, source_url: https://docs.scrubby.io/, checked: '2026-09-16', confidence: documented, note: Scrubby prices deep verification when the batch is submitted; result polling reports no charge field.}
|
||||
verified: '2026-09-16'
|
||||
example_response: examples/scrubby.people.email.verify.deep.results.json
|
||||
docs_url: https://docs.scrubby.io/
|
||||
|
||||
@@ -652,6 +652,7 @@ def _normalize(raw: dict, provider: str, directory: Path) -> dict:
|
||||
if raw.get("platform_auth") is not None else None
|
||||
),
|
||||
"strict_query": raw.get("strict_query") is True,
|
||||
"strict_body": raw.get("strict_body") is True,
|
||||
"cost": _effective_cost(raw),
|
||||
# Absent `tier` means core: the curated first wave predates the split, and treating an
|
||||
# unmarked endpoint as extended would hide it from the platform view entirely.
|
||||
@@ -762,6 +763,7 @@ def endpoint_view(ep: dict, provider_display: str, cat: Catalog | None = None) -
|
||||
# the dashboard can show what comes BACK (example_response) but not what to SEND
|
||||
"input": ep.get("input") or None,
|
||||
**({"strict_query": True} if ep.get("strict_query") else {}),
|
||||
**({"strict_body": True} if ep.get("strict_body") else {}),
|
||||
# the exact request that live-verified this endpoint — the Try-it drawer prefills from it
|
||||
# verbatim (it also carries the ground truth the input spec can't express: whether the
|
||||
# body is a bare object or an ARRAY of tasks, which dataforseo requires)
|
||||
|
||||
@@ -22,8 +22,8 @@ from treg import oauth_providers as P
|
||||
def test_openmart_surface_separates_platform_reads_from_byok_lifecycles():
|
||||
cat = cs.load()
|
||||
rows = cat.for_provider("openmart")
|
||||
assert len(rows) == 16
|
||||
assert len({(e["method"], e["path"]) for e in rows}) == 16
|
||||
assert len(rows) == 9
|
||||
assert len({(e["method"], e["path"]) for e in rows}) == 9
|
||||
assert {e["id"] for e in rows if cat.platform_eligible(e)} == {
|
||||
"openmart.businesses.search",
|
||||
"openmart.businesses.lookup.openmart",
|
||||
@@ -36,17 +36,15 @@ def test_openmart_surface_separates_platform_reads_from_byok_lifecycles():
|
||||
assert "openmart.account.balance" not in cat.by_id
|
||||
|
||||
|
||||
def test_openmart_pricing_and_lifecycle_boundaries_stay_visible():
|
||||
def test_openmart_pricing_and_account_boundaries_stay_visible():
|
||||
cat = cs.load()
|
||||
assert cat.by_id["openmart.people.find.batch"]["cost"]["value"] == 11
|
||||
assert cat.by_id["openmart.technologies.find.batch"]["cost"]["value"] == 2
|
||||
company_email = cat.by_id["openmart.companies.email.find.batch"]
|
||||
assert company_email["cost"]["value"] == .3
|
||||
assert company_email["cost"]["confidence"] == "documented"
|
||||
assert not any(
|
||||
eid.startswith("openmart.tasks.") or eid.endswith(".batch")
|
||||
for eid in cat.by_id if eid.startswith("openmart.")
|
||||
)
|
||||
fast = cat.by_id["openmart.businesses.search.ids"]
|
||||
assert fast["cost"]["value"] is None and fast["cost"]["confidence"] == "unknown"
|
||||
assert all(cat.by_id[key]["scope"] == "own_account" for key in (
|
||||
"openmart.tasks.batch.status", "openmart.tasks.batch.ids", "openmart.tasks.get",
|
||||
"openmart.deny-rules.create", "openmart.deny-rules.check",
|
||||
"openmart.deny-rules.delete",
|
||||
))
|
||||
|
||||
@@ -565,11 +565,12 @@ def test_contactout_catalog_distribution_preserves_ids_and_global_discovery():
|
||||
cat = catalog_store.load()
|
||||
entries = [e for e in cat.endpoints if e.get("provider") == "contactout"]
|
||||
assert Counter(e["platform"] for e in entries) == {
|
||||
"linkedin": 8, "people": 10, "companies": 2}
|
||||
"linkedin": 2, "people": 16, "companies": 2}
|
||||
for e in entries:
|
||||
assert e["capability"].split(".")[0] == e["platform"]
|
||||
assert cat.by_id["contactout.people.contact.work"]["platform"] == "linkedin"
|
||||
results, _ = catalog_store.search("contactout linkedin work email", cat, limit=100)
|
||||
assert cat.by_id["contactout.people.contact.work"]["platform"] == "people"
|
||||
assert cat.by_id["contactout.people.contact.personal"]["capability"] == "people.email.personal.find"
|
||||
results, _ = catalog_store.search("contactout people work email", cat, limit=100)
|
||||
assert any(e["id"] == "contactout.people.contact.work" for e, _ in results)
|
||||
|
||||
|
||||
@@ -580,12 +581,20 @@ def test_contactout_person_routes_cannot_recapture_pii():
|
||||
endpoints = yaml.safe_load(path.read_text())["endpoints"]
|
||||
safe = {"contactout.people.count", "contactout.people.email.verify",
|
||||
"contactout.companies.search", "contactout.companies.enrich"}
|
||||
structural = {"contactout.people.contact.work", "contactout.people.contact.phone"}
|
||||
for ep in endpoints:
|
||||
if ep["id"] in safe:
|
||||
continue
|
||||
assert ep["untestable"]
|
||||
assert not any(key in ep for key in ("test_request", "verified", "example_response"))
|
||||
assert not (path.parent / "examples" / (ep["id"] + ".json")).exists()
|
||||
assert not any(key in ep for key in ("test_request", "verified"))
|
||||
example = path.parent / "examples" / (ep["id"] + ".json")
|
||||
if ep["id"] in structural:
|
||||
assert ep["example_response"] == "examples/" + example.name
|
||||
payload = example.read_text()
|
||||
assert "example.invalid" in payload or "+10000000000" in payload
|
||||
else:
|
||||
assert "example_response" not in ep
|
||||
assert not example.exists()
|
||||
work = next(ep for ep in endpoints if ep["id"] == "contactout.people.enrich.work_email")
|
||||
assert work["cost"]["value"] == 0.17
|
||||
|
||||
@@ -620,6 +629,20 @@ def test_strict_query_contract_validation(patch, valid):
|
||||
assert bool(errors) is not valid
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch,valid', [
|
||||
({}, True),
|
||||
({'strict_body': 'yes'}, False),
|
||||
({'method': 'GET'}, False),
|
||||
({'input': {'body': {'items': {'type': 'array[object]', 'min': 2, 'max': 1}}}}, False),
|
||||
])
|
||||
def test_strict_body_contract_validation(patch, valid):
|
||||
ep = {'strict_body': True, 'method': 'POST', 'path': '/lookup',
|
||||
'input': {'body': {'items': {'type': 'array[object]', 'min': 1, 'max': 1}}}}
|
||||
errors = []
|
||||
validator.check_strict_body(ep | patch, 'example', errors)
|
||||
assert bool(errors) is not valid
|
||||
|
||||
|
||||
@pytest.mark.parametrize('patch,valid', [
|
||||
({}, True),
|
||||
({'platform_auth': 'provider'}, False),
|
||||
@@ -656,7 +679,7 @@ def test_missing_platform_auth_normalizes_as_absent():
|
||||
def test_dropleads_catalog_surface_is_bounded_and_excludes_internal_routes():
|
||||
catalog = catalog_store.load()
|
||||
rows = [ep for ep in catalog.endpoints if ep["provider"] == "dropleads"]
|
||||
assert len(rows) == 12
|
||||
assert len(rows) == 10
|
||||
assert all(catalog.platform_eligible(ep) for ep in rows)
|
||||
assert not any(
|
||||
"credits/balance" in ep["path"] or "export/cost" in ep["path"]
|
||||
@@ -681,18 +704,15 @@ def test_dropleads_catalog_surface_is_bounded_and_excludes_internal_routes():
|
||||
def test_prospeo_catalog_surface_excludes_account_info_and_prices_mobile_at_the_documented_maximum():
|
||||
catalog = catalog_store.load()
|
||||
rows = [ep for ep in catalog.endpoints if ep["provider"] == "prospeo"]
|
||||
assert len(rows) == 9
|
||||
assert len(rows) == 7
|
||||
assert not any(ep["path"] == "/account-information" for ep in rows)
|
||||
assert {ep["path"] for ep in rows} == {
|
||||
"/enrich-person", "/bulk-enrich-person", "/enrich-company",
|
||||
"/bulk-enrich-company", "/search-person", "/search-company",
|
||||
"/enrich-person", "/enrich-company", "/search-person", "/search-company",
|
||||
"/search-suggestions",
|
||||
}
|
||||
phone = catalog.by_id["prospeo.people.phone.find"]
|
||||
assert not phone.get("platform_blocked")
|
||||
assert phone["cost"]["value"] == 10
|
||||
bulk_mobile = catalog.by_id["prospeo.people.enrich.bulk"]["cost"]["modifiers"]
|
||||
assert bulk_mobile["enrich_mobile"]["add_credits_per_result"] == 9
|
||||
assert all(catalog.platform_eligible(ep) for ep in rows)
|
||||
|
||||
|
||||
@@ -703,21 +723,14 @@ def test_aiark_catalog_covers_the_selected_documented_surface():
|
||||
"aiark.people.search", "aiark.people.preview", "aiark.companies.search",
|
||||
"aiark.people.email.find", "aiark.people.phone.find", "aiark.people.enrich",
|
||||
"aiark.people.personality.analyze", "aiark.lists.upsert",
|
||||
"aiark.people.export.start", "aiark.people.export.results",
|
||||
"aiark.people.export.statistics", "aiark.people.export.submissions",
|
||||
"aiark.people.export.webhook.resend", "aiark.people.email.find.bulk",
|
||||
"aiark.people.email.find.bulk.results", "aiark.people.email.find.bulk.statistics",
|
||||
"aiark.people.email.find.bulk.submissions",
|
||||
"aiark.people.email.find.bulk.webhook.resend",
|
||||
}
|
||||
assert not any(ep["path"] in {
|
||||
"/v1/payments/credits", "/v1/people/export/single",
|
||||
"/v1/people/mobile-phone-finder",
|
||||
} for ep in endpoints.values())
|
||||
assert all(
|
||||
ep.get("platform_blocked")
|
||||
for eid, ep in endpoints.items()
|
||||
if ".export." in eid or ".bulk" in eid or eid == "aiark.lists.upsert"
|
||||
ep.get("platform_blocked") for eid, ep in endpoints.items()
|
||||
if eid == "aiark.lists.upsert"
|
||||
)
|
||||
assert endpoints["aiark.people.search"]["input"]["body"]["size"]["enum"] == [1]
|
||||
assert endpoints["aiark.people.search"]["platform_request"] == {"body.size": 1}
|
||||
@@ -754,12 +767,9 @@ def test_limadata_catalog_covers_basic_v2_and_keeps_unsafe_calls_byok_only():
|
||||
("POST", "/api/v1/research/extract"),
|
||||
("POST", "/api/v1/research/search"),
|
||||
("POST", "/api/v1/search/web"),
|
||||
("POST", "/api/v2/batch/ad_audiences"),
|
||||
("POST", "/api/v2/batch/email_verify"),
|
||||
("GET", "/api/v2/batch/results"),
|
||||
}
|
||||
platform = {ep["id"] for ep in rows if catalog.platform_eligible(ep)}
|
||||
assert len(rows) == 24 and len(platform) == 14
|
||||
assert len(rows) == 21 and len(platform) == 14
|
||||
assert {
|
||||
"limadata.people.enrich",
|
||||
"limadata.people.count",
|
||||
@@ -768,9 +778,6 @@ def test_limadata_catalog_covers_basic_v2_and_keeps_unsafe_calls_byok_only():
|
||||
"limadata.people.employees.search",
|
||||
"limadata.people.identity.resolve",
|
||||
"limadata.web.extract",
|
||||
"limadata.people.audience.batch.start",
|
||||
"limadata.people.email.verify.batch.start",
|
||||
"limadata.people.batch.results",
|
||||
}.isdisjoint(platform)
|
||||
|
||||
|
||||
@@ -794,18 +801,13 @@ def test_zerobounce_catalog_exposes_verified_single_record_tools_only():
|
||||
assert all(catalog.platform_eligible(ep) for ep in rows)
|
||||
|
||||
|
||||
def test_bounceban_catalog_has_one_platform_tool_and_complete_safe_byok_lifecycle():
|
||||
def test_bounceban_catalog_has_one_platform_tool_and_no_bulk_lifecycle():
|
||||
catalog = catalog_store.load()
|
||||
rows = [ep for ep in catalog.endpoints if ep["provider"] == "bounceban"]
|
||||
assert len(rows) == 9
|
||||
assert len(rows) == 4
|
||||
assert {ep["path"] for ep in rows} == {
|
||||
"/v1/verify/single",
|
||||
"/v1/verify/single/status",
|
||||
"/v1/verify/bulk",
|
||||
"/v1/verify/bulk/status",
|
||||
"/v1/verify/bulk/emails",
|
||||
"/v1/verify/bulk/dump",
|
||||
"/v1/verify/bulk/export",
|
||||
"/v1/account",
|
||||
}
|
||||
assert not any(ep["path"] in ("/v1/verify/bulk/file", "/v1/verify/bulk/destroy", "/v1/check")
|
||||
@@ -821,13 +823,17 @@ def test_bounceban_catalog_has_one_platform_tool_and_complete_safe_byok_lifecycl
|
||||
assert waterfall["platform_blocked"]
|
||||
|
||||
|
||||
def test_getleadsio_original_routes_are_available_to_byok_and_platform_callers():
|
||||
def test_getleadsio_scalar_routes_are_available_to_byok_and_platform_callers():
|
||||
catalog = catalog_store.load()
|
||||
rows = [ep for ep in catalog.endpoints if ep["provider"] == "getleadsio"]
|
||||
assert len(rows) == 12
|
||||
assert len(rows) == 11
|
||||
assert not any(ep["path"] in {
|
||||
"/api/v1/usage/fair-use", "/api/v1/contacts/health"
|
||||
} for ep in rows)
|
||||
assert all(catalog.platform_eligible(ep) for ep in rows)
|
||||
assert not any(ep["id"].endswith(".trial") for ep in rows)
|
||||
assert not any(ep.get("platform_request") or ep.get("platform_blocked") for ep in rows)
|
||||
scalar = [ep for ep in rows if ep["id"].startswith("getleadsio.people.enrich.from_")]
|
||||
assert len(scalar) == 3
|
||||
assert all(ep["strict_body"] and ep["input"]["body"]["items"]["max"] == 1
|
||||
for ep in scalar)
|
||||
|
||||
@@ -2108,6 +2108,28 @@ async def test_getleadsio_platform_call_is_free_and_preserves_the_requested_limi
|
||||
assert await _balance(clients) == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", [
|
||||
"getleadsio.people.enrich.from_email",
|
||||
"getleadsio.people.enrich.from_linkedin",
|
||||
"getleadsio.people.enrich.from_person",
|
||||
])
|
||||
@pytest.mark.parametrize("own_key", [False, True])
|
||||
async def test_getleadsio_enrichment_rejects_more_than_one_item_on_every_credential_tier(
|
||||
clients: AsyncClient, getleadsio_trial_on, endpoint, own_key):
|
||||
if own_key:
|
||||
created = await clients.post(
|
||||
"/secrets", json={"name": "getleadsio", "value": "OWN-GETLEADSIO"},
|
||||
)
|
||||
assert created.status_code == 200, created.text
|
||||
result = await clients.post(
|
||||
f"/call/{endpoint}", json={"items": [{"id": "one"}, {"id": "two"}]},
|
||||
)
|
||||
assert result.status_code == 400, result.text
|
||||
detail = result.json()["detail"]
|
||||
assert detail["error"] == "catalog_parameter_invalid"
|
||||
assert detail["parameter"] == "body.items"
|
||||
|
||||
|
||||
async def test_getleadsio_own_key_wins_and_is_unmetered(
|
||||
clients: AsyncClient, getleadsio_trial_on):
|
||||
await clients.post("/secrets", json={"name": "getleadsio", "value": "OWN-GETLEADSIO"})
|
||||
@@ -3216,8 +3238,6 @@ async def test_dropleads_byok_wins_and_is_never_metered(
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint,body,expected",
|
||||
[
|
||||
("dropleads.people.enrich.bulk", {"details": [{"id": str(i)} for i in range(10)]}, 36_000),
|
||||
("dropleads.people.enrich.verified.bulk", {"details": [{"id": str(i)} for i in range(3)]}, 10_800),
|
||||
("dropleads.companies.enrich", {"domains": [f"{i}.test" for i in range(25)],
|
||||
"companyNames": [str(i) for i in range(25)]}, 90_000),
|
||||
("dropleads.companies.search", {"filters": {},
|
||||
@@ -3263,8 +3283,6 @@ def test_dropleads_request_shapes_reserve_exact_valid_maxima(endpoint, body, exp
|
||||
{"error": False, "free": False, "results": [{"person": {"person_id": "p1"}}]}, 24_500),
|
||||
("prospeo.companies.search",
|
||||
{"error": False, "free": True, "results": [{"company": {"company_id": "c1"}}]}, 0),
|
||||
("prospeo.people.enrich.bulk",
|
||||
{"error": False, "total_cost": 3, "matched": []}, 73_500),
|
||||
("prospeo.search.suggestions",
|
||||
{"error": False, "location_results": []}, 0),
|
||||
("prospeo.people.email.find", {"error": True, "error_code": "NO_MATCH"}, 0),
|
||||
@@ -3275,14 +3293,6 @@ def test_prospeo_settles_only_from_response_evidence(endpoint, doc, expected):
|
||||
assert call_settle._observed_cost_micro(mk, json.dumps(doc).encode()) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("total_cost", [float("inf"), float("-inf"), float("nan")])
|
||||
def test_prospeo_bulk_non_finite_cost_keeps_the_estimate(total_cost):
|
||||
mk = _mk("prospeo", endpoint_id="prospeo.people.enrich.bulk",
|
||||
cost_type="per_result", unit_micro=24_500)
|
||||
body = json.dumps({"error": False, "total_cost": total_cost}).encode()
|
||||
assert call_settle._observed_cost_micro(mk, body) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"doc,expected",
|
||||
[
|
||||
@@ -3302,26 +3312,6 @@ def test_prospeo_phone_settlement_requires_an_actual_mobile(doc, expected):
|
||||
assert call_settle._observed_cost_micro(mk, json.dumps(doc).encode()) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint,body,expected",
|
||||
[
|
||||
("prospeo.people.enrich.bulk", {"data": [{"id": i} for i in range(4)]}, 98_000),
|
||||
("prospeo.people.enrich.bulk",
|
||||
{"enrich_mobile": True, "data": [{"id": i} for i in range(4)]}, 980_000),
|
||||
("prospeo.companies.enrich.bulk", {"data": [{"id": i} for i in range(50)]}, 1_225_000),
|
||||
],
|
||||
)
|
||||
def test_prospeo_bulk_reserves_the_maximum_documented_charge(endpoint, body, expected):
|
||||
catalog = catalog_store.load()
|
||||
ep = catalog.by_id[endpoint]
|
||||
cost = catalog.cost_view(ep["cost"], "prospeo")
|
||||
estimate, unit = call_resolution._marketplace_pricing(
|
||||
"prospeo", endpoint, cost, {}, json.dumps(body).encode()
|
||||
)
|
||||
assert estimate == expected
|
||||
assert unit == 24_500
|
||||
|
||||
|
||||
async def test_prospeo_platform_email_settles_one_credit(
|
||||
clients, monkeypatch, prospeo_platform_on,
|
||||
):
|
||||
|
||||
+12
-26
@@ -23,8 +23,8 @@ def moltsets_on(monkeypatch, platform_on):
|
||||
def test_surface_platform_boundary_and_shared_plan_rate():
|
||||
cat = store.load()
|
||||
rows = cat.for_provider("moltsets")
|
||||
assert len(rows) == 17
|
||||
assert len({(e["method"], e["path"]) for e in rows}) == 17
|
||||
assert len(rows) == 12
|
||||
assert len({(e["method"], e["path"]) for e in rows}) == 12
|
||||
enabled = {e["id"] for e in rows if cat.platform_eligible(e)}
|
||||
assert enabled == {
|
||||
"moltsets.people.email.find.name",
|
||||
@@ -41,6 +41,11 @@ def test_surface_platform_boundary_and_shared_plan_rate():
|
||||
assert cat.shared_plans["moltsets"] == {"usd": .01, "fee_usd_month": 27}
|
||||
assert all(e.get("verified") and e.get("example_file") for e in rows)
|
||||
assert not any(e["path"].startswith("/get_") for e in rows)
|
||||
assert {
|
||||
"moltsets.people.email.find", "moltsets.people.email.find.business",
|
||||
"moltsets.people.email.find.personal", "moltsets.people.email.find.personal-best",
|
||||
"moltsets.people.phone.find",
|
||||
}.isdisjoint(cat.by_id)
|
||||
|
||||
|
||||
def test_connection_and_binding_contract():
|
||||
@@ -105,13 +110,8 @@ async def test_platform_success_charges_and_miss_or_error_does_not(clients, molt
|
||||
("people.search", {"query": "engineer", "limit": 2}),
|
||||
("companies.search", {"query": "example", "limit": 2}),
|
||||
("linkedin.profile.search", {"name": "Alex Example"}),
|
||||
("people.email.find", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
|
||||
("people.email.find.business", {"linkedin_urls": ["https://linkedin.com/in/alex-example"]}),
|
||||
("people.email.find.personal", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
|
||||
("people.email.find.personal-best", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
|
||||
("people.phone.find", {"linkedin_url": "https://linkedin.com/in/alex-example"}),
|
||||
])
|
||||
async def test_variable_batch_and_phone_tools_are_not_shared_key_offers(
|
||||
async def test_variable_search_tools_are_not_shared_key_offers(
|
||||
clients, moltsets_on, monkeypatch, endpoint, body):
|
||||
async def forbidden(*args, **kwargs):
|
||||
raise AssertionError("platform guard must precede relay")
|
||||
@@ -122,13 +122,11 @@ async def test_variable_batch_and_phone_tools_are_not_shared_key_offers(
|
||||
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
|
||||
|
||||
async def test_byok_can_call_blocked_batch_and_phone_tools_without_treg_meter(clients, moltsets_on):
|
||||
async def test_byok_can_call_blocked_search_tools_without_treg_meter(clients, moltsets_on):
|
||||
await clients.post("/secrets", json={"name": "moltsets", "value": "OWN-MOLTSETS"})
|
||||
before = await _balance(clients)
|
||||
bodies = {
|
||||
"people.search": {"query": "engineer", "limit": 2},
|
||||
"people.email.find": {"linkedin_urls": ["https://linkedin.com/in/a", "https://linkedin.com/in/b"]},
|
||||
"people.phone.find": {"linkedin_url": "https://linkedin.com/in/a"},
|
||||
}
|
||||
for endpoint, body in bodies.items():
|
||||
response = await clients.post(
|
||||
@@ -143,18 +141,6 @@ async def test_byok_can_call_blocked_batch_and_phone_tools_without_treg_meter(cl
|
||||
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
|
||||
|
||||
def test_phone_price_keeps_both_upstream_meters_visible():
|
||||
from treg.routers.web import _price_label
|
||||
|
||||
cat = store.load()
|
||||
endpoint = cat.by_id["moltsets.people.phone.find"]
|
||||
cost = cat.cost_view(endpoint["cost"], "moltsets")
|
||||
assert cost["usd"] == .51
|
||||
assert _price_label(cost) == "$0.51/result"
|
||||
assert "$0.01" in cost["note"] and "$0.50" in cost["note"]
|
||||
assert endpoint["platform_blocked"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("five_hour,weekly,token_balance,expected", [
|
||||
(997, 4997, -1, 997),
|
||||
(0, 4997, -1, 0),
|
||||
@@ -205,8 +191,8 @@ async def test_provider_page_shows_complete_inventory_and_access_split(clients):
|
||||
response = await clients.get("/tools/moltsets")
|
||||
assert response.status_code == 200
|
||||
html = response.text
|
||||
assert "17 tools · 9 platform + BYOK · 8 BYOK only" in html
|
||||
assert "17 of 17 tools on this page are live-verified" in html
|
||||
assert "$0.51/result" in html
|
||||
assert "12 tools · 9 platform + BYOK · 3 BYOK only" in html
|
||||
assert "12 of 12 tools on this page are live-verified" in html
|
||||
assert "moltsets.people.phone.find" not in html
|
||||
for endpoint in store.load().for_provider("moltsets"):
|
||||
assert "<code>" + endpoint["id"] + "</code>" in html
|
||||
|
||||
+16
-7
@@ -1304,7 +1304,7 @@ async def test_lusha_is_the_last_rung_of_the_phone_waterfall_and_settles_on_its_
|
||||
get_settings.cache_clear()
|
||||
routed = "treg.people.phone.find"
|
||||
plan = (await clients.get(f"/catalog/endpoints/{routed}")).json()["routing"]["plan"]
|
||||
assert plan[-1]["endpoint_id"] == "lusha.people.phone.find" and len(plan) == 11, [c["endpoint_id"] for c in plan]
|
||||
assert plan[-1]["endpoint_id"] == "lusha.people.phone.find" and len(plan) == 12, [c["endpoint_id"] for c in plan]
|
||||
def misses():
|
||||
return {"aviato": [(404, {"message": "Not Found"})], "tomba": [(200, {"data": {"e164_format": None}})],
|
||||
"leadmagic": [(200, {"mobile_number": None, "credits_consumed": 0})],
|
||||
@@ -1504,11 +1504,6 @@ def test_bounceban_verdicts_join_existing_email_verification_route(result, valid
|
||||
assert cat.platform_eligible(cat.by_id[eid])
|
||||
for blocked in (
|
||||
"bounceban.people.email.verify.waterfall",
|
||||
"bounceban.people.email.verify.bulk",
|
||||
"bounceban.people.email.verify.bulk.status",
|
||||
"bounceban.people.email.verify.bulk.emails",
|
||||
"bounceban.people.email.verify.bulk.dump",
|
||||
"bounceban.people.email.verify.bulk.export",
|
||||
"bounceban.account.usage",
|
||||
):
|
||||
assert not cat.platform_eligible(cat.by_id[blocked])
|
||||
@@ -1981,6 +1976,18 @@ def test_row_values_and_nested_lookup_expressions(value, expected):
|
||||
|
||||
|
||||
_CONTACTOUT_DISCOVERY = [
|
||||
('people.email.find', 'people.contact.work',
|
||||
{'linkedin_url': 'https://www.linkedin.com/in/example'},
|
||||
'GET', {'profile': 'https://www.linkedin.com/in/example',
|
||||
'email_type': 'work', 'include_phone': False}, None,
|
||||
{'status_code': 200, 'profile': {'work_email': ['work@example.test']}},
|
||||
'email', 'work@example.test', 150_000),
|
||||
('people.phone.find', 'people.contact.phone',
|
||||
{'linkedin_url': 'https://www.linkedin.com/in/example'},
|
||||
'GET', {'profile': 'https://www.linkedin.com/in/example',
|
||||
'email_type': 'none', 'include_phone': True}, None,
|
||||
{'status_code': 200, 'profile': {'phone': ['+10000000000']}},
|
||||
'phone', '+10000000000', 250_000),
|
||||
('companies.search', 'companies.search', {'domain': 'example.test'},
|
||||
'POST', {}, {'domain': ['example.test']},
|
||||
{'status_code': 200, 'companies': [{'name': 'Example'}]}, 'companies', [{'name': 'Example'}], 20_000),
|
||||
@@ -2033,10 +2040,12 @@ async def test_contactout_discovery_empty_or_error_response_is_not_a_hit(
|
||||
assert not (await db.execute(select(Hold))).scalars().all()
|
||||
|
||||
|
||||
def test_contactout_pii_routes_are_not_enabled_without_verification_examples():
|
||||
def test_contactout_unverified_pii_routes_stay_direct_only():
|
||||
cat = catalog_store.load()
|
||||
for cap, child in [('people.search', 'people.search'), ('people.enrich', 'people.enrich'),
|
||||
('linkedin.user.profile', 'people.linkedin.enrich')]:
|
||||
eid = 'contactout.' + child
|
||||
assert eid not in cat.adapters
|
||||
assert eid not in cat.by_id['treg.' + cap]['routed_children']
|
||||
assert 'contactout.people.contact.personal' not in cat.adapters
|
||||
assert cat.by_id['contactout.people.contact.personal']['platform'] == 'people'
|
||||
|
||||
+2
-11
@@ -42,21 +42,12 @@ async def _balance(clients):
|
||||
return (await clients.get(f"/orgs/{org_id}/balance")).json()["balance_micro"]
|
||||
|
||||
|
||||
def test_scrubby_catalog_covers_the_documented_surface():
|
||||
def test_scrubby_catalog_exposes_single_verification_only():
|
||||
catalog = catalog_store.load()
|
||||
endpoints = {eid: ep for eid, ep in catalog.by_id.items() if eid.startswith("scrubby.")}
|
||||
assert set(endpoints) == {
|
||||
"scrubby.people.email.verify",
|
||||
"scrubby.people.email.verify.bulk",
|
||||
"scrubby.people.email.verify.bulk.results",
|
||||
"scrubby.people.email.verify.deep",
|
||||
"scrubby.people.email.verify.deep.results",
|
||||
}
|
||||
assert set(endpoints) == {"scrubby.people.email.verify"}
|
||||
assert endpoints["scrubby.people.email.verify"]["path"] == "/validate_email"
|
||||
assert "ownership" in endpoints["scrubby.people.email.verify.bulk"]["platform_blocked"]
|
||||
assert "ownership" in endpoints["scrubby.people.email.verify.deep"]["platform_blocked"]
|
||||
assert catalog.cost_view(endpoints["scrubby.people.email.verify"]["cost"], "scrubby")["usd"] == 0.008
|
||||
assert catalog.cost_view(endpoints["scrubby.people.email.verify.deep"]["cost"], "scrubby")["usd"] == 0.024
|
||||
|
||||
|
||||
@pytest.mark.parametrize("credits,expected", [(0, 0), (1, 8_000), (3, 24_000), (-1, None), (True, None), (1.5, None), (None, None)])
|
||||
|
||||
Reference in New Issue
Block a user