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), exceptPOST /oauth/token, which takesapplication/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 (UnicodeCc/Cf) are refused, with these exceptions: tab and newline inbody; U+200C, U+200D and U+FE0F (emoji sequences and joiners) inbodyand in plain-text fields. Bidi controls (U+202A–U+202E, U+2066–U+2069) are always refused. Onlybodycarries 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.
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:
issandsub: yourclient_id;aud: a single string, exactly this endpoint's URL (https://api.content.somo.financial/oauth/tokenin production,https://api.staging.content.somo.financial/oauth/tokenin staging). Arrays are refused;iat,exp:expno more than 300 seconds afteriat;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-urlencodedrequired
| 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 |
Responses
| Cache-Control | string Always 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
- Payload
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
- 200
- 400
- 401
- 403
- 429
- 500
- 503
{- "access_token": "scs_at_q8Yp2mZr7KcJx4LwVn3TfB6sHd1GaE9uQo5RiXy0PkM",
- "token_type": "Bearer",
- "expires_in": 900,
- "scope": "submissions:write submissions:read:own assets:write submit:property:example-property"
}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:
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 |
Request Body schema: application/jsonrequired
| property required | string (PropertyId) ^[a-z0-9][a-z0-9-]{1,40}$ Property id, e.g. |
| 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 |
| 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 ( |
| 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 |
Array of objects (AssetRef) <= 20 items At most one |
Responses
| Location | string <uri> URL of the new submission. |
| Idempotent-Replayed | string Value: "true"
|
| 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. |
| 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 |
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
- Payload
{- "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\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": [
- {
- "title": "Servicemembers Civil Relief Act (SCRA)",
- "publisher": "U.S. Department of Justice",
- "accessedAt": "2026-10-12"
}, - {
- "title": "Military tax information",
- "publisher": "Internal Revenue Service",
- "accessedAt": "2026-10-12"
}
], - "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": [
- {
- "assetId": "01920c4d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
- "role": "hero",
- "alt": "Moving truck parked outside base housing",
- "credit": "Partner staff photographer"
}
]
}Response samples
- 201
- 400
- 401
- 403
- 409
- 413
- 415
- 422
- 429
- 500
- 503
{- "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": {
- "format": "markdown",
- "content": "## Your state of legal residence usually stays the same …"
}, - "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
}
}, - "feedback": null,
- "publishAt": null,
- "publications": [ ],
- "correctionRequests": [ ],
- "history": [
- {
- "status": "received",
- "at": "2026-10-18T15:04:05Z"
}
]
}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:
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 |
| cursor | string <= 512 characters Opaque cursor from the previous page's |
| limit | integer [ 1 .. 100 ] Default: 25 Page size. |
Responses
| 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
- 200
- 400
- 401
- 403
- 429
- 500
{- "data": [
- {
- "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"
}
], - "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:
path Parameters
| id required | string <uuid> Submission id. |
Responses
| 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. |
| 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 |
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
- 200
- 401
- 403
- 404
- 429
- 500
{- "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": {
- "format": "markdown",
- "content": "## Your state of legal residence usually stays the same …"
}, - "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
}
}, - "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": [
- {
- "status": "received",
- "at": "2026-10-18T15:04:05Z"
}, - {
- "status": "in_review",
- "at": "2026-10-19T09:40:00Z"
}, - {
- "status": "changes_requested",
- "at": "2026-10-19T10:12:00Z"
}
]
}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:
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 |
Request Body schema: application/jsonrequired
| 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 |
| 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 ( |
| 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 |
Array of objects (AssetRef) <= 20 items At most one | |
| noteToEditor | string <= 2000 characters ^[^<>]*$ What changed, for the editor. |
Responses
| Idempotent-Replayed | string Value: "true"
|
| 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. |
| 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 |
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
- Payload
{- "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": [
- {
- "title": "Servicemembers Civil Relief Act (SCRA)"
}
], - "author": {
- "name": "Alex Example",
- "role": "expert",
- "consentOnFile": true
}, - "disclosure": {
- "aiAssisted": true,
- "sponsored": false
}, - "ymyl": true
}Response samples
- 201
- 400
- 401
- 403
- 404
- 409
- 413
- 415
- 422
- 429
- 500
- 503
{- "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": {
- "format": "markdown",
- "content": "string"
}, - "seo": {
- "metaTitle": "stringstri",
- "metaDescription": "stringstringstringstringstringstringstringstringst",
- "slug": "string",
- "focusKeyword": "string"
}, - "category": "string",
- "tags": [
- "string"
], - "sources": [
], - "author": {
- "name": "string",
- "role": "writer",
- "credentials": "string",
- "bio": "string",
- "consentOnFile": true
}, - "disclosure": {
- "aiAssisted": true,
- "aiAssistanceNotes": "string",
- "sponsored": true,
- "sponsorName": "string",
- "sponsoredLinks": [ ]
}, - "language": "en-US",
- "requestedPublishDate": "2019-08-24",
- "ymyl": false,
- "assets": [
- {
- "assetId": "9179b887-04ef-4ce5-ab3a-b5bbd39ea3c8",
- "role": "hero",
- "alt": "string",
- "caption": "string",
- "credit": "string"
}
]
}, - "feedback": { },
- "publishAt": "2019-08-24T14:15:22Z",
- "publications": [
- {
- "property": "example-property",
- "publishedAt": "2019-08-24T14:15:22Z",
- "state": "live"
}
], - "correctionRequests": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "kind": "correction",
- "status": "open",
- "reason": "string",
- "details": "string",
- "editorNote": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}
], - "history": [
- {
- "status": "received",
- "at": "2019-08-24T14:15:22Z"
}
]
}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:
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/jsonoptional
| reason | string <= 1000 characters ^[^<>]*$ |
Responses
| 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. |
| 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 |
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
- Payload
{- "reason": "The figures in section 2 are out of date; we will resubmit next month."
}Response samples
- 200
- 400
- 401
- 403
- 404
- 409
- 422
- 429
- 500
- 503
{- "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": {
- "format": "markdown",
- "content": "string"
}, - "seo": {
- "metaTitle": "stringstri",
- "metaDescription": "stringstringstringstringstringstringstringstringst",
- "slug": "string",
- "focusKeyword": "string"
}, - "category": "string",
- "tags": [
- "string"
], - "sources": [
], - "author": {
- "name": "string",
- "role": "writer",
- "credentials": "string",
- "bio": "string",
- "consentOnFile": true
}, - "disclosure": {
- "aiAssisted": true,
- "aiAssistanceNotes": "string",
- "sponsored": true,
- "sponsorName": "string",
- "sponsoredLinks": [ ]
}, - "language": "en-US",
- "requestedPublishDate": "2019-08-24",
- "ymyl": false,
- "assets": [
- {
- "assetId": "9179b887-04ef-4ce5-ab3a-b5bbd39ea3c8",
- "role": "hero",
- "alt": "string",
- "caption": "string",
- "credit": "string"
}
]
}, - "feedback": { },
- "publishAt": "2019-08-24T14:15:22Z",
- "publications": [
- {
- "property": "example-property",
- "publishedAt": "2019-08-24T14:15:22Z",
- "state": "live"
}
], - "correctionRequests": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "kind": "correction",
- "status": "open",
- "reason": "string",
- "details": "string",
- "editorNote": "string",
- "createdAt": "2019-08-24T14:15:22Z",
- "updatedAt": "2019-08-24T14:15:22Z"
}
], - "history": [
- {
- "status": "received",
- "at": "2019-08-24T14:15:22Z"
}
]
}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:
path Parameters
| id required | string <uuid> Submission id. |
Responses
| 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
- 200
- 401
- 403
- 404
- 409
- 429
- 500
{- "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": [
- {
- "property": "example-property",
- "publishedAt": "2026-10-21T13:00:10Z",
- "state": "live"
}
]
}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:
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 |
Request Body schema: application/jsonrequired
| 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
| Idempotent-Replayed | string Value: "true"
|
| 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"
|
| 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
- Payload
{- "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
- 201
- 400
- 401
- 403
- 404
- 409
- 413
- 415
- 422
- 429
- 500
- 503
{- "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"
}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:
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 |
Request Body schema: application/jsonrequired
| 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 |
| aiGenerated required | boolean |
Responses
| Idempotent-Replayed | string Value: "true"
|
| 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 |
Request samples
- Payload
{- "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
- 201
- 400
- 401
- 403
- 409
- 415
- 422
- 429
- 500
- 503
{- "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": {
- "source": "Photographer under contract to the partner",
- "type": "work_for_hire",
- "proof": "Agreement WFH-2026-031, clause 4 (assignment of copyright)"
}, - "rejectReason": null,
- "width": null,
- "height": null,
- "createdAt": "2026-10-18T15:01:00Z"
}, - "upload": {
- "method": "PUT",
- "headers": {
- "Content-Type": "image/jpeg",
- "Content-Length": "2483112",
- "x-amz-checksum-sha256": "nytcDkodfj9rjCpNHp8LfDpdjiHxxLcNnm88KxoNTl8="
}, - "expiresAt": "2026-10-18T15:16:00Z"
}
}List your assets
Your own assets, newest first, cursor-paginated like GET /v1/submissions. Requires assets:write.
Authorizations:
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 |
| limit | integer [ 1 .. 100 ] Default: 25 Page size. |
Responses
| 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
- 200
- 400
- 401
- 403
- 429
- 500
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "status": "pending_upload",
- "filename": "string",
- "contentType": "string",
- "sizeBytes": 0,
- "sha256": "string",
- "licence": {
- "source": "string",
- "type": "owned",
- "proof": "string"
}, - "aiGenerated": true,
- "rejectReason": "malware",
- "width": 0,
- "height": 0,
- "createdAt": "2019-08-24T14:15:22Z"
}
], - "nextCursor": "string"
}Read one of your assets
Status, and once clean, the dimensions of the processed image. Requires assets:write.
Authorizations:
path Parameters
| id required | string <uuid> Asset id. |
Responses
| 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 |
| 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
- 200
- 401
- 403
- 404
- 429
- 500
{- "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"
}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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "id": "01921233-0c0d-7e0f-8a1b-2c3d4e5f6a7b",
- "type": "submission.published",
- "schemaVersion": "1",
- "occurredAt": "2026-10-21T13:00:12Z",
- "data": {
- "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
- "externalRef": "ptr-2026-10-0042",
- "status": "published",
- "revision": 2,
- "property": "example-property",
- "publishedAt": "2026-10-21T13:00:10Z"
}
}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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "id": "01921b00-4444-7555-8666-777788889999",
- "type": "submission.updated",
- "schemaVersion": "1",
- "occurredAt": "2026-11-04T09:15:02Z",
- "data": {
- "submissionId": "01920c4e-8b7a-7c3d-9e2f-5a6b7c8d9e0f",
- "externalRef": "ptr-2026-10-0042",
- "status": "published",
- "revision": 2,
- "property": "example-property",
- "updatedAt": "2026-11-04T09:15:00Z"
}
}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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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": {
- "id": "01921500-1234-7abc-8def-0123456789ab",
- "status": "done",
- "note": "The filing deadline in section 3 now reads May 1."
}
}
}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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "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
|
| 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 |
| SCS-Delivery-Attempt required | integer >= 1 1 for the first delivery, then 2, 3, … on retries. |
Request Body schema: application/jsonrequired
| id required | string <uuid> Equal to the |
| 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
- Payload
{- "id": "01921600-0000-7111-8222-333344440000",
- "type": "endpoint.verification",
- "schemaVersion": "1",
- "occurredAt": "2026-10-18T14:00:01Z",
- "data": {
- "challenge": "3q2-7wEXAMPLEchallengeValue0000"
}
}Response samples
- 200
{- "challenge": "3q2-7wEXAMPLEchallengeValue0000"
}Read your webhook endpoint
Your registered endpoint, or 404 if none. The signing secret is never returned here.
Authorizations:
Responses
| 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
- 200
- 401
- 403
- 404
- 429
- 500
{- "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:
header Parameters
| Idempotency-Key | string [ 8 .. 128 ] characters ^[A-Za-z0-9._:-]+$ Optional; send one when creating the endpoint or rotating its secret.
|
Request Body schema: application/jsonrequired
| 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 |
| 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
| 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. |
| 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
- Payload
{- "events": [
- "submission.status_changed",
- "submission.received",
- "submission.changes_requested",
- "submission.approved",
- "submission.published",
- "submission.updated",
- "submission.rejected",
- "submission.unpublished"
]
}Response samples
- 200
- 201
- 400
- 401
- 403
- 404
- 409
- 415
- 422
- 429
- 500
- 503
{- "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:
Responses
Response samples
- 401
- 403
- 404
- 429
- 500
- 503
{- "title": "Invalid or expired access token",
- "status": 401,
- "requestId": "req_01920c4e8b7b"
}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:
Responses
Response samples
- 202
- 401
- 403
- 404
- 409
- 429
- 500
- 503
{- "eventId": "01921600-aaaa-7bbb-8ccc-ddddeeeeffff"
}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:
Responses
| 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
- 200
- 401
- 403
- 429
- 500
{- "data": [
- {
- "id": "example-property",
- "name": "Example Property",
- "status": "active",
- "ymylAllowed": true,
- "languages": [
- "en-US"
], - "categories": [
- {
- "slug": "money",
- "name": "Money"
}, - {
- "slug": "pcs-and-moving",
- "name": "PCS & Moving"
}, - {
- "slug": "family-life",
- "name": "Family Life"
}
], - "tags": [
- {
- "slug": "taxes",
- "name": "Taxes"
}, - {
- "slug": "pcs",
- "name": "PCS"
}, - {
- "slug": "budgeting",
- "name": "Budgeting"
}
], - "quota": {
- "limit": 10,
- "windowDays": 7,
- "remaining": 7,
- "nextSlotAt": "2026-10-21T09:30:00Z"
}
}
]
}