rerout 0.5.0

Official Rust SDK for the Rerout branded-link API.
Documentation

rerout

Official Rust SDK for the Rerout API.

Branded link infrastructure on Cloudflare. Create short links, render QR codes, read analytics, and verify webhook signatures.

Install

cargo add rerout

Or add it to Cargo.toml:

[dependencies]
rerout = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Every method is async and requires a Tokio runtime. The crate ships with rustls TLS and pulls in no OpenSSL dependency.

Usage

use rerout::{CreateLinkInput, Rerout};

#[tokio::main]
async fn main() -> Result<(), rerout::ReroutError> {
    let rerout = Rerout::new(std::env::var("REROUT_API_KEY").unwrap())?;

    let link = rerout
        .links()
        .create(
            &CreateLinkInput::new("https://example.com/q4-sale")
                .with_domain_hostname("go.brand.com")
                .with_code("q4"),
        )
        .await?;

    println!("{}", link.short_url); // https://go.brand.com/q4

    let stats = rerout.project().stats(7).await?;
    println!(
        "Last 7 days: {} clicks, {} QR scans",
        stats.total_clicks, stats.qr_scans,
    );

    Ok(())
}

API

Construction

Build a client through the builder. Only the API key is required.

use std::time::Duration;
use rerout::Rerout;

# fn main() -> Result<(), rerout::ReroutError> {
// Defaults: production base URL, 30s timeout.
let rerout = Rerout::new("rrk_live_xxx")?;

// Or customise via the builder.
let rerout = Rerout::builder("rrk_live_xxx")
    .base_url("https://api.staging.rerout.co")? // trailing slashes trimmed
    .timeout(Duration::from_secs(15))
    .user_agent("my-app/1.0")
    .build()?;
# let _ = rerout;
# Ok(())
# }

Links

# use rerout::{CreateLinkInput, ListLinksParams, Rerout, UpdateLinkInput};
# async fn run(rerout: Rerout) -> Result<(), rerout::ReroutError> {
let created = rerout.links().create(&CreateLinkInput::new("https://example.com")).await?;
let page = rerout.links().list(ListLinksParams { cursor: None, limit: Some(20) }).await?;
let one = rerout.links().get("q4").await?;
let patched = rerout.links()
    .update("q4", &UpdateLinkInput::new().set_is_active(false))
    .await?;
let removed = rerout.links().delete("q4").await?;
let stats = rerout.links().stats("q4", 30).await?;
# let _ = (created, page, one, patched, removed, stats);
# Ok(())
# }

UpdateLinkInput distinguishes "leave the field alone" from "clear it on the server". Use the set_* builders to assign a value and the clear_* builders to send an explicit JSON null:

use rerout::UpdateLinkInput;

let input = UpdateLinkInput::new()
    .set_target_url("https://example.com/new")
    .clear_expires_at(); // sends `expires_at: null`

An empty UpdateLinkInput is rejected client-side — links().update returns a bad_request configuration error without hitting the API.

Each returned Link carries a read-only tags field — a Vec<Tag> where every Tag has an id, name, and color. It is empty on create and populated by get, list, and update. Manage the tags themselves via the tags() namespace (below).

Project

# use rerout::Rerout;
# async fn run(rerout: Rerout) -> Result<(), rerout::ReroutError> {
let stats = rerout.project().stats(30).await?;
let project = rerout.project().me().await?;
# let _ = (stats, project);
# Ok(())
# }

QR codes

qr().url(...) is a pure builder — it does not call the API. qr().svg(...) fetches the rendered SVG with the bearer token attached.

# use rerout::{QrEcc, QrOptions, QrRefresh, Rerout};
# async fn run(rerout: Rerout) -> Result<(), rerout::ReroutError> {
let url = rerout.qr().url(
    "q4",
    &QrOptions::new()
        .with_size(12)
        .with_ecc(QrEcc::H)
        .with_domain("go.brand.com")
        .with_refresh(QrRefresh::On),
)?;

let svg = rerout.qr().svg("q4", &QrOptions::new()).await?;
# let _ = (url, svg);
# Ok(())
# }

Webhook management

Manage the endpoints that receive deliveries via webhooks(). This is the API-key-authenticated surface under /v1/projects/me/webhooks — distinct from the inbound signature verification below.

# use rerout::{CreateWebhookInput, Rerout};
# async fn run(rerout: Rerout) -> Result<(), rerout::ReroutError> {
let created = rerout
    .webhooks()
    .create(&CreateWebhookInput::new(
        "Order events",
        "https://hooks.brand.com/rerout",
        ["link.created", "link.clicked"],
    ))
    .await?;

// The signing secret is shown once — persist it now to verify deliveries.
println!("secret: {}", created.signing_secret);
let endpoint_id = created.endpoint.id;

let list = rerout.webhooks().list().await?;
println!("{} endpoint(s)", list.endpoints.len());

let removed = rerout.webhooks().delete(&endpoint_id).await?;
assert!(removed.deleted);
# Ok(())
# }

Tags

Manage the project's tags via tags(). This is the API-key-authenticated surface under /v1/projects/me/tags — the project is resolved from the key.

# use rerout::{CreateTagInput, Rerout, UpdateTagInput};
# async fn run(rerout: Rerout) -> Result<(), rerout::ReroutError> {
// List tags with their live link counts.
let list = rerout.tags().list().await?;
for summary in &list.tags {
    println!("{} ({} links)", summary.tag.name, summary.link_count);
}

// Create a tag — color is optional and defaults to `teal` server-side.
let tag = rerout
    .tags()
    .create(&CreateTagInput::new("Spring 2026").with_color("teal"))
    .await?;

// Update — omitted fields are left unchanged.
let renamed = rerout
    .tags()
    .update(&tag.id, &UpdateTagInput::new().with_name("Renamed"))
    .await?;

// Delete — also drops the tag's assignments from all links.
let removed = rerout.tags().delete(&renamed.id).await?;
assert!(removed.deleted);
# Ok(())
# }

Webhook signature verification

Verify incoming webhook deliveries with the standalone helper in rerout::webhooks. It performs a constant-time HMAC-SHA256 comparison and rejects stale timestamps.

use rerout::webhooks::{verify_rerout_signature, DEFAULT_TOLERANCE_SECONDS};

# let raw_body = "{}";
# let signature_header = "t=1,v1=00";
# let secret = "whsec_xxx";
let ok = verify_rerout_signature(
    raw_body,
    signature_header,           // value of the `X-Rerout-Signature` header
    secret,                     // endpoint signing secret (`whsec_…`)
    DEFAULT_TOLERANCE_SECONDS,  // 300s window; pass 0 to disable the clock check
    None,                       // injectable clock — `None` reads the system time
);
if !ok {
    // reject the delivery
}

Error handling

Every fallible method returns Result<T, ReroutError>. Match on the variant for branching, or read code() for the stable string identifier:

# use rerout::{CreateLinkInput, Rerout, ReroutError};
# async fn run(rerout: Rerout) {
match rerout.links().create(&CreateLinkInput::new("http://insecure.example")).await {
    Ok(link) => println!("{}", link.short_url),
    Err(err) => {
        eprintln!("{} (HTTP {}): {}", err.code(), err.status(), err.message());
        if err.is_rate_limited() { /* back off and retry */ }
        if err.is_server_error() { /* transient — retry later */ }
        if let ReroutError::Api { .. } = err { /* server replied non-2xx */ }
    }
}
# }

Synthetic codes are used when the server did not return a JSON body: network_error, timeout, unexpected_response, decode_error, unauthorized, forbidden, not_found, rate_limited, server_error, client_error.

Local development

cargo fmt --all
cargo clippy --all-targets -- -D warnings
cargo test
cargo doc --no-deps

License

MIT — see LICENSE.

Links