Developer how-to · Rust

Add e-signatures in Rust

Authenticate with OAuth2, upload a PDF with reqwest multipart, send it for signing, and verify webhook signatures with HMAC-SHA256. No SDK — just reqwest + tokio.

1

Add dependencies

toml
# Cargo.toml
[dependencies]
reqwest  = { version = "0.12", features = ["json", "multipart"] }
tokio    = { version = "1",    features = ["full"] }
serde    = { version = "1",    features = ["derive"] }
serde_json = "1"
2

Get an access token

rust
use reqwest::Client;
use serde::Deserialize;

#[derive(Deserialize)]
struct TokenResponse { access_token: String }

async fn get_token(client: &Client) -> String {
    let resp: TokenResponse = client
        .post("https://api.getsigned.app/oauth/token")
        .form(&[
            ("grant_type",   "client_credentials"),
            ("client_id",    &std::env::var("CLIENT_ID").unwrap()),
            ("client_secret",&std::env::var("CLIENT_SECRET").unwrap()),
        ])
        .send().await.unwrap()
        .json().await.unwrap();
    resp.access_token
}
3

Create envelope + upload PDF

rust
use reqwest::multipart;
use std::fs;

async fn create_envelope(client: &Client, token: &str) -> String {
    let pdf_bytes = fs::read("contract.pdf").unwrap();

    let form = multipart::Form::new()
        .part("document", multipart::Part::bytes(pdf_bytes)
            .file_name("contract.pdf")
            .mime_str("application/pdf").unwrap())
        .text("signers",
            r#"[{"name":"Sam","email":"sam@client.io"}]"#)
        .text("fields",
            r#"[{"type":"signature","page":1,"x":300,"y":120,"w":200,"h":60}]"#);

    let resp: serde_json::Value = client
        .post("https://api.getsigned.app/v1/envelopes")
        .bearer_auth(token)
        .multipart(form)
        .send().await.unwrap()
        .json().await.unwrap();

    resp["id"].as_str().unwrap().to_string()
}
4

Send + handle webhook

rust
// Send the envelope
async fn send_envelope(client: &Client, token: &str, env_id: &str) {
    client
        .post(format!(
            "https://api.getsigned.app/v1/envelopes/{env_id}/send"
        ))
        .bearer_auth(token)
        .send().await.unwrap();
}

// Axum webhook handler — verify HMAC then process
use axum::{extract::State, http::HeaderMap, body::Bytes};
use hmac::{Hmac, Mac};
use sha2::Sha256;

async fn webhook(headers: HeaderMap, body: Bytes) -> axum::http::StatusCode {
    let sig = headers.get("x-getsigned-signature")
        .and_then(|v| v.to_str().ok()).unwrap_or("");
    let secret = std::env::var("WEBHOOK_SECRET").unwrap();
    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
    mac.update(&body);
    // verify_slice is constant-time; decode the hex header back to raw bytes
    let provided = match hex::decode(sig) {
        Ok(bytes) => bytes,
        Err(_) => return axum::http::StatusCode::UNAUTHORIZED,
    };
    if mac.verify_slice(&provided).is_err() {
        return axum::http::StatusCode::UNAUTHORIZED;
    }
    let event: serde_json::Value = serde_json::from_slice(&body).unwrap();
    println!("Event: {}", event["event_type"]);
    axum::http::StatusCode::OK
}

For the webhook HMAC verification, add to Cargo.toml

Cargo.toml — HMAC deps
hmac  = { version = "0.12", features = ["std"] }
sha2  = "0.10"
hex   = "0.4"
axum  = "0.7"   # or actix-web = "4"

Frequently asked questions

Which Rust HTTP client should I use for the GetSigned API?

reqwest is the standard choice. Use version 0.12+ with the "json" and "multipart" features enabled in Cargo.toml. reqwest builds on tokio's async runtime, handles bearer auth via .bearer_auth(), and serializes multipart form data cleanly — the three things you need for the GetSigned integration. If you're on a synchronous runtime, reqwest also has a blocking module.

How do I upload a PDF as multipart form data in Rust?

Use reqwest::multipart::Form with a Part built from the file's bytes: multipart::Part::bytes(file_bytes).file_name("filename.pdf").mime_str("application/pdf"). The signers and fields are added as text parts with JSON string values. The Content-Type: multipart/form-data header is set automatically by reqwest when you call .multipart(form) on the request builder.

How do I verify webhook HMAC signatures in Rust?

Use the hmac crate with sha2: add hmac = { version = "0.12", features = ["std"] } and sha2 = "0.10" to Cargo.toml, plus hex for encoding. Create an Hmac<Sha256> from your webhook secret, call .update() with the raw request body bytes, finalize and hex-encode, then compare to the x-getsigned-signature header. Read the raw body bytes BEFORE any JSON deserialization — parsing the body first can alter whitespace and invalidate the signature.

Should I cache the OAuth2 access token in Rust?

Yes. Access tokens expire (typically 1 hour). Cache the token in an Arc<Mutex<Option<(String, Instant)>>> and re-fetch when it's within 60 seconds of expiry. This avoids a token round-trip on every API call. For multi-threaded Axum servers, share the cache via Axum's State extractor so all handler tasks use the same cached token.

Can I use this with Axum or Actix-web for the webhook endpoint?

Yes. The webhook handler example above uses Axum, which pairs naturally with reqwest (both are tokio-based). For Actix-web, replace axum::extract::* with actix-web::web::* equivalents — the HMAC verification logic is identical. The key constraint for either framework: extract the raw body bytes as Bytes before JSON parsing, so the HMAC is computed over the original payload.

Related: Go how-to · Webhook guide · Language-agnostic guide

Build in Rust. Sign with GetSigned.

Get free API keys →