Rust SDK

An async, strongly typed client for the CrawlKit API. Built on reqwest and serde with full tokio support. Build high-performance data pipelines and ship code that Google actually indexes.

Coming soon: cargo add crawlkit-client. The examples below show the planned API surface using reqwest + serde. You can use these patterns today.

Installation

# Future official crate
cargo add crawlkit-client

# For now, add these dependencies to Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
thiserror = "2"

Type Definitions

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
pub struct CreateDatasetRequest {
    pub name: String,
    pub description: Option<String>,
    pub columns: Vec<Column>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Column {
    pub name: String,
    pub data_type: ColumnType,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColumnType {
    Text,
    Integer,
    Float,
    Boolean,
    Jsonb,
    Timestamp,
}

#[derive(Debug, Deserialize)]
pub struct Dataset {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub columns: Vec<Column>,
    pub row_count: u64,
    pub created_at: String,
}

#[derive(Debug, Serialize)]
pub struct SpiderConfig {
    pub name: String,
    pub scope: SpiderScope,
    pub definition: SpiderDefinition,
    pub dataset_id: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct SpiderScope {
    pub start_urls: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct SpiderDefinition {
    pub handlers: Vec<SpiderHandler>,
    pub navigation: Option<SpiderNavigation>,
}

#[derive(Debug, Serialize)]
pub struct SpiderHandler {
    pub url_pattern: String,
    pub fields: Vec<SpiderField>,
}

#[derive(Debug, Serialize)]
pub struct SpiderField {
    pub name: String,
    pub selector: String,
    #[serde(rename = "type")]
    pub field_type: String,
    pub attribute: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct SpiderNavigation {
    pub follow_links: Option<String>,
    pub max_depth: Option<u32>,
}

#[derive(Debug, Deserialize)]
pub struct Spider {
    pub id: String,
    pub name: String,
    pub status: String,
    pub scope: serde_json::Value,
    pub dataset_id: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Deserialize)]
pub struct EnrichmentJob {
    pub job_id: String,
    pub status: String,
    pub source: String,
    pub dataset_id: String,
    pub total_rows: u64,
    pub processed_rows: u64,
    pub created_at: String,
}

#[derive(Debug, Deserialize)]
pub struct PipelineRun {
    pub run_id: String,
    pub status: String,
}

#[derive(Debug, Deserialize)]
pub struct SeoCheckResult {
    pub url: String,
    pub score: u32,
    pub issues: Vec<SeoIssue>,
    pub metadata: serde_json::Value,
    pub remediations: Option<Vec<serde_json::Value>>,
}

#[derive(Debug, Deserialize)]
pub struct SeoIssue {
    pub category: String,
    pub severity: String,
    pub message: String,
    pub affected_urls: Vec<String>,
    pub evidence: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct AuditResult {
    pub audit_id: String,
    pub score: u32,
    pub crawl_stats: serde_json::Value,
    pub issues: Vec<SeoIssue>,
}

Error Types

use thiserror::Error;

#[derive(Debug, Error)]
pub enum CrawlKitError {
    #[error("API error {status}: {message}")]
    Api {
        message: String,
        status: u16,
        retryable: bool,
        retry_after: Option<u64>,
    },

    #[error("HTTP error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Deserialization error: {0}")]
    Deserialize(#[from] serde_json::Error),
}

impl CrawlKitError {
    pub fn is_retryable(&self) -> bool {
        match self {
            CrawlKitError::Api { retryable, .. } => *retryable,
            CrawlKitError::Http(e) => e.is_timeout() || e.is_connect(),
            CrawlKitError::Deserialize(_) => false,
        }
    }

    pub fn retry_after(&self) -> Option<u64> {
        match self {
            CrawlKitError::Api { retry_after, .. } => *retry_after,
            _ => None,
        }
    }
}

Client Implementation

pub struct CrawlKitClient {
    client: reqwest::Client,
    base_url: String,
}

impl CrawlKitClient {
    pub fn new(api_key: &str) -> Self {
        Self::with_base_url(api_key, "https://api.crawlkit.app/api/v1")
    }

    pub fn with_base_url(api_key: &str, base_url: &str) -> Self {
        use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};

        let mut headers = HeaderMap::new();
        headers.insert(
            AUTHORIZATION,
            HeaderValue::from_str(&format!("Bearer {api_key}")).unwrap(),
        );
        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

        let client = reqwest::Client::builder()
            .default_headers(headers)
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .expect("failed to build HTTP client");

        Self {
            client,
            base_url: base_url.to_string(),
        }
    }

    async fn request<T: serde::de::DeserializeOwned>(
        &self,
        method: reqwest::Method,
        path: &str,
        body: Option<&impl Serialize>,
    ) -> Result<T, CrawlKitError> {
        let url = format!("{}{}", self.base_url, path);

        let mut req = self.client.request(method, &url);
        if let Some(body) = body {
            req = req.json(body);
        }

        let response = req.send().await?;
        let status = response.status().as_u16();

        if status >= 400 {
            let error_body: serde_json::Value = response.json().await?;
            return Err(CrawlKitError::Api {
                message: error_body["message"]
                    .as_str()
                    .unwrap_or("Unknown error")
                    .to_string(),
                status,
                retryable: error_body["retryable"].as_bool().unwrap_or(false),
                retry_after: error_body["retry_after"].as_u64(),
            });
        }

        Ok(response.json().await?)
    }

    // ── Data Platform ──

    pub async fn create_dataset(
        &self,
        req: &CreateDatasetRequest,
    ) -> Result<Dataset, CrawlKitError> {
        self.request(reqwest::Method::POST, "/datasets", Some(req)).await
    }

    pub async fn add_rows(
        &self,
        dataset_id: &str,
        rows: &[serde_json::Value],
    ) -> Result<serde_json::Value, CrawlKitError> {
        let body = serde_json::json!({
            "format": "json",
            "data": rows,
        });
        self.request(
            reqwest::Method::POST,
            &format!("/datasets/{dataset_id}/rows"),
            Some(&body),
        )
        .await
    }

    pub async fn create_spider(
        &self,
        config: &SpiderConfig,
    ) -> Result<Spider, CrawlKitError> {
        self.request(reqwest::Method::POST, "/spiders", Some(config)).await
    }

    pub async fn enrich(
        &self,
        dataset_id: &str,
        source_name: &str,
        input_mapping: &serde_json::Value,
        output_mapping: &serde_json::Value,
    ) -> Result<EnrichmentJob, CrawlKitError> {
        let body = serde_json::json!({
            "dataset_id": dataset_id,
            "source_name": source_name,
            "input_mapping": input_mapping,
            "output_mapping": output_mapping,
        });
        self.request(reqwest::Method::POST, "/enrichment/enrich", Some(&body)).await
    }

    pub async fn run_pipeline(
        &self,
        pipeline_id: &str,
    ) -> Result<PipelineRun, CrawlKitError> {
        self.request::<PipelineRun>(
            reqwest::Method::POST,
            &format!("/pipelines/{pipeline_id}/run"),
            None::<&serde_json::Value>.as_ref(),
        )
        .await
    }

    // ── SEO Analysis ──

    pub async fn check_url(
        &self,
        url: &str,
        js_rendering: bool,
    ) -> Result<SeoCheckResult, CrawlKitError> {
        let body = serde_json::json!({
            "url": url,
            "js_rendering": js_rendering,
        });
        self.request(reqwest::Method::POST, "/check-url", Some(&body)).await
    }

    pub async fn audit_site(
        &self,
        site_url: &str,
        max_pages: u32,
    ) -> Result<AuditResult, CrawlKitError> {
        let body = serde_json::json!({
            "site_url": site_url,
            "max_pages": max_pages,
        });
        self.request(reqwest::Method::POST, "/audit/run", Some(&body)).await
    }

    pub async fn dns_audit(
        &self,
        domain: &str,
    ) -> Result<serde_json::Value, CrawlKitError> {
        let body = serde_json::json!({"domain": domain});
        self.request(reqwest::Method::POST, "/dns/audit", Some(&body)).await
    }
}

Examples

Create a Dataset and Add Rows

#[tokio::main]
async fn main() -> Result<(), CrawlKitError> {
    let client = CrawlKitClient::new("ck_live_YOUR_API_KEY");

    // Create a dataset for web scraping results
    let dataset = client
        .create_dataset(&CreateDatasetRequest {
            name: "ecommerce_products".into(),
            description: Some("Product catalog from competitor sites".into()),
            columns: vec![
                Column { name: "product_name".into(), data_type: ColumnType::Text },
                Column { name: "price".into(), data_type: ColumnType::Float },
                Column { name: "category".into(), data_type: ColumnType::Text },
                Column { name: "in_stock".into(), data_type: ColumnType::Boolean },
                Column { name: "specs".into(), data_type: ColumnType::Jsonb },
            ],
        })
        .await?;

    println!("Dataset created: {}", dataset.id);

    // Add rows
    let rows = vec![
        serde_json::json!({
            "product_name": "Mechanical Keyboard",
            "price": 129.99,
            "category": "Electronics",
            "in_stock": true,
            "specs": {"switches": "Cherry MX Brown", "layout": "TKL"},
        }),
        serde_json::json!({
            "product_name": "4K Monitor",
            "price": 449.99,
            "category": "Electronics",
            "in_stock": true,
            "specs": {"resolution": "3840x2160", "panel": "IPS", "refresh": "60Hz"},
        }),
    ];

    let result = client.add_rows(&dataset.id, &rows).await?;
    println!("Inserted: {:?}", result);

    Ok(())
}

Deploy a Spider and Run Enrichment

#[tokio::main]
async fn main() -> Result<(), CrawlKitError> {
    let client = CrawlKitClient::new("ck_live_YOUR_API_KEY");

    // Create a spider for job listings
    let spider = client
        .create_spider(&SpiderConfig {
            name: "job_scraper".into(),
            scope: SpiderScope {
                start_urls: vec!["https://www.ycombinator.com/jobs".into()],
            },
            definition: SpiderDefinition {
                handlers: vec![SpiderHandler {
                    url_pattern: "/jobs/*".into(),
                    fields: vec![
                        SpiderField {
                            name: "title".into(),
                            selector: "h1.job-title".into(),
                            field_type: "text".into(),
                            attribute: None,
                        },
                        SpiderField {
                            name: "company".into(),
                            selector: ".company-name".into(),
                            field_type: "text".into(),
                            attribute: None,
                        },
                        SpiderField {
                            name: "apply_url".into(),
                            selector: "a.apply-btn".into(),
                            field_type: "attribute".into(),
                            attribute: Some("href".into()),
                        },
                    ],
                }],
                navigation: Some(SpiderNavigation {
                    follow_links: Some(".pagination a".into()),
                    max_depth: Some(5),
                }),
            },
            dataset_id: Some("ds_a1b2c3d4e5f6".into()),
        })
        .await?;

    println!("Spider {}: {}", spider.id, spider.status);

    // Enrich dataset with company data
    let job = client
        .enrich(
            "ds_a1b2c3d4e5f6",
            "company_data",
            &serde_json::json!({"url": "domain"}),
            &serde_json::json!({"company_name": "company", "employee_count": "employees"}),
        )
        .await?;

    println!("Enrichment job {}: {}", job.job_id, job.status);

    Ok(())
}

Check a URL for SEO Issues

#[tokio::main]
async fn main() -> Result<(), CrawlKitError> {
    let client = CrawlKitClient::new("ck_live_YOUR_API_KEY");

    // Ship code that Google actually indexes
    let result = client.check_url("https://crawlkit.app", false).await?;

    println!("SEO Score: {}/100", result.score);
    println!("Issues: {}", result.issues.len());

    for issue in &result.issues {
        if issue.severity == "critical" || issue.severity == "high" {
            println!("  [{}] {}", issue.severity.to_uppercase(), issue.message);
        }
    }

    Ok(())
}

Error Handling with Retry

use std::time::Duration;
use tokio::time::sleep;

async fn with_retry<T, F, Fut>(f: F, max_retries: u32) -> Result<T, CrawlKitError>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = Result<T, CrawlKitError>>,
{
    let mut attempt = 0;
    loop {
        match f().await {
            Ok(result) => return Ok(result),
            Err(e) if e.is_retryable() && attempt < max_retries => {
                let delay = e.retry_after().unwrap_or(2u64.pow(attempt));
                eprintln!("Retry {}/{max_retries} in {delay}s: {e}");
                sleep(Duration::from_secs(delay)).await;
                attempt += 1;
            }
            Err(e) => return Err(e),
        }
    }
}

// Usage
#[tokio::main]
async fn main() -> Result<(), CrawlKitError> {
    let client = CrawlKitClient::new("ck_live_YOUR_API_KEY");

    let result = with_retry(
        || client.check_url("https://crawlkit.app", false),
        3,
    )
    .await?;

    println!("Score: {}", result.score);
    Ok(())
}
PreviousPython SDK Nextcurl Examples