AI headshot sites are a real business. HeadshotPro and Aragon AI sell packages starting around $29–$35 (entry prices as of 2026; they run frequent promos, so check before you quote them). They take a handful of selfies and send back dozens of “studio” portraits. Their customers are job seekers, freelancers, sales agents, and companies that want a matching “Our Team” page.

In Indonesia there’s a second, bigger market these sites don’t serve: the pas foto. Job applications, internship forms, campus registrations, and plenty of HR portals ask for a formal photo on a red or blue background in 3x4 or 4x6 cm. Lots of people send a selfie taken in a bedroom, or go to a studio and wait in line.

With an image model that edits from reference photos, you can make both in a few minutes, for a few cents per image. In this guide:

  • the API cost per photo, worked out properly
  • a Python script that takes 2–3 selfies and generates several styles, a numbered contact sheet the client picks from on WhatsApp, and exports at LinkedIn and pas foto sizes
  • what to charge, realistic earnings, and the risks: “that’s not me” refunds, privacy law, and where you must say no

This isn’t a grey-area hustle. It’s a plain service. The main risks are unhappy clients and careless handling of people’s faces, and both are covered below.

What you need

ItemCostNotes
Python 3.10+$0Any laptop. No GPU needed, the model runs at Google.
google-genai, Pillow$0pip install
Gemini API key with billing onPay per imageaistudio.google.com → Get API key. Image models have no free API tier, so you pay from the first image.
WhatsApp$0Where your clients already are
Fiverr seller account (optional)20% of each orderFiverr’s standard seller fee

Pricing (check ai.google.dev/pricing before you rely on it):

  • Nano Banana 2.1 (gemini-nano-banana-2.1): $0.0336 per 1K image (about 1024 px on the long side area, plenty for CV and LinkedIn). Input (your prompt plus 3 selfies) is a few thousand tokens at $1.50 per 1M, well under a cent per call. It accepts up to 4 reference photos of a person, so 3 selfies fit.
  • Gemini 3 Pro Image (gemini-3-pro-image, “Nano Banana Pro”): slower and pricier at $0.134 per 1K/2K image. Use it for the faces Nano Banana 2.1 keeps getting wrong, or for paying B2B clients.

The original “Nano Banana” (gemini-2.5-flash-image, $0.039/image) that many older tutorials use was scheduled for shutdown on October 2, 2026. Don’t build on it.

Want to try the idea before you pay anything? Upload a selfie in the Gemini app or in AI Studio’s web UI and ask for a headshot. That’s free and manual. The script is for when you have 20 clients a week.

Why paid tier matters for privacy: under the Gemini API terms, Google may use free-tier prompts and outputs to improve its products. Paid-tier data isn’t used that way. You’re uploading strangers’ faces, so use the paid tier.

How the cost works out

One client, with you generating 4 styles × 4 variants = 16 images, plus about 4 retries:

ItemAmount
20 images × $0.0336$0.67
Input tokens (20 calls × ~4,000 tokens × $1.50/1M)$0.12
Total per client≈ $0.80 (≈ Rp 14,500 at Rp 18,000/USD)

The newer Gemini docs note that thinking tokens are billed by default on these models, so your real bill can run a bit higher than this table. Set a budget alert and compare against the Billing page after your first 10 clients.

The API isn’t the expensive part. Your time is: checking faces, chatting with the client, and redoing the ones that don’t look like them.

Step 1: Set up the project

mkdir headshots && cd headshots
python3 -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install google-genai pillow
export GEMINI_API_KEY="paste-your-key-here"           # Windows: set GEMINI_API_KEY=...

mkdir -p clients/rina   # one folder per client, put 2-3 of their photos in it

Selfie instructions to send every client (copy-paste into WhatsApp). Most bad results come from bad input:

Kirim 3 foto: wajah menghadap kamera, 1 sedikit miring kiri, 1 sedikit miring kanan. Minta teman foto pakai kamera belakang dari jarak ±1,5 meter (bukan selfie dekat, bikin hidung kelihatan lebih besar). Cahaya dari jendela, tanpa filter, tanpa kacamata hitam. Kirim sebagai dokumen biar gak dikompres.

A selfie held at arm’s length makes the nose look bigger and the face look narrower because of the wide lens. The model copies that distortion, and then the client says “kok hidungku gede.” A photo taken from 1.5 m with the rear camera avoids most of that.

Step 2: The script

Save as headshots.py:

import argparse, io, os, sys, time
from pathlib import Path
from PIL import Image, ImageDraw, ImageOps
from google import genai
from google.genai import types

MODEL = os.environ.get("HEADSHOT_MODEL", "gemini-nano-banana-2.1")
PRICE_PER_IMAGE = float(os.environ.get("PRICE_PER_IMAGE", "0.0336"))  # USD, check ai.google.dev/pricing

RULES = """Use the person in the reference photos. This is a real client, so likeness matters more than beauty.
- Keep the exact face shape, eyes, nose, mouth, skin tone, hairline, facial hair, glasses,
  and any hijab or head covering exactly as in the references.
- Do not slim the face, lighten the skin, change the age, or add makeup.
- Natural skin texture, no plastic smoothing.
- Head and shoulders, facing the camera, relaxed natural expression.
- Photorealistic, sharp focus on the eyes. No text, no watermark, no logo."""

STYLES = {
    "corporate": "Dark navy blazer over a plain white top. Plain light grey studio backdrop. "
                 "Soft key light from front-left, subtle rim light.",
    "smart-casual": "Plain dark knit top. Softly blurred modern office behind, warm window light.",
    "cv-red": "Dark blazer over a white top. Flat solid red background (#C00000), even light, "
              "no shadow on the background, centered, looking straight into the camera.",
    "cv-blue": "Dark blazer over a white top. Flat solid blue background (#1F4E9C), even light, "
               "no shadow on the background, centered, looking straight into the camera.",
}

# name: (width px, height px) at 300 dpi
SIZES = {"linkedin": (800, 800), "2x3": (236, 354), "3x4": (354, 472), "4x6": (472, 709)}

def load_refs(folder):
    files = sorted(p for p in Path(folder).iterdir() if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".webp"))
    if not files:
        sys.exit(f"No photos in {folder}")
    refs = []
    for p in files[:3]:
        img = ImageOps.exif_transpose(Image.open(p)).convert("RGB")  # fix phone rotation
        img.thumbnail((1536, 1536))
        refs.append(img)
    return refs

def generate_one(client, refs, style, ratio):
    prompt = f"Create a professional headshot.\n{RULES}\nStyle: {STYLES[style]}"
    for attempt in range(3):
        try:
            resp = client.models.generate_content(
                model=MODEL, contents=[prompt, *refs],
                config=types.GenerateContentConfig(
                    response_modalities=["IMAGE"],
                    image_config=types.ImageConfig(aspect_ratio=ratio, image_size="1K")))
        except Exception as e:
            print(f"    API error: {e}; retrying")
            time.sleep(5 * (attempt + 1))
            continue
        cand = resp.candidates[0] if resp.candidates else None
        for part in (cand.content.parts if cand and cand.content else []):
            if part.inline_data and part.inline_data.data:
                return Image.open(io.BytesIO(part.inline_data.data))
        print("    no image returned (refusal or safety filter); retrying")
    return None

def contact_sheet(paths, out, thumb_w=300, cols=4):
    thumb_h = thumb_w * 4 // 3
    rows = -(-len(paths) // cols)
    sheet = Image.new("RGB", (cols * thumb_w, rows * (thumb_h + 30)), "white")
    draw = ImageDraw.Draw(sheet)
    for i, p in enumerate(paths):
        x, y = (i % cols) * thumb_w, (i // cols) * (thumb_h + 30)
        sheet.paste(ImageOps.fit(Image.open(p).convert("RGB"), (thumb_w, thumb_h)), (x, y))
        draw.text((x + 8, y + thumb_h + 8), f"#{i + 1}  {p.stem}", fill="black")
    sheet.save(out, quality=85)

def cmd_generate(a):
    client = genai.Client()
    refs = load_refs(a.folder)
    out = Path("out") / Path(a.folder).name
    out.mkdir(parents=True, exist_ok=True)
    made = []
    for style in a.styles:
        for n in range(1, a.variants + 1):
            print(f"  {style} #{n}")
            img = generate_one(client, refs, style, a.ratio)
            if img is None:
                print("    gave up on this one")
                continue
            path = out / f"{style}_{n}.png"
            img.save(path)
            made.append(path)
    if made:
        contact_sheet(made, out / "_sheet.jpg")
    print(f"\n{len(made)} images in {out}/, contact sheet: {out}/_sheet.jpg")
    print(f"Approx. cost: ${len(made) * PRICE_PER_IMAGE:.2f} (refused attempts not counted)")

def cmd_export(a):
    src = Path(a.image)
    img = Image.open(src).convert("RGB")
    out = src.parent / "final"
    out.mkdir(exist_ok=True)
    for name, size in SIZES.items():
        # centering y=0.35 keeps the head in frame when cropping a portrait to square
        ImageOps.fit(img, size, centering=(0.5, 0.35)).save(
            out / f"{src.stem}_{name}.jpg", quality=95, dpi=(300, 300))
    print(f"Exported {len(SIZES)} sizes to {out}/")

ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("generate")
g.add_argument("folder")
g.add_argument("--styles", nargs="+", default=list(STYLES), choices=list(STYLES))
g.add_argument("--variants", type=int, default=4)
g.add_argument("--ratio", default="3:4")
g.set_defaults(fn=cmd_generate)
e = sub.add_parser("export")
e.add_argument("image")
e.set_defaults(fn=cmd_export)
args = ap.parse_args()
args.fn(args)

What it does:

  • load_refs takes up to 3 photos, fixes the sideways-phone-photo problem with exif_transpose, and shrinks them to 1536 px. More pixels don’t help likeness, they just slow the upload.
  • RULES is the part that decides whether clients are happy. Image models “improve” faces by default: slimmer, lighter skin, smoother. The rules forbid that and tell the model to keep glasses and hijab as they are. Without that line, the model sometimes removes a hijab or invents one. That’s a guaranteed complaint and, for many clients, an offensive one.
  • generate_one asks for an image only (response_modalities=["IMAGE"]) at a fixed aspect ratio and the cheapest 1K size. A 3:4 image at 1K is roughly 900×1200 px, which is plenty for both the 800 px LinkedIn crop and a 4x6 print at 300 dpi. Refusals happen now and then, so it retries up to 3 times.
  • contact_sheet puts every result in a numbered grid. You send one JPG on WhatsApp and the client replies “nomor 3 sama 9.” That saves a lot of back-and-forth.
  • export crops a chosen image to an 800×800 LinkedIn photo (LinkedIn’s minimum is 400×400) and to 2x3, 3x4 and 4x6 cm at 300 dpi, ready to print at any digital printing shop.

Step 3: Run it

python headshots.py generate clients/rina --styles corporate cv-red --variants 4
#   corporate #1
#   ...
#   8 images in out/rina/, contact sheet: out/rina/_sheet.jpg
#   Approx. cost: $0.27

# client picked #2 and #7
python headshots.py export out/rina/corporate_2.png
python headshots.py export out/rina/cv-red_3.png
#   Exported 4 sizes to out/rina/final/

For a client who only needs a pas foto, run --styles cv-red --variants 3 and you’ve spent about 10 cents.

Step 4: Quality check before anything goes to the client

Put each candidate next to the original selfie and check, in this order:

  1. Would their mother recognize them in one second? If you hesitate, delete it. Drift shows up most around the eyes and jaw.
  2. Glasses, hijab, beard, moles, scars: present and in the same place?
  3. Hands, ears, collar: extra fingers, merged earrings, a collar that melts into the neck.
  4. Background (CV styles): really flat? Some HR portals reject gradients or shadows.
  5. Skin tone: not lighter than the reference. If every result comes out lighter, add “skin tone must match reference exactly, do not lighten” at the top of RULES.

Expect to keep roughly half of what the model produces. That’s why the default is 4 variants per style. Only send the ones that pass. A contact sheet full of strangers loses the client.

What to charge

The global sites set the upper anchor ($29–$35 and up). Fiverr has many “AI headshot” gigs at lower prices. Locally, you’re competing with the photo studio down the street and with the client doing it themselves in the Gemini app.

Example pricing, not market data (assumes about Rp 18,000 per USD, the October 2026 rate):

PackageWhat’s in itPriceAPI cost
Pas foto1 photo, red or blue, 3x4 + 4x6 + 2x3 filesRp 25,000≈ Rp 2,000
Job seeker1 pas foto + 2 LinkedIn/CV headshots, 1 revision roundRp 75,000≈ Rp 14,500
Team page (B2B)Same backdrop and style for every staff member, per personRp 100,000–150,000≈ Rp 15,000–18,000

An example month (not a guarantee) once you have steady demand. Your first month will likely be a handful of friends and relatives. This assumes you’re selling through WhatsApp status, campus and alumni groups, and a few TikToks:

ItemAmount
25 job-seeker packages × Rp 75,000Rp 1,875,000
1 team of 10 × Rp 100,000Rp 1,000,000
API (~35 clients × ~$0.80)−Rp 505,000
Net≈ Rp 2,370,000 (≈ $130)
Your time~15–20 min per client, so ~10–12 hours/month

The B2B line is where the money is. One coffee chain, clinic, property agency or insurance team with 10–30 staff pays more than 15 students, and they all want the same look, which is easy to make: one style, same prompt, everyone. Agents (property, insurance) are a good niche because their face is on every flyer and listing.

Where to find buyers:

  • Graduation and job-fair season. Campus groups fill up with “butuh pas foto latar merah” right before application deadlines. Post the before/after (with permission, see below) then.
  • Fiverr for the English market, at prices well under the big sites. Fiverr takes 20%.
  • Direct B2B pitch: open a company’s “Our Team” page. If it’s a mix of selfies, cropped group photos, and grey placeholder avatars, offer to make one free sample from a photo the staff member sends you. Don’t make a sample from a photo you took off their website: that’s processing someone’s face without their consent (see Risks).

Risks: refunds, privacy, and where to say no

“That’s not me” refunds. This is the number-one complaint against every AI headshot service, the big ones included. Protect yourself:

  • Take payment after showing a watermarked contact sheet, or take 50% upfront. Don’t promise “you’ll look like a model.”
  • Include one revision round with new selfies. Most failures are input failures.
  • If it still doesn’t look like them after a revision, refund. Your API cost was about 15,000 rupiah. Arguing costs you more in reputation than that.

Personal data law. Indonesia’s UU No. 27 Tahun 2022 (UU PDP) treats biometric data as specific personal data, which needs extra care. Face photos used to identify someone fall in that category. In practice:

  • Process only photos the person sent you, themselves, for this purpose. Get a one-line yes in the chat: “Foto ini saya kirim untuk dibuatkan pas foto/headshot dan boleh diproses dengan AI.”
  • Delete selfies and outputs after delivery (say, 14 days). Put that in your service description.
  • Never post before/after content without written permission. Ask, and offer a discount for it if you want the marketing.
  • Use the paid API tier (see above), not random free web tools that keep uploads.

Where to say no:

  • Passports, visas, KTP, SIM, and other government ID photos. Indonesian passport photos are taken at the immigration office anyway. Many visa photo rules (the US State Department’s, for example) explicitly reject digitally altered photos. An AI-generated face can get an application rejected or worse. Sell CV, LinkedIn and form photos, not ID photos.
  • Exam or registration systems that verify faces, or anywhere the photo is used to confirm identity in person. If a portal says “pas foto asli/tidak diedit,” tell the client to use a real photo.
  • Someone else’s photo. “Bikinin headshot buat temenku, ini fotonya dari IG” is a no. Making realistic images of a person without their consent is exactly what Google’s Generative AI Prohibited Use Policy is aimed at, and it’s how this service turns into a deepfake shop.
  • Minors, unless a parent is the one ordering. Job seekers don’t need this anyway.

Platform rules. LinkedIn expects your profile photo to be your likeness. A headshot that looks like you in a nicer jacket is fine. One that makes you ten years younger isn’t. Google embeds an invisible SynthID watermark in generated images. Tell clients the photo is AI-made and never sell it as a studio shoot.

Reality check

“Can’t they just do this in the Gemini app for free?” Yes, and some will. Your value isn’t access to the model. It’s the selfie instructions, consistent prompts, filtering out the bad half, and correct crops at 300 dpi. Price for convenience and speed, not for “AI.”

The model will drift on some faces. Strong angles, heavy glasses glare, and low-light selfies drift the most. Some people just don’t come out right on the cheaper model. Switch to HEADSHOT_MODEL=gemini-3-pro-image (set PRICE_PER_IMAGE=0.134) for those, or refund.

Don’t go “perfect.” Over-smoothed, airbrushed results look obviously AI, and recruiters notice. The “natural skin texture” rule is there on purpose.

One prompt doesn’t fit every team. For B2B, make one sample, get sign-off on that exact style, then batch everyone. Changing the style after 15 people means doing it all again.

Prices drift and models get renamed. Google already retired the first Nano Banana model during 2026. When gemini-nano-banana-2.1 gets replaced, put the current image model in HEADSHOT_MODEL and update PRICE_PER_IMAGE from the pricing page.

It’s not passive. Each client is a short chat and a quality check. It scales with how fast you can review faces, and team contracts are how you get more rupiah per hour.

Final checklist

  • Billing on, paid tier (for privacy), budget alert set in Google Cloud
  • Selfie instructions saved as a WhatsApp quick reply
  • Consent line in chat before processing any photo
  • Every image checked against the original before it goes on a contact sheet
  • Payment after preview (or 50% upfront), one revision round, refund if it still isn’t them
  • No passport/visa/ID photos, no third-party photos, no minors
  • Selfies and outputs deleted 14 days after delivery

Start today: make your own headshot and pas foto with the script, post the before/after on your WhatsApp status with a price, and do the first five clients at a launch price to collect permission-granted examples.