AlterLabAlterLab
PricingComparePlaygroundBlogDocsChangelog
    AlterLabAlterLab
    PricingComparePlaygroundBlogDocsChangelog
    IntroductionQuickstartInstallationYour First Request
    REST APICrawl APIMap APISearch APISERP APINewExtract APIAIJob PollingAPI KeysSessions APINewEnterprise APIEnterprise
    AccountAutoAlertsAutoAuthAutoBillingAutoCrawlAutoExtractAutoIntegrationsAutoKeysAutoMapAutoMonitorsAutoOrganizationsAutoSchedulesAutoScrapeAutoSearchAutoSessionsAutoUser WebhooksAutoV1 EndpointsAutoWebhooksAuto
    OverviewPythonNode.js
    JavaScript RenderingOutput FormatsPDF & OCRCachingWebhooksJSON Schema FilteringWebSocket Real-TimeBring Your Own ProxyProAuthenticated ScrapingNewHTTP Methods & BodiesNewStructured ExtractionAIWeb SearchSite MappingWeb CrawlingBatch ScrapingSchedulerChange DetectionCloud Storage ExportSpend LimitsOrganizations & TeamsAlerts & NotificationsExtraction ProfilesAIBYOK ExtractionAIOAuth2 Machine-to-MachineSupport & TicketsUnsupported Targets
    Structured ExtractionAIE-commerce ScrapingNews MonitoringPrice MonitoringMulti-Page CrawlingMonitoring DashboardAI Agent / MCPMCPAI Research AgentAISite CrawlingData Pipeline to Cloud
    E-commerceLead GenerationChange MonitoringRAG & AI PipelinesAIResearch
    PricingRate LimitsError CodesChangelogVersioning
    From FirecrawlFrom ApifyFrom ScrapingBee / ScraperAPIFrom Crawl4AIFrom SpiderFirecrawl v0 API ReferenceLegacy
    OverviewMCP ServerAIn8n NodeLangChainAICrewAIAILlamaIndexAISupabaseChrome ExtensionSoon
    PlaygroundPricingStatus

    5,000 free requests · No credit card

    API Reference
    Enterprise

    Enterprise API

    B2B intake API for enterprise tenants. Bulk job submission with rate shaping, demographic browser profile management, S3 delivery triggers, and BYOP proxy outcome feedback.

    Authentication

    Enterprise accounts only

    All endpoints in this section require an enterprise account (account_type == 'enterprise'). Standard self-serve API keys are rejected with HTTP 403. To request enterprise access, contact support.

    Authenticate with the same X-API-Key header used across all AlterLab endpoints. Your API key must belong to an enterprise-provisioned account — the enterprise guard is enforced server-side on every B2B route.

    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    # All B2B methods are available under client.enterprise
    # Standard accounts receive HTTP 403 — enterprise provisioning required.

    Rate Shaping

    B2B jobs are not dispatched immediately. Submitted jobs enter a per-tenant holding set in Redis and are released into the main worker queue at the contracted throughput rate. This prevents traffic spikes from overwhelming the scraping fleet.

    Rate shape parameters can be supplied per bulk submission via the optional rate_shape_config field, or fall back to tenant-level defaults configured at provisioning.

    ParameterTypeDefaultDescription
    jobs_per_hourinteger1000Maximum jobs released per hour from the holding set (1–500,000).
    burst_limitinteger50000Maximum jobs allowed in the holding set at once. Submissions exceeding this return HTTP 429.
    priorityinteger5Worker queue priority (1=urgent, 3=high, 5=normal, 7=low, 10=background).

    Estimated completion

    The bulk submission response includes an estimated_completion_at timestamp based onjobs_per_hour. Poll GET /api/v1/b2b/jobs/bulk/{bulk_id} to track actual progress.

    Bulk Job Submission

    Submit up to 100,000 job specs in a single call. Each job is either a URL scrape job (url) or a SERP search job (query) — these are mutually exclusive per item.

    POST /api/v1/b2b/jobs/bulk

    POST
    /api/v1/b2b/jobs/bulk

    Submit a bulk job batch. Jobs enter the tenant holding set and are released at the contracted rate. Returns immediately after validation — non-blocking.

    Parameters

    NameTypeRequiredDescription
    bulk_idstring
    Required
    Client-supplied idempotency key (max 255 chars). Re-submitting an identical bulk_id returns the existing submission status without re-enqueuing jobs.
    jobsarray
    Required
    Array of job specs (1–100,000). Each item must have exactly one of: url (scrape job) or query (SERP search job).
    jobs[].urlstringOptionalTarget URL to scrape (max 2048 chars, http/https only). Mutually exclusive with query.
    jobs[].querystringOptionalSERP search query string (max 500 chars). When set, the job routes to the SERP pipeline. Mutually exclusive with url.
    jobs[].enginestringOptionalSearch engine for query jobs: 'duckduckgo' (default), 'google', or 'bing'. Only valid with query.
    jobs[].countrystringOptionalISO 3166-1 alpha-2 country code for geo-targeted results (e.g. 'us', 'gb'). Only valid with query.
    jobs[].session_pool_idstringOptionalDemographic profile ID (from POST /api/v1/b2b/profiles) to route this search through a demographically-targeted session. Only valid with query.
    jobs[].safe_searchbooleanOptionalOverride Google safe search. false = disable (recommended for ad-fraud detection). null = platform default.
    jobs[].metadataobjectOptionalArbitrary key-value metadata attached to the job for client-side tracking and correlation.
    rate_shape_configobjectOptionalThroughput control: { jobs_per_hour, burst_limit, priority }. Falls back to tenant defaults if omitted.
    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    response = client.enterprise.bulk_submit(
        bulk_id="batch-2026-07-01-001",
        jobs=[
            # Scrape job
            {"url": "https://example.com/product/123"},
            # SERP search job with demographic targeting
            {
                "query": "best running shoes",
                "engine": "google",
                "country": "us",
                "session_pool_id": "profile-f-25-34-us",
                "safe_search": False,
            },
        ],
        rate_shape_config={
            "jobs_per_hour": 5000,
            "burst_limit": 50000,
            "priority": 3,
        },
    )
    
    print(response.bulk_id)           # "batch-2026-07-01-001"
    print(response.job_count)         # 2
    print(response.status)            # "accepted"
    print(response.estimated_completion_at)

    Response (202 Accepted)

    {
      "bulk_id": "batch-2026-07-01-001",
      "job_count": 2,
      "tenant_id": "org_uuid",
      "status": "accepted",
      "estimated_completion_at": "2026-07-01T12:00:00Z"
    }

    Idempotency

    Re-submitting an identical bulk_id returns "status": "duplicate" without re-enqueuing jobs. Use a unique bulk_id per logical batch.

    GET /api/v1/b2b/jobs/bulk/{bulk_id}

    GET
    /api/v1/b2b/jobs/bulk/{bulk_id}

    Get the current status of a bulk submission — how many jobs have been released from the holding set into the worker queue.

    Parameters

    NameTypeRequiredDescription
    bulk_idstring
    Required
    The bulk submission ID returned by the POST endpoint.
    Python
    import time
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    # Poll until completed
    while True:
        status = client.enterprise.bulk_status("batch-2026-07-01-001")
        print(f"{status.jobs_released}/{status.job_count} released, status={status.status}")
        if status.status in ("completed", "failed"):
            break
        time.sleep(30)

    Response (200 OK)

    {
      "bulk_id": "batch-2026-07-01-001",
      "tenant_id": "org_uuid",
      "job_count": 1000,
      "jobs_released": 500,
      "jobs_pending": 500,
      "status": "draining",
      "submitted_at": "2026-07-01T10:00:00Z",
      "estimated_completion_at": "2026-07-01T12:00:00Z",
      "rate_shape_config": { "jobs_per_hour": 1000, "burst_limit": 50000, "priority": 5 }
    }
    Status valueMeaning
    acceptedSubmission received, draining not yet started.
    drainingJobs are being released to the worker queue.
    completedAll jobs have been released.
    failedSubmission failed — retry with a new bulk_id.
    burst_rejectedBurst limit exceeded at submission time — retry later.

    Demographic Profiles

    Build browser profiles with specific age/gender demographic signals by warming up a browser via YouTube visits. Completed profiles can be used as session_pool_id in SERP bulk jobs to route searches through demographically-targeted sessions — enabling ad content that only appears to specific audience segments.

    Warmup takes ~15–18 minutes

    Profile building is asynchronous. After calling POST /api/v1/b2b/profiles, poll GET /api/v1/b2b/profiles/{profile_id} until status == 'complete' before using the profile in jobs. ISP or residential proxies are strongly recommended for the warmup — datacenter IPs prevent YouTube from setting the demographic cookies.

    POST /api/v1/b2b/profiles

    POST
    /api/v1/b2b/profiles

    Build a demographic browser profile. Enqueues a YouTube warmup session (~15–18 min) that builds a browser profile with a specific age/gender demographic signal.

    Parameters

    NameTypeRequiredDescription
    age_bucketstring
    Required
    Target age range: '18-24', '25-34', '35-44', or '45-54'.
    genderstring
    Required
    Target gender: 'F' (Female) or 'M' (Male).
    geostringOptionalISO 3166-1 alpha-2 country code for geo-targeted consent cookies (e.g. 'US', 'GB'). Must be uppercase. Defaults to 'US'.Default: US
    video_countintegerOptionalYouTube videos to watch during warmup (1–25). 10–15 recommended for stable signal.Default: 12
    profile_idstringOptionalClient-supplied profile identifier (alphanumerics, underscores, hyphens, dots; max 64 chars). Auto-generated if omitted.
    proxy_urlstringOptionalProxy URL for the warmup browser (scheme://[user:pass@]host:port). ISP or residential proxies strongly preferred.
    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    response = client.enterprise.create_profile(
        age_bucket="25-34",
        gender="F",
        geo="US",
        profile_id="profile-f-25-34-us",
        video_count=12,
        proxy_url="http://user:[email protected]:8080",
    )
    
    print(response.profile_id)  # "profile-f-25-34-us"
    print(response.status)      # "building"
    print(response.message)     # "Profile warmup started. Poll GET /api/v1/b2b/profiles/{profile_id}..."

    Response (202 Accepted)

    {
      "profile_id": "profile-f-25-34-us",
      "status": "building",
      "age_bucket": "25-34",
      "gender": "F",
      "geo": "US",
      "created_at": "2026-07-01T10:00:00Z",
      "message": "Profile warmup started. Poll GET /api/v1/b2b/profiles/{profile_id} until status == 'complete'."
    }

    GET /api/v1/b2b/profiles/{profile_id}

    GET
    /api/v1/b2b/profiles/{profile_id}

    Get the current build status of a demographic profile. When status == 'complete', the response includes cookie_jar and inferred demographics.

    Parameters

    NameTypeRequiredDescription
    profile_idstring
    Required
    Profile ID returned by POST /api/v1/b2b/profiles.
    Python
    import time
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    # Poll until complete (~15-18 minutes)
    while True:
        profile = client.enterprise.get_profile("profile-f-25-34-us")
        print(f"Status: {profile.status}")
        if profile.status == "complete":
            print(f"Inferred: {profile.inferred_gender} / {profile.inferred_age_bucket}")
            print(f"Is inferred: {profile.is_inferred}")
            # Use in bulk jobs as session_pool_id
            break
        elif profile.status == "failed":
            print(f"Build failed: {profile.error}")
            break
        time.sleep(60)

    Response (200 OK) — complete

    {
      "profile_id": "profile-f-25-34-us",
      "status": "complete",
      "age_bucket": "25-34",
      "gender": "F",
      "geo": "US",
      "created_at": "2026-07-01T10:00:00Z",
      "updated_at": "2026-07-01T10:17:00Z",
      "inferred_age_bucket": "25-34",
      "inferred_gender": "Female",
      "is_inferred": true,
      "cookie_jar": {
        "VISITOR_INFO1_LIVE": "...",
        "NID": "...",
        "PREF": "...",
        "SOCS": "...",
        "CONSENT": "..."
      },
      "error": null
    }

    POST /api/v1/b2b/profiles/{profile_id}/verify

    POST
    /api/v1/b2b/profiles/{profile_id}/verify

    Verify the demographic inference for a profile by scraping adssettings.google.com. Returns 202 immediately — runs asynchronously. Useful when initial warmup returned is_inferred=false.

    Parameters

    NameTypeRequiredDescription
    profile_idstring
    Required
    Profile ID to verify.
    proxy_urlstringOptionalOptional proxy URL for the verification browser. Falls back to worker SERP_PROXY_URL if omitted.
    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    result = client.enterprise.verify_profile(
        profile_id="profile-f-25-34-us",
        proxy_url="http://user:[email protected]:8080",
    )
    
    print(result.is_inferred)   # True
    print(result.age_bucket)    # "25-34"
    print(result.gender)        # "Female"
    print(result.basis)         # "cookie"
    print(result.message)       # Human-readable summary

    Response (202 Accepted)

    {
      "profile_id": "profile-f-25-34-us",
      "is_inferred": true,
      "age_bucket": "25-34",
      "gender": "Female",
      "basis": "cookie",
      "message": "Demographic inference confirmed via adssettings.google.com"
    }

    Delivery Trigger

    Manually push a set of result records to your configured delivery destination (S3 bucket or webhook). Intended for integration testing and pilot validation — not a production delivery pathway. Result count is capped at 1,000 per call.

    Admin-provisioned delivery configs

    Delivery config creation and credential management are admin-only operations. Contact AlterLab to set up your S3 bucket or webhook delivery config. The tenant_id and config_id UUIDs are provided during onboarding.

    POST /api/v1/b2b/tenants/{tenant_id}/delivery-configs/{config_id}/trigger

    POST
    /api/v1/b2b/tenants/{tenant_id}/delivery-configs/{config_id}/trigger

    Trigger a manual delivery run. Serializes result records as JSONL and delivers to the configured S3 bucket or webhook. SOC 2 CC6.7: every trigger attempt is audit-logged.

    Parameters

    NameTypeRequiredDescription
    tenant_idstring (uuid)
    Required
    Organization (tenant) UUID. Must match the caller's org — mismatches return 404 to prevent IDOR.
    config_idstring (uuid)
    Required
    Delivery config UUID (provided at onboarding).
    batch_idstring
    Required
    Batch identifier for this delivery run (alphanumerics, '.', '_', '-'; max 128 chars). Becomes part of the S3 key. Example: 'test-2026-07-01' or 'marcode-pilot-001'.
    resultsarray
    Required
    Result records to deliver (1–1,000). Each element is an arbitrary JSON object. For S3: serialized as JSONL (one object per line).
    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    response = client.enterprise.trigger_delivery(
        tenant_id="your-tenant-uuid",
        config_id="your-config-uuid",
        batch_id="pilot-batch-001",
        results=[
            {"job_id": "job-1", "query": "running shoes", "ads": [...]},
            {"job_id": "job-2", "query": "hiking boots", "ads": [...]},
        ],
    )
    
    print(response.method)         # "s3"
    print(response.destination)    # "s3://your-bucket/prefix/pilot-batch-001.jsonl"
    print(response.result_count)   # 2
    print(response.bytes_written)  # 1024
    print(response.signature)      # HMAC-SHA256 audit signature

    Response (200 OK)

    {
      "delivered_at": "2026-07-01T10:30:00Z",
      "method": "s3",
      "bytes_written": 2048,
      "destination": "s3://your-bucket/results/pilot-batch-001.jsonl",
      "result_count": 2,
      "signature": "hmac-sha256-audit-signature"
    }

    BYOP Proxy Feedback

    If you use Bring Your Own Proxy (BYOP), this endpoint lets your proxy manager poll per-request SERP outcome events to know whether each request succeeded or was blocked by Google. Events are pushed to Redis by the worker after each SERP request and consumed atomically here.

    Events are consumed on read

    Each call pops events from the queue — they are not re-readable. Process all returned events before the next call. Events auto-expire after 24 hours if unread.

    GET /api/v1/b2b/proxy/feedback

    GET
    /api/v1/b2b/proxy/feedback

    Poll and clear per-request SERP outcome events for BYOP proxy management. Returns up to limit events, removing them from the queue atomically.

    Parameters

    NameTypeRequiredDescription
    limitintegerOptionalMaximum events to return (1–1,000).Default: 100
    Python
    import alterlab
    
    client = alterlab.Client(api_key="YOUR_API_KEY")
    
    # Poll for proxy outcome events (consume-on-read)
    response = client.enterprise.proxy_feedback(limit=500)
    
    for event in response.events:
        if event["status"] == "blocked":
            print(f"Job {event['job_id']} blocked: {event['block_reason']}")
            # Rotate proxy for this country
            rotate_proxy(event["country"])
        elif event["status"] == "success":
            print(f"Job {event['job_id']} succeeded via {event['session_pool_id']}")
    
    print(f"Processed {response.count} events")

    Response (200 OK)

    {
      "events": [
        {
          "job_id": "job-uuid-1",
          "query": "running shoes",
          "status": "success",
          "block_reason": null,
          "country": "us",
          "session_pool_id": "profile-f-25-34-us",
          "ts": "2026-07-01T10:15:00Z"
        },
        {
          "job_id": "job-uuid-2",
          "query": "hiking boots",
          "status": "blocked",
          "block_reason": "search_guard",
          "country": "gb",
          "session_pool_id": null,
          "ts": "2026-07-01T10:15:03Z"
        }
      ],
      "count": 2
    }

    Event fields

    FieldTypeDescription
    job_idstringSERP job identifier for correlation.
    querystringSearch query (truncated to 100 chars).
    statusstring"success" | "blocked" | "failed"
    block_reasonstring | null"search_guard" | "rate_limited" | "no_results" | "timeout" | "error" | null
    countrystring | nullISO 3166-1 alpha-2 geo code. null when no explicit geo was requested.
    session_pool_idstring | nullDemographic profile pool ID for this request. null for non-demographic requests.
    tsstringISO 8601 UTC timestamp of the event.
    Last updated: June 2026

    On this page