GEMINI LABJP
OUTAGE — Gemini recovers from one of its biggest outages (errors 1076/1099) as engineering mitigations take effectDAILY-BRIEF — The new Daily Brief agent works overnight, analyzing your inbox, calendar, and tasks into a personalized morning digestGEMINI-OMNI — Gemini Omni combines Gemini with Google's generative media models to produce consistent, high-quality video from a single promptENTERPRISE — Gemini 3.5 Flash is enabled by default in Gemini Enterprise as of Jun 8 and can no longer be turned offDEPRECATION — Image preview models (3.1-flash-image / 3-pro-image) shut down Jun 25; migrate to the GA versions nowFILE-SEARCH — File Search now supports multimodal search, natively embedding and searching images via gemini-embedding-2OUTAGE — Gemini recovers from one of its biggest outages (errors 1076/1099) as engineering mitigations take effectDAILY-BRIEF — The new Daily Brief agent works overnight, analyzing your inbox, calendar, and tasks into a personalized morning digestGEMINI-OMNI — Gemini Omni combines Gemini with Google's generative media models to produce consistent, high-quality video from a single promptENTERPRISE — Gemini 3.5 Flash is enabled by default in Gemini Enterprise as of Jun 8 and can no longer be turned offDEPRECATION — Image preview models (3.1-flash-image / 3-pro-image) shut down Jun 25; migrate to the GA versions nowFILE-SEARCH — File Search now supports multimodal search, natively embedding and searching images via gemini-embedding-2
Articles/API / SDK
API / SDK/2026-06-12Intermediate

Building an App Store Rejection Workflow with the Gemini API — From Structured Notices to Resolution Center Replies

How I use the Gemini API to parse App Store rejection notices into structured JSON, cross-check guidelines, draft Resolution Center replies, and run pre-submission checks as an indie developer.

Gemini API132App Store ReviewStructured Output6Indie Development4App Operations

Premium Article

One morning this spring, while I was pushing parallel updates to six of my apps, I opened App Store Connect and found two unread messages waiting in the Resolution Center. One was a Guideline 2.3.3 issue about screenshots; the other was a 5.1.1 issue about privacy disclosures. When your release queue is already full, a rejection is not just rework. The real burden is the traffic control: which app, which issue, in what order.

As an indie developer, there is no teammate to hand review correspondence to. I have been through enough rejections over the years that the process itself does not rattle me, but during that stretch of overlapping updates — a StoreKit 2 migration and new device resolutions landing at the same time — the read-research-reply-fix loop was visibly eating into development hours. So I wired parts of it into the Gemini API. It worked better than I expected, and this is a record of how the pipeline fits together and where it stops being useful.

Reading the notice was the actual bottleneck

Before any automation, I timed where the hours actually went. Rejection handling breaks down into four stages.

  1. Read the notice and identify each issue
  2. Look up the relevant guideline text and understand what is being asked
  3. Decide whether to fix the build or respond with an explanation
  4. Write the Resolution Center reply, or fix and resubmit

On average, one rejection cost me about 90 minutes. The surprising part was that the heavy stages were 1 and 2 — reading and researching — not the fixing. Review notices embed specific findings inside boilerplate, often bundle several issues into one message, and vary wildly in granularity. Some are concrete ("replace this screenshot"); others leave broad room for interpretation ("verify your app meets the guideline's requirements").

Misreading a notice leads straight to a second rejection, so I would end up reading every sentence carefully while other apps' work sat idle. Stack that on top of routine operations — staged rollout monitoring, AdMob report checks — and a single rejection could wreck the rhythm of half a day. The design goal of this pipeline is simple: let the machine do the reading and researching, and keep the judging and fixing for myself.

Converting a notice into three-layer JSON

The core of the pipeline turns the notice body into structured data using Gemini's structured output (response_schema). One notice becomes a list of individual findings, organized in three layers.

  • Notice level: app name, submission ID, and a flag for whether a reply alone might resolve it
  • Finding level: guideline number, what the reviewer is asking for, and the target (binary, metadata, or screenshot)
  • Evidence level: a verbatim quote from the notice body

The third layer — verbatim evidence — is the part that earns its keep in production. If you let the model output only summaries, you will not notice when it invents a requirement the notice never made. Forcing a quoted excerpt alongside each finding means I can verify mechanically that the quote actually exists in the source text, which catches hallucinated findings before they reach my to-do list.

Here is the working code, tidied up for reading. It takes the notice text and returns a Pydantic model containing the findings.

from google import genai
from pydantic import BaseModel
 
class RejectionItem(BaseModel):
    guideline: str      # e.g. "2.3.3"
    requirement: str    # summary of what the reviewer asks for
    evidence: str       # verbatim quote from the notice (required)
    target: str         # "binary" / "metadata" / "screenshot"
    action: str         # the concrete step on my side
 
class RejectionReport(BaseModel):
    app_name: str
    submission_id: str
    items: list[RejectionItem]
    reply_only_candidate: bool  # might a reply alone resolve this?
 
client = genai.Client()
 
def parse_rejection(notice_text: str) -> RejectionReport:
    prompt = f"""The following is an App Store review rejection notice.
Break it down into individual findings according to the schema.
Constraints:
- Never add facts that are not stated in the notice
- The evidence field must quote the notice verbatim
- If a finding is ambiguous, write "needs human judgment" in action
 
---
{notice_text}
"""
    res = client.models.generate_content(
        model="gemini-3-flash",
        contents=prompt,
        config={
            "response_mime_type": "application/json",
            "response_schema": RejectionReport,
        },
    )
    return res.parsed
 
def verify_evidence(report: RejectionReport, notice_text: str) -> list[str]:
    """Check each quote actually exists in the source text."""
    broken = []
    normalized = " ".join(notice_text.split())
    for item in report.items:
        quoted = " ".join(item.evidence.split())
        if quoted[:80] not in normalized:
            broken.append(item.guideline)
    return broken

Two notes on why it is written this way. First, gemini-3-flash is enough. Parsing a notice is a reading task, not a reasoning task, and the lower latency suits the tempo of "a notice just arrived, break it down now." Second, verify_evidence is deliberately a separate, deterministic function. Quote verification is plain string matching, so I do not delegate it to a probabilistic model. Separating generation from verification is the principle that runs through the whole pipeline.

A word on the two fields I went back and forth on. I kept target to exactly three values because my downstream work branches exactly three ways: a binary finding means rebuild and resubmit, a metadata finding means edits inside App Store Connect, and a screenshot finding means setting up capture devices. Finer-grained categories only increased classification jitter without changing what I do next. The reply_only_candidate flag marks the optimistic path — findings that might be resolved with an explanation instead of a fix — and starting with those can move a review forward without waiting on a rebuild. It is a candidate flag, though; the final call always comes after I read the guideline myself.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A working Python schema that converts rejection notices into three-layer JSON (guideline number, verbatim evidence, action) with gemini-3-flash
Three principles for Resolution Center replies, plus a before/after of the prompt that drafts them in under 120 words
An NDJSON rejection history covering 9 rejections across 6 apps that cut handling time from roughly 90 to 30 minutes per case
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API / SDK2026-06-04
Don't make Gemini judge your AdMob report — confine structured output to extraction
When deciding AdMob floors (eCPM thresholds), letting Gemini make the decision itself is dangerous. Confine structured output to 'extracting a messy report into typed data,' and keep the threshold judgment in deterministic code — here is the reasoning and implementation, with the actual decision rules from running 42 groups.
API / SDK2026-06-03
Reconciling Orphaned Gemini Files API Uploads Across a Fleet of Apps
Files API uploads quietly expire after 48 hours. Here's how I keep orphaned files and quota under control across six apps, using reconciliation against my own database and a scheduled cleanup job — written up as production notes from running wallpaper apps.
API / SDK2026-05-25
Automating App Localization QA with the Gemini API: A Structured-Output Pipeline That Catches Translation Drift Early
Lessons from running 14-language localization across a 50M-download personal app portfolio, distilled into a production-ready Gemini 2.5 Pro structured-output evaluation pipeline that catches translation drift before users do.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →