---
title: "Validate a campaign spec (dry-run)"
method: POST
path: "/campaigns/validate"
tags: ["Creators"]
---

# Validate a campaign spec (dry-run)

`POST /campaigns/validate`

## Request

- Base URL: `https://viral.app/api/v1`
- URL: `https://viral.app/api/v1/campaigns/validate`
- Auth: API key in header `x-api-key`

Validates a campaign spec exactly as create or update would and returns the normalized spec, the sentences that describe every rule-set, the sheet's alerts, and a previewHash to pass to the write — without writing anything. Pass id to dry-run an edit (the changes are merged into the campaign's current configuration and the changed sentences are listed), fromJobId to derive a draft from a job listing, or fromCampaignId to start from a copy.

## Request body

- object: Dry-run a campaign spec: validates it exactly as create or update would, and returns the normalized spec, the sentences that describe it, the sheet's alerts and a previewHash — without writing anything.
  - `spec` object: The campaign to dry-run. Alone: a complete spec for a new campaign. With id: only the changes, merged into the campaign's current configuration. With fromJobId or fromCampaignId: overrides applied on top of the derived draft.
    - `name` string: Campaign name
    - `description` string: Campaign description
    - `currency` 'USD' | 'EUR' | 'CHF' | 'GBP' | 'CNY' | 'JPY' | 'KRW' | 'THB' | 'IDR' | 'VND' | 'RUB': Currency used for campaign payouts
    - `schedule` object
      - `interval` 'day' | 'week' | 'month': Payout cycle unit. Monthly = month × 1, biweekly = week × 2, weekly = week × 1.
      - `intervalCount` integer: How many units make one payout cycle (1–12).
      - `anchor` 'rolling' | 'fixed': rolling: each creator's cycle starts the day they join, so creators are added and paid on their own dates. fixed: everyone shares the same calendar cycle and payouts go out in one batch per cycle; a creator added mid-cycle gets that cycle as a first period cut to the join day, a fixed salary in it paid pro rata. Required — it locks once the campaign is in use.
      - `startsOn` string, date: Campaign start (YYYY-MM-DD). Defaults to today for rolling campaigns and to the start of the current week or month for fixed weekly or monthly ones. New campaigns cannot start before the 1st of the previous month.
      - `timeZone` string: IANA time zone the periods and dates follow: a period ends at midnight there, a video belongs to the period it was published in there, and "today" is the zone's day. Views are still counted per UTC day. Defaults to "UTC" on create; editable until a payout depends on its periods or a period is canceled (`CAMPAIGN_FIELD_LOCKED`).
    - `videoTarget` integer: Expected number of videos per payment period
    - `videoTargetUnit` 'videos' | 'creatives' | 'per_platform': What the video target counts: "videos" (every post on every platform, a cross-post on three platforms counts 3), "creatives" (each video once, its copies on other platforms included) or "per_platform" (a count per platform in videoTargetPlatforms; videoTarget is their sum). Omitted: unchanged (sending videoTargetPlatforms alone switches to "per_platform"). Base payouts that stop following the target this way keep its current number as their own.
    - `videoTargetPlatforms` object, nullable: Videos per platform per cycle, for videoTargetUnit "per_platform" (e.g. {"tiktok": 30, "instagram": 30}). Posts on other platforms do not count toward the target.
    - `briefId` string, nullable: Campaign brief ID with orgbrief_ prefix
    - `payouts` object: Replaces every payout of the campaign when present.
      - `fixedSalary` object: A guaranteed amount per cycle, regardless of videos.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `basePayouts` object[]
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `perVideo` number, required: Paid per published video (until the first step).
        - `steps` object[]: Rate steps: a video is paid the rate of the step its place in the period falls in. An update that omits them keeps them; [] removes them.
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `startsAfter` union, required: Videos before the step: a count or "videoTarget". Rises and stays below the end.
            - integer
            - 'videoTarget'
          - `perVideo` number, required: Paid per video from this step on.
        - `limit` object, nullable: Omitted: the base payout stops at the video target. null: unlimited, every extra video is paid.
          - `videos` union: Videos paid per cycle: a count (above the target: an overshoot allowance), or "videoTarget".
            - integer
            - 'videoTarget'
          - `amount` number: Deprecated: payouts.baseCap. Only with one base payout; without videos only this cap limits it.
        - `minViews` integer: Only videos reaching this many total views by the end of the payout period they were published in earn the base payout. The same as a views condition with countWhen byPublicationPeriodEnd; send one or the other.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any': all: a video must meet every condition. any: one condition is enough.
          - `items` union[], required
            - union
              - …
        - `scope` 'per_video' | 'per_creative': per_video (the default): every video counts. per_creative: copies of one video posted to several of this payout's platforms (grouped as a creative) count as one creative, their views added up: it earns one base payout, and views conditions judge the creative's combined views (directly in a match all group, never next to a totalVideos row that carries). It counts in the first period at whose end its views meet a reached atLeast condition, otherwise only in the period its first copy was posted in. Needs two or more platforms; not with creatives once.
        - `creatives` 'once': once: only each creative's winner counts. When the creator posts the same video to several of this payout's platforms (copies grouped as a creative), the copy with the most views earns one base payout, in the first period any copy is counted; views conditions judge each copy on its own. Needs two or more platforms; not with scope per_creative. Omit to count every copy.
      - `cpmPayouts` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any': all: a video must meet every condition. any: one condition is enough.
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` integer, required: Views where the tier starts (inclusive).
          - `to` integer, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `rate` number, required: Paid per 1,000 views in this tier.
      - `flatBonuses` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `mode` 'highest' | 'aggregated': highest: only the highest unlocked tier is paid. aggregated: every unlocked tier adds its amount. Defaults to highest with two or more tiers or a tier that carries `to`, aggregated for a single open-ended tier.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any': all: a video must meet every condition. any: one condition is enough.
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` integer, required: Views where the tier starts (inclusive).
          - `to` integer, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `amount` number, required: Bonus paid when the tier is reached.
      - `baseCap` object: The most all base payouts together pay per cycle, rate steps included.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `performanceCap` object: Maximum combined CPM payouts and flat bonuses per cycle. Base payout and fixed salary are not affected. Needs at least one CPM payout or flat bonus.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `eligibility` object: How long and which videos count toward performance payouts.
        - `windowDays` integer: Days after publication a video keeps counting toward performance payouts.
        - `publicationScope` 'include_earlier' | 'current_period_only': include_earlier: videos published before the period still count while inside their window. current_period_only: only videos published within the period count, and windows are cut off at period end.
  - `id` string: Dry-run an edit of this campaign instead of a new one.
  - `fromJobId` string: Derive the draft from a job listing's pay fields: cadence from its pay frequency, the weekly video minimum scaled to the cycle, amounts converted. The payout window is never derivable and comes back in `missing`.
  - `fromCampaignId` string: Start from a copy of an existing campaign: its spec with a new name and a fresh start date.

## Response `200`

OK

- object: The dry-run result: normalized spec, sentences, alerts and a previewHash, or the issues.
  - `ok` boolean, required: true when create or update would accept this spec as it is.
  - `mode` 'create' | 'edit', required
  - `spec` object: The normalized spec with every default filled. Send exactly this to create or update.
    - `name` string, required: Campaign name
    - `description` string: Campaign description
    - `currency` 'USD' | 'EUR' | 'CHF' | 'GBP' | 'CNY' | 'JPY' | 'KRW' | 'THB' | 'IDR' | 'VND' | 'RUB', required: Currency used for campaign payouts
    - `schedule` object, required: Payout cadence and window.
      - `interval` 'day' | 'week' | 'month', required: Payout cycle unit. Monthly = month × 1, biweekly = week × 2, weekly = week × 1.
      - `intervalCount` number, required: How many units make one payout cycle.
      - `anchor` 'rolling' | 'fixed', required: rolling: each creator's cycle starts the day they join, so creators are added and paid on their own dates. fixed: everyone shares the same calendar cycle and payouts go out in one batch per cycle; a creator added mid-cycle gets that cycle as a first period cut to the join day, a fixed salary in it paid pro rata. Required — it locks once the campaign is in use.
      - `startsOn` string, required: Campaign start (YYYY-MM-DD).
      - `timeZone` string: IANA time zone the periods and dates follow: a period ends at midnight there, a video belongs to the period it was published in there, and "today" is the zone's day. Views are still counted per UTC day. Defaults to "UTC" on create; editable until a payout depends on its periods or a period is canceled (`CAMPAIGN_FIELD_LOCKED`).
    - `videoTarget` number, required: Videos a creator should publish per cycle, in videoTargetUnit: every post on every platform ("videos", the default), each video once whatever platforms it is posted to ("creatives"), or the sum of videoTargetPlatforms ("per_platform", where the platforms' sum replaces the value sent).
    - `videoTargetUnit` 'videos' | 'creatives' | 'per_platform': What the video target counts: "videos" (every post on every platform, a cross-post on three platforms counts 3), "creatives" (each video once, its copies on other platforms included) or "per_platform" (a count per platform in videoTargetPlatforms; videoTarget is their sum).
    - `videoTargetPlatforms` object, nullable: Videos per platform per cycle, with videoTargetUnit "per_platform".
    - `briefId` string, nullable: Campaign brief ID with orgbrief_ prefix
    - `payouts` object, required
      - `fixedSalary` object: A guaranteed amount per cycle, regardless of videos.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `basePayouts` object[]
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `perVideo` number, required: Paid per published video (until the first step).
        - `steps` object[]: Rate steps: a video is paid the rate of the step its place in the period falls in. An update that omits them keeps them; [] removes them.
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `startsAfter` union, required: Videos before the step: a count or "videoTarget". Rises and stays below the end.
            - number
            - 'videoTarget'
          - `perVideo` number, required: Paid per video from this step on.
        - `limit` object, nullable: Omitted: the base payout stops at the video target. null: unlimited, every extra video is paid.
          - `videos` union: Videos paid per cycle: a count (above the target: an overshoot allowance), or "videoTarget".
            - number
            - 'videoTarget'
          - `amount` number: Deprecated: payouts.baseCap. Only with one base payout; without videos only this cap limits it.
        - `minViews` number: Only videos reaching this many total views by the end of the payout period they were published in earn the base payout. The same as a views condition with countWhen byPublicationPeriodEnd; send one or the other.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `scope` 'per_video' | 'per_creative': per_video (the default): every video counts. per_creative: copies of one video posted to several of this payout's platforms (grouped as a creative) count as one creative, their views added up: it earns one base payout, and views conditions judge the creative's combined views (directly in a match all group, never next to a totalVideos row that carries). It counts in the first period at whose end its views meet a reached atLeast condition, otherwise only in the period its first copy was posted in. Needs two or more platforms; not with creatives once.
        - `creatives` 'once': once: only each creative's winner counts. When the creator posts the same video to several of this payout's platforms (copies grouped as a creative), the copy with the most views earns one base payout, in the first period any copy is counted; views conditions judge each copy on its own. Needs two or more platforms; not with scope per_creative. Omit to count every copy.
      - `cpmPayouts` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` number, required: Views where the tier starts (inclusive).
          - `to` number, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `rate` number, required: Paid per 1,000 views in this tier.
      - `flatBonuses` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `mode` 'highest' | 'aggregated': highest: only the highest unlocked tier is paid. aggregated: every unlocked tier adds its amount. Defaults to highest with two or more tiers or a tier that carries `to`, aggregated for a single open-ended tier.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` number, required: Views where the tier starts (inclusive).
          - `to` number, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `amount` number, required: Bonus paid when the tier is reached.
      - `baseCap` object: The most all base payouts together pay per cycle, rate steps included.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `performanceCap` object: Maximum combined CPM payouts and flat bonuses per cycle. Base payout and fixed salary are not affected. Needs at least one CPM payout or flat bonus.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `eligibility` object: How long and which videos count toward performance payouts.
        - `windowDays` number: Days after publication a video keeps counting toward performance payouts.
        - `publicationScope` 'include_earlier' | 'current_period_only': include_earlier: videos published before the period still count while inside their window. current_period_only: only videos published within the period count, and windows are cut off at period end.
  - `summary` object: The campaign's rules in plain sentences, one per rule-set — the same wording the campaign sheet shows.
    - `schedule` string, required
    - `videoTarget` string: What the video target counts, when it is not every post: each video once (videoTargetUnit creatives) or a count per platform (per_platform). Absent for a videos target, whose count needs no sentence.
    - `fixedSalary` string
    - `basePayouts` string[], required
    - `baseCap` string
    - `cpmPayouts` string[], required
    - `flatBonuses` string[], required
    - `performanceCap` string
    - `eligibility` string
    - `text` string, required: Every sentence above, in reading order.
    - `headline` string, required: One line: payouts and cadence, e.g. “€10.00 / video + CPM €1.00–€1.50 · monthly, rolling”.
  - `previewHash` string: Hash of the normalized spec. Pass it to create or update to guarantee the campaign written is the one reviewed.
  - `issues` object[], required: What create or update would reject, with the spec path of each problem.
    - `path` string, required: Dotted path into the spec, e.g. payouts.cpmPayouts.0.tiers.1.from.
    - `message` string, required
  - `warnings` object[], required
    - `code` 'long_payout_cycle' | 'daily_cadence' | 'future_start' | 'eligibility_window_exceeds_interval' | 'flat_bonus_waits_for_window' | 'bonus_tiers_unpaid_after_end' | 'performance_cap_below_bonus' | 'base_payouts_overlap' | 'cpm_payouts_overlap' | 'flat_bonuses_overlap' | 'manual_payouts_only' | 'rules_apply_immediately' | 'views_conditions_changed_after_payouts' | 'total_videos_changed_after_payouts' | 'payout_conditions_removed' | 'creative_payouts_removed' | 'time_zone_changed' | 'derived_from_job' | 'video_target_followers_frozen', required
    - `title` string, required
    - `message` string, required
  - `lockedFields` string[]
  - `changes` object[]: Edit dry-runs only: every rule-set whose sentence would change, before and after.
    - `section` string, required: What changed: name, videoTarget (count, unit or platforms), currency, schedule, fixedSalary, basePayouts, baseCap, cpmPayouts, flatBonuses, performanceCap or eligibility.
    - `before` string, nullable, required
    - `after` string, nullable, required
  - `customRateImpact` object: Edit dry-runs only, when creators have custom rates on the campaign: the values the edit would reset, the changed values creators override, and notices.
    - `resets` object[], required: Creators' custom values on payouts the edit removes. The update needs resetRemovedCustomRates to reset them.
      - `creatorId` string, required: Organization creator ID with orgcre_ prefix
      - `key` string, required
      - `label` string, required
      - `value` string, required
    - `overridden` object[], required: Values the edit changes that some creators have their own of. They keep theirs unless the update lists the value in applyToCustomRates.
      - `id` string, nullable, required: The payout's id; for the video target null, or the platform on a per-platform target.
      - `field` 'perVideo' | 'videoCap' | 'amountCap' | 'rate' | 'amount' | 'videoTarget', required
      - `key` string, required
      - `label` string, required
      - `before` string, required
      - `after` string, required
      - `creatorIds` string[], required
    - `notices` object[], required
      - `creatorIds` string[], required
      - `message` string, required
  - `draft` object: Job-derived runs only: the partial spec the listing yields. Complete the `missing` paths and validate again.
    - `name` string
    - `currency` 'USD' | 'EUR' | 'CHF' | 'GBP' | 'CNY' | 'JPY' | 'KRW' | 'THB' | 'IDR' | 'VND' | 'RUB', required: Currency used for campaign payouts
    - `schedule` object, required
      - `interval` 'day' | 'week' | 'month', required: Payout cycle unit. Monthly = month × 1, biweekly = week × 2, weekly = week × 1.
      - `intervalCount` number, required: How many units make one payout cycle.
    - `videoTarget` number
    - `videoTargetUnit` 'videos' | 'creatives' | 'per_platform': What the video target counts: "videos" (every post on every platform, a cross-post on three platforms counts 3), "creatives" (each video once, its copies on other platforms included) or "per_platform" (a count per platform in videoTargetPlatforms; videoTarget is their sum).
    - `videoTargetPlatforms` object, nullable
    - `payouts` object, required
      - `fixedSalary` object: A guaranteed amount per cycle, regardless of videos.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `basePayouts` object[]
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `perVideo` number, required: Paid per published video (until the first step).
        - `steps` object[]: Rate steps: a video is paid the rate of the step its place in the period falls in. An update that omits them keeps them; [] removes them.
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `startsAfter` union, required: Videos before the step: a count or "videoTarget". Rises and stays below the end.
            - number
            - 'videoTarget'
          - `perVideo` number, required: Paid per video from this step on.
        - `limit` object, nullable: Omitted: the base payout stops at the video target. null: unlimited, every extra video is paid.
          - `videos` union: Videos paid per cycle: a count (above the target: an overshoot allowance), or "videoTarget".
            - number
            - 'videoTarget'
          - `amount` number: Deprecated: payouts.baseCap. Only with one base payout; without videos only this cap limits it.
        - `minViews` number: Only videos reaching this many total views by the end of the payout period they were published in earn the base payout. The same as a views condition with countWhen byPublicationPeriodEnd; send one or the other.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `scope` 'per_video' | 'per_creative': per_video (the default): every video counts. per_creative: copies of one video posted to several of this payout's platforms (grouped as a creative) count as one creative, their views added up: it earns one base payout, and views conditions judge the creative's combined views (directly in a match all group, never next to a totalVideos row that carries). It counts in the first period at whose end its views meet a reached atLeast condition, otherwise only in the period its first copy was posted in. Needs two or more platforms; not with creatives once.
        - `creatives` 'once': once: only each creative's winner counts. When the creator posts the same video to several of this payout's platforms (copies grouped as a creative), the copy with the most views earns one base payout, in the first period any copy is counted; views conditions judge each copy on its own. Needs two or more platforms; not with scope per_creative. Omit to count every copy.
      - `cpmPayouts` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` number, required: Views where the tier starts (inclusive).
          - `to` number, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `rate` number, required: Paid per 1,000 views in this tier.
      - `flatBonuses` object[]
        - `scope` 'per_video' | 'per_creative' | 'all_content': per_video: applied to every video with its own views. per_creative: copies of one video posted to several platforms (grouped as a creative) count as one video, their views added up; a video in no creative counts on its own. all_content: applied once per cycle to the creator's aggregated views.
        - `creatives` 'bestInPeriod' | 'bestInWindow': Pay only each creative's winner: when the creator posts the same video to several of this payout's platforms (copies grouped as a creative), only the copy with the most views earns it, judged in the payout period (bestInPeriod) or in each copy's eligibility window (bestInWindow). A creative is never paid twice. Needs two or more platforms; not with scope per_creative or a current_period_only eligibility; bonuses per video take bestInWindow only, and CPM payouts per video with an atLeast views condition too. Omit to pay every copy.
        - `mode` 'highest' | 'aggregated': highest: only the highest unlocked tier is paid. aggregated: every unlocked tier adds its amount. Defaults to highest with two or more tiers or a tier that carries `to`, aggregated for a single open-ended tier.
        - `platforms` string[]: Platforms this entry applies to. Omit for all platforms. Entries of the same family may share platforms: a video on a shared platform earns each entry that covers it, and the dry-run warns.
        - `conditions` object: Which videos take part in this payout: content type (video/slideshow), tags (custom, project or geography tag ids; anyOf, allOf, noneOf), hashtags (anyOf, allOf, noneOf), views (atLeast or below, per video), totalViews (atLeast or below, the creator's total views) and totalVideos (atLeast or below, the creator's number of videos). On a per-video payout a video must match to be paid; on an all-content payout only matching videos' views add up. Views conditions work per video: base payouts count a video in the period it reaches an atLeast threshold (countWhen reached, the default) or only by the end of its publication period (countWhen byPublicationPeriodEnd, the same as minViews, and the only variant of a base below); per-video CPM payouts and bonuses pay a video's earlier views once it reaches an atLeast threshold, and pay a below one only for periods the video ends under the count. totalViews judges each payout period on the creator's total views over the payout's platforms, whatever the other conditions say (countFrom periodVideos: views of the videos published in the period, counted to its end; eligibleVideos: views gained in the period by every video inside its eligibility window): a period that misses pays nothing through this payout, and its views are never paid later. totalVideos judges each payout period on every video the creator published in it, on every platform (a post on three platforms counts 3), whatever the other conditions and the payout's platforms say; its value is a count or "videoTarget" (the campaign's video target, or the creator's own with custom rates, never prorated, counted in the target's unit: under creatives each video once whatever platforms it is on, under per_platform every listed platform must reach its own count and other platforms do not count). A period that misses pays nothing through this payout; on a base payout an atLeast row may carry the missed period's videos instead (carry previousPeriod: paid in the next period if that one reaches its own minimum; untilPaid: every missed period waits for the first that does), priced as if paid in the period they were posted in (its rate steps, its end and its base payout cap, never the paying period's), only directly in a match all group. One atLeast and one below make a range. Periods follow the campaign's schedule.timeZone; views are counted per UTC day. The platform list always applies on top.
          - `match` 'all' | 'any', required
          - `items` union[], required
            - union
              - …
        - `tiers` object[], required
          - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
          - `from` number, required: Views where the tier starts (inclusive).
          - `to` number, nullable: Views where the tier ends (exclusive); the next tier's from must equal it. Omit or null on the last tier for an open end.
          - `amount` number, required: Bonus paid when the tier is reached.
      - `baseCap` object: The most all base payouts together pay per cycle, rate steps included.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `performanceCap` object: Maximum combined CPM payouts and flat bonuses per cycle. Base payout and fixed salary are not affected. Needs at least one CPM payout or flat bonus.
        - `id` string: Stable id of the stored rule (read it from GET). Send it back to keep the rule, and any creator's custom rate on it, attached through edits; omit it for new entries.
        - `amount` number, required: Amount in major units of the campaign currency (10 = 10.00).
      - `eligibility` object: How long and which videos count toward performance payouts.
        - `windowDays` number: Days after publication a video keeps counting toward performance payouts.
        - `publicationScope` 'include_earlier' | 'current_period_only': include_earlier: videos published before the period still count while inside their window. current_period_only: only videos published within the period count, and windows are cut off at period end.
  - `missing` string[]

## Other responses

- `400`: 400
- `401`: 401
- `402`: 402
- `404`: 404
- `408`: 408
- `429`: 429
- `500`: 500

---

[API](https://skmtc.dev/viral/apis/viral-app-api.md) · [All operations](https://skmtc.dev/viral/apis/viral-app-api/llms.txt) · [OpenAPI document](https://skmtc.dev/viral/apis/viral-app-api/revisions/31e97fa79ef1?raw)
