Skip to content

Resources

The api layer maps one-to-one to API routes. Models in dinio_sdk::models mirror the API’s JSON; enum fields that may grow (levels, statuses, TLS modes) are kept as String, so a newer API never breaks deserialization. Each has a typed accessor that returns None for a value this SDK version doesn’t know — tls_mode(), kind(), source_kind(), manager(). Input types use the typed enums.

Create methods that a package limit counts — zones(), health_checks(), client_identities(), sinkhole_lists() and sinkhole_roles() — return Created<T> { value, warnings }: the new object and the API’s warnings (usually empty; see Packages and limits).

Everything in a zone: a zone key reaches its own zone, a personal key the zones its user is Owner or Administrator of.

Method Returns Route
zones().list(PageRequest) Page<Zone> GET /zones
zones().list_all() Vec<Zone> every page
zones().get(zone) Zone GET /zones/:ref
zones().create(name) Created<Zone> POST /zones
zones().create_api_key(zone) Secret POST /zones/:ref/api-key
zones().revoke_api_key(zone) Zone DELETE /zones/:ref/api-key
zones().delete(zone) () DELETE /zones/:ref

zone is a &str: a uuid or a zone name. PageRequest { page, per_page } takes Option<u32> for both; Default uses the API’s defaults. create_api_key returns the new key — the only time it is shown; see API keys.

Method Returns Route
zones().export(zone, ZoneFileFormat) String GET /zones/:ref/export
zones().import(zone, ZoneFileFormat, file, dry_run) ImportReport POST /zones/:ref/import
zones().discover(zone, &nameservers) Discovery POST /zones/:ref/discover

ZoneFileFormat is Json, Bind or Csv; ZoneFileFormat::from_file_name guesses it from an extension. The SDK takes and returns file content as a String — reading and writing files is up to you.

use dinio_sdk::models::ZoneFileFormat;
let zones = client.zones();
let file = zones.export("example.com", ZoneFileFormat::Bind).await?;
let preview = zones.import("example.org", ZoneFileFormat::Bind, file.clone(), true).await?;
if !preview.has_errors() {
let report = zones.import("example.org", ZoneFileFormat::Bind, file, false).await?;
println!("{} records created", report.records.created);
}

A refused import is ErrorCode::ImportFailed, with the report in ApiError::import_report(). discover returns a proposal in Discovery::document to trim and import; it takes up to a minute, and the SDK waits at least 90 s for it, whatever the client’s timeout. An empty nameservers slice asks the zone’s delegation.

let found = client.zones().discover("example.com", &["ns1.old-provider.example".into()]).await?;
let mut document = found.document;
document.records.retain(|r| r.name != "old");
let file = serde_json::to_string(&document)?;
client.zones().import("example.com", ZoneFileFormat::Json, file, false).await?;
Method Returns Route
free_domains().list() FreeDomainList GET /free-domains
free_domains().available(domain, label) FreeLabelAvailability GET /free-domains/:domain/available

Neither needs a key. A lease is zones().create with the full name.

Method Returns Route
records().list(zone_id, &filter, PageRequest) Page<RecordValue> GET /records
records().list_all(zone_id, &filter) Vec<RecordValue> every page
records().get(zone_id, name, type, with_geo_bans) RecordSet GET /records/:name
records().create(&NewRecordValue) RecordValue POST /records
records().update(uuid, &RecordValuePatch) RecordValue PATCH /records/:uuid
records().set_ttl(zone_id, name, type, ttl) RecordSet PATCH /records/:name/ttl
records().delete(uuid) () DELETE /records/:uuid

Lists return record values, each with its ttl. For a view by record set, pass them to ops::group_values. filter is RecordFilter { name, record_type }, both Option<String>. Names are full names; see qualify_name.

RecordValue::managed_by marks a value Dinio keeps; is_locked() is true for Redirect and Acme values (and kinds this SDK version doesn’t know), which the API refuses to change with ErrorCode::ManagedRecord.

use dinio_sdk::models::NewRecordValue;
let value = client
.records()
.create(&NewRecordValue {
zone_id: zone.uuid,
name: "www.example.com".into(),
record_type: "A".into(),
data: "192.0.2.1".into(),
ttl: Some(300), // only for a new record set
geo_routing: None,
balance_ratio: None,
host_health: None,
})
.await?;
client.records().set_ttl(zone.uuid, "www.example.com", "A", 600).await?;
client.records().delete(value.uuid).await?; // with its geo bans and health check
Method Returns Route
health_checks().get(record_id) HealthCheck GET /health-checks/:record_id
health_checks().create(&NewHealthCheck) Created<HealthCheck> POST /health-checks
health_checks().update(record_id, &HealthCheckPatch) HealthCheck PATCH /health-checks/:record_id
health_checks().delete(record_id) () DELETE /health-checks/:record_id

The check’s kind and config are one enum, HealthCheckConfig::{Http, Tcp, Ping}. On a returned HealthCheck, typed_config() gives it back as that enum. Times are in seconds; see Timing for the limits. Checks work on A, AAAA, SRV and MX values; other types are ErrorCode::Validation.

use dinio_sdk::models::{HealthCheckConfig, HttpCheck, NewHealthCheck};
client
.health_checks()
.create(&NewHealthCheck {
record_id: value.uuid,
config: HealthCheckConfig::Http(HttpCheck {
url: "https://192.0.2.1/health".into(),
expected_status: Some(vec![200]),
..Default::default()
}),
name: None,
interval: 60, // s
timeout: 5, // s
retries: None,
retry_interval: None,
})
.await?;

create switches the value’s host_health to HostHealthMode::Auto; delete turns Auto back into None. A check has no on/off switch of its own: pause it by setting the value to Up or Down, resume it with Auto.

use dinio_sdk::models::{HostHealthMode, RecordValuePatch};
client
.records()
.update(value.uuid, &RecordValuePatch {
host_health: Some(HostHealthMode::Down), // maintenance; back to Auto afterwards
..Default::default()
})
.await?;

RecordValue::host_health is the mode label as a string; host_health_mode() gives it as HostHealthMode. In a HealthCheckPatch, config replaces the whole config and must keep the check’s type.

Method Returns Route
geo_bans().create(record_id, area) GeoBan POST /geo-bans
geo_bans().update(uuid, area) GeoBan PATCH /geo-bans/:uuid
geo_bans().delete(uuid, record_id) () DELETE /geo-bans/:uuid
geo().catalog() GeoCatalog GET /geo

area is a GeoArea { level, code }; build it from a name with GeoCatalog::parse_area. record_id on delete is an Option<Uuid> guard: when set, the ban must belong to that value. Read bans with records().get(…, true) — each value’s geo_bans is filled in.

let catalog = client.geo().catalog().await?;
client.geo_bans().create(value.uuid, catalog.parse_area("HR")?).await?;
Method Returns Route
zone_members().list(zone_id) Vec<ZoneMember> GET /zone-members
zone_members().add(zone_id, email) () POST /zone-members
zone_members().remove(zone_id, user_id) () DELETE /zone-members/:zone_id/:user_id

add succeeds the same way whether the user was added, already was a member, or no account has that email (there are no invitations); list shows the result, each member with their email. ops::find_member finds a member by email for remove.

Method Returns Route
redirects().list(zone_id, PageRequest) Page<Redirect> GET /redirects
redirects().list_all(zone_id) Vec<Redirect> every page
redirects().get(name) Redirect GET /redirects/:name
redirects().create(&NewRedirect) Redirect POST /redirects
redirects().update(name, &RedirectPatch) Redirect PATCH /redirects/:name
redirects().delete(name) () DELETE /redirects/:name

name is the redirect’s host name; zone_id on the lists is an Option<Uuid> filter. Lists don’t include certificate. NewRedirect::tls left None lets the API choose (None: no certificate of its own).

use dinio_sdk::models::{NewRedirect, RedirectTls};
use dinio_sdk::ops;
let zone = ops::find_zone_for_host(&client, "go.example.com").await?;
let redirect = client
.redirects()
.create(&NewRedirect {
zone_id: zone.uuid,
name: "go.example.com".into(),
code: 302,
location: "https://www.example.com/campaign".into(),
tls: Some(RedirectTls::Auto),
})
.await?;
if let Some(cert) = &redirect.certificate {
println!("certificate: {}", cert.status); // "Pending", later "Valid"
}
Method Returns Route
certificates().list(zone_id, PageRequest) Page<Certificate> GET /certificates
certificates().list_all(zone_id) Vec<Certificate> every page
certificates().covering(name) Vec<Certificate> GET /certificates?name=
certificates().get(uuid) Certificate GET /certificates/:uuid
certificates().create(&NewCertificate) Certificate POST /certificates
certificates().replace(uuid, &CertificateRenewal) Certificate PUT /certificates/:uuid
certificates().delete(uuid) () DELETE /certificates/:uuid

NewCertificate { chain_pem, key_pem, zone_id } takes the PEM text; its Debug never shows the key. replace and delete of a source: "Acme" certificate return ErrorCode::ManagedCertificate.

Client identities and sinkhole lists belong to your user: they need a personal key, and a zone key gets ErrorCode::Forbidden on every method.

Method Returns Route
client_identities().list(&ClientIdentityFilter) Vec<ClientIdentity> GET /client-identities
client_identities().get(identity) ClientIdentity GET /client-identities/:ref
client_identities().create(&NewClientIdentity) Created<ClientIdentity> POST /client-identities
client_identities().update(uuid, &ClientIdentityPatch) ClientIdentity PATCH /client-identities/:uuid
client_identities().regenerate_identify(uuid) ClientIdentity POST /client-identities/:uuid/identify
client_identities().delete(uuid) () DELETE /client-identities/:uuid

ClientIdentityFilter { uuid, identify }; get takes a uuid or the identify value. In a NewClientIdentity, identify is Some(address) for Ip and None for Hostname and Path — Dinio generates it. In a ClientIdentityPatch, name: Some(None) removes the name and enabled: Some(false) switches the identity off.

Method Returns Route
sinkhole_lists().list(shared) Vec<SinkholeList> GET /sinkhole-lists
sinkhole_lists().get(uuid) SinkholeList GET /sinkhole-lists/:uuid
sinkhole_lists().roles_page(uuid, limit, cursor) SinkholeRolePage GET /sinkhole-lists/:uuid?include=roles
sinkhole_lists().all_roles(uuid) Vec<SinkholeRole> every page
sinkhole_lists().create(&NewSinkholeList) Created<CreatedSinkholeList> POST /sinkhole-lists
sinkhole_lists().update(uuid, &SinkholeListPatch) SinkholeList PATCH /sinkhole-lists/:uuid
sinkhole_lists().set_shared(uuid, is_shared) SinkholeList PATCH /sinkhole-lists/:uuid
sinkhole_lists().delete(uuid) () DELETE /sinkhole-lists/:uuid
sinkhole_roles().get(uuid) SinkholeRole GET /sinkhole-roles/:uuid
sinkhole_roles().create(&NewSinkholeRole) Created<SinkholeRole> POST /sinkhole-roles
sinkhole_roles().update(uuid, &SinkholeRolePatch) SinkholeRole PATCH /sinkhole-roles/:uuid
sinkhole_roles().delete(uuid) () DELETE /sinkhole-roles/:uuid
sinkhole_links().list(&SinkholeLinkFilter) Vec<SinkholeLink> GET /sinkhole-links
sinkhole_links().create(client_identity_id, list_id) SinkholeLink POST /sinkhole-links
sinkhole_links().delete(client_identity_id, list_id) () DELETE /sinkhole-links/:cid/:sid

NewSinkholeList::link_to applies the new list to your identities in the same request; the created links come back in CreatedSinkholeList::links. In a SinkholeListPatch, name: Some(None) removes the name. get returns the list with its source, without rules. A list can have up to 500 000 rules, so they come in pages: roles_page takes a limit (Option<u32>, 1–5 000, default 500) and the next_cursor of the previous page, and returns SinkholeRolePage { roles, next_cursor, total }; all_roles reads every page.

use dinio_sdk::models::{
ClientIdentityType, NewClientIdentity, NewSinkholeList, NewSinkholeRole, SinkholeListType,
SinkholeRoleType,
};
let phone = client
.client_identities()
.create(&NewClientIdentity {
identify: None, // generated for Path
id_type: ClientIdentityType::Path,
name: Some("phone".into()),
})
.await?;
println!("DoH path part: {}", phone.value.identify);
let created = client
.sinkhole_lists()
.create(&NewSinkholeList {
list_type: SinkholeListType::Blacklist,
name: Some("ads".into()),
is_shared: None,
link_to: vec![phone.value.uuid],
})
.await?;
client
.sinkhole_roles()
.create(&NewSinkholeRole {
list_id: created.value.list.uuid,
sinkhole_type: SinkholeRoleType::Wildcard,
pattern: "*.tracker.example".into(),
})
.await?;
Method Returns Route
sinkhole_lists().import(uuid, file, SinkholeImportMode, dry_run) SinkholeImportReport POST /sinkhole-lists/:uuid/import
sinkhole_lists().export(uuid, SinkholeExportFormat) String GET /sinkhole-lists/:uuid/export
sinkhole_lists().set_source(uuid, &NewSinkholeSource, dry_run) SinkholeSourceResult PUT /sinkhole-lists/:uuid/source
sinkhole_lists().set_refresh_interval(uuid, Option<u32>) SinkholeListSource PATCH /sinkhole-lists/:uuid/source
sinkhole_lists().refresh(uuid, force) SinkholeSourceResult POST /sinkhole-lists/:uuid/refresh
sinkhole_lists().delete_source(uuid) () DELETE /sinkhole-lists/:uuid/source

file is the file’s content as a String — hosts, plain names, Adblock/AdGuard, dnsmasq or the Csv export, mixed per line. SinkholeImportMode is Merge or Replace; SinkholeExportFormat is Csv, Adblock, Hosts or Domains. NewSinkholeSource { url, refresh_interval_s } takes the interval in seconds, None for only on request. The report carries the API’s warnings; SinkholeSourceResult { source, import, … } has import: None when a refresh found the file unchanged. See Import and export for the formats and limits. import, set_source and refresh wait at least 120 s (SinkholeLists::IMPORT_TIMEOUT), or the client’s timeout if it is longer.

use dinio_sdk::models::{NewSinkholeSource, SinkholeExportFormat, SinkholeImportMode};
let lists = client.sinkhole_lists();
let list_id = created.value.list.uuid;
let preview = lists.import(list_id, hosts.clone(), SinkholeImportMode::Replace, true).await?;
println!("+{} -{}", preview.added, preview.removed);
lists.import(list_id, hosts, SinkholeImportMode::Replace, false).await?;
let csv = lists.export(list_id, SinkholeExportFormat::Csv).await?;
// Or let Dinio download it, now and once a day. Such a list takes no manual rules.
lists
.set_source(list_id, &NewSinkholeSource {
url: "https://blocklists.example.com/hosts.txt".into(),
refresh_interval_s: Some(86_400),
}, false)
.await?;
Method Returns Route
stats().zones_queries(group_by, &query) StatsSeriesResult GET /stats/zones/queries
stats().zones_top(&query) ZoneTopList GET /stats/zones/top
stats().zone_queries(zone, group_by, name, &query) StatsSeriesResult GET /stats/zones/:ref/queries
stats().zone_nx(zone, &query) ZoneNxStats GET /stats/zones/:ref/nx
stats().zone_names(zone, &query) ZoneNamesStats GET /stats/zones/:ref/names
stats().zone_nx_names(zone, &query) StatsTopList GET /stats/zones/:ref/nx-names
stats().client_events(scope, group_by, &query) StatsSeriesResult GET /stats/clients/events
stats().client_rcodes(scope, group_by, &query) StatsSeriesResult GET /stats/clients/rcodes
stats().client_blocked(scope, &query) StatsTopList GET /stats/clients/blocked

Series take a StatsSeriesQuery { from, to, step, compare, mode, fill, limit }, top lists a StatsTopQuery { from, to, limit }; Default leaves everything to the API. from and to are Timestamps, made from a SystemTime or an RFC 3339 string. group_by is an Option<StatsGroup>. ClientScope is Mine (default: all your identities) or Client(uuid). zones_queries and zones_top sum over every zone you can see. A range further back than the package allows is ErrorCode::PlanLimit. Times in responses are milliseconds; see Statistics for the fields.

use std::time::{Duration, SystemTime};
use dinio_sdk::api::{StatsGroup, StatsSeriesQuery, StatsStep};
let week = StatsSeriesQuery {
from: Some((SystemTime::now() - Duration::from_secs(7 * 86_400)).into()),
step: Some(StatsStep::Day),
..Default::default()
};
let result = client
.stats()
.zone_queries("example.com", Some(StatsGroup::Qtype), None, &week)
.await?;
for series in &result.series {
println!("{} {}", series.label, series.total);
}
Method Returns Route
audit_logs().list(&AuditLogFilter) CursorPage<AuditLog> GET /audit-logs
audit_logs().list_all(&AuditLogFilter) Vec<AuditLog> every page

AuditLogFilter has from, to, zone_id, action (exact, or a prefix ending in *), outcome, category, actor_type, actor_user_id, resource_type, resource_id, request_id, limit and cursor, all optional. list returns CursorPage { items, next_cursor }; pass next_cursor back as cursor for the next page, or let list_all do it.

Method Returns Route
users().get(user) User GET /users/:ref
users().create_api_key(uuid) Secret POST /users/:uuid/api-key
users().revoke_api_key(uuid) User DELETE /users/:uuid/api-key

user is your uuid or email. A new personal key replaces the one the client is using, so build a new client with it.

Method Returns Route
plans().list() Vec<Plan> GET /plans
account().subscriptions() Vec<Subscription> GET /account/subscriptions
account().choose(PlanLine, code, Option<BillingPeriod>) Subscription PUT /account/subscriptions/:line
account().usage() Vec<LineUsage> GET /account/usage

plans().list() needs no key. PlanLine is Auth or Filter; BillingPeriod (Monthly or Yearly) is required for a paid package and refused for a free one. account() needs a personal key. See Packages and usage.

ApiWarning::plan_usage() reads a PLAN_USAGE warning as (key, limit, used); ApiError::plan_limit() reads a PLAN_LIMIT error (see Errors). ops::next_plan_for(&plans, key, limit) is the first self-service package of that line that allows more — the upgrade to offer; ops::next_plan_for_minimum does the same for a setting where less is better (auth.healthchecks.min_interval_s).

use dinio_sdk::models::PlanLine;
use dinio_sdk::ops;
let plans = client.plans().list().await?;
client.account().choose(PlanLine::Auth, "free", None).await?;
let created = client.zones().create("example.com").await?;
for warning in &created.warnings {
if let Some((key, limit, used)) = warning.plan_usage() {
let next = ops::next_plan_for(&plans, key, limit);
eprintln!("{key}: {used} of {limit}; more with {:?}", next.map(|p| &p.name));
}
}

Don’t hard-code limits or prices: read them from plans().list() and account().usage().