skip to content →
FIELD_MANUAL / LIVEREVISIONv1.3 · 2026.10.04Updated 2026-10-04CHAPTERS12 FILEDCH_05INTEGRATION
[ CH_05.06 / FIELD_MANUAL ]

Custom API

[ CH_05.06 / CUSTOM_API ]

Publishing to any endpoint

The Custom API adapter lets you ship posts to any REST endpoint. VectraSEO creates a post by POSTing JSON to {endpoint_url}{publish_path}, and updates it by PUTting the same payload to {endpoint_url}{publish_path}/{id}. The payload follows ingest contract v2. For destinations with product capability rules, expose a public /.well-known/product-capabilities.json contract and save its URL in project settings so unsupported claims can be remediated or held before publish.

POST /posts — payload.json
{
  "contract_version": 2,
  "title": "Post Title",
  "seo_title": "Search Result Title",
  "body_html": "<div data-vectraseo-body><p>Full HTML content...</p></div>",
  "slug": "post-title",
  "tags": ["seo", "blogging"],
  "meta_description": "Useful search snippet for this post.",
  "excerpt": "Reader-facing post summary.",
  "author": "Author Name",
  "author_entity": {
    "name": "Author Name",
    "url": "https://yoursite.com/about/author",
    "image": "https://yoursite.com/img/author.jpg"
  },
  "canonical_url": "https://yoursite.com/blog/post-title",
  "og_image": "https://cdn.vectraseo.com/posts/.../hero.png",
  "json_ld": {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Search Result Title",
    "description": "Useful search snippet for this post.",
    "datePublished": "2026-03-30T00:00:00Z",
    "dateModified": "2026-06-12T00:00:00Z",
    "author": { "@type": "Person", "name": "Author Name" },
    "publisher": { "@type": "Organization", "name": "Your Business", "url": "https://yoursite.com" },
    "image": "https://cdn.vectraseo.com/posts/.../hero.png",
    "mainEntityOfPage": { "@type": "WebPage", "@id": "https://yoursite.com/blog/post-title" }
  },
  "images": [
    {
      "filename": "image.png",
      "data": "<base64-encoded>",
      "alt": "Alt text"
    }
  ],
  "render_hints": {
    "show_table_of_contents": false,
    "show_in_this_guide": false
  },
  "published_at": "2026-03-30T00:00:00Z",
  "updated_at": "2026-06-12T00:00:00Z",
  "status": "published"
}

Contract v2 fields

Every v2 field is additive, so an endpoint written for the earlier payload keeps working.

FIELD
MEANING
contract_version
Always 2.
canonical_url
canonical_base_url + / + slug. Empty unless canonical_base_url is configured.
author_entity
{name, url?, image?} from author_name (else author, else the business name), author_url and author_image_url. The flat author string carries the same name.
og_image
The post's hero image as a public CDN URL; empty when the post has no image. Independent of the base64 images array.
json_ld
A complete schema.org Article node, ready to embed in a <script type="application/ld+json"> tag on the post page.
published_at
The post's first publication moment. It stays the same on every republish.
updated_at
Moves on every send. Use it for your sitemap <lastmod> so refreshes reach crawlers.
render_hints
The body already has its final structure; templates should not insert their own table of contents or "In this guide" block.
status
The post's state on your site: published, on a create and on an update alike.

Configuration fields

FIELD
REQ
DESCRIPTION
endpoint_url
YES
Base URL of the target API (e.g. https://api.mysite.com)
api_key
YES
Authentication key sent with each request
auth_header
opt
Header name for API key. Default: Authorization (Bearer prefix).
publish_path
opt
POST path appended to base URL. VECTRASEO preset: /blog/publish
capability_contract_url
opt
Public product capability contract, usually /.well-known/product-capabilities.json
author
opt
Legacy flat author string. Defaults to the project business name. Prefer author_name.
canonical_base_url
opt
Public base URL posts live under. Sets canonical_url (base + "/" + slug) and the JSON-LD mainEntityOfPage.
author_name
opt
Author display name for author_entity and JSON-LD. Overrides author.
author_url
opt
Author profile or about page.
author_image_url
opt
Author headshot URL.

Success response

Answer a create with JSON carrying the post's id and public url. VectraSEO stores that id and addresses every later update and delete as {publish_path}/{id}, so persist it on your side. VectraSEO treats any 2xx as “the post is live”: on a static or CDN-backed site, return it only after the page has been re-rendered, written and its CDN path invalidated. When canonical_base_url is set and the url you return is on a different host, VectraSEO keeps the canonical URL instead.

POST {your publish_path} → 2xx
{
  "id": "post-123",
  "url": "https://yoursite.com/blog/post-title"
}

Updates, republishes and deletes

REQUEST
WHEN IT IS SENT
POST {publish_path}
The first publish of a post.
PUT {publish_path}/{id}
Every later change: edits, republishes and one-click fixes. VectraSEO never re-POSTs to edit a live post.
DELETE {publish_path}/{id}
A post is removed or unpublished.
> implement_put_or_fixes_are_skipped

If your API has no route for PUT {publish_path}/{id} and answers 404 or 405, VectraSEO treats it as a setup gap on your endpoint, not a content problem. The post keeps its published state, the attempt is shown on the post as a hold, and one-click fixes report skipped with reason destination_no_update_route. Your live page never reflects the fix until the route exists. Make PUT an upsert keyed on the {id} in the path, and have it do the same render and cache-invalidation work as your POST.

  • PUT and DELETE carry the same auth header as POST, and PUT sends the full payload.
  • A create is attempted up to 3 times, with backoff, on a 5xx, 408, 429 or connection error. Any other 4xx fails fast and keeps your response body, so return one only for a genuine validation error.
  • A failed update (outage, rejection or missing route) never changes a live post's published state; the attempt is recorded on the post.
  • Behind a CDN, allow POST, PUT and DELETE on the publish path. A POST-only behaviour silently blocks republishes and deletes.

The body marker

body_html arrives wrapped in one transparent element, <div data-vectraseo-body>…</div>. Render it verbatim and keep its data-* attribute. It has no styling effect, and it is how a one-click fix recovers the exact editable content from your live page. Without it, VectraSEO falls back to your page's single <main> or <article> element and checks it against the last body it sent; if that is not a confident match, the fix is skipped (extract_low_confidence) rather than guessed. A post without the marker gains it the next time it is republished.

Product capability contract

Use this optional public JSON contract to tell VectraSEO which destination-product capabilities are supported, unsupported, planned, beta, deprecated, or unknown.

GET /.well-known/product-capabilities.json
{
  "schema_version": "1.0",
  "product": { "name": "Emcognito", "aliases": ["Emcognito aliases"] },
  "updated_at": "2026-06-12T00:00:00Z",
  "cache_ttl_seconds": 3600,
  "capabilities": [
    {
      "id": "custom_domains",
      "name": "Custom domains",
      "status": "unsupported",
      "blocked_claims": ["custom domain", "custom domains"],
      "allowed_qualifiers": ["does not support", "planned", "roadmap", "subdomain"],
      "preferred_wording": "Emcognito aliases currently use the shared emcognito.com domain; custom subdomain support is planned, but custom domains are not available today."
    }
  ]
}

Rejection response (tell VectraSEO what to avoid)

When you reject a publish because the post claims something your product can’t back, return 422 with a structured violations array. VectraSEO records each rejected claim on the project and excludes it from future generations. A bare 400 with no body is treated as a transient failure and the same claim may be generated again.

POST {your publish_path} → 422
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": "capability_claim_violation",
  "message": "Post asserts claims this product cannot back.",
  "violations": [
    {
      "capability_id": "open_source",
      "matched_claim": "open source",
      "status": "unsupported",
      "field": "body_html",
      "preferred_wording": "Sendant's source code is not public; do not describe it as open source."
    }
  ]
}

For a post that has never been published, a structured rejection like this also starts a free content repair automatically, a limited number of times, and the publish is retried after it. A post that was published before is never repaired this way.

[ END_OF_MANUAL ]
[ NEED_HELP? ]

Can't find what
you're looking for?

Reach out to the team. We answer every email and we read every bug report.