Skip to Content
APIEndpointsImport crawler logs

Import crawler logs

Bulk-imports your own AI-crawler server log rows into Asky’s crawler tracking, in Asky’s fixed schema — for when you already have raw server logs (for example an export from your own hosting) rather than relying solely on the live Cloudflare Worker capture.

Provide either rows (a JSON array of row objects) or csv_text (the same columns as CSV, header row required) — not both. Each row needs path, user_agent and timestamp; ip, referrer and query are optional. bot_name and visit_type are never accepted as input — they are always computed server-side from user_agent, the same classification the live Worker capture uses, so an imported visit and a Worker-captured one are indistinguishable afterward.

This is not a raw-file upload. Both rows and csv_text are plain text in the JSON body.

Requires the write:crawler_logs scope, and the workspace must be enabled for API writes.

POST /v1/brands/{brand_id}/crawler-logs/import

Authorization

HeaderTypeRequired
AuthorizationBearer asky_sk_…required
Idempotency-Keystringrequired

Path parameters

ParameterTypeRequiredDescription
brand_iduuidrequiredFrom List brands

Body

application/json

FieldTypeRequiredDescription
hoststringrequiredOne of the brand’s active domains. See List website hosts
rowsobject[], 1-20,000one of rows/csv_textRow objects: { path, user_agent, timestamp, ip?, referrer?, query? }
csv_textstringone of rows/csv_textCSV with header row path,user_agent,timestamp,ip,referrer,query

Row schema

FieldTypeRequiredDescription
pathstringrequiredMust start with /
user_agentstringrequiredThe raw User-Agent header. Classified server-side into bot_name/visit_type
timestampstring (ISO 8601)requiredWhen the request happened
ipstringoptionalIPv4 or IPv6. Dropped (not the row) if it doesn’t parse as one
referrerstringoptionalTruncated at 2048 characters
querystringoptionalThe request’s query string. Truncated at 2048 characters

Request

curl --request POST \ --url 'https://api.askylabs.com/v1/brands/242546de-4cee-4a7d-952e-f6aa60c63ef8/crawler-logs/import' \ --header 'Authorization: Bearer <api-key>' \ --header 'Idempotency-Key: 5b0e77d2-13a9-4f6a-8ce1-9a3f5e21c0aa' \ --header 'Content-Type: application/json' \ --data '{ "host": "www.askylabs.com", "rows": [ { "path": "/pricing", "user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)", "timestamp": "2026-08-01T10:15:00Z", "ip": "203.0.113.4", "referrer": null, "query": null } ] }'

Response

{ "data": { "import": { "id": "9c3e1a7b-4d2f-4e6a-8c1b-2f7a9d3e5b60", "host": "www.askylabs.com", "format": "json", "status": "queued", "total_rows": 1, "inserted_rows": 0, "skipped_rows": 0, "error_message": null, "created_at": "2026-08-31T14:20:00.000Z", "updated_at": "2026-08-31T14:20:00.000Z", "completed_at": null } }, "request_id": "req_6a1c8f0b3e9d47a5b6c2" }

Response fields

FieldTypeDescription
data.import.iduuidrequired. Our own internal reference for this import; there is no endpoint to look it up later
data.import.hoststringrequired. The resolved domain the import targeted
data.import.formatstringrequired. json or csv, whichever you sent
data.import.statusstringrequired. Always queued at creation
data.import.total_rowsintegerrequired. Rows found in the payload
data.import.inserted_rowsintegerrequired. Always 0 at creation — populated once processing completes
data.import.skipped_rowsintegerrequired. Always 0 at creation
data.import.error_messagestring | nullrequired. Always null at creation
data.import.created_at / updated_attimestamptzrequired
data.import.completed_attimestamptz | nullrequired. null until processing finishes
request_idstringrequired. Also returned as the x-request-id header. Quote it when contacting support

Errors

StatusCodeWhen
400invalid_requestUnknown field, missing host, neither or both of rows/csv_text, malformed JSON/CSV, zero rows, over 20,000 rows, or the payload exceeds 15MB
401invalid_tokenThe key is missing, malformed, or does not exist
401token_expiredThe key passed its expiry date
401token_revokedThe key was revoked
403insufficient_scopeThe key does not carry write:crawler_logs
403plan_requiredThis workspace is not enabled for API writes
404not_foundThe brand does not exist, this key is not allowed to reach it, or host does not match any of the brand’s active domains
429rate_limitedOver 60 requests per minute

Notes

Processing is asynchronous, and there is no status endpoint. This call parses your payload just enough to fail fast (malformed JSON/CSV, too many rows, too large) and returns status: "queued" immediately; the rest of processing happens in the background. There is no endpoint to check progress or final row counts afterward — a 200 response here means the payload was well-formed and queued, not that every row was inserted. If you need to confirm data landed, check the effect downstream (e.g. via the crawler-analytics endpoints) rather than polling this import.

bot_name and visit_type are never accepted from you. They are always computed server-side from user_agent, using the same classification the live Cloudflare Worker capture applies. This keeps an imported visit and a Worker-captured one indistinguishable in every crawler-analytics tool afterward.

A larger dataset is multiple calls, not one bigger call. The 20,000-row cap per call is a deliberate ceiling, not a temporary limitation — split a large historical export into sequential import calls instead of raising it.

Row-level failures do not fail the whole import. A row missing path/user_agent/timestamp, or with an unparseable timestamp, is skipped and counted in skipped_rows — the rest of the import still proceeds. A malformed ip is dropped from just that row (stored as null) rather than skipping the row entirely.

Idempotency-Key is required. Reuse it when retrying an attempt and the original response is replayed rather than a second import being created. See Idempotency.

Last updated on