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.
"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.
Returns a paginated list of publisher site records. Defaults to active sites only (inactive=0).
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| 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
}
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
}
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.
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
| Field | Type | Allowed 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
| Field | Type | Allowed 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 */ }
}
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.
"inactive": 0.
There is no separate undelete endpoint.
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 */ }
}
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
}
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 }
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 }
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
| Param | Type | Default | Description |
|---|---|---|---|
| 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
}
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" }
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 */ }
}
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.
| Field | Type | Notes |
|---|---|---|
| id | int | Auto-assigned primary key. Read-only. |
| created_at | timestamp | Set on insert. Read-only. |
| updated_at | int (unix) | Set automatically on every write. Do not send this. |
| Field | Type | Allowed Values / Notes |
|---|---|---|
| publisher | string | Publisher email. FK to publishers table. |
| website | string | Domain, e.g. "example.com" |
| link_type | enum | "dofollow" "nofollow" |
| tat | enum | Turnaround days. "1""2""3""4""5""6""7" |
| permanent_link | enum | "yes" "no" |
| tenure | int | How long (months) they expect the link to stay live. Nullable. |
| renewable | enum | "yes" "no" |
| renewable_comments | text | Free text notes on renewal terms. |
| live_link | string | URL of an example live placement on this site. |
| indexed | enum | "yes" "no" |
| Field | Notes |
|---|---|
| price_general | Standard niche placement. |
| price_cbd | CBD / cannabis niche. |
| price_adult | Adult content niche. |
| price_gambling | Gambling niche. |
| price_crypto | Crypto niche. |
| price_payday | Payday loans niche. |
| price_general_edits | Price to edit an existing post (general). |
| price_cbd_edits | Edit price, CBD. |
| price_adult_edits | Edit price, adult. |
| price_gambling_edits | Edit price, gambling. |
| price_crypto_edits | Edit price, crypto. |
| price_payday_edits | Edit price, payday. |
| youtube_embed_price | Add-on price for embedding a YouTube video. |
| updates_after_live_price | Add-on price for post edits after the link is live. |
| supporting_links_price | Add-on price for adding internal supporting links. |
| content_charge | Additional charge if publisher requires us to supply content. |
| Field | Notes |
|---|---|
| youtube_embed | Whether publisher allows YouTube embeds. "yes" "no" |
| updates_after_live | 1 if publisher allows edits after the post goes live. |
| supporting_links | 1 if publisher allows internal supporting links in the post. |
| Field | Type | Allowed Values / Notes |
|---|---|---|
| do_we_post | enum | Whether Loganix writes and posts the content. "yes" "no" |
| mark_posts_choice | enum | Whether posts are marked as sponsored. "yes" "no" |
| article_style | enum | "parasite""guest_post""listicles""how_tos""pr""all""other" |
| other_article_style | string | Free text if article_style is "other". |
| client_link_type | enum | What type of page we can link to. "blog""service""homepage""all""other" |
| other_link_type_text | string | Free text if client_link_type is "other". |
| client_url | string | Example client URL that has been placed on this site. |
| link_count_per_post | int | Max client links allowed per post. |
| how_many_posts | int | Max posts per month. |
| preapprove_topic | enum | Whether publisher must approve the topic first. "yes" "no" |
| word_count_minimum | int | Minimum word count for submitted articles. |
| add_links_to_content | enum | Whether we can add links within the article body. "yes" "no" |
| anchor_limit | enum | Whether they cap anchor text repetition. "yes" "no" |
| anchor_limit_details | string | Details of their anchor limit policy. |
| accepted_anchor_types | text | Types of anchor text they accept (branded, naked, keyword, etc.). |
| low_metrics | enum | Whether they accept low-metrics clients. "yes" "no" |
| low_metrics_charge | string | Any extra charge for low-metrics placements. |
| Field | Type | Notes |
|---|---|---|
| author_bio | enum | "yes" "no" |
| bio_instructions | string | What they require in the bio if author_bio is yes. |
| Field | Type | Notes |
|---|---|---|
| site_backend_url | text | URL for the CMS backend (WordPress login page, etc.). |
| site_username | string | CMS login username. |
| site_password | string | CMS login password. |
| guidelines_url | string | Link to their published editorial guidelines. |
| guidelines_text | text | Full text of their guidelines if no URL exists. |
| topic_guidelines | text | Notes on which topics they accept or reject. |
| site_notes | text | Internal ops notes about this site. |
| Field | Type | Notes |
|---|---|---|
| ai_detection_tool | string | Which AI detection tool they run (e.g. "Originality.ai"). Empty string if none. |
| plagiarism_tools | string | Which plagiarism checker(s) they use. Empty string if none. |
| Field | Notes |
|---|---|
| consent_communication | 1 if publisher consented to marketing communication. |
| privacy_consent | 1 if publisher accepted our privacy policy. |
| terms_conditions | 1 if publisher accepted our terms and conditions. |
| Field | Type | Notes |
|---|---|---|
| inactive | int | 0 = active. 1 = soft-deleted. Use DELETE endpoint to set this, or PATCH with 0 to restore. |
| lock_publisher | int | 0 = unlocked (vendors can compete). 1 = locked to direct publisher only. Use /lock and /unlock endpoints. |
| ux_score | int | Internal UX quality score, 0–100. |
| last_metrics_run | int (unix) | Timestamp of the last time metrics were fetched for this site. |
| client_deleted | int | Whether the client side of this record was removed. 0 or 1. |