NameByes API v1
JSON over HTTPS. Base URL: https://dn.majhe.com/api/v1
Authentication
Create a key on the Developers page and send it on every request. Keys act as you, limited to their scopes. Keep them on your server: the API offers no CORS, so browser code cannot use them.
curl https://dn.majhe.com/api/v1/listings?tld=com \
-H "Authorization: Bearer nbk_xxxxxxxxxxxx_..."
| Scope | Allows |
|---|---|
read | Read public marketplace data: listings, listing details, sold comps |
account:read | Read your own listings, orders, offers and account summary |
listings:write | Change price, offer settings and listed/unlisted state of your own listings |
offers:write | Make offers and respond to offers (accept, decline, counter, withdraw) |
Errors, limits, pagination
- Errors:
{"error": {"code": "insufficient_scope", "message": "..."}}with a matching HTTP status (400, 401, 403, 404, 409, 413, 415, 422, 429). - Rate limits: 120 reads and 30 writes per minute per key;
429responses includeRetry-After. Offers share the website's limit of 20 per 10 minutes. - Lists take
?page=Nand return{"data": [...], "page": N, "has_more": true|false}. - Money is whole USDC (
amount_usdc,price_usdc). Times are ISO 8601 UTC.
Endpoints
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /listings | read | Domains for sale. Filters: q, tld, category, min_price, max_price, min_len, max_len, sort, view=all|featured|auctions. |
GET | /listings/{id} | read | One listing, with buy-now quote (fee included) and comparable sales. |
GET | /comps | read | Completed sales + summary stats. Filters: q, tld, min_price, max_price, min_len, max_len, period=30|90|365|all, sort. |
GET | /fees | read | Current fee and who pays it. ?price=1000 adds a quote. |
GET | /me | account:read | Your account summary and this key's scopes. |
GET | /my/listings | account:read | Your listings with 30-day views. status, q, sort, page. |
POST | /my/listings/bulk | listings:write | Change up to 200 of your listings at once (see below). |
GET | /my/orders | account:read | Orders where you are buyer or seller. Authorization codes are never returned. |
GET | /my/orders/{id} | account:read | One order with its timeline. |
GET | /my/offers | account:read | Your offers (as buyer and seller). |
GET | /my/offers/{id} | account:read | One offer: history, whose turn it is, and the version to respond to. |
POST | /offers | offers:write | Make an offer. |
POST | /offers/{id}/respond | offers:write | Accept, decline, counter or withdraw. |
GET | /webhooks/event-types | none | All webhook event types. |
Bulk listing changes
POST /api/v1/my/listings/bulk
{"ids": [12, 15], "action": "price_pct", "value": -10}
-> {"data": {"updated": 2, "skipped": []}}
Actions: price_set (value = new price), price_pct (-90..300), offers_on, offers_off, min_offer_pct (10-95, 0 clears),
lease_on (months, down_payment_percent), lease_off, unlist, relist, broker_set (value = seller-paid broker commission, 2-15 %), broker_off. Listings with a running order or lease are skipped and reported.
Sold comps
GET /api/v1/comps?tld=com&max_len=5&period=365
{"data": [{"domain": "brand.com", "price": 12000, "date": "2026-10-02", "sale_type": "sale", ...}],
"stats": {"count": 41, "median": 3200, "average": 5110, "highest": 48000, "total": 209510}, ...}
Offers
POST /api/v1/offers
{"domain_id": 42, "amount_usdc": 800, "message": "Optional, up to 300 characters"}
POST /api/v1/offers/7/respond
{"action": "counter", "amount_usdc": 950, "version": 3}
Always send the version you read from GET /my/offers/{id}. If the offer changed in the meantime you get 409 stale instead of acting on old terms. Accepting reserves the domain and the buyer pays on the website.
Webhooks
Add an https endpoint on the Developers page. We POST JSON like this:
POST https://your-server/hook
NameByes-Event: order.funded
NameByes-Delivery: 5f0c...
NameByes-Signature: t=1767225600,v1=3b1f...
{"id": "evt_tl_1832", "type": "order.funded", "created": "2026-01-01T00:00:00.000Z",
"data": {"order_id": "...", "domain": "brand.com", "amount_usdc": 12000, "your_role": "seller", "actor": "buyer"}}
- Reply with any 2xx within 10 seconds. Redirects are not followed.
- Failures are retried after 1m, 5m, 30m, 2h, 6h, 12h, 24h (8 attempts, about two days). After 25 failures in a row the endpoint is paused and you get a notification; resuming re-sends the last 3 days of failed events.
- Delivery is at least once: the same event can arrive twice. Use the event
idto ignore repeats. - Events are produced within about a minute of the change.
| Type | When |
|---|---|
order.funded | The buyer paid into escrow |
order.transfer_started | The seller started the domain transfer |
order.buyer_confirmed | The buyer confirmed they received the domain |
order.auto_confirmed | The registry showed the domain at the buyer's registrar and the order was confirmed automatically |
order.released | Payment was released to the seller |
order.disputed | A dispute was opened |
order.dispute_resolved | An admin resolved the dispute |
order.refund_requested | The buyer requested a refund after a missed deadline |
order.refund_contested | The seller contested a refund request |
order.refund_withdrawn | The buyer withdrew a refund request |
order.refunded | The buyer was refunded |
order.cancelled_by_seller | The seller cancelled and refunded the order |
order.deadline_extended | The seller used the one-time deadline extension |
order.registrar_changed | The registry shows the domain at a different registrar |
order.evidence_added | A party uploaded dispute evidence |
sale.completed | A sale or lease-to-own completed (you were the buyer or seller) |
offer.created | A new offer was made |
offer.countered | A counter-offer was made |
offer.accepted | An offer was accepted |
offer.declined | An offer was declined |
offer.withdrawn | An offer was withdrawn |
offer.expired | An offer expired without an answer |
bid.placed | Someone bid on your auction |
message.received | You received a message about a domain |
ping | Test event (sent when you press Send test) |
Verifying signatures
Compute HMAC-SHA256 of <t>.<raw body> with your signing secret, compare it to v1 in constant time, and reject timestamps older than 5 minutes.
// Node.js / Workers
import { createHmac, timingSafeEqual } from "node:crypto"
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex")
return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
# Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])