Testnet
Developers

Resolve any .htr name

A free, read-only REST API for name resolution and profile lookups on the Hathor Network.

01Basics
Base URLhttps://thoth.id/api/v1
AuthenticationNone. All endpoints are public reads.
VersioningURL-versioned. v1 only gets additive changes.
CORSAccess-Control-Allow-Origin: *

Names are case-insensitive and accepted with or without .htr. Responses echo the canonical form.

02Conventions
Rate limit120 requests / 60s per IP
CachingETag + If-None-Match → 304
Errorsapplication/problem+json
TracingX-Request-Id on uncached responses

Rate-limit state is reported in RateLimit-* headers on uncached responses; CDN-cached responses omit them and don't count against the limit. 429s include Retry-After.

Version1.0.0
Endpoints9
Batchup to 50 items
03Resolution

Forward (name → address) and reverse (address → name) resolution.

GET/api/v1/names/{name}/address

Resolve a name to an address

Strict forward resolution: returns the resolving address only while the name is active. Expired names return 404 NAME_EXPIRED, so a 200 is always safe to send funds to.

{name}A .htr name, with or without the suffix. Case-insensitive; the canonical lowercase form is echoed back.
Cache-Controlpublic, max-age=15, s-maxage=30, stale-while-revalidate=60
curl
curl https://thoth.id/api/v1/names/jackal.htr/address
JavaScript
const res = await fetch("https://thoth.id/api/v1/names/jackal.htr/address");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "name": "jackal.htr",
  "address": "WjcD1MGLi1TS8pH1JRcYzd9bJ1YPejVRtH"
}
GET/api/v1/addresses/{address}/name

Reverse-resolve an address to its primary name

Returns the primary name the address set as manager, if that name is active. verified is true when the name also resolves forward to the same address; only display unverified names with care.

{address}A base58 Hathor address.
Cache-Controlpublic, max-age=15, s-maxage=30, stale-while-revalidate=60
curl
curl https://thoth.id/api/v1/addresses/WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq/name
JavaScript
const res = await fetch("https://thoth.id/api/v1/addresses/WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq/name");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "address": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
  "name": "biu.htr",
  "verified": true
}
POST/api/v1/batch/resolve

Resolve many names and addresses at once

Resolve up to 50 items in total (names + addresses) in one request. Results keep the input order and carry a per-item error (same codes as problem responses). Node outages fail the whole request with 502/504. Counts as 1 + ⌊items / 5⌋ rate-limit units. Not cached.

Cache-Controlno-store
curl
curl -X POST https://thoth.id/api/v1/batch/resolve \
  -H "Content-Type: application/json" \
  -d '{"names":["jackal.htr","thoth","nobody-here"],"addresses":["WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq"]}'
JavaScript
const res = await fetch("https://thoth.id/api/v1/batch/resolve", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({"names":["jackal.htr","thoth","nobody-here"],"addresses":["WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq"]}),
});
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "names": [
    {
      "input": "jackal.htr",
      "name": "jackal.htr",
      "address": "WjcD1MGLi1TS8pH1JRcYzd9bJ1YPejVRtH",
      "error": null
    },
    {
      "input": "thoth",
      "name": "thoth.htr",
      "address": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
      "error": null
    },
    {
      "input": "nobody-here",
      "name": "nobody-here.htr",
      "address": null,
      "error": {
        "code": "NAME_NOT_FOUND",
        "message": "nobody-here.htr is not registered."
      }
    }
  ],
  "addresses": [
    {
      "input": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
      "address": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
      "name": "biu.htr",
      "verified": true,
      "error": null
    }
  ]
}
04Names

Name records, status, availability and fee quotes.

GET/api/v1/names/{name}

Get a name record

Full on-chain record: status, resolving address, manager, owner, NFT and expiration. Names whose grace period has lapsed return 404 NAME_NOT_FOUND.

{name}A .htr name, with or without the suffix. Case-insensitive; the canonical lowercase form is echoed back.
Cache-Controlpublic, max-age=15, s-maxage=30, stale-while-revalidate=60
curl
curl https://thoth.id/api/v1/names/test.htr
JavaScript
const res = await fetch("https://thoth.id/api/v1/names/test.htr");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "name": "test.htr",
  "label": "test",
  "status": "active",
  "resolvingAddress": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
  "manager": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
  "owner": "WSoSypkhE8i3iZxZvXCq91vWDwr8wKFVVq",
  "nftDeposited": true,
  "tokenUid": "00f34fabeb19991b7981c3e4439d713856a9ee9ff0725ed6023c335da52eb5e0",
  "expiresAt": "2027-02-10T14:56:07.000Z",
  "expiresAtUnix": 1802271367,
  "gracePeriodEndsAt": "2027-03-12T14:56:07.000Z",
  "gracePeriodEndsAtUnix": 1804863367
}
GET/api/v1/names/{name}/availability

Check availability and quote the fee

Whether anyone can register the name right now, its status, and the registration fee for one period. Unregistered names return 200 with available: true.

{name}A .htr name, with or without the suffix. Case-insensitive; the canonical lowercase form is echoed back.
Cache-Controlpublic, max-age=5, s-maxage=10, stale-while-revalidate=20
curl
curl https://thoth.id/api/v1/names/abc/availability
JavaScript
const res = await fetch("https://thoth.id/api/v1/names/abc/availability");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "name": "abc.htr",
  "label": "abc",
  "available": true,
  "status": "available",
  "expiresAt": null,
  "expiresAtUnix": null,
  "gracePeriodEndsAt": null,
  "gracePeriodEndsAtUnix": null,
  "fee": {
    "currency": "HTR",
    "decimals": 2,
    "baseFee": 1,
    "multiplier": 20,
    "amount": 20,
    "formatted": "0.20",
    "periodDays": 365
  }
}
05Profiles

Profile records attached to a name (avatar, description, socials).

GET/api/v1/names/{name}/profile

Get profile records

Profile records of a registered name: typed well-known keys, a displayable avatarUrl, and the raw key/value map as stored on-chain.

{name}A .htr name, with or without the suffix. Case-insensitive; the canonical lowercase form is echoed back.
Cache-Controlpublic, max-age=30, s-maxage=60, stale-while-revalidate=300
curl
curl https://thoth.id/api/v1/names/test.htr/profile
JavaScript
const res = await fetch("https://thoth.id/api/v1/names/test.htr/profile");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "name": "test.htr",
  "status": "active",
  "avatarUrl": "https://abeesgqcvlollxwc.public.blob.vercel-storage.com/domain-images/test-hsdSU0hh1L5qF21L54mTn4QMzHCb1u.htr/avatar",
  "records": {
    "avatar": "https://abeesgqcvlollxwc.public.blob.vercel-storage.com/domain-images/test-hsdSU0hh1L5qF21L54mTn4QMzHCb1u.htr/avatar",
    "description": null,
    "website": null,
    "email": null,
    "twitter": null,
    "github": null,
    "discord": null,
    "telegram": null
  },
  "raw": {
    "avatar_link": "https://abeesgqcvlollxwc.public.blob.vercel-storage.com/domain-images/test-hsdSU0hh1L5qF21L54mTn4QMzHCb1u.htr/avatar"
  }
}
06Addresses

Names managed by an address.

GET/api/v1/addresses/{address}/names

List names managed by an address

All names the address manages (max 100 per address, enforced on-chain) plus its primary name. An address with no names returns an empty list.

{address}A base58 Hathor address.
Cache-Controlpublic, max-age=15, s-maxage=30, stale-while-revalidate=60
curl
curl https://thoth.id/api/v1/addresses/WjcD1MGLi1TS8pH1JRcYzd9bJ1YPejVRtH/names
JavaScript
const res = await fetch("https://thoth.id/api/v1/addresses/WjcD1MGLi1TS8pH1JRcYzd9bJ1YPejVRtH/names");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "address": "WjcD1MGLi1TS8pH1JRcYzd9bJ1YPejVRtH",
  "primaryName": "jackal.htr",
  "names": [
    "jackal.htr"
  ],
  "total": 1
}
07Meta

Contract configuration, health and API description.

GET/api/v1/config

Get contract configuration

Fee tiers, profile limits, grace period and API limits. Changes rarely; cache it on your side too.

Cache-Controlpublic, max-age=300, s-maxage=3600, stale-while-revalidate=86400
curl
curl https://thoth.id/api/v1/config
JavaScript
const res = await fetch("https://thoth.id/api/v1/config");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "domain": "htr",
  "network": "testnet-india",
  "contractId": "00001f87ed606c28465afac15fe3805736993f77d4cc83da026531e120469d73",
  "blueprintId": "000000009108a3ab3c24297df5e33679177fb8a051c133bcad467a8a23bf0dd3",
  "contractVersion": "1.0.0",
  "gracePeriodDays": 30,
  "fees": {
    "currency": "HTR",
    "decimals": 2,
    "baseFee": 1,
    "periodDays": 365,
    "tiers": [
      {
        "minLength": 3,
        "maxLength": 3,
        "multiplier": 20,
        "amount": 20,
        "formatted": "0.20"
      },
      {
        "minLength": 4,
        "maxLength": 4,
        "multiplier": 10,
        "amount": 10,
        "formatted": "0.10"
      },
      {
        "minLength": 5,
        "maxLength": null,
        "multiplier": 1,
        "amount": 1,
        "formatted": "0.01"
      }
    ]
  },
  "limits": {
    "nameMinLength": 3,
    "nameMaxLength": 80,
    "maxProfileEntries": 20,
    "maxProfileKeyLength": 50,
    "maxProfileValueLength": 1000,
    "maxTotalProfileSize": 10000,
    "maxTokenSymbolLength": 5,
    "maxManagedNamesPerAddress": 100
  },
  "api": {
    "version": "1.0.0",
    "batchMaxItems": 50,
    "rateLimit": {
      "limit": 120,
      "windowSeconds": 60
    }
  }
}
GET/api/v1/health

Health check

API liveness plus reachability of the Hathor node(s). Returns 503 when no node is reachable. Not rate limited, never cached.

Cache-Controlno-store
curl
curl https://thoth.id/api/v1/health
JavaScript
const res = await fetch("https://thoth.id/api/v1/health");
if (!res.ok) {
  const problem = await res.json(); // application/problem+json
  throw new Error(`${problem.code}: ${problem.detail}`);
}
const data = await res.json();
Response 200
{
  "status": "ok",
  "apiVersion": "1.0.0",
  "time": "2026-09-23T21:00:00.000Z",
  "upstream": {
    "primary": {
      "reachable": true,
      "latencyMs": 142,
      "network": "testnet-india",
      "nodeVersion": "0.71.0"
    },
    "fallback": null
  }
}
08Errors

Every error is an RFC 9457 problem with a stable code. Branch on code, not on detail.

INVALID_NAME400

The name is not a valid .htr name (3-80 chars, a-z, 0-9 and single inner hyphens).

INVALID_ADDRESS400

The value is not a valid base58 Hathor address.

INVALID_REQUEST400

The request body or query string failed validation. See errors for details.

BATCH_TOO_LARGE400

The batch request contains more items than allowed.

NAME_NOT_FOUND404

The name is not registered, or its registration and grace period have lapsed.

NAME_EXPIRED404

The name is registered but expired (in its grace period), so it does not resolve.

PRIMARY_NAME_NOT_SET404

The address has no primary name, or its primary name is no longer active.

ROUTE_NOT_FOUND404

No API resource exists at this path.

METHOD_NOT_ALLOWED405

The HTTP method is not supported on this resource. See the Allow header.

PAYLOAD_TOO_LARGE413

The request body exceeds the maximum accepted size.

UNSUPPORTED_MEDIA_TYPE415

Request bodies must be sent as application/json.

RATE_LIMITED429

The client exceeded its rate limit. Retry after Retry-After seconds.

INTERNAL_ERROR500

An unexpected error occurred. Include X-Request-Id when reporting it.

UPSTREAM_ERROR502

The Hathor node returned an error or an unexpected response.

UPSTREAM_TIMEOUT504

The Hathor node did not respond in time. Safe to retry.

09Caching

Data comes from the chain, so responses are cached briefly at the edge. Clients should honour Cache-Control and revalidate with If-None-Match.

Resolution & recordspublic, max-age=15, s-maxage=30, stale-while-revalidate=60
Profilespublic, max-age=30, s-maxage=60, stale-while-revalidate=300
Availabilitypublic, max-age=5, s-maxage=10, stale-while-revalidate=20
Configpublic, max-age=300, s-maxage=3600, stale-while-revalidate=86400
404 responsespublic, max-age=5, s-maxage=15, stale-while-revalidate=30
Batch, health, 5xx, 429no-store
Example problem (404)
{
  "type": "https://thoth.id/developers#error-name-not-found",
  "title": "Name not found",
  "status": 404,
  "detail": "nobody-here.htr is not registered.",
  "instance": "/api/v1/names/nobody-here.htr/address",
  "code": "NAME_NOT_FOUND",
  "requestId": "5b0f3c1e-8d43-4c55-9d0a-2a57d1c2e9f1"
}