This API is not available yet

Every endpoint documented here returns 404 today.

Developers

The Linkyard API

A REST interface over the same pipeline the dashboard uses: bearer auth, idempotent order creation, signed webhooks, no SDK. This page is the design it will be built to.

This API is not available yet

No endpoint in the API reference is live: a request to any of them returns 404 today. What is published is the specification being built to, so an integration can be designed against a stable contract before the routes ship.

Treat everything below as the contract being worked towards, not as something you can call. Nothing on this page is a description of shipped behaviour.

Authentication

Requests will carry your key as a bearer token, and a key will be shown once at creation. Keys are not being issued yet, because there is nothing for one to authenticate against.

Authorization: Bearer ly_live_…

Base URL

Endpoints will be relative to this, on the canonical host — redirects strip the Authorization header. Nothing is served under it yet.

https://seo.nulldesign.co/api/v1

Payment

Orders will draw down your account balance. There is no card on file to charge, which is why balance is the only funding path planned for API orders.

402 insufficient_balance

Your first call

Free, and it will not touch your balance — the natural way to confirm a key works. Run it today and you get an HTML 404 from the framework, not the JSON error envelope documented below, because the route does not exist.

curl https://seo.nulldesign.co/api/v1/account \
  -H "Authorization: Bearer ly_live_..."

Reference

Endpoints

The ordering, project, marketplace and reporting surfaces of the dashboard — not billing, affiliate or account management. None of it is built yet: every endpoint below is marked accordingly, and every one of them returns 404.

Account

Your balance, usage and limits.

GET/accountnot implementedfree

Retrieve the authenticated account

Returns email, balance in cents, webhook configuration and current usage against the project and report caps.

curl https://seo.nulldesign.co/api/v1/account \
  -H "Authorization: Bearer ly_live_..."

Products

The catalog and the intake schema each product expects.

GET/productsnot implementedfree

List the public catalog

Excludes subscription products and anything with special pricing. Includes volume tiers.

GET/products/{id}/intake-schemanot implementedfree

Get the intake JSON Schema for a product

Returns a JSON Schema describing exactly what the `intake` object must contain for this product. Validate against it before creating an order.

Marketplace

Publisher inventory and its filter facets.

GET/marketplace/listingsnot implementedrate limited

List publisher inventory

Paginated. Domains are included for verified accounts. Heavily rate limited because the full inventory is the product.

typestring
guest_post | link_insert | quote_link | homepage_link
min_drinteger
Lower bound on Domain Rating.
max_priceinteger
Upper bound in cents.
nichestring
Repeatable. Matches any listed niche.
pageinteger
1-based. 25 per page.
GET/marketplace/filtersnot implementedfree

List available filter values

Distinct niches, languages, countries and the price and DR bounds currently in inventory.

Orders

Create orders, poll delivery, read the message thread.

POST/orders/validatenot implementedfree

Dry-run an order

Validates intake, prices the order and checks your balance without charging anything or consuming uploads. Always call this first while you are building an integration.

POST/ordersnot implementedcharges balance

Create an order

Deducts the total from your balance and queues fulfillment. Pass `idempotency_key` so a retried request cannot create a duplicate.

items*array
Each item: product_id, quantity, intake.
project_idstring
Attaches the order to a project.
idempotency_keystring
Safe retries. Unique per order.
curl -X POST https://seo.nulldesign.co/api/v1/orders \
  -H "Authorization: Bearer ly_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "order-2026-08-10-001",
    "items": [{
      "product_id": "prod_...",
      "quantity": 5,
      "intake": {
        "target_url": "https://example.com/pricing",
        "anchor_text": "pricing page",
        "keyword": "project management pricing"
      }
    }]
  }'
GET/orders/{id}not implementedfree

Retrieve an order

Status, items, pricing and the intake state of each item.

GET/orders/{id}/deliverynot implementedfree

Poll for deliverables

Live URLs, anchors and verification state. For a managed-budget parent this consolidates progress across every child order.

GET/orders/{id}/childrennot implementedfree

List child orders

Managed-budget parents only.

GET/orders/{id}/messagesnot implementedfree

Read the message thread

POST/orders/{id}/messagesnot implementedfree

Post a reply

Notifies the person handling the order by email.

File uploads

Attach files to an order's intake.

POST/intake-attachmentsnot implementedfree

Upload a file

Max 10 MB. Images, PDF, text, CSV, Word and Excel. The returned id expires after 24 hours and can be used by exactly one order.

Projects

The domains, keywords and brand terms everything is measured against.

POST/projectsnot implementedfree

Create a project

Only `name` is required.

GET/projectsnot implementedfree

List projects

Newest first.

GET/projects/{id}not implementedfree

Retrieve a project

PATCH/projects/{id}not implementedfree

Update a project

`urls` and `keywords` replace the whole list rather than appending.

Reports

White-label report pages and their custom domains.

POST/reportsnot implementedfree

Create a branded report

Requires an active reporting subscription and a valid project_id. Supplying `custom_domain` registers the domain and provisions SSL automatically.

GET/reportsnot implementedfree

List reports

GET/reports/{id}not implementedfree

Retrieve a report

PATCH/reports/{id}not implementedfree

Update branding, domain or active state

Webhooks

Get pushed updates instead of polling.

POST/webhooksnot implementedfree

Register an endpoint

Returns a signing secret, shown once.

Webhooks

Get told instead of asking

The plan: register an endpoint and we push order events to it, retrying a failed delivery up to five times with exponential backoff, starting a minute out, before dropping it.

Outbound webhooks are not sending

Nothing is delivered to registered endpoints today. This is separate from the endpoints above: even once the REST routes ship, the delivery worker has to be built before any of the events below arrive.

Events

order.created
An order was accepted and queued.
order.status_changed
Any order-level status transition.
order.item_delivered
A single placement went live and verified.
order.completed
Every item on the order is delivered.
order.fully_delivered
Managed budget: every child order finished.
order.failed
An item could not be delivered and was refunded.
order.message_received
Staff replied on an order thread.

Verifying a delivery

Every request carries a X-LY-Signature header. Recompute the HMAC over {timestamp}.{rawBody} using your signing secret and compare in constant time. Reject anything older than five minutes.

X-LY-Signature: t=1786377600,v1=8f3c...

// Node
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("="))
  );
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) return false;

  return timingSafeEqual(
    Buffer.from(parts.v1),
    Buffer.from(expected)
  );
}

Use the raw request body, not a re-serialized JSON object — any whitespace difference changes the signature.

Errors

The envelope every error will use. None of these codes is reachable yet — an unbuilt route answers with a plain 404.

{
  "error": {
    "code": "validation_failed",
    "message": "intake.target_url must be a valid URL",
    "fields": ["items.0.intake.target_url"]
  }
}
400 invalid_request
Malformed body, unsupported file type, or an unavailable product.
401 unauthorized
Missing, malformed or revoked API key.
402 insufficient_balance
Top up, or lower the quantity.
402 subscription_required
Reports need an active reporting plan.
404 not_found
No such resource, or it belongs to another account.
409 conflict
Slug or custom domain already in use.
422 validation_failed
Intake did not match the product's schema. See `error.fields`.
429 rate_limited
Back off; check the Retry-After header.
500 server_error
Our fault. Safe to retry with the same idempotency key.

Limits

What the API will enforce once it ships. Nothing enforces them today.

Rate limits

All endpoints
60 requests / minute per key
GET /marketplace/listings
20 requests / 24 hours per account
POST /intake-attachments
100 uploads / hour per account

Account caps

Projects per account
150
Reports per account
150
Tracked keywords
By plan
Attachment size
10 MB

Idempotency

Pass a unique idempotency_key on every order. A retry with the same key returns the original order instead of creating a second one — which matters, because order creation moves money.

Keys are not being issued yet

Key creation is paused until the endpoints exist. A key issued today would authenticate nothing, and because a key is shown only once it would have to be reissued anyway. When the routes ship, the Developers tab in your dashboard is where keys will live. Until then, the fastest way to influence what gets built first is to tell us what you need from it.