Skip to content

API documentation

A simple JSON API over HTTPS, plus webhooks that call your server when something happens. Search listings, read categories, extensions and recent sales, and (with a key) manage your listings, offers, orders, portfolio and webhook endpoints.

Base address

https://aiodomain.com/api/v1/<route>

Every answer is JSON. Open https://aiodomain.com/api/v1 for the live index: it lists every route with its method and scope. Query parameters go in the address for GET; a POST body may be a form (application/x-www-form-urlencoded) or JSON (application/json). Money is in USD. Times are YYYY-MM-DD HH:MM:SS in the site's time zone.

Authentication

Routes marked public work without a key, at a lower rate limit. Every other route needs an API key.

Send the key in the Authorization header, or in an X-API-Key header. A key looks like ad_ + 12 characters + _ + 40 characters; only a fingerprint of it is stored, so copy it when it is shown.

Authorization: Bearer ad_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Sign in or create an account, then make a key under API Keys (up to 5 per account). A key can be limited to a list of IP addresses and given an end date.

A revoked key, an expired key, a key used from an address outside its list, or a key of a suspended account answers 401 or 403.

Scopes

A key holds one or more scopes; a route needs the scope shown next to it. Pick the fewest scopes the integration needs.

  • public - no key needed
  • read - Read: search, listings, categories, sales (higher limits)
  • listings - Listings: create and update your own listings
  • profile - Profile: your account details, watchlist, notifications
  • trade - Trade: your offers and orders
  • portfolio - Portfolio: your domains, registrar sync
  • webhooks - Webhooks: manage your endpoints

Rate limits

  • Without a key: 30 requests per minute per IP address.
  • With a key: 120 requests per minute by default. Staff can give a key its own limit.
  • Membership plans raise the limit of every key of the member (Free: the default; Pro: 300 per minute; Business: 1000 per minute; Enterprise: 5000 per minute). The higher of the key's own limit and the plan's applies.

When the limit is reached the API answers 429 with "error": "Rate limit reached...". Wait a minute and try again; do not retry in a tight loop.

Errors

Every answer carries "ok". A failure has "ok": false, an "error" text meant for a human, and an HTTP status that says what kind of failure it was:

{
  "ok": false,
  "error": "This key lacks the \"listings\" scope"
}
StatusMeaning
200Done. The answer has "ok": true and the data.
400The request was understood but refused by a rule: a bad domain name, a price under the minimum, a listing that is not yours, a webhook address that is not https. The error text says which.
401No key, an invalid key, or a route that needs a key.
403The key lacks the scope, is used from a wrong address, or its account is not active.
404Unknown route (the answer lists the routes) or unknown version.
405Wrong method: the answer names the one to use.
429Rate limit reached.

Versioning

The version is the first path segment: /api/v1. Within v1, fields are added but not removed or renamed; new routes appear in the index. Any address without v1 answers 404 and points here. A breaking change would ship as /api/v2 next to v1, and v1 would keep working for at least a year after that.

Routes

29 routes, read from the running API

Public routes no key

RouteMethodWhat it does
/api/v1/domainsGETSearch listings: q, tld, type, price_min, price_max, len_min, len_max, category, verified, featured, sort, page, per
/api/v1/domainGETOne listing: /api/v1/domain/<name>
/api/v1/availabilityGETIs a domain listed here, registered elsewhere or free: ?domain=
/api/v1/extensionsGETExtensions with listing counts
/api/v1/categoriesGETCategories with listing counts
/api/v1/salesGETRecently sold domains (public ones)
/api/v1/eventsGETThe webhook event types
/api/v1/reviewsGETPublished reviews of a member: ?user=<username>

Read scope read

RouteMethodWhat it does
/api/v1/valuationGETA value estimate with confidence for ?domain= (an estimate, not a guarantee)

Listings scope listings

RouteMethodWhat it does
/api/v1/me/listingsGETYour listings: status, q, page
/api/v1/me/listings/createPOSTCreate a listing: name, listing_type, price, min_offer, description, category_id, tags, submit
/api/v1/me/listings/updatePOSTUpdate a listing: id + the fields of create
/api/v1/listingsGETMy listings: ?status=active|pending|draft|paused|sold&q=&page=
/api/v1/listing-createPOSTCreate a listing: name, listing_type (fixed|offer|fixed_offer), price, min_offer, title, description, category_id, tags, submit (1 = send for approval)
/api/v1/listing-updatePOSTUpdate a listing: /api/v1/listing-update/<id> with price, min_offer, title, description, tags, category_id
/api/v1/listing-statusPOSTPause, resume or submit a listing: /api/v1/listing-status/<id> with action=pause|resume|submit

Profile scope profile

RouteMethodWhat it does
/api/v1/meGETThe account behind the key
/api/v1/me/watchlistGETYour watchlist
/api/v1/me/notificationsGETYour newest notifications
/api/v1/membershipGETMy plan and its features

Trade scope trade

RouteMethodWhat it does
/api/v1/offersGETMy offers: ?side=buyer|seller&status=
/api/v1/ordersGETMy orders: ?side=buyer|seller&status=

Portfolio scope portfolio

RouteMethodWhat it does
/api/v1/portfolioGETMy portfolio: ?status=expiring|listed|unlisted|locked&registrar=&q=
/api/v1/portfolio-addPOSTAdd a domain to the portfolio: domain, registrar, expires_at, auto_renew, notes
/api/v1/portfolio-syncPOSTSync a connected registrar account now: account_id

Webhooks scope webhooks

RouteMethodWhat it does
/api/v1/webhooksGETMy webhook endpoints
/api/v1/webhook-createPOSTAdd an endpoint: url, events (array or comma list), description. The secret is returned once.
/api/v1/webhook-deletePOSTRemove an endpoint: /api/v1/webhook-delete/<id>
/api/v1/webhook-pingPOSTSend a test ping: /api/v1/webhook-ping/<id>

Routes written with /<id> take the id as the next path segment, for example POST /api/v1/listing-status/318 with action=pause. Lists are paged with page; the answer carries total and, where it applies, per_page.

Examples

Search for domains

curl "https://aiodomain.com/api/v1/domains?q=cloud&tld=com,io&price_max=5000&sort=price_asc"

One listing

curl "https://aiodomain.com/api/v1/domain/example.com"

Recent sales

curl "https://aiodomain.com/api/v1/sales?limit=10"

Your listings (scope listings)

curl -H "Authorization: Bearer YOUR_KEY" "https://aiodomain.com/api/v1/listings?status=active"

Create a listing and send it for approval (scope listings)

curl -X POST -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"name":"example.com","listing_type":"fixed_offer","price":1500,"min_offer":500,"description":"A short, memorable name","submit":1}' \
  "https://aiodomain.com/api/v1/listing-create"

Pause a listing (scope listings)

curl -X POST -H "Authorization: Bearer YOUR_KEY" -d "action=pause" "https://aiodomain.com/api/v1/listing-status/318"

Your offers and orders (scope trade)

curl -H "Authorization: Bearer YOUR_KEY" "https://aiodomain.com/api/v1/offers?side=seller&status=open"
curl -H "Authorization: Bearer YOUR_KEY" "https://aiodomain.com/api/v1/orders?side=buyer"

Add a domain to your portfolio (scope portfolio)

curl -X POST -H "Authorization: Bearer YOUR_KEY" -d "domain=example.net" -d "registrar=Namecheap" -d "expires_at=2027-05-01" "https://aiodomain.com/api/v1/portfolio-add"

Register a webhook endpoint (scope webhooks)

curl -X POST -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/aiodomain","events":["order.paid","offer.received"],"description":"My CRM"}' \
  "https://aiodomain.com/api/v1/webhook-create"

The answer carries the endpoint secret once. Keep it with your receiver; it is not returned again.

Webhooks

We call your server when something happens to your listings, offers, orders, auctions, portfolio or account.

Register an https endpoint under API Keys -> Webhooks or through POST /api/v1/webhook-create, and pick the events. Each account keeps up to 3 endpoints by default; membership plans allow more. Private network addresses and plain http are refused.

Events

EventGroupSent when
listing.approvedListingsA listing of mine went live
listing.rejectedListingsA listing of mine was rejected
listing.expiredListingsA listing of mine expired
listing.soldListingsA listing of mine sold
domain.verifiedListingsOwnership of a domain was verified
offer.receivedOffersAn offer arrived on my listing
offer.counteredOffersAn offer of mine was countered
offer.acceptedOffersAn offer was accepted
offer.declinedOffersAn offer was declined
order.createdOrdersAn order was placed (buyer or seller side)
order.paidOrdersAn order was paid
order.completedOrdersAn order completed
order.cancelledOrdersAn order was cancelled or refunded
transfer.updatedOrdersA domain transfer changed status
auction.bidAuctionsA bid on my auction, or I was outbid
auction.endedAuctionsAn auction of mine or one I bid on ended
inquiry.receivedMessagesA buyer sent an inquiry
subscription.updatedAccountMy membership changed
portfolio.expiringPortfolioA domain in my portfolio expires soon
review.receivedAccountI received a review
pingAccountTest ping

The same list is at GET /api/v1/events. Subscribing to * through the API receives every event.

The delivery

A delivery is one POST with a JSON body. id is the delivery id, event the name, created_at the time of the event (UTC), site the marketplace address and data the facts: ids, the domain, amounts in USD, the status. Payloads carry public facts only; read the record through the API for the rest.

{
    "id": "evt_9f1c2a7b3d4e5f6a7b8c9d0e",
    "event": "order.paid",
    "created_at": "2026-11-06T13:45:44+00:00",
    "site": "https://aiodomain.com/",
    "data": {
        "order_id": 1042,
        "number": "ORD-2026-1042",
        "kind": "buy_now",
        "status": "paid",
        "domain": "example.com",
        "listing_id": 318,
        "total": 1530,
        "price": 1500,
        "currency": "USD",
        "buyer_id": 77,
        "seller_id": 12
    }
}

Headers and the signature

Every delivery is a JSON POST with these headers:

X-AIOD-Eventthe event name, for example order.paid
X-AIOD-Deliverythe delivery id (evt_...); the same id on every retry, so you can ignore a repeat
X-AIOD-TimestampUnix time when it was sent; refuse it when older than 5 minutes
X-AIOD-Signaturesha256= HMAC-SHA256 of timestamp + "." + raw body with your endpoint secret

Compute the HMAC over the raw request body, compare with a constant-time function, then answer 2xx within 10 seconds. Anything else is retried.

POST /hooks/aiodomain HTTP/1.1
Content-Type: application/json
User-Agent: AIODomain-Webhooks/2.1.1
X-AIOD-Event: order.paid
X-AIOD-Delivery: evt_9f1c2a7b3d4e5f6a7b8c9d0e
X-AIOD-Timestamp: 1793972744
X-AIOD-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + body)>

Sign the raw body bytes exactly as received, never a re-encoded copy. Rotate the secret from the endpoint page whenever it may have leaked; the old secret stops working at once.

Verify in PHP

<?php
$secret = getenv('AIOD_WEBHOOK_SECRET');            // the secret shown once when you made the endpoint
$body   = file_get_contents('php://input');
$ts     = $_SERVER['HTTP_X_AIOD_TIMESTAMP'] ?? '';
$sig    = $_SERVER['HTTP_X_AIOD_SIGNATURE'] ?? '';

$want = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($want, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit('bad signature');
}
$event = json_decode($body, true);
// $event['id'] is the delivery id: store it and skip a repeat
// $event['event'] is the name, $event['data'] the facts
http_response_code(200);
echo 'ok';

Verify in Node.js

const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.AIOD_WEBHOOK_SECRET;

app.post('/hooks/aiodomain', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-AIOD-Timestamp') || '';
  const sig = req.get('X-AIOD-Signature') || '';
  const want = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + req.body).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  if (!fresh || want.length !== sig.length || !crypto.timingSafeEqual(Buffer.from(want), Buffer.from(sig))) {
    return res.status(400).send('bad signature');
  }
  const event = JSON.parse(req.body.toString());
  // event.id is the delivery id: remember it and skip a repeat
  res.status(200).send('ok');
});
app.listen(3000);

Answers, retries and switching off

  • Answer any 2xx status within 10 seconds. Do the work after answering when it takes longer; we only look at the status.
  • A timeout, a connection error or any other status is a failed attempt. We try again after 1 min, 5 min, 30 min, 2 h, 12 h; after the last try the delivery is marked failed. Redirects are not followed.
  • After 20 failed deliveries in a row the endpoint is switched off and you receive an e-mail. Fix the receiver, then switch it on again from the endpoint page; a successful delivery resets the count.
  • Every delivery and its answer is listed on the endpoint page, and any delivery can be re-sent from there by hand.

Idempotency

A retry carries the same X-AIOD-Delivery id and the same body as the first attempt, only the timestamp and signature differ. Store the ids you have handled (a day is enough) and answer 200 without acting when one comes again. Events can also arrive out of order after a retry: compare created_at or read the record through the API before acting on a status.

Test your receiver with the Send a test ping button: it delivers a ping event at once and shows the status and answer your server gave.

Need help with the API or webhooks?

Write to us and tell us what you are building.