Propose a write to a connected account (reply to a review, update a listing, set keywords, upload screenshots, change Apple Ads bids / keywords / negatives); always start with dry_run=true.

POSThttps://api.appgoat.ai/v1/actions
0+ credits0 (dry run, pending approval) or 1 when auto-appliedoperationId createAction

Computes the before → after diff from the live account and runs guardrails (policy / tone / claims probabilities for text, plus deterministic checks like character limits and bid jumps). Free.

  • dry_run: true → a draft (24 h) with the diff: show it to the user. To go ahead, call POST /v1/actions/{id}/approve.
  • otherwise → pending_approval (a person approves it in the dashboard, link in approve_url), or applied immediately when the org has an auto-apply rule for this type that the guardrail score clears (Studio, 1 credit). Blocking check failures (over a character limit, nothing to change, not editable) return 400 with checks (a dry run returns them in guardrail.checks_failed instead).

Params per type (app = a linked App Store id or package name, locale like en-US):

  • review.reply (asc, play_console): { app, review_id, text } — review ids from GET /v1/connections/{id}/reviews. Play replies ≤ 350 chars.
  • listing.update (asc, play_console): { app, locale, fields: { name|title, subtitle, description, promotional_text, whats_new, marketing_url, support_url, privacy_policy_url } } (Play: title, short_description, full_description, video).
  • keywords.set (asc): { app, locale, keywords: [..] | "a,b,c", mode?: replace|add|remove } — 100 bytes comma-joined.
  • screenshots.upload (asc, play_console): { app, locale, display_type: APP_IPHONE_67 … (asc) | image_type: phoneScreenshots … (play), images: [{ url }], replace?: true }.
  • ads.keyword.bid (apple_ads): { campaign_id, ad_group_id, keywords: [{ id, bid?, status?: ENABLED|PAUSED }], max_increase?: 1 }.
  • ads.keyword.add (apple_ads): { campaign_id, ad_group_id, keywords: [{ text, match_type?: EXACT|BROAD, bid? }] }.
  • ads.negative.add (apple_ads): { campaign_id, ad_group_id?, keywords: [{ text, match_type? }] }.

Request

curl -X POST "https://api.appgoat.ai/v1/actions" \
  -H "x-api-key: $APPGOAT_API_KEY" \
  -H "content-type: application/json" \
  -d '{"type":"review.reply","dry_run":true,"source":"playbook:reply-to-every-review","params":{"app":"1234567890","review_id":"00000043-a2fa-4f01-0000-000000000000","text":"Thanks for the detailed report! Version 2.4.1 fixes the login loop."}}'

Request body

JSON, required.

typeActionType
connectionstring
Connection id; optional when exactly one active connection fits the type (and params.app).
paramsobject
dry_runboolean
sourcestring
Who proposes it: api, mcp or playbook:<name> (shown in the approvals list).

Response

201 with data: Action. See the envelope for meta.

idstring
typeActionType
connection_idstring
status"draft" | "pending_approval" | "approved" | "applied" | "failed" | "rejected"
paramsobject
diffDiff
guardrailGuardrail
resultany
What the provider returned when applied.
errorstring | null
created_bystring
api, mcp, playbook:<name> or dashboard.
approved_bystring | null
user:<id>, auto (rule) or api.
rejected_reasonstring | null
creditsinteger
created_atstring
applied_atstring | null
expires_atstring | null
approve_urlstring | null
Dashboard page where a person approves it.
auditobject[]
Only on GET /actions/{id}.
audit[].eventstring
audit[].actorstring
audit[].atstring
audit[].detailobject | null
Example
{
  "data": {
    "id": "act_Qm3xXk2Lw9PaZr0cVb",
    "type": "review.reply",
    "connection_id": "string",
    "status": "draft",
    "params": {},
    "diff": {
      "target": {},
      "changes": [
        {}
      ],
      "summary": "string",
      "notes": [
        "string"
      ]
    },
    "guardrail": {
      "status": "passed",
      "score": 4.5,
      "checks": {},
      "warnings": [
        "string"
      ],
      "checks_failed": [
        {}
      ],
      "model": "string"
    },
    "result": null,
    "error": "string",
    "created_by": "string",
    "approved_by": "string",
    "rejected_reason": "string",
    "credits": 1,
    "created_at": "2026-10-22T10:00:00Z",
    "applied_at": "2026-10-22T10:00:00Z",
    "expires_at": "2026-10-22T10:00:00Z",
    "approve_url": "string",
    "audit": [
      {
        "event": "string",
        "actor": "string",
        "at": "2026-10-22T10:00:00Z",
        "detail": {}
      }
    ]
  },
  "meta": {
    "credits_used": 1,
    "credits_remaining": 9981,
    "request_id": "req_123"
  }
}

Status codes

201
The action (draft, pending_approval or applied).
A parameter is missing or invalid (error.param names it).
Missing or invalid API key.
insufficient_credits (the call costs more than you have left; nothing was done) or plan_limit (a feature of a higher plan).
Not found.
409
The state does not allow this (already applied, expired, connection read-only or invalid, account changed since the dry run).
Too many requests per second for your plan.
502
App Store Connect / Google Play / Apple Ads answered with an error (message passed through).