Loganix Ops

/ Publisher Sites API

Publisher Sites API

This API reads and writes the publisher_sites table in Skynet (MySQL). That table holds the inventory record for each site a direct publisher has submitted to us — their pricing, content rules, turnaround time, and operational notes.

Every write immediately updates Skynet and logs a before/after snapshot to publisher_sites_diff. A human reviews those snapshots at /publisher-sites-review and marks them correct or rolls them back.

Auth & Format

No API key required — this is an internal ops tool on a private network.

All request bodies must be JSON (Content-Type: application/json). All responses are JSON. Prices are returned as floats. Timestamps (created_at) are ISO 8601 strings. The updated_at field is a Unix timestamp integer — the API sets it automatically on every write; you never need to send it.

Include "changed_by" in every write body so the diff log shows who or what made the change. Use a descriptive value like "ai-agent-email-parser" or your tool name.

Also include "reason" on every write — free text explaining why the change was made and what it is based on. This is shown to the human reviewer right next to the before/after. Put whatever you based the change on here: the body of the publisher's email, a quote from their guidelines, or the URL where you found the pricing (e.g. "Publisher emailed 2026-06-24: 'raising guest post to $175'" or "Found on https://example.com/write-for-us — listed price $175"). It accepts any length of text.

How the Diff Log Works

Every write (create, update, delete, lock, unlock) does two things atomically: applies the change to publisher_sites and writes a record to publisher_sites_diff showing exactly which fields changed, what their values were before and after, and the reason the change was made.

A human can then visit /publisher-sites-review, scan each change alongside its reason, and choose one of three actions: Correct (the change was right — mark it reviewed), Edit (it was almost right — fix the values in place, which applies the correction and marks the original as edited), or Rollback (revert it — restores the record to its previous state). Edit and Rollback each log their own diff entry, so the audit trail is complete.

The reason field is the key to review. Whatever made the change should pass a "reason" — the body of the publisher's email, a quote from their guidelines, or the URL where the pricing was found. The human reviewer sees it right next to the before/after, so they can confirm the change matches its source without leaving the page. See Auth & Format.

GET /api/publisher-sites

Returns a paginated list of publisher site records. Defaults to active sites only (inactive=0).

Query Parameters

ParamTypeDefaultDescription
q string "" Search by website domain or publisher email (partial match).
publisher string "" Filter to an exact publisher email address.
inactive int 0 0 = active sites only. 1 = soft-deleted sites only.
limit int 50 Rows per page. Max 200.
offset int 0 Row offset for pagination.

Example

GET /api/publisher-sites?q=techcrunch&limit=10
{
  "rows": [ { ... }, { ... } ],
  "total": 42,
  "limit": 10,
  "offset": 0
}
GET /api/publisher-sites/:id

Returns a single publisher site record by its numeric id. Returns 404 if not found.

Example

GET /api/publisher-sites/5
{
  "id": 5,
  "publisher": "webtechmania27@gmail.com",
  "website": "techmaina.com",
  "link_type": "dofollow",
  "tat": "1",
  "price_general": 80.0,
  // ... all 67 fields
}

POST /api/publisher-sites

Creates a new publisher site record. Only website and publisher are strictly required — all other fields are optional and can be added later via PATCH. Returns the new record's id and the full row as written.

Note for AI agents: The publisher field must be an email address. It must match an existing record in the publishers table (by pub_email or login_email). If you are parsing an email from a publisher, use the sender's email address as this value.

Required Fields

FieldTypeAllowed Values
website string Domain name, e.g. "example.com"
publisher string Publisher's email address. This is the foreign key to the publishers table.

Recommended Fields

FieldTypeAllowed Values
link_type enum
"dofollow" "nofollow"
tat enum Turnaround time in business days.
"1""2""3""4""5""6""7"
price_general decimal Standard placement price in USD. e.g. 80.00
permanent_link enum
"yes" "no"
do_we_post enum Whether we (Loganix) write and post the content.
"yes" "no"

Example Request

POST /api/publisher-sites
Content-Type: application/json

{
  "changed_by": "ai-email-parser",
  "reason": "New publisher signed up via outreach reply 2026-06-24; pricing from their email",
  "website": "example.com",
  "publisher": "owner@example.com",
  "link_type": "dofollow",
  "tat": "3",
  "price_general": 120.00,
  "permanent_link": "yes",
  "do_we_post": "yes"
}

Response (201 Created)

{
  "id": 9955,
  "row": { /* full record as saved */ }
}
PATCH /api/publisher-sites/:id

Updates one or more fields on an existing record. Send only the fields you want to change — everything else stays as-is. The updated_at field is set automatically. Returns the updated full row.

Undelete: To restore a soft-deleted site, send "inactive": 0. There is no separate undelete endpoint.
Fields you cannot change via PATCH: id and created_at are read-only. Use the dedicated /lock and /unlock endpoints for lock_publisher — though sending it via PATCH also works and will be logged correctly.

Example — update pricing

PATCH /api/publisher-sites/5
Content-Type: application/json

{
  "changed_by": "ai-email-parser",
  "reason": "Publisher emailed 2026-06-24: 'bumping general posts to $95, CBD to $140 effective now'",
  "price_general": 95.00,
  "price_cbd": 140.00
}

Example — restore a deleted site

PATCH /api/publisher-sites/5
Content-Type: application/json

{
  "changed_by": "ops-dashboard",
  "inactive": 0
}

Response

{
  "id": 5,
  "row": { /* full record as saved */ }
}
DELETE /api/publisher-sites/:id

Soft-deletes a site by setting inactive = 1. The record stays in the database and can be restored at any time with PATCH {"inactive": 0}. The diff log records this as a delete operation.

Request Body (optional)

DELETE /api/publisher-sites/5
Content-Type: application/json

{
  "changed_by": "ai-email-parser"
}

Response

{
  "id": 5,
  "inactive": 1
}
POST /api/publisher-sites/:id/lock

Sets lock_publisher = 1 on this site. A locked site is assigned exclusively to its direct publisher — vendor resellers cannot compete on it. Use this when we have confirmed the direct owner of a domain and want to protect them from vendor pricing.

Example

POST /api/publisher-sites/5/lock
Content-Type: application/json

{ "changed_by": "ai-email-parser" }

Response

{ "id": 5, "lock_publisher": 1 }
POST /api/publisher-sites/:id/unlock

Sets lock_publisher = 0 — removes the publisher lock so vendors can compete on this site again.

Example

POST /api/publisher-sites/5/unlock
Content-Type: application/json

{ "changed_by": "ops-dashboard" }

Response

{ "id": 5, "lock_publisher": 0 }

GET /api/publisher-sites/diffs

Returns the change log. Defaults to pending (unreviewed) changes. Each row includes a changed_fields array listing every field that changed with its before and after values.

Query Parameters

ParamTypeDefaultDescription
status string "pending" "pending" — not yet reviewed. "correct" — marked correct. "edited" — resolved by correcting values in place. "rolled_back" — reverted. "all" — no filter.
limit int 50 Max 200.
offset int 0 Pagination offset.

Response Shape

{
  "rows": [
    {
      "id": 1,
      "ps_id": 5,
      "website": "techmaina.com",
      "publisher": "webtechmania27@gmail.com",
      "operation": "update",
      "changed_by": "ai-email-parser",
      "reason": "Publisher emailed: raising general posts to $95",
      "changed_fields": [
        { "field": "price_general", "before": 80.0, "after": 95.0 }
      ],
      "status": "pending",
      "reviewed_by": null,
      "reviewed_at": null,
      "created_at": "2026-06-24T18:00:00"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST /api/publisher-sites/diffs/:diff_id/correct

Marks a diff record as correct — the change was verified and no action is needed. Nothing is written to publisher_sites.

Example

POST /api/publisher-sites/diffs/1/correct
Content-Type: application/json

{ "reviewed_by": "josh@loganix.com" }

Response

{ "diff_id": 1, "status": "correct" }
POST /api/publisher-sites/diffs/:diff_id/edit

Resolves a pending change by correcting the values in place — use this when the change was almost right but a value needs fixing. Applies your corrected values to the live publisher_sites row (which logs its own diff entry for the audit trail), then marks this original diff as edited. Pass only the fields you want to set under a "fields" object.

Request Body

POST /api/publisher-sites/diffs/2/edit
Content-Type: application/json

{
  "reviewed_by": "josh@loganix.com",
  "fields": {
    "price_general": 130.00
  }
}

Response

{
  "diff_id": 2,
  "status": "edited",
  "ps_id": 5,
  "row": { /* full record after the correction */ }
}
POST /api/publisher-sites/diffs/:diff_id/rollback

Reverts the change. Restores publisher_sites to the snapshot stored in before_json, marks the original diff as rolled_back, and logs the reversal itself as a new diff entry for audit trail purposes. Returns 400 if you try to roll back a create operation (there is no before state).

Example

POST /api/publisher-sites/diffs/1/rollback
Content-Type: application/json

{ "reviewed_by": "josh@loganix.com" }

Response

{ "diff_id": 1, "status": "rolled_back", "ps_id": 5 }

All Fields Reference

These fields apply to both POST (create) and PATCH (update). Fields marked read-only cannot be set via the API. The changed_by key is consumed by the API and never stored on the record itself.

Identity (read-only)
FieldTypeNotes
idintAuto-assigned primary key. Read-only.
created_attimestampSet on insert. Read-only.
updated_atint (unix)Set automatically on every write. Do not send this.
Core Identity
FieldTypeAllowed Values / Notes
publisherstringPublisher email. FK to publishers table.
websitestringDomain, e.g. "example.com"
link_typeenum
"dofollow" "nofollow"
tatenumTurnaround days.
"1""2""3""4""5""6""7"
permanent_linkenum
"yes" "no"
tenureintHow long (months) they expect the link to stay live. Nullable.
renewableenum
"yes" "no"
renewable_commentstextFree text notes on renewal terms.
live_linkstringURL of an example live placement on this site.
indexedenum
"yes" "no"
Pricing (all nullable decimals, USD)
FieldNotes
price_generalStandard niche placement.
price_cbdCBD / cannabis niche.
price_adultAdult content niche.
price_gamblingGambling niche.
price_cryptoCrypto niche.
price_paydayPayday loans niche.
price_general_editsPrice to edit an existing post (general).
price_cbd_editsEdit price, CBD.
price_adult_editsEdit price, adult.
price_gambling_editsEdit price, gambling.
price_crypto_editsEdit price, crypto.
price_payday_editsEdit price, payday.
youtube_embed_priceAdd-on price for embedding a YouTube video.
updates_after_live_priceAdd-on price for post edits after the link is live.
supporting_links_priceAdd-on price for adding internal supporting links.
content_chargeAdditional charge if publisher requires us to supply content.
Add-ons (boolean: 0 or 1)
FieldNotes
youtube_embedWhether publisher allows YouTube embeds.
"yes" "no"
updates_after_live1 if publisher allows edits after the post goes live.
supporting_links1 if publisher allows internal supporting links in the post.
Content & Posting Rules
FieldTypeAllowed Values / Notes
do_we_postenumWhether Loganix writes and posts the content.
"yes" "no"
mark_posts_choiceenumWhether posts are marked as sponsored.
"yes" "no"
article_styleenum
"parasite""guest_post""listicles""how_tos""pr""all""other"
other_article_stylestringFree text if article_style is "other".
client_link_typeenumWhat type of page we can link to.
"blog""service""homepage""all""other"
other_link_type_textstringFree text if client_link_type is "other".
client_urlstringExample client URL that has been placed on this site.
link_count_per_postintMax client links allowed per post.
how_many_postsintMax posts per month.
preapprove_topicenumWhether publisher must approve the topic first.
"yes" "no"
word_count_minimumintMinimum word count for submitted articles.
add_links_to_contentenumWhether we can add links within the article body.
"yes" "no"
anchor_limitenumWhether they cap anchor text repetition.
"yes" "no"
anchor_limit_detailsstringDetails of their anchor limit policy.
accepted_anchor_typestextTypes of anchor text they accept (branded, naked, keyword, etc.).
low_metricsenumWhether they accept low-metrics clients.
"yes" "no"
low_metrics_chargestringAny extra charge for low-metrics placements.
Author Bio
FieldTypeNotes
author_bioenum
"yes" "no"
bio_instructionsstringWhat they require in the bio if author_bio is yes.
Access & Guidelines
FieldTypeNotes
site_backend_urltextURL for the CMS backend (WordPress login page, etc.).
site_usernamestringCMS login username.
site_passwordstringCMS login password.
guidelines_urlstringLink to their published editorial guidelines.
guidelines_texttextFull text of their guidelines if no URL exists.
topic_guidelinestextNotes on which topics they accept or reject.
site_notestextInternal ops notes about this site.
AI & Plagiarism
FieldTypeNotes
ai_detection_toolstringWhich AI detection tool they run (e.g. "Originality.ai"). Empty string if none.
plagiarism_toolsstringWhich plagiarism checker(s) they use. Empty string if none.
Consent (boolean: 0 or 1)
FieldNotes
consent_communication1 if publisher consented to marketing communication.
privacy_consent1 if publisher accepted our privacy policy.
terms_conditions1 if publisher accepted our terms and conditions.
Status & Flags
FieldTypeNotes
inactiveint0 = active. 1 = soft-deleted. Use DELETE endpoint to set this, or PATCH with 0 to restore.
lock_publisherint0 = unlocked (vendors can compete). 1 = locked to direct publisher only. Use /lock and /unlock endpoints.
ux_scoreintInternal UX quality score, 0–100.
last_metrics_runint (unix)Timestamp of the last time metrics were fetched for this site.
client_deletedintWhether the client side of this record was removed. 0 or 1.