Skip to main content
The notra crate is a community-maintained Rust SDK for the Notra API. It provides typed request builders, response structs, and async support via tokio.
This SDK is community-maintained. Source and docs are available at crates.io/crates/notra and GitHub.

Installation

Add the crate to your Cargo.toml:
[dependencies]
notra = "0.1.1"
tokio = { version = "1", features = ["full"] }

Quick Start

use notra::Notra;

#[tokio::main]
async fn main() -> Result<(), notra::NotraError> {
    let notra = Notra::builder()
        .bearer_auth(std::env::var("NOTRA_API_KEY").unwrap())
        .build()?;

    let result = notra.content()
        .list_posts()
        .send()
        .await?;

    for post in &result.posts {
        println!("{} ({})", post.title, post.id);
    }

    Ok(())
}

Authentication

Pass your API key via the builder:
use notra::Notra;

let notra = Notra::builder()
    .bearer_auth(std::env::var("NOTRA_BEARER_AUTH").unwrap())
    .build()?;
You can also configure a custom base URL and timeout:
use std::time::Duration;
use notra::Notra;

let notra = Notra::builder()
    .bearer_auth("your-api-key")
    .server_url("https://api.usenotra.com") // default
    .timeout(Duration::from_secs(60))       // default: 30s
    .build()?;

Posts

List posts

use notra::Notra;
use notra::models::{PostStatus, Sort};

let result = notra.content()
    .list_posts()
    .status(PostStatus::Published)
    .sort(Sort::Desc)
    .limit(10)
    .page(1)
    .send()
    .await?;

for post in &result.posts {
    println!("{}", post.title);
}

println!(
    "Page {} of {}",
    result.pagination.current_page,
    result.pagination.total_pages.unwrap_or(1)
);

Get a single post

let result = notra.content()
    .get_post("post_abc")
    .send()
    .await?;

if let Some(post) = result.post {
    println!("{}: {}", post.title, post.status);
}

Update a post

use notra::models::PostStatus;

let updated = notra.content()
    .update_post("post_abc")
    .title("Ship notes for week 11")
    .markdown("# Ship notes\n\nWe shipped a faster editor.")
    .status(PostStatus::Published)
    .send()
    .await?;

println!("{:?}", updated.post);

Delete a post

notra.content()
    .delete_post("post_abc")
    .send()
    .await?;

Post Generation

Queue a generation job

use notra::models::*;

let job = notra.content()
    .create_post_generation(ContentType::Changelog)
    .lookback_window(LookbackWindow::Last7Days)
    .brand_identity_id("brand-id")
    .github_integration_ids(vec!["integration-1".into()])
    .repositories(vec![
        Repository { owner: "my-org".into(), repo: "my-repo".into() },
    ])
    .data_points(DataPoints {
        commits: true,
        pull_requests: true,
        releases: true,
        linear_data: false,
    })
    .send()
    .await?;

println!("Job started: {}", job.job.id);

Poll job status

let status = notra.content()
    .get_post_generation("job-id")
    .send()
    .await?;

println!("Status: {:?}", status.job.status);

for event in &status.events {
    println!("  {} at {}", event.event_type, event.created_at);
}

Brand Identities

List brand identities

let result = notra.content()
    .list_brand_identities()
    .send()
    .await?;

for bi in &result.brand_identities {
    println!("{} (default: {})", bi.name, bi.is_default);
}

Generate a brand identity from a website

let job = notra.content()
    .create_brand_identity("https://example.com")
    .name("My Brand")
    .send()
    .await?;

// Poll for completion
let status = notra.content()
    .get_brand_identity_generation(&job.job.id)
    .send()
    .await?;

println!("Step: {:?}, Status: {:?}", status.job.current_step, status.job.status);

Update a brand identity

use notra::models::{ToneProfile, Language};

let updated = notra.content()
    .update_brand_identity("identity-id")
    .name("Updated Brand")
    .tone_profile(ToneProfile::Professional)
    .language(Language::English)
    .audience("Developers")
    .is_default(true)
    .send()
    .await?;

Delete a brand identity

notra.content()
    .delete_brand_identity("identity-id")
    .send()
    .await?;

Integrations

List integrations

let integrations = notra.content()
    .list_integrations()
    .send()
    .await?;

for gh in &integrations.github {
    println!("{}/{} ({})", gh.owner, gh.repo, gh.default_branch);
}

Connect a GitHub repository

let gh = notra.content()
    .create_github_integration("my-org", "my-repo")
    .branch("main")
    .auth_token("ghp_xxx") // optional, for private repos
    .send()
    .await?;

println!("Connected: {}", gh.github.display_name);

Disconnect an integration

notra.content()
    .delete_integration("integration-id")
    .send()
    .await?;

Error Handling

The SDK uses a NotraError enum with three variants:
use notra::{Notra, NotraError};

match notra.content().get_post("bad-id").send().await {
    Ok(response) => {
        if let Some(post) = response.post {
            println!("Found: {}", post.title);
        }
    }
    Err(NotraError::Api { status, message }) => {
        eprintln!("API error ({}): {}", status, message);
    }
    Err(NotraError::Http(e)) => eprintln!("Network error: {e}"),
    Err(NotraError::Builder(msg)) => eprintln!("Config error: {msg}"),
}

Types

All types are exported under notra::models:
use notra::models::{
    Post, PostStatus, ContentType, Sort,
    Pagination, Organization,
    LookbackWindow, DataPoints, Repository,
    BrandIdentity, ToneProfile, Language,
    GithubIntegration, LinearIntegration,
    PostGenerationJob, JobStatus,
};