verifex 0.2.0

Official Rust SDK for the Verifex sanctions screening API
Documentation
use std::time::Duration;

use reqwest::Client;
use serde::de::DeserializeOwned;

use crate::errors::VerifexError;
use crate::types::*;

const DEFAULT_BASE_URL: &str = "https://api.verifex.dev";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const USER_AGENT: &str = "verifex-rust/0.2.0";

/// Verifex sanctions screening API client.
///
/// # Example
///
/// ```rust,no_run
/// use verifex::{Verifex, ScreenRequest};
///
/// # async fn example() -> Result<(), verifex::VerifexError> {
/// let client = Verifex::new("vfx_your_api_key");
///
/// let result = client.screen(ScreenRequest {
///     name: "Vladimir Putin".into(),
///     type_: Some("person".into()),
///     ..Default::default()
/// }).await?;
///
/// println!("{}: {} matches", result.risk_level, result.total_matches);
/// # Ok(())
/// # }
/// ```
pub struct Verifex {
    api_key: String,
    base_url: String,
    client: Client,
}

impl Verifex {
    /// Creates a new client with the given API key.
    pub fn new(api_key: &str) -> Self {
        Self {
            api_key: api_key.to_string(),
            base_url: DEFAULT_BASE_URL.to_string(),
            client: Client::builder()
                .timeout(DEFAULT_TIMEOUT)
                .user_agent(USER_AGENT)
                .build()
                .expect("failed to build HTTP client"),
        }
    }

    /// Sets a custom API base URL.
    pub fn with_base_url(mut self, url: &str) -> Self {
        self.base_url = url.trim_end_matches('/').to_string();
        self
    }

    /// Sets the request timeout.
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.client = Client::builder()
            .timeout(timeout)
            .user_agent(USER_AGENT)
            .build()
            .expect("failed to build HTTP client");
        self
    }

    // ── Screening ──────────────────────────────────────────────────────

    /// Screen a single entity against all sanctions lists.
    pub async fn screen(&self, req: ScreenRequest) -> Result<ScreenResult, VerifexError> {
        self.post("/v1/screen", &req).await
    }

    /// Screen multiple entities in one request (Pro plan required, max 100).
    pub async fn batch_screen(
        &self,
        entities: Vec<ScreenRequest>,
    ) -> Result<BatchScreenResult, VerifexError> {
        #[derive(serde::Serialize)]
        struct Body {
            entities: Vec<ScreenRequest>,
        }
        self.post("/v1/screen/batch", &Body { entities }).await
    }

    // ── Usage ──────────────────────────────────────────────────────────

    /// Get current month's usage statistics.
    pub async fn usage(&self) -> Result<UsageStats, VerifexError> {
        self.get("/v1/usage").await
    }

    // ── API Keys ───────────────────────────────────────────────────────

    /// List all API keys for the authenticated user.
    pub async fn list_keys(&self) -> Result<Vec<ApiKeyInfo>, VerifexError> {
        self.get("/v1/keys").await
    }

    /// Create a new API key.
    pub async fn create_key(&self, name: &str) -> Result<ApiKeyCreated, VerifexError> {
        #[derive(serde::Serialize)]
        struct Body<'a> {
            name: &'a str,
        }
        self.post("/v1/keys", &Body { name }).await
    }

    /// Permanently revoke an API key.
    pub async fn revoke_key(&self, key_id: &str) -> Result<(), VerifexError> {
        let url = format!("{}/v1/keys/{}", self.base_url, key_id);
        let resp = self
            .client
            .delete(&url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .send()
            .await?;

        if resp.status().is_success() {
            Ok(())
        } else {
            Err(parse_error(resp).await)
        }
    }

    // ── Health ──────────────────────────────────────────────────────────

    /// Check API health status (no authentication required).
    pub async fn health(&self) -> Result<HealthResponse, VerifexError> {
        let url = format!("{}/v1/health", self.base_url);
        let resp = self.client.get(&url).send().await?;

        if resp.status().is_success() {
            Ok(resp.json().await?)
        } else {
            Err(parse_error(resp).await)
        }
    }

    // ── Internal HTTP helpers ──────────────────────────────────────────

    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, VerifexError> {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .get(&url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .send()
            .await?;

        if resp.status().is_success() {
            Ok(resp.json().await?)
        } else {
            Err(parse_error(resp).await)
        }
    }

    async fn post<T: DeserializeOwned, B: serde::Serialize>(
        &self,
        path: &str,
        body: &B,
    ) -> Result<T, VerifexError> {
        let url = format!("{}{}", self.base_url, path);
        let resp = self
            .client
            .post(&url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .json(body)
            .send()
            .await?;

        if resp.status().is_success() {
            Ok(resp.json().await?)
        } else {
            Err(parse_error(resp).await)
        }
    }
}

async fn parse_error(resp: reqwest::Response) -> VerifexError {
    let status = resp.status().as_u16();
    let body: serde_json::Value = resp.json().await.unwrap_or_default();

    let message = body["error"]
        .as_str()
        .unwrap_or("Unknown error")
        .to_string();
    let code = body["code"].as_str().unwrap_or("UNKNOWN").to_string();
    let request_id = body["request_id"].as_str().unwrap_or("").to_string();

    match status {
        401 => VerifexError::Auth {
            message,
            request_id,
        },
        402 => VerifexError::QuotaExceeded {
            message,
            request_id,
        },
        429 => VerifexError::RateLimit {
            message,
            retry_after: 60,
            request_id,
        },
        _ => VerifexError::Api {
            message,
            code,
            status,
            request_id,
        },
    }
}