Skip to content

Repository files navigation

regfish DNS & TLS API Go Client

CI Go Reference Go Report Card

Go client for the regfish API v1 — domains, DNS records and zones, DNSSEC, web hosting and TLS certificates.

Install

go get github.com/regfish/regfish-dnsapi-go

Quick start

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	regfishapi "github.com/regfish/regfish-dnsapi-go"
)

func main() {
	client := regfishapi.NewClient(os.Getenv("RF_API_KEY"))
	ctx := context.Background()

	record, err := client.AddRecord(ctx, regfishapi.Record{
		Name: "www.example.com.",
		Type: regfishapi.RecordTypeA,
		Data: "10.2.3.4",
		TTL:  600,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("created rrid", record.ID)
}

The API key is created in the regfish dash under Account → Security → API keys and is sent as the x-api-key header.

Examples

Runnable programs live in examples/:

Example Shows
create-record create, read and delete a record; branching on the error code
permission-check inspecting the key and failing a deploy on an under-scoped one
zone-export zone summary and BIND export
resilient-writer a retry loop that does not hammer a permanent refusal
RF_API_KEY=... RF_TEST_DOMAIN=example.com go run ./examples/permission-check

Configuration

client := regfishapi.NewClient(apiKey,
	regfishapi.WithBaseURL("https://api.regfish.com"),
	regfishapi.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
	regfishapi.WithUserAgent("my-app/1.0"),
)

The default endpoint is https://api.regfish.de. https://api.regfish.com serves the same API and is the endpoint listed in the OpenAPI specification.

Error handling

Every failed call returns an *APIError carrying the HTTP status as well as the regfish error code and message:

_, err := client.AddRecord(ctx, record)
switch {
case regfishapi.ErrorCode(err) == regfishapi.CodeResourceRecordAlreadyExists:
	// record with this name/type/data already exists
case regfishapi.IsRateLimited(err):
	// daily or per-minute limit reached
case err != nil:
	log.Fatal(err)
}

Use err.Reason() rather than reading Message or Detail directly — the API puts its explanation in message on some endpoints and in error on others.

Denials: why a call was refused

An HTTP status is not enough to decide what to do about a refusal. DenialOf turns the response into one value you can branch on:

switch regfishapi.DenialOf(err) {
case regfishapi.DenialPermission:
	if perm, ok := regfishapi.MissingPermission(err); ok {
		log.Fatalf("grant %s to this API key in the dash", perm)
	}
	log.Fatalf("this API key is not permitted to do that: %v", err)
case regfishapi.DenialScope:
	log.Fatal("this is a scope-bound platform credential; use a customer key")
case regfishapi.DenialUnmappedOperation:
	op, _ := regfishapi.DeniedOperation(err) // server-side gap, report it
	log.Fatalf("regfish has no permission mapping for %q", op)
case regfishapi.DenialGuardian:
	log.Print("Domain-Guardian blocked this — confirm it in the dash")
case regfishapi.DenialQuota:
	// retry after the window or the UTC day rolls over
case regfishapi.DenialUnauthenticated:
	log.Fatal("the API key was not accepted")
}

MissingPermission and DeniedOperation read the name out of the API's own message, so they are never stale — but they report false if the server ever rewords it. Always handle the not-ok branch.

The shorthands are IsPermissionDenied, IsDomainGuardianBlocked and IsPermanent.

If you retry on 5xx, check IsDomainGuardianBlocked first. The API reports a Domain-Guardian block as HTTP 403 on the domain endpoints but as HTTP 500 on the DNS record and DNSSEC endpoints, where nothing but the message text separates it from a genuine, retryable server fault. A naive retry loop will hammer a decision that cannot succeed until a human clicks a button in the dash:

for attempt := 0; attempt < 5; attempt++ {
	_, err = client.AddRecord(ctx, record)
	if err == nil || regfishapi.IsPermanent(err) {
		break
	}
	time.Sleep(time.Duration(1<<attempt) * time.Second)
}

IsPermanent is one-directional: true means no retry can clear it, false means only "not known to be permanent". Cap your retries either way.

API keys and permissions

A key is one of three kinds, and TokenInfo.Kind is the only safe way to tell them apart:

Kind permissions on the wire Meaning
KeyKindLegacy null Created before RBAC — full access
KeyKindRBAC [...], possibly [] Exactly the listed permissions; [] means none
KeyKindCapability null, plus a scope Platform credential (e.g. ACME DNS-01), limited to a fixed set of operations whatever its permissions

null and [] mean opposite things, and a capability-scoped key looks like a legacy key if you only read Permissions. Use Permits, which returns a tri-state and is scope-aware:

info, err := client.GetTokenInfo(ctx)
switch info.Permits(regfishapi.PermissionDNSWrite) {
case regfishapi.DecisionAllowed: // probably fine
case regfishapi.DecisionDenied:  // will be refused
case regfishapi.DecisionUnknown: // scope-bound key — permissions do not describe it
}

Check the key once at startup, so an under-scoped key fails the deploy instead of the first write hours later:

report, err := client.CheckPermissions(ctx,
	regfishapi.PermissionDNSRead, regfishapi.PermissionDNSWrite)
if err != nil {
	log.Fatal(err)
}
if !report.Satisfied() {
	log.Fatal(report) // "regfish API key kAbC [rbac]: missing dns:write"
}

Gate on Satisfied(), not on len(report.Missing): a capability-scoped credential has no missing permissions and still cannot do the work, so those land in report.Undetermined and a Missing-only check would wave it through.

The library never refuses a call on its own. It keeps no copy of the server's operation-to-permission table, so it can never deny something the server would have allowed, and it cannot go stale when regfish adds an endpoint. CheckPermissions and Permits are advisory diagnostics; the API remains the only authority. A clean report is not a promise either — it cannot see Domain-Guardian rules, domain ownership, plan entitlements or quotas.

To rehydrate cached token metadata, use ParseTokenInfo rather than json.Unmarshal: a payload that lost its permissions member would otherwise decode to a nil slice and read as a legacy key with full access.

Coverage

All v1 operations are implemented. Every method takes a context.Context as its first argument.

The Permission column is what to pass to CheckPermissions. It is documentation, not enforcement — the library never consults it, so it cannot cause a request to be refused. It mirrors the server's mapping as of API version 1.6.2 (2026-08-01); /meta/token and the dash are authoritative. Note that read and write do not split the way the method names suggest: DownloadCertificate needs tls:read, and every hosting method needs hosting:read.

The operationId column matters when handling denials: DeniedOperation returns the API's operationId, which differs from the Go method name in one case (ListRecordsByDomainGetRecordsByDomain).

Meta

Method Endpoint operationId Permission
GetTokenInfo GET /meta/token GetTokenInfo — (any valid key)
CheckPermissions GET /meta/token GetTokenInfo — (any valid key)
DownloadOpenAPISpec GET /openapi.yaml DownloadOpenAPISpec — (any valid key)

Domains

Method Endpoint operationId Permission
ListDomains GET /domains ListDomains domain:read
GetDomainByName GET /domains/{domain} GetDomainByName domain:read
GetNameserversByDomain GET /domains/{domain}/nameservers GetNameserversByDomain domain:read
PutNameserversByDomain PUT /domains/{domain}/nameservers PutNameserversByDomain domain:write
RequestAuthinfoByDomain POST /domains/{domain}/authinfo RequestAuthinfoByDomain domain:write

DNS records

Method Endpoint operationId Permission
ListRecordsByDomain GET /dns/{domain}/rr GetRecordsByDomain dns:read
GetRecordByRRID GET /dns/rr/{rrid} GetRecordByRRID dns:read
AddRecord POST /dns/rr AddRecord dns:write
PatchRecord PATCH /dns/rr PatchRecord dns:write
PatchRecordByRRID PATCH /dns/rr/{rrid} PatchRecordByRRID dns:write
DeleteRecordByRRID DELETE /dns/rr/{rrid} DeleteRecordByRRID dns:write

AddRecord detects the zone from the record's fully qualified Name. PatchRecord matches an existing record by name and type — and, for every type except A and AAAA, by its current data — and requires a unique match; use PatchRecordByRRID to change a record's name or type. ListRecordsByDomain does not include NS records; GetDNSZoneByDomain does.

DNS zones

Method Endpoint operationId Permission
ListDNSZones GET /dns/zones ListDNSZones dns:read
GetDNSZoneByDomain GET /dns/zones/{domain} GetDNSZoneByDomain dns:read
ExportDNSZoneBindByDomain GET /dns/zones/{domain}/export ExportDNSZoneBindByDomain dns:read

DNSSEC

Method Endpoint operationId Permission
GetDNSSECByDomain GET /dns/{domain}/dnssec GetDNSSECByDomain dns:read
PutDNSSECByDomain PUT /dns/{domain}/dnssec PutDNSSECByDomain dns:write
CancelDNSSECByDomain POST /dns/{domain}/dnssec/cancel CancelDNSSECByDomain dns:write
VerifyDNSSECByDomain POST /dns/{domain}/dnssec/verify VerifyDNSSECByDomain dns:write
ListDNSSECJobsByDomain GET /dns/{domain}/dnssec/jobs ListDNSSECJobsByDomain dns:read

Hosting (read-only)

Method Endpoint operationId Permission
ListHostingPackages GET /hosting/packages ListHostingPackages hosting:read
GetHostingPackage GET /hosting/packages/{id} GetHostingPackage hosting:read
ListHostingAliases GET /hosting/packages/{id}/aliases ListHostingAliases hosting:read
ListHostingDatabases GET /hosting/packages/{id}/databases ListHostingDatabases hosting:read

TLS

Method Endpoint operationId Permission
ListTLSProducts GET /tls/products ListTLSProducts tls:read
ListCertificates GET /tls/certificate ListCertificates tls:read
CreateCertificate POST /tls/certificate CreateCertificate tls:write
GetCertificateByID GET /tls/certificate/{id} GetCertificateByID tls:read
CompleteCertificate POST /tls/certificate/{id}/complete CompleteCertificate tls:write
CancelCertificate POST /tls/certificate/{id}/cancel CancelCertificate tls:write
RevokeCertificate POST /tls/certificate/{id}/revoke RevokeCertificate tls:write
CancelIssuedCertificateOrder POST /tls/certificate/{id}/order-cancel CancelIssuedCertificateOrder tls:write
ReissueCertificate POST /tls/certificate/{id}/reissue ReissueCertificate tls:write
DownloadCertificate GET /tls/certificate/{id}/download/{format} DownloadCertificate tls:read
ListOrganizations GET /tls/organization ListOrganizations tls:read
CreateOrganization POST /tls/organization CreateOrganization tls:write
GetOrganizationByID GET /tls/organization/{id} GetOrganizationByID tls:read
PatchOrganizationByID PATCH /tls/organization/{id} PatchOrganizationByID tls:write

Migration from v0

The previous methods still work and delegate to the new ones, but they are deprecated because they cannot be cancelled:

Deprecated Replacement
GetRecord(rrid) GetRecordByRRID(ctx, rrid)
CreateRecord(rec) AddRecord(ctx, rec)
UpdateRecord(rec) PatchRecord(ctx, rec)
UpdateRecordById(rrid, rec) PatchRecordByRRID(ctx, rrid, rec)
DeleteRecord(rrid) DeleteRecordByRRID(ctx, rrid)
GetRecordsByDomain(domain) ListRecordsByDomain(ctx, domain)
Request(...) RequestContext(ctx, ...)

Behavioural changes to be aware of — this is not a drop-in no-op:

  • Failed requests now return *APIError with the API's error code and message instead of a bare request failed with status code N. Code that matched on the old error string will not match any more.
  • NewClient sets a 60 second timeout on its default HTTP client, where v0 had none. Calls that previously hung indefinitely now fail; pass WithHTTPClient(&http.Client{}) to restore the old behaviour.
  • Write payloads no longer carry id, and Record gained four read-only response fields (Auto, Active, TSCreated, TSUpdated), which changes what json.Marshal of a Record produces.

Testing

Unit tests run against a stub server and need no credentials:

go test ./...

The integration tests talk to the live API and create, update and delete a real DNS record on the domain you point them at:

RF_API_KEY=... RF_TEST_DOMAIN=example.com go test -tags integration -v

RF_API_BASE_URL overrides the endpoint. Subtests whose permission the key does not hold are skipped rather than failed, so a least-privilege RBAC key reports honestly instead of looking like a library regression.

About

regfish-dnsapi-go is a Go client library that facilitates seamless interaction with the Regfish DNS API

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages