honk-me (Rust)
Official Rust client for Honk, the inbox that turns events from your apps, jobs, scripts and CI into calm, grouped push notifications on your phone.
- Async-first on Tokio,
reqwestwith rustls (no OpenSSL). A synchronous client with theblockingfeature. - Retries with backoff,
Retry-After, a total deadline and an idempotency key on every send, so a retry never creates a duplicate. - Rust 1.85+.
#![forbid(unsafe_code)].
Keep the key on the server. An ingestion key (
honk_…) lets anyone post into your project. Use this crate in services, jobs, CLIs and CI only; never ship it inside a desktop, mobile or WebAssembly front end. Read it from the environment.
Install
or, for synchronous code, cargo add honk-me --features blocking.
Create a project and an ingestion key at honk-me.app. Its Integrations page generates ready-to-paste code.
Quick start
use Honk;
async
Or explicitly: Honk::new("https://honk-me.app", key)?. A missing or malformed URL or key is
Error::InvalidConfiguration. Honk is cheap to clone: create one per application and share
it, so connections are kept alive.
The Honk scale
Every severity has a horn name. Use either; the SDK always sends the canonical value.
| Horn | Severity | Helper | Constant |
|---|---|---|---|
| light honk | light (info) |
honk.light(title, message) |
Severity::LIGHT (= Severity::Info) |
| beep-beep | beep (success) |
honk.beep(…) |
Severity::BEEP |
| loud honk | loud (warning) |
honk.loud(…) |
Severity::LOUD |
| long honk | long (error) |
honk.long(…) |
Severity::LONG |
| blast | blast (critical) |
honk.blast(…) |
Severity::BLAST |
"loud".parse::<Severity>(), "LOUD" and "warning" are the same event (also for
idempotency). long and blast push at least as high priority. info(), success(),
warning(), error() and critical() remain as synonyms.
Every helper returns a PendingSend: chain more fields, then .await it.
use Priority;
honk.loud
.group_key
.priority
.metadata
.await?;
Recipe: notify me when a customer asks for something
Give each request its own group (requests/<id>) and its own idempotency key. Two different
customers never fold into one notification, and a retried webhook or job never buzzes twice.
use ;
let msg = new
.title
.severity
.priority
.category
.group_key
.url; // https only, "Open link"
honk.send.await?;
Problems and recoveries
A problem opens an incident for its group key; a recovery closes it.
honk.problem.await?;
honk.recovery.await?;
Sending a message
send(&message, idempotency_key) returns Accepted { id, duplicate, received_at } once Honk
has durably stored the event (202); that does not mean a push was delivered.
| Field | Type | Notes |
|---|---|---|
message |
String |
required, 1–8192 bytes of UTF-8 |
title |
Option<String> |
≤ 160 characters, one line |
severity |
Option<Severity> |
default info |
priority |
Option<Priority> |
Low, Normal, High, Urgent (urgent needs a key with allow_urgent) |
category |
Option<Category> |
Infrastructure, Security, Backups, Deployments, Payments, Customers, Sales, Automation, Personal, Other |
source, environment, channel |
Option<String> |
≤ 64 / 32 / 64; fall back to the client's defaults |
group_key |
Option<String> |
≤ 128; same key, same group |
event_type |
Option<EventType> |
Event, Problem, Recovery (recovery needs a group key) |
occurred_at |
Option<SystemTime> |
sent as RFC 3339 |
url, image_url |
Option<String> |
https only, no credentials, ≤ 2048 bytes; no fragment in image_url |
actions |
Vec<Action> |
up to 3 buttons, .action(title, url); see Actions |
metadata |
BTreeMap<String, MetadataValue> |
≤ 16 keys [A-Za-z0-9_.-]{1,64}; strings (≤ 512), numbers, booleans |
ttl_seconds |
Option<u32> |
60–86400 |
source_sequence |
Option<u64> |
monotonic per source; needs a group key |
idempotency_key is your stable key ("order-42-failed", 1–128 printable ASCII characters)
or None for a new UUIDv7. A replay within 24 hours returns the original id with
duplicate: true; the same key with a different payload is Error::Conflict.
Actions
Up to three buttons on a message, in display order (the first is the primary). Honk never opens
them; the phone does when you tap one: Mail for mailto:, the Phone app for tel:, Messages for
sms:, Safari for https://.
honk.light
.group_key
.action
.action
.await?;
Message::action works the same way, and message.actions is a plain Vec<Action>. A title is
1–40 characters on one line, shown as sent. A URL is at most 2048 bytes without spaces:
https:// (no credentials), mailto: with one address and an optional ?subject=…&body=…,
tel: with a number, or sms: with a number and an optional ?body=…. Anything else (http:,
javascript:, app schemes) is Error::Validation on actions[i].url.
Configuration
use Duration;
let honk = builder
.url
.key
.timeout // per attempt
.retries // after the first attempt; 0 disables
.deadline // total, waits included
.source // defaults for source, environment, channel
.environment
.validate // false: leave value checks to the server
.user_agent
.build?;
Retries happen only on network errors, timeouts, 429 and 5xx: exponential backoff with full
jitter (random(0, min(8 s, 0.5 s·2ⁿ))), never sooner than Retry-After, all under the
deadline. A wait that would cross the deadline (a daily quota) fails fast with retry_after.
Redirects are reported, never followed.
Errors
| Variant | When | What to do |
|---|---|---|
Error::Validation |
rejected locally (local == true) or 400/413/415/422; fields lists every problem |
fix the message |
Error::Auth |
401 invalid_key, 403 priority_not_allowed, project_suspended, workspace_suspended |
fix the key or the priority |
Error::Quota |
429 quota_exceeded (daily, resets at UTC midnight), rate_limited or overloaded, after retries |
retry after retry_after |
Error::Conflict |
409 idempotency_conflict: same key, different payload |
use a new key, or send the original payload |
Error::Network |
unreachable on every attempt | retry later, same key |
Error::Timeout |
attempts timed out; the message may or may not be stored | retry later, same key |
Error::Server |
5xx on every attempt |
retry later, same key |
Error::Http |
404 (wrong URL), a redirect, an answer without an id |
fix the URL |
Error::InvalidConfiguration |
a missing or malformed URL or key | fix the configuration |
Each variant except the last carries a Failure with status, code, message, fields,
local, request_id, idempotency_key, attempts and retry_after.
use Error;
match honk.send.await
Blocking client
// Cargo.toml: honk-me = { version = "0.2", features = ["blocking"] }
use Honk;
let honk = from_env?;
honk.beep.send?;
honk.send?;
It runs the async client on a private single-threaded Tokio runtime, with the same retries and guarantees. Don't call it from inside an async runtime; use the async client there.
Development
HONK_URL=… HONK_KEY=…
The version lives in Cargo.toml (VERSION and the User-Agent honk-me-rust/<version> come
from it). Releases: push a tag vX.Y.Z matching it and the release workflow publishes to
crates.io with trusted publishing (see CHANGELOG.md).
Links
- honk-me.app: the Honk inbox (web, iPhone).
- docs.rs/honk-me: the API reference.
- Other SDKs: Node.js, PHP / Laravel, Go + CLI, Swift, Kotlin / Java, n8n.
MIT License.