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
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.
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.
| Parameter | Type | Default | Description |
|---|---|---|---|
| jobs_per_hour | integer | 1000 | Maximum jobs released per hour from the holding set (1–500,000). |
| burst_limit | integer | 50000 | Maximum jobs allowed in the holding set at once. Submissions exceeding this return HTTP 429. |
| priority | integer | 5 | Worker queue priority (1=urgent, 3=high, 5=normal, 7=low, 10=background). |
Estimated completion
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
/api/v1/b2b/jobs/bulkSubmit a bulk job batch. Jobs enter the tenant holding set and are released at the contracted rate. Returns immediately after validation — non-blocking.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| bulk_id | string | Required | Client-supplied idempotency key (max 255 chars). Re-submitting an identical bulk_id returns the existing submission status without re-enqueuing jobs. |
| jobs | array | Required | Array of job specs (1–100,000). Each item must have exactly one of: url (scrape job) or query (SERP search job). |
| jobs[].url | string | Optional | Target URL to scrape (max 2048 chars, http/https only). Mutually exclusive with query. |
| jobs[].query | string | Optional | SERP search query string (max 500 chars). When set, the job routes to the SERP pipeline. Mutually exclusive with url. |
| jobs[].engine | string | Optional | Search engine for query jobs: 'duckduckgo' (default), 'google', or 'bing'. Only valid with query. |
| jobs[].country | string | Optional | ISO 3166-1 alpha-2 country code for geo-targeted results (e.g. 'us', 'gb'). Only valid with query. |
| jobs[].session_pool_id | string | Optional | Demographic profile ID (from POST /api/v1/b2b/profiles) to route this search through a demographically-targeted session. Only valid with query. |
| jobs[].safe_search | boolean | Optional | Override Google safe search. false = disable (recommended for ad-fraud detection). null = platform default. |
| jobs[].metadata | object | Optional | Arbitrary key-value metadata attached to the job for client-side tracking and correlation. |
| rate_shape_config | object | Optional | Throughput control: { jobs_per_hour, burst_limit, priority }. Falls back to tenant defaults if omitted. |
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
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}
/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
| Name | Type | Required | Description |
|---|---|---|---|
| bulk_id | string | Required | The bulk submission ID returned by the POST endpoint. |
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 value | Meaning |
|---|---|
| accepted | Submission received, draining not yet started. |
| draining | Jobs are being released to the worker queue. |
| completed | All jobs have been released. |
| failed | Submission failed — retry with a new bulk_id. |
| burst_rejected | Burst 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
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
/api/v1/b2b/profilesBuild 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
| Name | Type | Required | Description |
|---|---|---|---|
| age_bucket | string | Required | Target age range: '18-24', '25-34', '35-44', or '45-54'. |
| gender | string | Required | Target gender: 'F' (Female) or 'M' (Male). |
| geo | string | Optional | ISO 3166-1 alpha-2 country code for geo-targeted consent cookies (e.g. 'US', 'GB'). Must be uppercase. Defaults to 'US'.Default: US |
| video_count | integer | Optional | YouTube videos to watch during warmup (1–25). 10–15 recommended for stable signal.Default: 12 |
| profile_id | string | Optional | Client-supplied profile identifier (alphanumerics, underscores, hyphens, dots; max 64 chars). Auto-generated if omitted. |
| proxy_url | string | Optional | Proxy URL for the warmup browser (scheme://[user:pass@]host:port). ISP or residential proxies strongly preferred. |
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}
/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
| Name | Type | Required | Description |
|---|---|---|---|
| profile_id | string | Required | Profile ID returned by POST /api/v1/b2b/profiles. |
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
/api/v1/b2b/profiles/{profile_id}/verifyVerify 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
| Name | Type | Required | Description |
|---|---|---|---|
| profile_id | string | Required | Profile ID to verify. |
| proxy_url | string | Optional | Optional proxy URL for the verification browser. Falls back to worker SERP_PROXY_URL if omitted. |
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 summaryResponse (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
tenant_id and config_id UUIDs are provided during onboarding.POST /api/v1/b2b/tenants/{tenant_id}/delivery-configs/{config_id}/trigger
/api/v1/b2b/tenants/{tenant_id}/delivery-configs/{config_id}/triggerTrigger 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
| Name | Type | Required | Description |
|---|---|---|---|
| tenant_id | string (uuid) | Required | Organization (tenant) UUID. Must match the caller's org — mismatches return 404 to prevent IDOR. |
| config_id | string (uuid) | Required | Delivery config UUID (provided at onboarding). |
| batch_id | string | 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'. |
| results | array | Required | Result records to deliver (1–1,000). Each element is an arbitrary JSON object. For S3: serialized as JSONL (one object per line). |
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 signatureResponse (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
GET /api/v1/b2b/proxy/feedback
/api/v1/b2b/proxy/feedbackPoll and clear per-request SERP outcome events for BYOP proxy management. Returns up to limit events, removing them from the queue atomically.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | integer | Optional | Maximum events to return (1–1,000).Default: 100 |
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
| Field | Type | Description |
|---|---|---|
| job_id | string | SERP job identifier for correlation. |
| query | string | Search query (truncated to 100 chars). |
| status | string | "success" | "blocked" | "failed" |
| block_reason | string | null | "search_guard" | "rate_limited" | "no_results" | "timeout" | "error" | null |
| country | string | null | ISO 3166-1 alpha-2 geo code. null when no explicit geo was requested. |
| session_pool_id | string | null | Demographic profile pool ID for this request. null for non-demographic requests. |
| ts | string | ISO 8601 UTC timestamp of the event. |