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 neededread- Read: search, listings, categories, sales (higher limits)listings- Listings: create and update your own listingsprofile- Profile: your account details, watchlist, notificationstrade- Trade: your offers and ordersportfolio- Portfolio: your domains, registrar syncwebhooks- 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"
}
| Status | Meaning |
|---|---|
200 | Done. The answer has "ok": true and the data. |
400 | The 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. |
401 | No key, an invalid key, or a route that needs a key. |
403 | The key lacks the scope, is used from a wrong address, or its account is not active. |
404 | Unknown route (the answer lists the routes) or unknown version. |
405 | Wrong method: the answer names the one to use. |
429 | Rate 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
| Route | Method | What it does |
|---|---|---|
/api/v1/domains | GET | Search listings: q, tld, type, price_min, price_max, len_min, len_max, category, verified, featured, sort, page, per |
/api/v1/domain | GET | One listing: /api/v1/domain/<name> |
/api/v1/availability | GET | Is a domain listed here, registered elsewhere or free: ?domain= |
/api/v1/extensions | GET | Extensions with listing counts |
/api/v1/categories | GET | Categories with listing counts |
/api/v1/sales | GET | Recently sold domains (public ones) |
/api/v1/events | GET | The webhook event types |
/api/v1/reviews | GET | Published reviews of a member: ?user=<username> |
Read scope read
| Route | Method | What it does |
|---|---|---|
/api/v1/valuation | GET | A value estimate with confidence for ?domain= (an estimate, not a guarantee) |
Listings scope listings
| Route | Method | What it does |
|---|---|---|
/api/v1/me/listings | GET | Your listings: status, q, page |
/api/v1/me/listings/create | POST | Create a listing: name, listing_type, price, min_offer, description, category_id, tags, submit |
/api/v1/me/listings/update | POST | Update a listing: id + the fields of create |
/api/v1/listings | GET | My listings: ?status=active|pending|draft|paused|sold&q=&page= |
/api/v1/listing-create | POST | Create 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-update | POST | Update a listing: /api/v1/listing-update/<id> with price, min_offer, title, description, tags, category_id |
/api/v1/listing-status | POST | Pause, resume or submit a listing: /api/v1/listing-status/<id> with action=pause|resume|submit |
Profile scope profile
| Route | Method | What it does |
|---|---|---|
/api/v1/me | GET | The account behind the key |
/api/v1/me/watchlist | GET | Your watchlist |
/api/v1/me/notifications | GET | Your newest notifications |
/api/v1/membership | GET | My plan and its features |
Trade scope trade
| Route | Method | What it does |
|---|---|---|
/api/v1/offers | GET | My offers: ?side=buyer|seller&status= |
/api/v1/orders | GET | My orders: ?side=buyer|seller&status= |
Portfolio scope portfolio
| Route | Method | What it does |
|---|---|---|
/api/v1/portfolio | GET | My portfolio: ?status=expiring|listed|unlisted|locked®istrar=&q= |
/api/v1/portfolio-add | POST | Add a domain to the portfolio: domain, registrar, expires_at, auto_renew, notes |
/api/v1/portfolio-sync | POST | Sync a connected registrar account now: account_id |
Webhooks scope webhooks
| Route | Method | What it does |
|---|---|---|
/api/v1/webhooks | GET | My webhook endpoints |
/api/v1/webhook-create | POST | Add an endpoint: url, events (array or comma list), description. The secret is returned once. |
/api/v1/webhook-delete | POST | Remove an endpoint: /api/v1/webhook-delete/<id> |
/api/v1/webhook-ping | POST | Send 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
| Event | Group | Sent when |
|---|---|---|
listing.approved | Listings | A listing of mine went live |
listing.rejected | Listings | A listing of mine was rejected |
listing.expired | Listings | A listing of mine expired |
listing.sold | Listings | A listing of mine sold |
domain.verified | Listings | Ownership of a domain was verified |
offer.received | Offers | An offer arrived on my listing |
offer.countered | Offers | An offer of mine was countered |
offer.accepted | Offers | An offer was accepted |
offer.declined | Offers | An offer was declined |
order.created | Orders | An order was placed (buyer or seller side) |
order.paid | Orders | An order was paid |
order.completed | Orders | An order completed |
order.cancelled | Orders | An order was cancelled or refunded |
transfer.updated | Orders | A domain transfer changed status |
auction.bid | Auctions | A bid on my auction, or I was outbid |
auction.ended | Auctions | An auction of mine or one I bid on ended |
inquiry.received | Messages | A buyer sent an inquiry |
subscription.updated | Account | My membership changed |
portfolio.expiring | Portfolio | A domain in my portfolio expires soon |
review.received | Account | I received a review |
ping | Account | Test 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-06T12:57:50+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-Event | the event name, for example order.paid |
|---|---|
| X-AIOD-Delivery | the delivery id (evt_...); the same id on every retry, so you can ignore a repeat |
| X-AIOD-Timestamp | Unix time when it was sent; refuse it when older than 5 minutes |
| X-AIOD-Signature | sha256= 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: 1793969870 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.