{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/postlake-dev/json-schema/postlake-dev-post-schema.json",
  "title": "Post",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/postlake-dev-openapi.json#/components/schemas/Post",
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "sourceDraftId": {
      "type": "string",
      "description": "Immutable original draft ID. New approvals preserve this as id; legacy audited approvals may resolve an older ID to a different canonical id."
    },
    "snapshot": {
      "type": "string",
      "description": "Computed full-state approval fence returned by GET. Includes media, options and metadata."
    },
    "state": {
      "$ref": "#/$defs/PostState"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time"
    },
    "clientId": {
      "type": "string",
      "description": "Stable machine principal that created the post. Absent for human-created dashboard posts."
    },
    "surface": {
      "type": "string",
      "enum": [
        "dashboard",
        "api",
        "agent"
      ],
      "description": "Where the post was created, independent of the principal identity."
    },
    "approval": {
      "type": "object",
      "description": "Present for approval-gated agent work, including the owner's final decision when one has been made.",
      "properties": {
        "requestedAt": {
          "type": "string",
          "format": "date-time"
        },
        "decision": {
          "type": "string",
          "enum": [
            "approved",
            "rejected"
          ]
        },
        "decidedAt": {
          "type": "string",
          "format": "date-time"
        },
        "reason": {
          "type": "string"
        }
      },
      "required": [
        "requestedAt"
      ]
    },
    "scheduledAt": {
      "oneOf": [
        {
          "type": "string",
          "format": "date-time"
        },
        {
          "type": "null"
        }
      ]
    },
    "timezone": {
      "type": "string",
      "description": "IANA timezone the caller scheduled in, when they named one."
    },
    "scheduledAtLocal": {
      "type": "string",
      "description": "Wall-clock time in timezone, computed on read. Not stored."
    },
    "text": {
      "type": "string"
    },
    "campaign": {
      "type": "string"
    },
    "assetMetadata": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      }
    },
    "media": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Advisory notes from create or edit, for example PNG converted to JPEG for TikTok."
    },
    "platformOptions": {
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "pinterest": {
              "type": "object",
              "properties": {
                "boardId": {
                  "type": "string"
                },
                "link": {
                  "type": "string"
                },
                "altText": {
                  "type": "string"
                }
              }
            }
          }
        },
        {
          "type": "null"
        }
      ]
    },
    "targets": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/PostTarget"
      }
    }
  },
  "required": [
    "id",
    "state",
    "createdAt",
    "scheduledAt",
    "text",
    "media",
    "platformOptions",
    "targets"
  ],
  "$defs": {
    "NormalisedError": {
      "type": "object",
      "description": "`type` is the coarse category you route on and is deliberately small, so it cannot say WHICH input was wrong. `code`, `fix`, `docs` and `param` are what turn a rejection into something a caller can act on rather than guess at. They are optional only in the sense that not every error has all four.",
      "properties": {
        "type": {
          "type": "string",
          "description": "Coarse category to route on, e.g. rate_limited, auth_expired, platform_rejected."
        },
        "message": {
          "type": "string"
        },
        "platform": {
          "oneOf": [
            {
              "$ref": "#/$defs/Platform"
            },
            {
              "type": "null"
            }
          ]
        },
        "retryable": {
          "type": "boolean"
        },
        "code": {
          "type": "string",
          "description": "Stable, granular handle to branch on, e.g. text_too_long. Also the anchor in the error reference."
        },
        "fix": {
          "type": "string",
          "description": "The next action, in plain words. Safe to show a person as-is."
        },
        "docs": {
          "type": "string",
          "format": "uri",
          "description": "Where the rule is written down, so it is learned once rather than rediscovered."
        },
        "param": {
          "type": "string",
          "description": "The request field at fault, so a form can mark the right input."
        }
      },
      "required": [
        "type",
        "message",
        "retryable"
      ]
    },
    "Platform": {
      "type": "string",
      "enum": [
        "bluesky",
        "threads",
        "x",
        "linkedin",
        "instagram",
        "tiktok",
        "facebook",
        "youtube",
        "pinterest"
      ]
    },
    "PostState": {
      "type": "string",
      "enum": [
        "draft",
        "awaiting_approval",
        "rejected",
        "queued",
        "scheduled",
        "processing",
        "partial",
        "published",
        "failed"
      ]
    },
    "PostTarget": {
      "type": "object",
      "properties": {
        "removal": {
          "type": "object",
          "description": "Present after accepted network removal on a partial result. Retry skips accepted destinations; verified separately confirms absence.",
          "properties": {
            "accepted": {
              "type": "boolean",
              "enum": [
                true
              ]
            },
            "verified": {
              "type": "boolean"
            },
            "at": {
              "type": "string",
              "format": "date-time"
            }
          },
          "required": [
            "accepted",
            "verified",
            "at"
          ]
        },
        "account": {
          "type": "string"
        },
        "platform": {
          "$ref": "#/$defs/Platform"
        },
        "state": {
          "$ref": "#/$defs/TargetState"
        },
        "profile": {
          "type": "string",
          "description": "Username of the profile this channel belongs to. Resolved when the post is read, so it follows the channel if it moves. Absent once the channel is disconnected."
        },
        "channelName": {
          "type": "string",
          "description": "The channel's handle at the time of reading, not the one frozen at publish. Absent once the channel is disconnected."
        },
        "connection": {
          "type": "object",
          "description": "The native account or destination identity captured when this target was scheduled. It is used only to repair a disconnected channel safely.",
          "properties": {
            "platformAccountId": {
              "type": "string"
            },
            "destinationId": {
              "type": "string"
            },
            "destinationName": {
              "type": "string"
            },
            "handle": {
              "type": "string"
            }
          }
        },
        "remoteId": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "url": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ]
        },
        "publishedAt": {
          "oneOf": [
            {
              "type": "string",
              "format": "date-time"
            },
            {
              "type": "null"
            }
          ]
        },
        "permalinkPending": {
          "type": "boolean",
          "description": "True when publication succeeded but an exact public permalink is still being resolved."
        },
        "error": {
          "oneOf": [
            {
              "$ref": "#/$defs/NormalisedError"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "account",
        "platform",
        "state",
        "remoteId",
        "url",
        "publishedAt",
        "error"
      ]
    },
    "TargetState": {
      "type": "string",
      "enum": [
        "queued",
        "scheduled",
        "processing",
        "published",
        "failed"
      ]
    }
  }
}