Skip to the reference
Media Partners

Somo Media Partner program

API reference

The full contract for the Somo Content Service API, version 1. New to the API? Start with the integration guide. You can also download the OpenAPI file.

Somo Content Service API (1.0.0)

Download OpenAPI specification:Download

Submit articles for editorial review and publication on Somo properties.

The Somo Content Service (SCS) API is how Somo media partners submit articles. Every article is reviewed by a human editor before anything is published.

Contract rules

  • This is v1. Fields are only ever added, never changed or removed. Ignore fields you do not recognize.
  • Requests and responses are JSON (application/json, UTF-8), except POST /oauth/token, which takes application/x-www-form-urlencoded (RFC 6749).
  • Unknown request fields are refused (validation-failed), so a misspelled field is an error.
  • Times are RFC 3339 in UTC. Ids are UUIDs. String formats (uri, uuid, date, date-time) are validated, not just documented.
  • Text fields are plain text: < and > are refused. Before validation, CRLF and CR become LF and every text field is NFC-normalized. Control and format characters (Unicode Cc/Cf) are refused, with these exceptions: tab and newline in body; U+200C, U+200D and U+FE0F (emoji sequences and joiners) in body and in plain-text fields. Bidi controls (U+202A–U+202E, U+2066–U+2069) are always refused. Only body carries markup.
  • A path id that is not a UUID answers 404 not-found, exactly like an unknown id or another client's id.
  • Tolerate unknown values. Responses and webhooks may gain new enum values (statuses, event types, reasons, states) and new fields. Treat an unknown value as "something else", never as an error.
  • Every response carries X-Request-Id; quote it when you contact us.

Onboarding. There is no registration or sign-up endpoint. Credentials are issued by Somo after onboarding through the Somo Media Partner program: you send us your public key, we register it and give you a client_id.

Idempotency. Successes and schema-level validation answers are stored per Idempotency-Key and replayed. Everything else (including validation that depends on a property's configuration) is worked out again. After fixing a refused request, send it with a new key: the old key with a changed body is 409 idempotency-key-reused.

Authentication. OAuth 2.0 client credentials with private_key_jwt client authentication (RFC 7523). Exchange a signed assertion at POST /oauth/token for a 15-minute bearer token. An API key is available only by exception and is sent as the bearer token directly.

Scopes. submissions:write, submissions:read:own, assets:write, webhooks:manage, plus one submit:property:<property id> per property you may submit to (for example submit:property:example-property). Property scopes are not listed in the security requirements below because their names depend on the property.

Errors are RFC 9457 problem details (application/problem+json) with a stable type URI. The type is what to branch on; title and detail are for people and may change. Known types (all under https://api.content.somo.financial/problems/, the same in every environment):

type status meaning
invalid-request 400 malformed JSON, bad header or query parameter
idempotency-key-required 400 Idempotency-Key missing or malformed
invalid-token 401 missing, expired or revoked token
insufficient-scope 403 the token lacks a scope this call needs
property-not-allowed 403 the property is unknown, or you may not submit to it (one answer for both)
not-found 404 no such resource, or it is not yours (one answer for both, on reads and writes)
invalid-state 409 the submission's status does not allow this action
publishing-in-progress 409 POST /v1/submissions/{id}/withdraw only: a publication of this submission is under way or waiting on an editor; retry after Retry-After
idempotency-key-reused 409 same key, different request
secret-rotation-in-progress 409 a webhook secret rotation is still in its 24-hour overlap
duplicate-external-ref 409 externalRef already used on an open submission
payload-too-large 413 request body over 256 KB
unsupported-media-type 415 wrong Content-Type
validation-failed 422 the body breaks the schema or a content rule (for example a non-https link, or HTML with CDATA, a declaration, a processing instruction or a comment containing markup); see errors
asset-not-usable 422 a referenced asset is missing or not yours (reason: not_found, one answer for both), or yours but rejected / expired
ymyl-not-allowed 422 your agreement does not cover YMYL topics
property-paused 422 the property is not accepting submissions
rate-limited 429 too many requests; see Retry-After
quota-exceeded 429 your submission quota for this property is used up; see Retry-After
internal-error 500 our fault; safe to retry with the same Idempotency-Key
service-unavailable 503 the request couldn't be completed right now (for example, an earlier request with the same Idempotency-Key is still running); retry after Retry-After

Webhooks are listed under webhooks. Each delivery is signed: SCS-Signature: t=<unix seconds>,v1=<lowercase hex HMAC-SHA256 of "<t>.<raw body>">. The HMAC key is the UTF-8 bytes of your whole signingSecret string, prefix included. Reject a t more than 5 minutes from your clock.

Auth

Exchange a signed client assertion for an access token.

Get an access token

OAuth 2.0 client credentials grant with private_key_jwt client authentication.

The client_assertion is a JWT signed with your private key, using the algorithm registered for that key (RS256 or ES256). Its header names a registered kid and carries no jku, jwk, x5u, x5c or crit (refused). Its claims are:

  • iss and sub: your client_id;
  • aud: a single string, exactly this endpoint's URL (https://api.content.somo.financial/oauth/token in production, https://api.staging.content.somo.financial/oauth/token in staging). Arrays are refused;
  • iat, exp: exp no more than 300 seconds after iat;
  • jti: a unique value, never reused (a replay is refused).

Send the assertion as a 3-part compact JWS (not a JWE), and no other client authentication: an Authorization: Basic header alongside it is refused. If you also send client_id, it must equal iss. The token lives 15 minutes (expires_in: 900). There is no refresh token: sign a new assertion. Errors follow RFC 6749 §5.2, not problem details.

Request Body schema: application/x-www-form-urlencoded
required
grant_type
required
string
Value: "client_credentials"
client_assertion_type
required
string
Value: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
client_assertion
required
string <= 8192 characters

The signed JWT (compact serialization).

scope
string <= 2048 characters

Space-separated scopes. Omit to receive every scope your credential holds.

client_id
string <uuid>

Optional. If sent, must equal the assertion's iss.

Responses

Response Headers
Cache-Control
string

Always no-store.

Value: "no-store"
Response Schema: application/json
access_token
required
string

Opaque bearer token. Do not parse it.

token_type
required
string
Value: "Bearer"
expires_in
required
integer
Value: 900
scope
required
string

Space-separated scopes granted.

Request samples

Content type
application/x-www-form-urlencoded
grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6InBhcnRuZXIta2V5LTIwMjYtMTAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiI3ZjNjMmE5MC0xYjRkLTRlNWYtOGE2Yi0wYzFkMmUzZjRhNWIiLCJzdWIiOiI3ZjNjMmE5MC0xYjRkLTRlNWYtOGE2Yi0wYzFkMmUzZjRhNWIiLCJhdWQiOiJodHRwczovL2FwaS5jb250ZW50LnNvbW8uZmluYW5jaWFsL29hdXRoL3Rva2VuIiwiaWF0IjoxNzYwNzk2ODAwLCJleHAiOjE3NjA3OTcxMDAsImp0aSI6IjNiOWY1YzJlLTdkNGEtNGYxYi05YzhlLTJhNmQ0ZTdmMWIzYyJ9.c2lnbmF0dXJl&scope=submissions%3Awrite%20submissions%3Aread%3Aown%20assets%3Awrite%20submit%3Aproperty%3Aexample-property

Response samples

Content type
application/json
{
  • "access_token": "scs_at_q8Yp2mZr7KcJx4LwVn3TfB6sHd1GaE9uQo5RiXy0PkM",
  • "token_type": "Bearer",
  • "expires_in": 900,
  • "scope": "submissions:write submissions:read:own assets:write submit:property:example-property"
}

Submissions

Submit articles, read their status, answer change requests, withdraw.

Submit an article

Creates a submission in status received with revision 1. Requires submissions:write and submit:property:<property>. Counts against your quota for that property.

Images must be uploaded first (POST /v1/assets) and referenced by id, in assets and, for inline images, as asset:<id> in the body. External image URLs are refused.

Authorizations:
oauth2apiKey
header Parameters
Idempotency-Key
required
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$
Example: 6f1c2b3a-9d8e-4f7a-b6c5-d4e3f2a1b0c9

Unique per request you intend to make once (a UUID is ideal). Retrying with the same key and the same body returns the original response with Idempotent-Replayed: true; the same key with a different body is 409 idempotency-key-reused. Scoped to your credential; kept 30 days.

Request Body schema: application/json
required
property
required
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$

Property id, e.g. example-property.

schemaVersion
required
string
Value: "1"
title
required
string [ 20 .. 110 ] characters ^[^<>]*$
excerpt
required
string [ 50 .. 300 ] characters ^[^<>]*$

Plain text summary.

required
object (Body)
required
object (Seo)
required
Array of objects (Source) [ 1 .. 50 ] items
required
object (AuthorClaim)

Who wrote or reviewed the piece. A claim; editors decide the published byline.

required
object (Disclosure)
externalRef
string^[A-Za-z0-9._:/-]{1,128}$

Your own id for the article. Unique among your submissions that are not rejected or withdrawn.

category
string <= 80 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$

A category slug from GET /v1/properties.

tags
Array of strings <= 10 items unique [ items <= 80 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$ ]
language
string^[A-Za-z]{2,3}(-[A-Za-z]{2})?$
Default: "en-US"

BCP 47 language tag (language, optional region), any case; normalized to canonical case (en-us → en-US). Must be one the property accepts (GET /v1/properties).

requestedPublishDate
string <date>

Advisory only. Editors decide the schedule.

ymyl
boolean
Default: false

Set true if the article gives money, health, benefits or legal guidance. Such articles get an additional subject-matter review. This is a submission-level flag; in responses content.ymyl echoes the submission's current flag (which an editor may also have set). Refused (422 ymyl-not-allowed) if your agreement does not cover YMYL for this property.

Array of objects (AssetRef) <= 20 items

At most one hero, and each assetId at most once (checked by the API, 422).

Responses

Response Headers
Location
string <uri>

URL of the new submission.

Idempotent-Replayed
string
Value: "true"

true when this is the stored response to an earlier request with the same Idempotency-Key.

RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
property
required
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$

Property id, e.g. example-property.

status
required
string (SubmissionStatus)
Enum: "received" "in_review" "changes_requested" "approved" "scheduled" "published" "rejected" "withdrawn" "unpublished"
title
required
string
currentRevision
required
integer >= 1

Your latest revision's number (1 for the original, 2 for your first revision, …). Counts your revisions only; editor edits are never numbered here.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
statusChangedAt
required
string <date-time>
required
object (ArticleContent)

The article itself. Shared by submissions and revisions.

required
null or object

The latest editor note addressed to you (with changes requested or a rejection), or null.

publishAt
required
string or null <date-time>

Set while scheduled.

required
Array of objects (PublicationRef)

Every site this submission went live on (empty until published).

required
Array of objects (CorrectionRequest)

Your correction and takedown requests for this submission, oldest first.

required
Array of objects

Status changes, oldest first.

externalRef
string or null

Request samples

Content type
application/json
{
  • "schemaVersion": "1",
  • "property": "example-property",
  • "externalRef": "ptr-2026-10-0042",
  • "title": "How a PCS move affects your state income taxes",
  • "excerpt": "Moving on orders can change which state taxes you. Here is how military residency rules work, what the spouse residency law covers, and what to check before you file.",
  • "body": {
    • "format": "markdown",
    • "content": "## Your state of legal residence usually stays the same\n\nUnder the [Servicemembers Civil Relief Act](https://www.justice.gov/servicemembers/servicemembers-civil-relief-act-scra),\nmoving on military orders does not by itself change your state of legal residence for income tax.\n\n![Moving truck outside base housing](asset:01920c4d-1a2b-7c3d-8e4f-5a6b7c8d9e0f)\n\n## What changes for spouses\n\nThe Military Spouses Residency Relief Act and later amendments let an eligible spouse keep the same\nstate of residence as the service member. Check your new state's revenue department for how it applies.\n"
    },
  • "seo": {
    • "metaTitle": "PCS moves and state income tax, explained",
    • "metaDescription": "How military residency rules decide which state taxes you after a PCS move, and what spouses need to know before filing.",
    • "slug": "pcs-move-state-income-taxes",
    • "focusKeyword": "pcs state income tax"
    },
  • "category": "money",
  • "tags": [
    • "taxes",
    • "pcs"
    ],
  • "sources": [],
  • "author": {
    • "name": "Alex Example",
    • "role": "expert",
    • "credentials": "Enrolled Agent; former installation tax center volunteer",
    • "consentOnFile": true
    },
  • "disclosure": {
    • "aiAssisted": true,
    • "aiAssistanceNotes": "Outline drafted with an AI tool; text written and fact-checked by the author.",
    • "sponsored": false,
    • "sponsoredLinks": [ ]
    },
  • "ymyl": true,
  • "assets": [
    • {
      }
    ]
}

Response samples

Content type
application/json
{
  • "id": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
  • "externalRef": "ptr-2026-10-0042",
  • "property": "example-property",
  • "status": "received",
  • "title": "How a PCS move affects your state income taxes",
  • "currentRevision": 1,
  • "createdAt": "2026-10-18T15:04:05Z",
  • "updatedAt": "2026-10-18T15:04:05Z",
  • "statusChangedAt": "2026-10-18T15:04:05Z",
  • "content": {
    • "schemaVersion": "1",
    • "title": "How a PCS move affects your state income taxes",
    • "excerpt": "Moving on orders can change which state taxes you. Here is how military residency rules work, what the spouse residency law covers, and what to check before you file.",
    • "body": {
      },
    • "seo": {
      },
    • "sources": [],
    • "author": {
      },
    • "disclosure": {
      }
    },
  • "feedback": null,
  • "publishAt": null,
  • "publications": [ ],
  • "correctionRequests": [ ],
  • "history": [
    • {
      }
    ]
}

List your submissions

Your own submissions, newest first. Requires submissions:read:own. Cursor-paginated: pass nextCursor from one page as cursor to get the next; nextCursor is null on the last page.

Authorizations:
oauth2apiKey
query Parameters
status
Array of strings (SubmissionStatus) <= 9 items
Items Enum: "received" "in_review" "changes_requested" "approved" "scheduled" "published" "rejected" "withdrawn" "unpublished"
Example: status=changes_requested,approved

Only submissions in these statuses (comma-separated).

property
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$
Examples: property=example-property

Only submissions to this property.

updatedSince
string <date-time>
Example: updatedSince=2026-10-19T00:00:00Z

Only submissions updated at or after this time (status change, revision, publication, or a correction request's status). Use it to sync.

externalRef
string^[A-Za-z0-9._:/-]{1,128}$

Submissions with this externalRef (a list; usually one, plus any older rejected or withdrawn ones).

cursor
string <= 512 characters

Opaque cursor from the previous page's nextCursor.

limit
integer [ 1 .. 100 ]
Default: 25

Page size.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
required
Array of objects (SubmissionSummary)
nextCursor
required
string or null

Response samples

Content type
application/json
{
  • "data": [
    • {
      }
    ],
  • "nextCursor": "eyJjIjoiMjAyNi0xMC0xOFQxNTowNDowNVoiLCJpIjoiMDE5MjBjNGUifQ"
}

Read one of your submissions

Status, the latest editor feedback addressed to you, the schedule, the published URL and the content of your latest revision. Requires submissions:read:own. Another client's submission is 404.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Submission id.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
property
required
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$

Property id, e.g. example-property.

status
required
string (SubmissionStatus)
Enum: "received" "in_review" "changes_requested" "approved" "scheduled" "published" "rejected" "withdrawn" "unpublished"
title
required
string
currentRevision
required
integer >= 1

Your latest revision's number (1 for the original, 2 for your first revision, …). Counts your revisions only; editor edits are never numbered here.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
statusChangedAt
required
string <date-time>
required
object (ArticleContent)

The article itself. Shared by submissions and revisions.

required
null or object

The latest editor note addressed to you (with changes requested or a rejection), or null.

publishAt
required
string or null <date-time>

Set while scheduled.

required
Array of objects (PublicationRef)

Every site this submission went live on (empty until published).

required
Array of objects (CorrectionRequest)

Your correction and takedown requests for this submission, oldest first.

required
Array of objects

Status changes, oldest first.

externalRef
string or null

Response samples

Content type
application/json
Example
{
  • "id": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
  • "externalRef": "ptr-2026-10-0042",
  • "property": "example-property",
  • "status": "changes_requested",
  • "title": "How a PCS move affects your state income taxes",
  • "currentRevision": 1,
  • "createdAt": "2026-10-18T15:04:05Z",
  • "updatedAt": "2026-10-19T10:12:00Z",
  • "statusChangedAt": "2026-10-19T10:12:00Z",
  • "content": {
    • "schemaVersion": "1",
    • "title": "How a PCS move affects your state income taxes",
    • "excerpt": "Moving on orders can change which state taxes you. Here is how military residency rules work, what the spouse residency law covers, and what to check before you file.",
    • "body": {
      },
    • "seo": {
      },
    • "sources": [],
    • "author": {
      },
    • "disclosure": {
      }
    },
  • "feedback": {
    • "note": "Please cite the state revenue department for the residency rules in section 2, and remove the link to the tax-software comparison page or declare it as sponsored.",
    • "at": "2026-10-19T10:12:00Z"
    },
  • "publishAt": null,
  • "publications": [ ],
  • "correctionRequests": [ ],
  • "history": [
    • {
      },
    • {
      },
    • {
      }
    ]
}

Send a revised version after changes were requested

Allowed only while the submission is changes_requested; anything else is 409 invalid-state. The revision replaces the whole article (send every field, not a patch) and moves the submission back to in_review. Requires submissions:write, and the same property checks as a create (scope, binding, paused, YMYL). Another client's submission is 404. Does not count against your quota.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Submission id.

header Parameters
Idempotency-Key
required
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$
Example: 6f1c2b3a-9d8e-4f7a-b6c5-d4e3f2a1b0c9

Unique per request you intend to make once (a UUID is ideal). Retrying with the same key and the same body returns the original response with Idempotent-Replayed: true; the same key with a different body is 409 idempotency-key-reused. Scoped to your credential; kept 30 days.

Request Body schema: application/json
required
schemaVersion
required
string
Value: "1"
title
required
string [ 20 .. 110 ] characters ^[^<>]*$
excerpt
required
string [ 50 .. 300 ] characters ^[^<>]*$

Plain text summary.

required
object (Body)
required
object (Seo)
required
Array of objects (Source) [ 1 .. 50 ] items
required
object (AuthorClaim)

Who wrote or reviewed the piece. A claim; editors decide the published byline.

required
object (Disclosure)
category
string <= 80 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$

A category slug from GET /v1/properties.

tags
Array of strings <= 10 items unique [ items <= 80 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$ ]
language
string^[A-Za-z]{2,3}(-[A-Za-z]{2})?$
Default: "en-US"

BCP 47 language tag (language, optional region), any case; normalized to canonical case (en-us → en-US). Must be one the property accepts (GET /v1/properties).

requestedPublishDate
string <date>

Advisory only. Editors decide the schedule.

ymyl
boolean
Default: false

Set true if the article gives money, health, benefits or legal guidance. Such articles get an additional subject-matter review. This is a submission-level flag; in responses content.ymyl echoes the submission's current flag (which an editor may also have set). Refused (422 ymyl-not-allowed) if your agreement does not cover YMYL for this property.

Array of objects (AssetRef) <= 20 items

At most one hero, and each assetId at most once (checked by the API, 422).

noteToEditor
string <= 2000 characters ^[^<>]*$

What changed, for the editor.

Responses

Response Headers
Idempotent-Replayed
string
Value: "true"

true when this is the stored response to an earlier request with the same Idempotency-Key.

RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
property
required
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$

Property id, e.g. example-property.

status
required
string (SubmissionStatus)
Enum: "received" "in_review" "changes_requested" "approved" "scheduled" "published" "rejected" "withdrawn" "unpublished"
title
required
string
currentRevision
required
integer >= 1

Your latest revision's number (1 for the original, 2 for your first revision, …). Counts your revisions only; editor edits are never numbered here.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
statusChangedAt
required
string <date-time>
required
object (ArticleContent)

The article itself. Shared by submissions and revisions.

required
null or object

The latest editor note addressed to you (with changes requested or a rejection), or null.

publishAt
required
string or null <date-time>

Set while scheduled.

required
Array of objects (PublicationRef)

Every site this submission went live on (empty until published).

required
Array of objects (CorrectionRequest)

Your correction and takedown requests for this submission, oldest first.

required
Array of objects

Status changes, oldest first.

externalRef
string or null

Request samples

Content type
application/json
{
  • "schemaVersion": "1",
  • "noteToEditor": "Added the state revenue source for section 2 and removed the tax-software link.",
  • "title": "How a PCS move affects your state income taxes",
  • "excerpt": "Moving on orders can change which state taxes you. Here is how military residency rules work, what the spouse residency law covers, and what to check before you file.",
  • "body": {
    • "format": "markdown",
    • "content": "## Your state of legal residence usually stays the same\n\nUnder the [Servicemembers Civil Relief Act](https://www.justice.gov/servicemembers/servicemembers-civil-relief-act-scra),\nmoving on military orders does not by itself change your state of legal residence for income tax.\n"
    },
  • "seo": {
    • "metaTitle": "PCS moves and state income tax, explained",
    • "metaDescription": "How military residency rules decide which state taxes you after a PCS move, and what spouses need to know before filing."
    },
  • "sources": [],
  • "author": {
    • "name": "Alex Example",
    • "role": "expert",
    • "consentOnFile": true
    },
  • "disclosure": {
    • "aiAssisted": true,
    • "sponsored": false
    },
  • "ymyl": true
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "externalRef": "string",
  • "property": "example-property",
  • "status": "received",
  • "title": "string",
  • "currentRevision": 1,
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "statusChangedAt": "2019-08-24T14:15:22Z",
  • "content": {
    • "schemaVersion": "1",
    • "title": "stringstringstringst",
    • "excerpt": "stringstringstringstringstringstringstringstringst",
    • "body": {
      },
    • "seo": {
      },
    • "category": "string",
    • "tags": [
      ],
    • "sources": [
      ],
    • "author": {
      },
    • "disclosure": {
      },
    • "language": "en-US",
    • "requestedPublishDate": "2019-08-24",
    • "ymyl": false,
    • "assets": [
      ]
    },
  • "feedback": { },
  • "publishAt": "2019-08-24T14:15:22Z",
  • "publications": [
    • {
      }
    ],
  • "correctionRequests": [
    • {
      }
    ],
  • "history": [
    • {
      }
    ]
}

Withdraw a submission

Allowed from received, in_review, changes_requested, approved and scheduled, as long as publishing has not started; otherwise 409 publishing-in-progress (see that response). Once publishing has started, we complete it, or confirm it did not happen, before any other change takes effect. An absent or empty body is the same as {} for idempotency. Withdrawing an already withdrawn submission returns it unchanged (200). After publication, file a correction request instead. Requires submissions:write. Another client's submission is 404.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Submission id.

header Parameters
Idempotency-Key
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$

Optional on this call; same semantics as on create calls.

Request Body schema: application/json
optional
reason
string <= 1000 characters ^[^<>]*$

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
property
required
string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$

Property id, e.g. example-property.

status
required
string (SubmissionStatus)
Enum: "received" "in_review" "changes_requested" "approved" "scheduled" "published" "rejected" "withdrawn" "unpublished"
title
required
string
currentRevision
required
integer >= 1

Your latest revision's number (1 for the original, 2 for your first revision, …). Counts your revisions only; editor edits are never numbered here.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
statusChangedAt
required
string <date-time>
required
object (ArticleContent)

The article itself. Shared by submissions and revisions.

required
null or object

The latest editor note addressed to you (with changes requested or a rejection), or null.

publishAt
required
string or null <date-time>

Set while scheduled.

required
Array of objects (PublicationRef)

Every site this submission went live on (empty until published).

required
Array of objects (CorrectionRequest)

Your correction and takedown requests for this submission, oldest first.

required
Array of objects

Status changes, oldest first.

externalRef
string or null

Request samples

Content type
application/json
{
  • "reason": "The figures in section 2 are out of date; we will resubmit next month."
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "externalRef": "string",
  • "property": "example-property",
  • "status": "received",
  • "title": "string",
  • "currentRevision": 1,
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "statusChangedAt": "2019-08-24T14:15:22Z",
  • "content": {
    • "schemaVersion": "1",
    • "title": "stringstringstringst",
    • "excerpt": "stringstringstringstringstringstringstringstringst",
    • "body": {
      },
    • "seo": {
      },
    • "category": "string",
    • "tags": [
      ],
    • "sources": [
      ],
    • "author": {
      },
    • "disclosure": {
      },
    • "language": "en-US",
    • "requestedPublishDate": "2019-08-24",
    • "ymyl": false,
    • "assets": [
      ]
    },
  • "feedback": { },
  • "publishAt": "2019-08-24T14:15:22Z",
  • "publications": [
    • {
      }
    ],
  • "correctionRequests": [
    • {
      }
    ],
  • "history": [
    • {
      }
    ]
}

Read the final published text

The article as published, including any edits our editors made, and where it is live. Available once the submission has been published at least once; before that 409 invalid-state. Requires submissions:read:own. Another client's submission is 404.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Submission id.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
submissionId
required
string <uuid>
revisionPublished
required
integer

Your revision number the published text is based on. Editors may have edited it; this is the text as published.

title
required
string
excerpt
required
string
html
required
string

The sanitized body as published.

required
object
required
object
required
Array of objects (PublicationRef)
editedAtDestination
boolean
Default: false

True when an editor accepted changes made directly on the site; the live page may then differ from this text.

Response samples

Content type
application/json
{
  • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
  • "revisionPublished": 2,
  • "title": "How a PCS move affects your state income taxes",
  • "excerpt": "Moving on orders can change which state taxes you. Here is how military residency rules work, what the spouse residency law covers, and what to check before you file.",
  • "html": "<h2>Your state of legal residence usually stays the same</h2><p>Under the <a href=\"https://www.justice.gov/servicemembers/servicemembers-civil-relief-act-scra\" rel=\"noopener\">Servicemembers Civil Relief Act</a>, moving on military orders does not by itself change your state of legal residence for income tax.</p>",
  • "seo": {
    • "metaTitle": "PCS moves and state income tax, explained",
    • "metaDescription": "How military residency rules decide which state taxes you after a PCS move, and what spouses need to know before filing.",
    • "slug": "pcs-move-state-income-taxes"
    },
  • "byline": {
    • "name": "Alex Example",
    • "reviewedBy": "Example Reviewer"
    },
  • "publications": []
}

Ask for a correction or takedown of a published article

Allowed only while the submission is published and has no other open request of the same kind (otherwise 409 invalid-state); a takedown request can be filed while a correction request is open. The request is recorded and queued to our editors, who handle it under your agreement's SLA. A proposedRevision is validated and sanitized exactly like a revision (422 on failure), then kept as a suggestion for the editor, never applied as is. A correction an editor applies is updated in place (same URL) and reported as submission.updated. A takedown request works even if your access to the property has since been revoked; a correction request with a proposedRevision needs your current access to the property, like a revision. Follow its status in GET /v1/submissions/{id} (correctionRequests). Requires submissions:write. Another client's submission is 404.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Submission id.

header Parameters
Idempotency-Key
required
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$
Example: 6f1c2b3a-9d8e-4f7a-b6c5-d4e3f2a1b0c9

Unique per request you intend to make once (a UUID is ideal). Retrying with the same key and the same body returns the original response with Idempotent-Replayed: true; the same key with a different body is 409 idempotency-key-reused. Scoped to your credential; kept 30 days.

Request Body schema: application/json
required
kind
required
string
Enum: "correction" "takedown"
reason
required
string [ 10 .. 1000 ] characters ^[^<>]*$
details
string <= 5000 characters ^[^<>]*$
object (RevisionCreate)

The article itself. Shared by submissions and revisions.

Responses

Response Headers
Idempotent-Replayed
string
Value: "true"

true when this is the stored response to an earlier request with the same Idempotency-Key.

RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
kind
required
string
Enum: "correction" "takedown"
status
required
string
Enum: "open" "accepted" "declined" "done"

open: received, not yet looked at. accepted: an editor is acting on it. done: the editor closed it as completed; the note says what was done. declined: the editor closed it without the requested change; the note says why. Only our editors change it, and closing it sends correction_request.closed.

reason
required
string
details
required
string or null
editorNote
required
string or null

Our editor's note to you, once handled.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>

Request samples

Content type
application/json
{
  • "kind": "correction",
  • "reason": "The state filing deadline in section 3 changed for the 2026 tax year.",
  • "details": "The state revenue department moved the deadline to May 1; the article still says April 15."
}

Response samples

Content type
application/json
{
  • "id": "01921500-1234-7abc-8def-0123456789ab",
  • "kind": "correction",
  • "status": "open",
  • "reason": "The state filing deadline in section 3 changed for the 2026 tax year.",
  • "details": "The state revenue department moved the deadline to May 1; the article still says April 15.",
  • "editorNote": null,
  • "createdAt": "2026-11-02T09:00:00Z",
  • "updatedAt": "2026-11-02T09:00:00Z"
}

Assets

Upload images through a presigned URL; every image is scanned and re-encoded.

Register an image and get a presigned upload URL

Declare the file (type, exact size, SHA-256) and its license; the response holds a presigned PUT URL valid for 15 minutes. Upload the exact bytes with exactly the headers returned in upload.headers. The asset then moves pending_upload → scanning → processing → clean (or rejected). Poll GET /v1/assets/{id}. An asset can be referenced by a submission before it is clean, but the submission cannot be approved until every asset is. Requires assets:write.

Retrying with the same Idempotency-Key returns the asset with a freshly signed upload while it is still pending_upload, and upload: null once it has moved on.

Accepted types: JPEG, PNG, WebP, AVIF; at most 10 MB and 40 megapixels; one frame (no animation). Every image is malware-scanned, checked by its bytes (not its name), re-encoded and stripped of metadata. Originals are never published.

Authorizations:
oauth2apiKey
header Parameters
Idempotency-Key
required
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$
Example: 6f1c2b3a-9d8e-4f7a-b6c5-d4e3f2a1b0c9

Unique per request you intend to make once (a UUID is ideal). Retrying with the same key and the same body returns the original response with Idempotent-Replayed: true; the same key with a different body is 409 idempotency-key-reused. Scoped to your credential; kept 30 days.

Request Body schema: application/json
required
filename
required
string [ 1 .. 200 ] characters ^[^<>]*$
contentType
required
string
Enum: "image/jpeg" "image/png" "image/webp" "image/avif"
sizeBytes
required
integer [ 1 .. 10485760 ]
sha256
required
string^[0-9a-f]{64}$

Hex SHA-256 of the exact bytes you will upload.

required
object (Licence)

The image's license. Field and value names keep the spelling licence (for example stock_licence).

aiGenerated
required
boolean

Responses

Response Headers
Idempotent-Replayed
string
Value: "true"

true when this is the stored response to an earlier request with the same Idempotency-Key.

RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
required
object (Asset)
required
object or null

Null on an idempotent replay once the asset is no longer pending_upload.

Request samples

Content type
application/json
{
  • "filename": "pcs-moving-truck.jpg",
  • "contentType": "image/jpeg",
  • "sizeBytes": 2483112,
  • "sha256": "9f2b5c0e4a1d7e3f6b8c2a4d1e9f0b7c3a5d8e2f1c4b7a0d9e6f3c2b1a0d4e5f",
  • "aiGenerated": false,
  • "licence": {
    • "source": "Photographer under contract to the partner",
    • "type": "work_for_hire",
    • "proof": "Agreement WFH-2026-031, clause 4 (assignment of copyright)"
    }
}

Response samples

Content type
application/json
{
  • "asset": {
    • "id": "01920c4d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    • "status": "pending_upload",
    • "filename": "pcs-moving-truck.jpg",
    • "contentType": "image/jpeg",
    • "sizeBytes": 2483112,
    • "sha256": "9f2b5c0e4a1d7e3f6b8c2a4d1e9f0b7c3a5d8e2f1c4b7a0d9e6f3c2b1a0d4e5f",
    • "aiGenerated": false,
    • "licence": {
      },
    • "rejectReason": null,
    • "width": null,
    • "height": null,
    • "createdAt": "2026-10-18T15:01:00Z"
    },
  • "upload": {}
}

List your assets

Your own assets, newest first, cursor-paginated like GET /v1/submissions. Requires assets:write.

Authorizations:
oauth2apiKey
query Parameters
status
Array of strings (AssetStatus) <= 6 items
Items Enum: "pending_upload" "scanning" "processing" "clean" "rejected" "expired"

Only assets in these statuses (comma-separated).

cursor
string <= 512 characters

Opaque cursor from the previous page's nextCursor.

limit
integer [ 1 .. 100 ]
Default: 25

Page size.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
required
Array of objects (Asset)
nextCursor
required
string or null

Response samples

Content type
application/json
{
  • "data": [
    • {
      }
    ],
  • "nextCursor": "string"
}

Read one of your assets

Status, and once clean, the dimensions of the processed image. Requires assets:write.

Authorizations:
oauth2apiKey
path Parameters
id
required
string <uuid>

Asset id.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
id
required
string <uuid>
status
required
string (AssetStatus)
Enum: "pending_upload" "scanning" "processing" "clean" "rejected" "expired"
filename
required
string
contentType
required
string
sizeBytes
required
integer
sha256
required
string
required
object (Licence)

The image's license. Field and value names keep the spelling licence (for example stock_licence).

aiGenerated
required
boolean
rejectReason
required
string or null
Enum: "malware" "scan_failed" "type_mismatch" "unsupported_type" "too_large" "too_many_pixels" "decode_failed" null
width
required
integer or null

Of the processed image, once clean.

height
required
integer or null
createdAt
required
string <date-time>

Response samples

Content type
application/json
Example
{
  • "id": "01920c4d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  • "status": "clean",
  • "filename": "pcs-moving-truck.jpg",
  • "contentType": "image/jpeg",
  • "sizeBytes": 2483112,
  • "sha256": "9f2b5c0e4a1d7e3f6b8c2a4d1e9f0b7c3a5d8e2f1c4b7a0d9e6f3c2b1a0d4e5f",
  • "aiGenerated": false,
  • "licence": {
    • "source": "Photographer under contract to the partner",
    • "type": "work_for_hire",
    • "proof": "Agreement WFH-2026-031, clause 4 (assignment of copyright)"
    },
  • "rejectReason": null,
  • "width": 2560,
  • "height": 1707,
  • "createdAt": "2026-10-18T15:01:00Z"
}

Webhooks

Register the endpoint that receives signed status events.

The status changed Webhook | POST

Sent on every status change, alongside the specific event. Treat it as the authoritative record of state. Not sent on creation (there is no previous status; submission.received covers it) or for changes that keep the status (a reschedule sends submission.scheduled, a correction submission.updated). Correction requests are not covered: their close comes as correction_request.closed, and accepted shows only via GET /v1/submissions/{id} (or updatedSince).

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.status_changed"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921233-0c0d-7e0f-8a1b-2c3d4e5f6a70",
  • "type": "submission.status_changed",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-21T13:00:12Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "published",
    • "revision": 2,
    • "from": "approved",
    • "to": "published",
    • "at": "2026-10-21T13:00:12Z"
    }
}

A submission was received Webhook | POST

Sent after POST /v1/submissions.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.received"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object (EventSubmissionData)

Responses

Request samples

Content type
application/json
{
  • "id": "01920c4e-90ab-7cde-8f01-23456789abcd",
  • "type": "submission.received",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-18T15:04:05Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "received",
    • "revision": 1
    }
}

A revision was received Webhook | POST

Sent after each accepted POST /v1/submissions/{id}/revisions. The submission is back in review.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.revised"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object (EventSubmissionData)

Responses

Request samples

Content type
application/json
{
  • "id": "01920f9c-0d1e-7f20-8132-435465768798",
  • "type": "submission.revised",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-19T18:02:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "in_review",
    • "revision": 2
    }
}

An editor asked for changes Webhook | POST

Answer with POST /v1/submissions/{id}/revisions.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.changes_requested"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01920f1a-0b1c-7d2e-8f3a-4b5c6d7e8f90",
  • "type": "submission.changes_requested",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-19T10:12:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "changes_requested",
    • "revision": 1,
    • "feedback": "Please cite the state revenue department for the residency rules in section 2, and remove the link to the tax-software comparison page or declare it as sponsored."
    }
}

The submission was approved for publication Webhook | POST

Publication follows, now or on a schedule (submission.scheduled).

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.approved"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object (EventSubmissionData)

Responses

Request samples

Content type
application/json
{
  • "id": "01921201-aa00-7b11-8c22-3d44e55f6a77",
  • "type": "submission.approved",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-20T16:00:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "approved",
    • "revision": 2
    }
}

Publication was scheduled Webhook | POST

Carries the planned time. It may be rescheduled; the latest event wins.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.scheduled"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921206-bb11-7c22-8d33-4e55f66a7b88",
  • "type": "submission.scheduled",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-20T16:05:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "scheduled",
    • "revision": 2,
    • "publishAt": "2026-10-21T13:00:00Z"
    }
}

The article is live Webhook | POST

Carries the live URL. Sent once for each site the article goes live on.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.published"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921233-0c0d-7e0f-8a1b-2c3d4e5f6a7b",
  • "type": "submission.published",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-21T13:00:12Z",
  • "data": {}
}

Published content changed Webhook | POST

Sent when published content changed: an approved correction applied in place, the article re-published after its page was changed on the site, or re-published after it was removed from the site (then url is the new address).

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.updated"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921b00-4444-7555-8666-777788889999",
  • "type": "submission.updated",
  • "schemaVersion": "1",
  • "occurredAt": "2026-11-04T09:15:02Z",
  • "data": {}
}

The submission was rejected Webhook | POST

Rejection is final for this submission.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.rejected"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "019212ff-1111-7222-8333-444455556666",
  • "type": "submission.rejected",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-20T11:30:00Z",
  • "data": {
    • "submissionId": "01920c55-0000-7111-8222-333344445555",
    • "externalRef": "ptr-2026-10-0043",
    • "property": "example-property",
    • "status": "rejected",
    • "revision": 1,
    • "feedback": "The article substantially duplicates a piece already published on the site."
    }
}

You withdrew the submission Webhook | POST

Confirms a POST /v1/submissions/{id}/withdraw.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.withdrawn"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object (EventSubmissionData)

Responses

Request samples

Content type
application/json
{
  • "id": "01921310-2222-7333-8444-555566667777",
  • "type": "submission.withdrawn",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-20T12:00:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "withdrawn",
    • "revision": 1
    }
}

The article was taken down Webhook | POST

Our editors unpublished it, for example after a takedown request.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "submission.unpublished"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object (EventSubmissionData)

Responses

Request samples

Content type
application/json
{
  • "id": "01921a00-3333-7444-8555-666677778888",
  • "type": "submission.unpublished",
  • "schemaVersion": "1",
  • "occurredAt": "2026-11-03T10:00:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "unpublished",
    • "revision": 2
    }
}

An editor closed one of your correction requests Webhook | POST

Sent when an editor marks a correction request done or declined, with their note.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "correction_request.closed"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921c00-5555-7666-8777-888899990000",
  • "type": "correction_request.closed",
  • "schemaVersion": "1",
  • "occurredAt": "2026-11-04T09:30:00Z",
  • "data": {
    • "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
    • "externalRef": "ptr-2026-10-0042",
    • "property": "example-property",
    • "status": "published",
    • "revision": 2,
    • "correctionRequest": {
      }
    }
}

A test event you asked for Webhook | POST

Sent by POST /v1/webhooks/test. Always delivered, whatever you subscribed to.

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "webhook.test"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "01921600-aaaa-7bbb-8ccc-ddddeeeeffff",
  • "type": "webhook.test",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-18T14:05:00Z",
  • "data": { }
}

Prove you control the endpoint Webhook | POST

Sent after PUT /v1/webhooks creates the endpoint, changes its URL, or re-sends the same URL while the endpoint is pending_verification or verification_failed. Answer 200 within 10 seconds with {"challenge": <the same value>}; until then other events are held (up to 24 hours).

header Parameters
SCS-Signature
required
string^t=[0-9]{10},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64}...
Example: t=1760796245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is over the string <t>.<raw request body> with your signing secret. During a secret rotation two v1= values are present; accept if either matches. Reject if t is more than 300 seconds from your clock.

SCS-Event-Id
required
string <uuid>

The event id, the same on every retry. Use it to ignore duplicates.

SCS-Event-Type
required
string

The event type, equal to the body's type.

SCS-Delivery-Attempt
required
integer >= 1

1 for the first delivery, then 2, 3, … on retries.

Request Body schema: application/json
required
id
required
string <uuid>

Equal to the SCS-Event-Id header.

type
required
string

The event type. Tolerate types you do not know.

Value: "endpoint.verification"
schemaVersion
required
string
Value: "1"
occurredAt
required
string <date-time>
required
object

Responses

Response Schema: application/json
challenge
required
string

Request samples

Content type
application/json
{
  • "id": "01921600-0000-7111-8222-333344440000",
  • "type": "endpoint.verification",
  • "schemaVersion": "1",
  • "occurredAt": "2026-10-18T14:00:01Z",
  • "data": {
    • "challenge": "3q2-7wEXAMPLEchallengeValue0000"
    }
}

Response samples

Content type
application/json
{
  • "challenge": "3q2-7wEXAMPLEchallengeValue0000"
}

Read your webhook endpoint

Your registered endpoint, or 404 if none. The signing secret is never returned here.

Authorizations:
oauth2apiKey

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
url
required
string <uri>
events
required
Array of strings (WebhookEventType)
Items Enum: "submission.status_changed" "submission.received" "submission.revised" "submission.changes_requested" "submission.approved" "submission.scheduled" "submission.published" "submission.updated" "submission.rejected" "submission.withdrawn" "submission.unpublished" "correction_request.closed"
status
required
string
Enum: "pending_verification" "active" "verification_failed" "disabled"
secretRotatingUntil
required
string or null <date-time>

While set, deliveries are signed with both the old and the new secret.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>

Response samples

Content type
application/json
{
  • "events": [
    • "submission.status_changed",
    • "submission.received",
    • "submission.changes_requested",
    • "submission.approved",
    • "submission.published",
    • "submission.updated",
    • "submission.rejected",
    • "submission.unpublished"
    ],
  • "status": "active",
  • "secretRotatingUntil": null,
  • "createdAt": "2026-10-18T14:00:00Z",
  • "updatedAt": "2026-10-18T14:00:00Z"
}

Create or replace your webhook endpoint

One endpoint per partner. A new endpoint, or a changed URL, must pass verification first: we POST a signed endpoint.verification event and your endpoint must echo its challenge within 10 seconds (see webhooks). Until then it is pending_verification: events are held (up to 24 hours, then dropped) and delivered once it is verified. A new PUT cancels any verification still pending. Verification requests are limited to 5 per partner per hour (429 rate-limited). Every verification we issue counts: creating the endpoint, changing its URL, and re-sending the same URL while it is pending_verification or verification_failed. An optional Idempotency-Key makes the call safe to retry: a replay within 10 minutes returns the same response, including the same new signingSecret while it is still current (see the parameter). So a lost rotation response is recoverable. HTTPS on port 443 only, a hostname resolving only to public addresses; redirects are not followed. The signing secret is returned only in the response that creates the endpoint (201) or rotates it (rotateSecret: true); store it then. During a rotation the old secret keeps signing alongside the new one for 24 hours; another rotation inside that window is 409 secret-rotation-in-progress. The URL must be https on port 443 with a DNS host name: no IP literals, no user@, no fragment. Requires webhooks:manage.

Authorizations:
oauth2apiKey
header Parameters
Idempotency-Key
string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$

Optional; send one when creating the endpoint or rotating its secret.

  • Within 10 minutes, a retry with the same key and body returns the same status and body, including the same new signingSecret, as long as that secret is still the endpoint's current one.
  • After 10 minutes, or once the secret has been rotated again, a retry returns the original status code with the endpoint's current view and no signingSecret.
  • The same key with a different body is 409 idempotency-key-reused.
  • After DELETE /v1/webhooks, a retry answers 404 not-found.
  • We never store the secret with the key.
Request Body schema: application/json
required
url
required
string <uri> <= 2048 characters ^https://(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[...

https, port 443 only, a DNS host name (no IP literal; IDN as xn-- punycode), no userinfo, no fragment; a query string is allowed. This pattern is a pre-filter; the address is checked again when we connect.

events
required
Array of strings (WebhookEventType) non-empty unique
Items Enum: "submission.status_changed" "submission.received" "submission.revised" "submission.changes_requested" "submission.approved" "submission.scheduled" "submission.published" "submission.updated" "submission.rejected" "submission.withdrawn" "submission.unpublished" "correction_request.closed"
rotateSecret
boolean
Default: false

Issue a new signing secret; the old one keeps signing for 24 hours.

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
url
required
string <uri>
events
required
Array of strings (WebhookEventType)
Items Enum: "submission.status_changed" "submission.received" "submission.revised" "submission.changes_requested" "submission.approved" "submission.scheduled" "submission.published" "submission.updated" "submission.rejected" "submission.withdrawn" "submission.unpublished" "correction_request.closed"
status
required
string
Enum: "pending_verification" "active" "verification_failed" "disabled"
secretRotatingUntil
required
string or null <date-time>

While set, deliveries are signed with both the old and the new secret.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
signingSecret
string

Shown only when created or rotated.

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
url
required
string <uri>
events
required
Array of strings (WebhookEventType)
Items Enum: "submission.status_changed" "submission.received" "submission.revised" "submission.changes_requested" "submission.approved" "submission.scheduled" "submission.published" "submission.updated" "submission.rejected" "submission.withdrawn" "submission.unpublished" "correction_request.closed"
status
required
string
Enum: "pending_verification" "active" "verification_failed" "disabled"
secretRotatingUntil
required
string or null <date-time>

While set, deliveries are signed with both the old and the new secret.

createdAt
required
string <date-time>
updatedAt
required
string <date-time>
signingSecret
string

Shown only when created or rotated.

Request samples

Content type
application/json
{
  • "events": [
    • "submission.status_changed",
    • "submission.received",
    • "submission.changes_requested",
    • "submission.approved",
    • "submission.published",
    • "submission.updated",
    • "submission.rejected",
    • "submission.unpublished"
    ]
}

Response samples

Content type
application/json
{
  • "events": [
    • "submission.status_changed"
    ],
  • "status": "pending_verification",
  • "secretRotatingUntil": "2019-08-24T14:15:22Z",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z",
  • "signingSecret": "string"
}

Remove your webhook endpoint

Stops all deliveries; pending events are dropped. 404 if you have none. Requires webhooks:manage.

Authorizations:
oauth2apiKey

Responses

Response samples

Content type
application/problem+json
{}

Send a test event to your endpoint

Queues one signed webhook.test event to your registered endpoint and returns its event id. Watch for it to confirm your signature check works. 404 if you have no endpoint; 409 invalid-state until it is verified; at most 5 per hour (429). Requires webhooks:manage.

Authorizations:
oauth2apiKey

Responses

Response samples

Content type
application/json
{
  • "eventId": "01921600-aaaa-7bbb-8ccc-ddddeeeeffff"
}

Properties

The properties you may submit to, with their categories and tags.

List the properties you may submit to

Only properties you hold submit:property:<id> for and that your agreement covers, with the categories and tags they accept (kept in sync with the site and refreshed hourly) and your remaining quota. Requires submissions:read:own.

Authorizations:
oauth2apiKey

Responses

Response Headers
RateLimit-Limit
integer

The capacity of this credential's token bucket (IETF RateLimit header draft semantics, applied to a token bucket).

RateLimit-Remaining
integer

Tokens left in the bucket now.

RateLimit-Reset
integer

Seconds until the bucket is full again.

Response Schema: application/json
required
Array of objects (Property)

Response samples

Content type
application/json
{
  • "data": [
    • {
      }
    ]
}

Health

Liveness.

Liveness

No authentication. Says only that the API is up.

Responses

Response Schema: application/json
status
required
string
Value: "ok"

Response samples

Content type
application/json
{
  • "status": "ok"
}