# B2B Agency & SaaS Tech Stack Detector - DNS & HTTP (`afkdigital/newapi15-b2b-tech-stack-detector`) Actor

Inspect public DNS TXT, CNAME, MX and HTTPS response headers for Shopify, HubSpot, Klaviyo and SaaS signals. Get versioned evidence, conservative confidence, JSON/CSV exports, SSRF protection and clear error status. $0.005 per completed unique domain.

- **URL**: https://apify.com/afkdigital/newapi15-b2b-tech-stack-detector.md
- **Developed by:** [AFK Digital](https://apify.com/afkdigital) (community)
- **Categories:** Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 successfully examined unique domains

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## B2B Agency & SaaS Tech Stack Detector — DNS & HTTP

Inspect public infrastructure signals for a list of hostnames. This Actor reads DNS TXT, CNAME and MX records and public HTTPS response headers. It returns versioned evidence, conservative confidence and explicit error states for sales research, agency prospecting and technical inventory.

**Price: $0.005 per successfully examined unique hostname ($5 per 1,000).** A complete examination with no matching signal, or conflicting signals, returns **unknown** and is billable. Duplicates, invalid inputs, blocked destinations, incomplete examinations, technical failures and budget skips are not charged.

**A signal is not proof of an active subscription, partnership, customer relationship, current product use or domain ownership.** DNS can be stale or deliberately configured; response headers can be imitated. Never use these results as verified Shopify, HubSpot or Klaviyo customer or partner lists.

Product version: **1.0.0**. Signature ruleset: **2026-10-20.1**.

### Quick start

The default input checks three hostnames with a $0.025 input cap:

```json
{
  "domains": ["www.allbirds.com", "civilclothing.com", "iana.org"],
  "technologies": [],
  "maxDomains": 5,
  "timeoutSeconds": 8,
  "maxCostUsd": 0.025
}
```

These are examples of public domains, not endorsements or commercial relationship claims. Public DNS, headers and access rules can change between runs.

#### Input

| Field | Meaning | Limits / default |
| --- | --- | --- |
| domains | Hostnames or HTTP/HTTPS URLs. Only the normalized hostname is inspected. | 1–1,000 strings; each at most 2,048 characters |
| technologies | Technology IDs to include in output. Empty means all supported rules. | Empty array by default |
| maxDomains | Maximum unique entries selected in input order after normalization and deduplication. Invalid unique entries also occupy a selection slot. | 1–1,000; default 5 |
| timeoutSeconds | Total DNS and HTTP time budget per hostname, including retries. | 2–20 seconds; default 8 |
| maxCostUsd | Maximum custom-event charge for this run. This is also checked against Apify's native maximum total charge. | $0–$5; default $0.025 |

Technology IDs: **shopify**, **hubspot**, **klaviyo**, **google_mail**, **microsoft_mail**, **sendgrid**, **cloudflare**, **vercel**.

Hostnames are trimmed, lowercased, converted to ASCII IDNA form and stripped of a trailing dot. URL paths, query strings and fragments are discarded for the initial request. For example, HTTPS://EXAMPLE.COM/path and example.com share one inspection. **www.example.com and example.com remain separate hostnames** because their DNS and HTTP configuration can differ. No apex/www variants are guessed.

URLs containing credentials, IP literals, nonstandard ports, unsupported schemes and invalid or internal hostnames are rejected. The initial web check always uses HTTPS on port 443, including when an input URL starts with http://. There is no HTTP downgrade fallback.

### Technology coverage

The first release contains **13 rules for 8 platform families**. Every evidence item includes its exact rule ID, rule version, ruleset version, source and documentation URL. The full machine-readable rules are in rules/signatures.v1.json; matching details are documented in docs/SIGNATURES.md.

| Platform | Accepted public evidence | Limited interpretation |
| --- | --- | --- |
| Shopify | Exact CNAME target shops.myshopify.com | Storefront routing configuration |
| HubSpot | Numeric group website CNAME; direct supported SPF include | Website routing or email configuration, not proof of CRM use |
| Klaviyo | klaviyo-site-verification TXT; branded-email CNAME below klaviyodns.com | Verification artifact or branded-email configuration |
| Google mail | Published Google mail MX targets; direct \_spf.google.com SPF include | Inbound mail routing or outbound email configuration |
| Microsoft mail | Tenant MX below mail.protection.outlook.com; direct spf.protection.outlook.com include | Microsoft mail routing or email configuration |
| SendGrid | CNAME at/below sendgrid.net; direct sendgrid.net SPF include | Email/tracking routing or email configuration |
| Cloudflare | cf-ray and cf-cache-status together in the same response | HTTP edge handling signal |
| Vercel | x-vercel-id plus server: Vercel together in the same response | HTTP delivery signal |

Only direct records at the supplied hostname are queried. There is no SPF include recursion, DKIM selector guessing, HTML/JavaScript analysis, cookie analysis, certificate scraping, WHOIS, nameserver inference or browser automation. A/AAAA lookups are used only to secure HTTP connections.

A Google verification TXT record alone is not a Google Workspace match. Generic server headers alone do not identify Shopify, HubSpot or Klaviyo. Substring lookalikes such as shops.myshopify.com.attacker.example do not match vendor targets.

The technology filter controls reported matches. All routing rules are still evaluated to detect contradictions, so selecting one technology cannot conceal conflicting evidence.

### Confidence and unknown

Confidence describes the specificity of an observed configuration pattern, **not a probability of commercial use**:

- **high**: an exact or tightly bounded routing match, such as a documented CNAME or MX target.
- **medium**: a direct SPF include or a pair of service-specific HTTP headers.
- **low**: a verification artifact that may remain after use stops.
- **unknown**: no selected own-host match, conflicting routing evidence, or incomplete checks.

The overall confidence is the strongest accepted own-host rule. It does not upgrade every matched platform to that confidence; use each evidence item's confidence for individual conclusions.

Multiple SPF providers or layered Cloudflare/Vercel signals can coexist. Incompatible direct platform CNAME routes, or both supported Google and Microsoft inbound-mail routes, are treated conservatively as a conflict. Evidence is retained, but detected_platforms is empty and confidence_level is unknown.

Redirected hosts are checked independently for network safety. Headers from a different hostname are retained as **redirect_target_only** evidence and are not attributed to the original hostname. An apex redirect to a www host can therefore return unknown for the apex even when the target has a recognizable header.

**Unknown never means “this technology is absent.”** Proxies, flattened CNAMEs, access restrictions, unsupported configurations and stale or changing DNS can prevent a match.

### Output and exports

Each selected unique entry produces a dataset row during a normal completed run:

| Field | Meaning |
| --- | --- |
| domain | Normalized hostname; empty for rejected input so URL credentials are not echoed |
| detected_platforms | Supported technology IDs accepted for the inspected hostname |
| dns_txt_matches | Matched TXT evidence; verification tokens are redacted |
| cname_targets | Direct CNAME targets returned by DNS |
| mx_signals | Matched MX evidence |
| http_header_signals | Matched header evidence with source hostname |
| signal_evidence | Rule ID/version, ruleset, platform, source, observed value, confidence, interpretation and documentation |
| confidence_level | high, medium, low or unknown |
| checked_at | UTC ISO timestamp |
| status | Completion, uncertainty or failure state |

Additional fields include ruleset_version, checks, errors, conflicts, query_counts, http_checks, billable, billing_status and charge_usd. Invalid entries include input_index, not their raw text. No response bodies, cookies, authorization headers or full redirect URLs are stored. Klaviyo verification values are replaced with \[redacted].

Use the run's **Dataset** to export JSON or CSV. The Output tab also links to **SUMMARY** and **RESULTS** in the default key-value store. Nested evidence is preserved in JSON and serialized in CSV; JSON is preferable for detailed downstream processing.

#### Status and billing

| Status | Interpretation | Domain event |
| --- | --- | --- |
| ok | All required checks completed and selected own-host evidence matched | $0.005 |
| unknown | All required checks completed; no selected match or conflicting evidence | $0.005 |
| partial | Some checks completed, but at least one failed | No charge |
| error / timeout | Technical failure or deadline exceeded | No charge |
| blocked / invalid | Unsafe destination, redirect or invalid input | No charge |
| skipped_budget | Insufficient input/native charge allowance | No charge |
| skipped_runtime | The network-work cutoff was reached | No charge |

A failed or partial row can retain observed evidence for diagnosis, but its detected_platforms is empty and confidence_level is unknown. HTTP 403, NXDOMAIN, SERVFAIL and timeouts are not successful negative detections.

The Actor charges exactly one **domain-examined** event for each eligible unique hostname in a run, regardless of how many technologies match or how many controlled retries were needed. A separate new run examines and bills eligible domains again. There is no per-technology, per-retry, startup or dataset-item event charged by this Actor.

At $0.005 per eligible domain: 5 domains cost $0.025, 100 cost $0.50 and 1,000 cost $5. These figures concern this Actor's event price; they do not promise that 1,000 slow domains finish in one run. The smaller of maxCostUsd and Apify's native remaining event allowance applies. The input cap is rounded down to whole events: $0.004 allows no examination. A zero cap starts no domain network checks.

#### Recovery and ambiguous charging

Each normalized hostname has a persistent journal. Evidence is stored before a charge attempt; a charge-intent record is saved before calling the platform. Charges are serialized and both caps are checked before each event.

If execution stops during an ambiguous charge attempt, the journal reserves that unit and **does not retry the charge** after restart. The row reports an uncertain billing status and a null charge_usd. Reconcile such cases against Apify's run event counts. Confirmed earlier platform charges are also included in the remaining budget, even if a resumed input selects different domains.

Dataset writes use a durable output-intent marker to avoid duplicate pushes after an uncertain interruption. An interruption between intent and push can omit a dataset row; a later successful resume reconstructs the canonical RESULTS record from the journals. Runs terminated before finalization may lack SUMMARY or RESULTS. This conservative recovery strategy prioritizes avoiding duplicate charges and duplicate output over silently repeating ambiguous operations.

### Network safety and limits

Only public DNS and public HTTPS HEAD responses are used. No pages are rendered, no login is attempted and no response body is consumed. Known authentication redirects, including encoded paths, are rejected; 401/403 responses are errors.

Before every HTTPS hop and retry, the complete A/AAAA address set is resolved twice. Private, loopback, link-local, CGNAT, multicast, documentation, reserved and unsafe IPv6 transition addresses are rejected. Mixed public/private answers and changed address sets fail closed. The connection uses a validated pinned address, normal TLS certificate verification and an actual socket-peer check. Every redirect is revalidated. Unsafe protocols, ports, IP-literal destinations, internal names and HTTPS downgrades are blocked.

These conservative checks can reject legitimate rapidly rotating DNS. HEAD restrictions can also prevent a result even when a browser could load the site. The Actor does not bypass such restrictions.

Fixed limits:

- At most **3 hostnames** inspected concurrently.
- At most **24 DNS requests** and **4 HTTP attempts** per hostname, including retries and security lookups.
- At most **2 redirects** and **1 retry** for a transient operation, within the total hostname deadline.
- At most **16 KiB** per DNS result set and HTTP header block; selected stored header values are capped at 2,048 characters.
- New inspections stop early enough to fit their per-host timeout inside a **240-second network-work window**.
- An independent **290-second process guard** also bounds initialization, journal recovery and finalization. The default Apify platform timeout is 300 seconds.

Limit counters can include a rejected limit check, so a diagnostic counter can be one greater than the number of requests actually sent. A hard timeout can leave a partial dataset and recoverable journals as described above.

### Development and deployment

Runtime: Node.js 22, Apify SDK 3.7.0 and ipaddr.js 2.2.0 with a committed lockfile.

```sh
npm ci --ignore-scripts
npm test
npm start
```

A local run does not charge Apify events. Cloud execution refuses to start unless the configured pricing is pay per event with **domain-examined = $0.005** and no other nonzero event. Build configuration and schemas are in .actor/. The Docker build runs the automated safety, matching and billing tests.

Production installation: /opt/NEWAPI/NEWAPI15. No shared service or other project is required. Deployment helper tools are excluded from the Actor source bundle and container. Credentials are read by the operator's existing credential store, never included in source, input examples or logs.

### Documentation sources

- [Apify pay per event](https://docs.apify.com/actors/publishing/monetize/pay-per-event)
- [Apify ChargingManager](https://docs.apify.com/sdk/js/reference/class/ChargingManager)
- [Shopify DNS connection](https://help.shopify.com/en/manual/domains/add-a-domain/connecting-domains/connect-domain-manual)
- [HubSpot website DNS](https://knowledge.hubspot.com/domains-and-urls/update-your-dns-records)
- [HubSpot email authentication](https://knowledge.hubspot.com/marketing-email/manage-email-authentication-in-hubspot)
- [Klaviyo branded sending domains](https://help.klaviyo.com/hc/en-us/articles/115000357752)
- [Google mail MX](https://knowledge.workspace.google.com/admin/domains/set-up-mx-records-for-google-workspace)
- [Google SPF](https://knowledge.workspace.google.com/admin/security/set-up-spf)
- [Microsoft external DNS records](https://learn.microsoft.com/en-us/microsoft-365/enterprise/external-domain-name-system-records?view=o365-worldwide)
- [SendGrid domain authentication](https://www.twilio.com/docs/sendgrid/ui/account-and-settings/how-to-set-up-domain-authentication)
- [Cloudflare response headers](https://developers.cloudflare.com/fundamentals/reference/http-headers/)
- [Vercel response headers](https://vercel.com/docs/headers/response-headers)

# Actor input Schema

## `domains` (type: `array`):

Normalize and deduplicate hosts; www stays distinct.

## `technologies` (type: `array`):

Empty means all signatures. Completeness checks always run.

## `maxDomains` (type: `integer`):

Limit after deduplication, including invalid entries.

## `timeoutSeconds` (type: `number`):

Includes DNS, redirects and retries.

## `maxCostUsd` (type: `number`):

USD 0.005 per completed unique domain. Zero performs no network checks; native Apify cap also applies.

## Actor input object example

```json
{
  "domains": [
    "www.allbirds.com",
    "civilclothing.com",
    "iana.org"
  ],
  "technologies": [],
  "maxDomains": 5,
  "timeoutSeconds": 8,
  "maxCostUsd": 0.025
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `canonicalResults` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "www.allbirds.com",
        "civilclothing.com",
        "iana.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("afkdigital/newapi15-b2b-tech-stack-detector").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "www.allbirds.com",
        "civilclothing.com",
        "iana.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("afkdigital/newapi15-b2b-tech-stack-detector").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "www.allbirds.com",
    "civilclothing.com",
    "iana.org"
  ]
}' |
apify call afkdigital/newapi15-b2b-tech-stack-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,afkdigital/newapi15-b2b-tech-stack-detector"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QlS82IKbmgpYTgfek/builds/vvVty4XYSoI3ad7P3/openapi.json
