Short answer
The Linkbreakers Rust SDK enables programmatic link management, visitor identification, QR code generation, and analytics access from your Rust backend applications. Install it with cargo add linkbreakers, authenticate with a secret API key, and start creating trackable links with async/await patterns for high-performance applications.
Installation
Add the SDK to your project using Cargo:
cargo add linkbreakers
Or manually add to your Cargo.toml:
[dependencies]
linkbreakers = "1.0"
tokio = { version = "1", features = ["full"] }
The SDK uses tokio for async runtime and reqwest for HTTP requests.
Understanding API keys for Rust
The Rust SDK is designed for backend/server-side use only and requires a secret API key with full workspace access.
Secret keys
Backend-only. These JWT tokens grant full API access and must never be exposed in public repositories or client-side code.
What they allow:
- Create, update, and delete links
- Generate and customize QR codes
- Identify and track visitors
- Access analytics and metrics
- Manage workspace resources
- Full API access
Where to use:
- Web servers (Axum, Actix, Rocket)
- Microservices
- CLI tools
- Background workers
- API gateways
- High-performance data processing
Security note: Rust applications typically run on servers, making them perfect for secret key usage. Never log keys, commit them to git, or expose them in error messages.
Creating a secret API key
Create your API key from the dashboard:
- Log in to app.linkbreakers.com
- Navigate to Dashboard → API Tokens
- Click Create API Token
- Select Secret Key (full API access)
- Give it a descriptive name (e.g., "Production Backend" or "Rust Service")
- Copy the generated JWT token immediately
Important: You won't see the key again after creation. Store it in environment variables or a secret management system.
Quick start
Initialize the client and create your first shortened link:
use linkbreakers::{
apis::configuration::Configuration,
apis::links_api,
models::CreateLinkRequest,
};
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure API client with your secret key
let api_key = env::var("LINKBREAKERS_SECRET_KEY")?;
let mut config = Configuration::new();
config.bearer_access_token = Some(api_key);
config.base_path = "https://api.linkbreakers.com".to_string();
// Create a shortened link
let create_req = CreateLinkRequest {
destination: "https://example.com/product".to_string(),
name: Some("Product Launch - Spring 2025".to_string()),
tags: Some(vec!["marketing".to_string(), "product-launch".to_string()]),
..Default::default()
};
let response = links_api::links_service_create(&config, create_req).await?;
if let Some(link) = response.link {
println!("Short URL: {}", link.shortlink.unwrap_or_default());
println!("Link ID: {}", link.id.unwrap_or_default());
println!("QR Code: {}", link.qrcode_signed_url.unwrap_or_default());
}
Ok(())
}
Environment variables (best practice)
Never hardcode API keys. Use environment variables:
use linkbreakers::apis::configuration::Configuration;
use std::env;
fn get_config() -> Result<Configuration, env::VarError> {
let api_key = env::var("LINKBREAKERS_SECRET_KEY")?;
let mut config = Configuration::new();
config.bearer_access_token = Some(api_key);
config.base_path = "https://api.linkbreakers.com".to_string();
Ok(config)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = get_config()?;
// Use the config for API operations
// ...
Ok(())
}
Set your environment variable:
# .env file (use dotenv crate to load)
export LINKBREAKERS_SECRET_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3Jrc3BhY2VfaWQiOiIxMjM0IiwidHlwZSI6InNlY3JldCJ9...
Loading .env files with dotenv:
[dependencies]
dotenv = "0.15"
use dotenv::dotenv;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env file
dotenv().ok();
let api_key = std::env::var("LINKBREAKERS_SECRET_KEY")?;
// ...
Ok(())
}
Creating and managing links
Basic link creation
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::CreateLinkRequest,
};
async fn create_basic_link(config: &Configuration) -> Result<(), Box<dyn std::error::Error>> {
let create_req = CreateLinkRequest {
destination: "https://example.com/summer-sale".to_string(),
name: Some("Summer Sale 2025".to_string()),
tags: Some(vec!["campaign".to_string(), "summer".to_string(), "email".to_string()]),
..Default::default()
};
let response = links_api::links_service_create(config, create_req).await?;
if let Some(link) = response.link {
println!("Created: {}", link.shortlink.unwrap_or_default());
}
Ok(())
}
Advanced link options
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::CreateLinkRequest,
};
use std::collections::HashMap;
async fn create_advanced_links(config: &Configuration) -> Result<(), Box<dyn std::error::Error>> {
// Create link with custom shortlink
let custom_req = CreateLinkRequest {
destination: "https://example.com/docs".to_string(),
shortlink: Some("docs".to_string()), // Creates lbr.ai/docs
name: Some("Documentation Portal".to_string()),
fallback_destination: Some("https://example.com/404".to_string()),
tags: Some(vec!["internal".to_string(), "docs".to_string()]),
..Default::default()
};
let custom_link = links_api::links_service_create(config, custom_req).await?;
// Create link with metadata for tracking
let mut metadata = HashMap::new();
metadata.insert("product_id".to_string(), "123".to_string());
metadata.insert("campaign_id".to_string(), "spring-2025".to_string());
metadata.insert("utm_source".to_string(), "email".to_string());
metadata.insert("utm_medium".to_string(), "newsletter".to_string());
metadata.insert("utm_campaign".to_string(), "product-launch".to_string());
let tracked_req = CreateLinkRequest {
destination: "https://example.com/product/123".to_string(),
name: Some("Product 123 - Email Campaign".to_string()),
metadata: Some(metadata),
tags: Some(vec!["product".to_string(), "email".to_string()]),
..Default::default()
};
let tracked_link = links_api::links_service_create(config, tracked_req).await?;
// Create link with QR code (wait for generation)
let qr_req = CreateLinkRequest {
destination: "https://example.com/event-registration".to_string(),
name: Some("Conference 2025 Registration".to_string()),
wait_for_qrcode: Some(true), // Blocks until QR is generated
tags: Some(vec!["event".to_string(), "conference".to_string(), "qr-code".to_string()]),
..Default::default()
};
let qr_link = links_api::links_service_create(config, qr_req).await?;
if let Some(link) = qr_link.link {
println!("Download QR code: {}", link.qrcode_signed_url.unwrap_or_default());
}
Ok(())
}
Listing and filtering links
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::{Linkbreakersv1LinksSortField, Linkbreakersv1SortDirection},
};
async fn list_links(config: &Configuration) -> Result<(), Box<dyn std::error::Error>> {
let response = links_api::links_service_list(
config,
Some(50), // page_size
None, // page_token
Some(vec!["marketing".to_string(), "email".to_string()]), // tags
Some("campaign".to_string()), // search
Some(Linkbreakersv1LinksSortField::UpdatedAt), // sort_by
Some(Linkbreakersv1SortDirection::Desc), // sort_direction
None, // directory_id
None, // include_all_directories
None, // include
)
.await?;
if let Some(links) = response.links {
for link in links {
println!("{}: {}", link.name.unwrap_or_default(), link.shortlink.unwrap_or_default());
println!(" Tags: {:?}", link.tags.unwrap_or_default());
println!(" Created: {}", link.created_at.unwrap_or_default());
}
}
Ok(())
}
Updating and deleting links
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::UpdateLinkRequest,
};
async fn update_and_delete_link(
config: &Configuration,
link_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Get a specific link
let link_resp = links_api::links_service_get(
config,
link_id,
Some(vec!["tags".to_string(), "qrcodeSignedUrl".to_string()]),
)
.await?;
// Update link
let update_req = UpdateLinkRequest {
name: Some("Updated Campaign Name".to_string()),
tags: Some(vec!["marketing".to_string(), "q2-2025".to_string(), "updated".to_string()]),
..Default::default()
};
links_api::links_service_update(config, link_id, update_req).await?;
// Delete link
links_api::links_service_delete(config, link_id).await?;
Ok(())
}
Bulk link creation
Create hundreds of links efficiently in a single API call:
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::{CreateBulkLinksRequest, CreateLinkRequest},
};
use std::collections::HashMap;
struct Product {
id: String,
name: String,
url: String,
}
async fn create_bulk_links(config: &Configuration) -> Result<(), Box<dyn std::error::Error>> {
// Prepare bulk links (e.g., from a product catalog)
let products = vec![
Product {
id: "001".to_string(),
name: "Widget A".to_string(),
url: "https://example.com/products/001".to_string(),
},
Product {
id: "002".to_string(),
name: "Widget B".to_string(),
url: "https://example.com/products/002".to_string(),
},
Product {
id: "003".to_string(),
name: "Widget C".to_string(),
url: "https://example.com/products/003".to_string(),
},
];
let links_to_create: Vec<CreateLinkRequest> = products
.iter()
.map(|p| {
let mut metadata = HashMap::new();
metadata.insert("product_id".to_string(), p.id.clone());
metadata.insert("sku".to_string(), format!("SKU-{}", p.id));
CreateLinkRequest {
destination: p.url.clone(),
name: Some(format!("Product: {}", p.name)),
tags: Some(vec!["product".to_string(), "catalog".to_string()]),
metadata: Some(metadata),
..Default::default()
}
})
.collect();
// Create all links in one request
let bulk_req = CreateBulkLinksRequest {
links: links_to_create,
};
let response = links_api::links_service_create_bulk(config, bulk_req).await?;
if let Some(links) = response.links {
println!("Created {} links", links.len());
for link in links {
println!(" {}: {}", link.name.unwrap_or_default(), link.shortlink.unwrap_or_default());
}
}
Ok(())
}
When to use bulk creation:
- Importing product catalogs
- Generating links from database records
- Campaign setup with many URLs
- Automated link generation pipelines
Visitor identification and tracking
Track and identify visitors using their LBID (Linkbreakers ID) from click/scan events:
use linkbreakers::{
apis::{configuration::Configuration, visitors_api},
models::{IdentifyRequest, VisitorInput},
};
use std::collections::HashMap;
async fn identify_visitor(
config: &Configuration,
lbid: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Prepare visitor data
let mut visitor_data: HashMap<String, serde_json::Value> = HashMap::new();
// System fields (prefixed with "$")
visitor_data.insert("$email".to_string(), "john.doe@example.com".into());
visitor_data.insert("$phone".to_string(), "+14155551234".into());
visitor_data.insert("$firstName".to_string(), "John".into());
visitor_data.insert("$lastName".to_string(), "Doe".into());
// Custom attributes (no "$" prefix)
visitor_data.insert("company".to_string(), "Acme Corporation".into());
visitor_data.insert("jobTitle".to_string(), "Product Manager".into());
visitor_data.insert("plan".to_string(), "enterprise".into());
visitor_data.insert("signupDate".to_string(), "2024-01-15".into());
visitor_data.insert("industry".to_string(), "SaaS".into());
visitor_data.insert("leadScore".to_string(), 85.into());
let visitor_input = VisitorInput {
data: Some(visitor_data),
};
let identify_req = IdentifyRequest {
lbid: lbid.to_string(),
visitor: Box::new(visitor_input),
set_once: Some(false), // true = only update empty fields
};
let response = visitors_api::visitors_service_identify(config, identify_req).await?;
if let Some(visitor) = response.visitor {
println!("New profile created: {}", response.created.unwrap_or(false));
println!("Visitor ID: {}", visitor.id.unwrap_or_default());
println!("Email: {}", visitor.email.unwrap_or_default());
}
Ok(())
}
Understanding LBID:
When visitors click your Linkbreakers links with conversion tracking enabled, a ?lbid=... parameter is added to the destination URL. This LBID connects the visitor to the specific link click, enabling attribution and analytics.
Extract LBID from incoming requests and pass it to the identification API to build visitor profiles. Learn more in our What is LBID? guide.
Working with visitor data
use linkbreakers::{
apis::{configuration::Configuration, visitors_api},
models::{VisitorInput, VisitorsServiceUpdateBody},
};
use std::collections::HashMap;
async fn work_with_visitors(
config: &Configuration,
visitor_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Get visitor details
let visitor = visitors_api::visitors_service_get(
config,
visitor_id,
Some(vec!["devices".to_string(), "events".to_string(), "links".to_string()]),
)
.await?;
if let Some(v) = visitor {
println!("Visitor: {}", v.email.unwrap_or_default());
println!("Devices: {}", v.devices.map(|d| d.len()).unwrap_or(0));
println!("Events: {}", v.events.map(|e| e.len()).unwrap_or(0));
}
// List visitors with filtering
let visitors_list = visitors_api::visitors_service_list(
config,
Some(100), // page_size
None, // page_token
Some("user@example.com".to_string()), // email
Some("link-uuid".to_string()), // link_id
Some("Acme Corp".to_string()), // search
None, // include
None, // response_format
)
.await?;
// Update visitor attributes
let mut update_data: HashMap<String, serde_json::Value> = HashMap::new();
update_data.insert("$email".to_string(), "updated@example.com".into());
update_data.insert("plan".to_string(), "enterprise".into());
update_data.insert("lastActivity".to_string(), "2025-03-31".into());
let visitor_update = VisitorInput {
data: Some(update_data),
};
let update_body = VisitorsServiceUpdateBody {
visitor: Some(Box::new(visitor_update)),
};
visitors_api::visitors_service_update(config, visitor_id, update_body).await?;
Ok(())
}
Accessing analytics and metrics
Retrieve event data and workspace metrics:
use linkbreakers::apis::{configuration::Configuration, events_api, metrics_api};
use chrono::{Duration, Utc};
async fn get_analytics(
config: &Configuration,
link_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Get events for a specific link (last 30 days)
let end_date = Utc::now();
let start_date = end_date - Duration::days(30);
let events_resp = events_api::events_service_list(
config,
Some(start_date), // start_date
Some(end_date), // end_date
Some(1000), // page_size
None, // page_token
Some(link_id.to_string()), // link_id
Some(vec!["visitor".to_string(), "device".to_string(), "link".to_string()]), // include
None, // response_format
)
.await?;
if let Some(events) = &events_resp.events {
println!("Total events: {}", events.len());
for (i, event) in events.iter().enumerate() {
if i >= 10 {
break;
}
println!(" {} - {}", event.action.clone().unwrap_or_default(), event.created_at.clone().unwrap_or_default());
}
}
// Get workspace metrics
let metrics = metrics_api::metrics_service_get_workspace_metrics(config).await?;
println!("\nWorkspace Stats:");
println!(" Total links: {}", metrics.total_links.unwrap_or(0));
println!(" Total clicks: {}", metrics.total_clicks.unwrap_or(0));
println!(" Total visitors: {}", metrics.total_visitors.unwrap_or(0));
Ok(())
}
Framework integration
Axum
use axum::{
extract::State,
http::StatusCode,
routing::{get, post},
Json, Router,
};
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::CreateLinkRequest,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
config: Arc<Configuration>,
}
#[derive(Deserialize)]
struct CreateLinkBody {
destination: String,
name: Option<String>,
tags: Option<Vec<String>>,
}
#[derive(Serialize)]
struct LinkResponse {
success: bool,
shortlink: String,
qr_code: Option<String>,
}
async fn create_link(
State(state): State<AppState>,
Json(body): Json<CreateLinkBody>,
) -> Result<Json<LinkResponse>, StatusCode> {
let create_req = CreateLinkRequest {
destination: body.destination,
name: body.name,
tags: body.tags,
..Default::default()
};
match links_api::links_service_create(&state.config, create_req).await {
Ok(response) => {
if let Some(link) = response.link {
Ok(Json(LinkResponse {
success: true,
shortlink: link.shortlink.unwrap_or_default(),
qr_code: link.qrcode_signed_url,
}))
} else {
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("LINKBREAKERS_SECRET_KEY")?;
let mut config = Configuration::new();
config.bearer_access_token = Some(api_key);
config.base_path = "https://api.linkbreakers.com".to_string();
let state = AppState {
config: Arc::new(config),
};
let app = Router::new()
.route("/api/create-link", post(create_link))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}
Actix Web
use actix_web::{web, App, HttpResponse, HttpServer, Result};
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::CreateLinkRequest,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
struct AppState {
config: Arc<Configuration>,
}
#[derive(Deserialize)]
struct CreateLinkBody {
destination: String,
name: Option<String>,
tags: Option<Vec<String>>,
}
#[derive(Serialize)]
struct LinkResponse {
success: bool,
shortlink: String,
qr_code: Option<String>,
}
async fn create_link(
data: web::Data<AppState>,
body: web::Json<CreateLinkBody>,
) -> Result<HttpResponse> {
let create_req = CreateLinkRequest {
destination: body.destination.clone(),
name: body.name.clone(),
tags: body.tags.clone(),
..Default::default()
};
match links_api::links_service_create(&data.config, create_req).await {
Ok(response) => {
if let Some(link) = response.link {
Ok(HttpResponse::Ok().json(LinkResponse {
success: true,
shortlink: link.shortlink.unwrap_or_default(),
qr_code: link.qrcode_signed_url,
}))
} else {
Ok(HttpResponse::InternalServerError().finish())
}
}
Err(_) => Ok(HttpResponse::InternalServerError().finish()),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let api_key = std::env::var("LINKBREAKERS_SECRET_KEY").unwrap();
let mut config = Configuration::new();
config.bearer_access_token = Some(api_key);
config.base_path = "https://api.linkbreakers.com".to_string();
let app_state = web::Data::new(AppState {
config: Arc::new(config),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.route("/api/create-link", web::post().to(create_link))
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
Error handling
Implement proper error handling for production:
use linkbreakers::{
apis::{configuration::Configuration, links_api},
models::CreateLinkRequest,
};
async fn create_link_with_error_handling(
config: &Configuration,
) -> Result<(), Box<dyn std::error::Error>> {
let create_req = CreateLinkRequest {
destination: "https://example.com/page".to_string(),
name: Some("My Link".to_string()),
..Default::default()
};
match links_api::links_service_create(config, create_req).await {
Ok(response) => {
if let Some(link) = response.link {
println!("Success: {}", link.shortlink.unwrap_or_default());
}
}
Err(e) => {
eprintln!("API Error: {:?}", e);
return Err(Box::new(e));
}
}
Ok(())
}
Frequently asked questions
Can I use the Rust SDK with popular web frameworks?
Yes! The SDK is framework-agnostic and works seamlessly with Axum, Actix Web, Rocket, Warp, and any Rust web framework.
How do I handle rate limits?
The SDK will return errors if you exceed rate limits. Implement exponential backoff retry logic for production applications.
Does the SDK support async/await?
Yes, the SDK is fully async and built on tokio. All API calls are async functions that return futures.
How do I get the LBID for visitor identification?
The LBID appears as a ?lbid= query parameter when visitors click your links with conversion tracking enabled. Extract it from request parameters and pass it to visitors_service_identify().
Can I create bulk links efficiently?
Yes, use links_service_create_bulk() to create up to 1000 links in a single API call, which is much more efficient than individual requests.
What Rust versions are supported?
The SDK supports Rust 1.70+ with full async/await support and modern Rust features.
How do I test without affecting production data?
Create test mode API keys from your dashboard. They operate on isolated test data separate from production analytics. You can identify test vs production keys by decoding the JWT and checking the environment claim.
Sources
Last reviewed
This article was last reviewed on March 31, 2025.
About the Author
Laurent Schaffner
Founder & Engineer at Linkbreakers
Passionate about building tools that help businesses track and optimize their digital marketing efforts. Laurent founded Linkbreakers to make QR code analytics accessible and actionable for companies of all sizes.
Related Articles
How to use the Linkbreakers API
Complete guide to integrating with the Linkbreakers API - create QR codes, manage links, customize designs, track analytics, and automate workflows programmatically.
Analytics API
Access comprehensive QR code and visitor analytics through the Linkbreakers API. Learn how to retrieve campaign performance data, visitor insights, and engagement metrics programmatically for business intelligence integration.
How to integrate Linkbreakers with existing tech stack
Integrate Linkbreakers with your CRM, marketing automation, analytics platforms, and business systems through APIs, webhooks, and direct integrations. Learn best practices for seamless tech stack integration.
On this page
Need more help?
Can't find what you're looking for? Get in touch with our support team.
Contact Support