URLQuery API Client: Building Malware Analysis Pipelines Without the Framework Bloat
Hook
Most security APIs hide complexity behind helper methods. This one makes you implement your own polling loops—and that's exactly why security engineers choose it.
Context
When a suspicious URL lands in your SOC queue, you need a verdict: phishing page, exploit kit, or false alarm. Automated sandboxes solve this by executing URLs in instrumented browsers, capturing DOM mutations, network traffic, and behavioral signatures. URLQuery.net provides this as a hosted service, but their REST API presents an integration challenge. Unlike file analysis where you upload-then-poll, URL sandboxing has variable execution times—a simple redirect completes in seconds while a sophisticated JavaScript payload might take five minutes to reveal its behavior.
Most API clients handle this variability by baking in retry logic and timeout assumptions. The urlquery-api-go library takes the opposite approach: it exposes raw HTTP primitives and leaves workflow orchestration to you. Each API endpoint maps to a single function that constructs requests, handles authentication, and deserializes JSON. No automatic polling. No opinionated retry strategies. No connection pooling beyond Go's defaults. This minimalism matters for security tooling where execution patterns vary wildly between batch threat hunting jobs and real-time incident response.
Technical Insight
The library's architecture centers on a package-level API key and discrete functions per endpoint. Authentication uses SetDefaultKey() to store credentials globally, with per-request override support through optional parameters. Response handling deserializes JSON into predefined structs that mirror URLQuery.net's data model—Report, QueueStatus, SearchResult—without additional validation layers.
Here's the canonical usage pattern for submitting a URL and retrieving results:
package main
import (
"fmt"
"time"
"github.com/urlquery/urlquery-api-go"
)
func analyzeURL(target string) error {
urlquery.SetDefaultKey("your-api-key")
// Submit URL with access control and alert preferences
resp, err := urlquery.Submit(target, urlquery.Public, false)
if err != nil {
return fmt.Errorf("submission failed: %w", err)
}
queueID := resp.QueueID
// Manual polling loop - library provides no helper
for {
status, err := urlquery.GetQueueStatus(queueID)
if err != nil {
return fmt.Errorf("status check failed: %w", err)
}
if status.Status == "done" {
report, err := urlquery.GetReport(status.ReportID)
if err != nil {
return fmt.Errorf("report retrieval failed: %w", err)
}
fmt.Printf("Verdict: %s\nMalicious: %t\n",
report.Verdict, report.Malicious)
return nil
}
time.Sleep(10 * time.Second) // You choose the interval
}
}
Notice the explicit polling loop. Unlike libraries that abstract this into a synchronous "submit and wait" method, you control the sleep interval, timeout logic, and cancellation semantics. This matters when building threat intelligence pipelines where you're submitting hundreds of URLs concurrently—you might want aggressive 5-second polls for high-priority incident response URLs while background threat hunting jobs can poll every 30 seconds to conserve API quota.
The Report struct exposes URLQuery.net's full analytical surface. Beyond basic verdict flags, you get structured access to browser DOM events, HTTP transaction logs, screenshot URLs, and behavioral alerts. This enables sophisticated filtering logic:
func containsExploitKit(report *urlquery.Report) bool {
// Check for common exploit kit indicators
for _, alert := range report.Alerts {
if alert.Type == "exploit_kit" ||
alert.Category == "code_injection" {
return true
}
}
// Analyze HTTP transactions for suspicious patterns
for _, tx := range report.Transactions {
if tx.StatusCode == 302 &&
containsSuspiciousRedirect(tx.Location) {
return true
}
}
return false
}
The Search functionality supports historical analysis. You can query past reports by domain, IP, ASN, or custom tags. This transforms the library from a one-off verdict tool into a threat hunting platform:
// Find all reports for a suspicious domain in the last 30 days
results, err := urlquery.Search("domain:evil.example.com", 100)
if err != nil {
log.Fatal(err)
}
for _, result := range results.Results {
fmt.Printf("Report %s: Verdict=%s, Date=%s\n",
result.ReportID, result.Verdict, result.Timestamp)
}
The library's minimal dependency footprint—only Go's standard library—reflects a deliberate security posture. In security tooling, transitive dependencies represent attack surface. By avoiding third-party HTTP clients, loggers, or serialization frameworks, the library reduces supply chain risk. The trade-off is manual error handling and no structured logging integration, but for security teams this is often preferable to inheriting vulnerabilities from abandoned dependencies.
Gotcha
The library's minimalism becomes a liability in production environments without supporting infrastructure. There's no built-in rate limiting, so exceeding API quota returns generic HTTP 429 errors without actionable feedback about remaining credits or reset windows. You'll need to implement your own quota tracking by parsing response headers or maintaining external state.
Error handling is primitive. HTTP failures return wrapped errors with status codes, but the library doesn't parse API-specific error payloads that URLQuery.net might include in response bodies. A submission rejection due to invalid URL format versus insufficient account credits produces identical error objects. Robust implementations require custom response body inspection:
if err != nil {
if httpErr, ok := err.(*urlquery.HTTPError); ok {
// Library doesn't expose this - you'll parse raw errors
if httpErr.StatusCode == 402 {
// Assume payment required, but can't distinguish
// "quota exceeded" from "subscription expired"
}
}
}
The absence of context.Context support is conspicuous for modern Go. There's no way to propagate cancellation signals, enforce deadlines, or integrate with distributed tracing systems. If your service uses context for timeout management, you'll wrap library calls in goroutines with select statements—adding boilerplate that context support would eliminate. No caching layer exists for report retrieval, so repeatedly fetching the same ReportID hits the API unnecessarily, wasting quota and adding latency to batch analysis jobs.
Verdict
Use if: You're building custom security automation where URLQuery.net's specific browser instrumentation adds coverage beyond VirusTotal or URLScan.io, you already have retry/timeout infrastructure and want direct API control without framework assumptions, or you're integrating malware verdicts into SOAR platforms where workflow orchestration happens at a higher layer. The minimal abstraction is valuable when sandboxing latency varies wildly and you need precise control over polling intervals and resource allocation across concurrent analysis jobs.
Skip if: You need batteries-included sandbox orchestration with managed polling and automatic retries, you're evaluating multiple sandbox APIs and want vendor abstraction (this library locks you to URLQuery.net), your service requires context.Context for cancellation and tracing, or existing tools like VirusTotal already provide sufficient malware coverage. Also skip if you're unfamiliar with URLQuery.net's API behavior—the library exposes complexity rather than hiding it, so understanding upstream rate limits and execution patterns is prerequisite knowledge.