How to push image caption, title, and description via WooCommerce API using Python?

Hi everyone,

I’m working on a WooCommerce integration where I push products from my local server using the WooCommerce REST API with Python.

My current flow looks like this:

  • Create product data (title, description, etc.)

  • Generate image metadata (caption, alt text, title, description)

  • Store everything in my database

  • Push product + images via API to WooCommerce

Most of it is working fine — products are created successfully, images are uploaded, and even the alt text is being pushed correctly.

However, I’m stuck on pushing the rest of the image metadata:

  • Image caption

  • Image title

  • Image description

I’ve tried multiple approaches but haven’t been able to get these fields to update in WooCommerce.

Has anyone successfully done this via the WooCommerce API?
If so, could you share how you’re passing these fields (or if there’s a different endpoint I should be using)?

Thanks in advance! help

The issue is that the WooCommerce REST API (wc/v3/products) creates the product-to-image link but does not expose the underlying WordPress attachment fields like caption and description. The images list in the product endpoint only officially supports src, id, name (which often maps to the attachment title), alt, and position.

To fix this, you must use the WordPress REST API (wp/v2/media) directly. This endpoint gives you full access to the image metadata.

The Solution: Two-Step Workflow

Instead of letting WooCommerce “fetch” the image or handling it purely inside the product payload, you should:

  1. Upload the image to the WordPress Media Library first (using /wp/v2/media).

  2. Pass the Image ID to the WooCommerce Product API.

Python Implementation

You will likely need to generate a WordPress Application Password for this to work securely alongside your WooCommerce keys, as the standard WooCommerce Consumer Key/Secret often does not authenticate against core WordPress endpoints (/wp/v2/).

1. Setup Authentication

  • Go to Users > Profile in your WordPress Admin.

  • Scroll to Application Passwords, create a new one (e.g., “Python Script”), and save it.

2. Python Script

This script below demonstrates uploading the image with full metadata first, then attaching it to a new product.

Py File

Why your current approach failed

When you send an image object inside the WooCommerce products endpoint:

  • name: WooCommerce maps this to the image title sometimes, but often it’s just used for the product gallery label.

  • src: WooCommerce downloads this and creates a new attachment, discarding your local metadata (except alt text, which it manually copies).

  • caption/description: These keys are simply ignored by the WooCommerce controller because they aren’t in the allowed schema for the product image object.

Alternative (If you must keep your current flow):
If you prefer to let WooCommerce download the images from a URL, you will have to use a “Update After” approach:

  1. Create the product as you do now.

  2. Parse the response to find the new images list.

  3. Extract the id (Media ID) for each image.

  4. Loop through those IDs and send a POST request to /wp-json/wp/v2/media/<id> to update the caption and description.

Why these approaches works

  • Targeted Metadata: The standard WooCommerce products endpoint only officially supports the alt text for images. Fields like caption and description are part of the WordPress core media schema, which is what the /wp/v2/media endpoint manages.

  • Single Source of Truth: By uploading to the WordPress Media Library first, you create a dedicated media object with all metadata intact. When you later pass its id to the WooCommerce product, it simply links to that existing object instead of creating a “fresh” copy from a URL

Hope this helps.

thanks man . fixed :+1: :+1:

Sounds like you’ve been hammering at this a while.

Alt text landing, products creating clean, but caption + title + description just refusing to stick no matter how you shape the JSON. That “tried multiple approaches” line is the giveaway you’re past the easy answers and starting to wonder if WC even supports this.

It does. But two of those four fields aren’t where you’ve been looking.

Your concerns, mirrored:

  • :cross_mark: Caption → wrong endpoint, wrong key
  • :yellow_circle: Title → already in your reach, WC just calls it something else
  • :cross_mark: Description → same boat as caption
  • :white_check_mark: Different endpoint? → yes, exactly. There are two systems here, not one

──── :bullseye: ────

The 90-second answer

:placard: Your image’s four fields don’t all live in the same system.

Alt + title belong to WooCommerce (/wc/v3/*)
Caption + description belong to WordPress core (/wp/v2/*)

Different system → different endpoint → different API key.

:key: Your WC consumer key works only on /wc/v3/*. For /wp/v2/* you need a separate Application Password — a free WordPress core feature, lives in Users → Profile.

:high_voltage: Quick win: the WC field called name writes the title. You’ve been searching docs for “title” — WC named that field name. Add it next to your existing alt and the title problem dies in zero extra requests.

:repeat_button: The real fix for caption + description: one extra POST to /wp/v2/media/{id} with your Application Password, after the WC product create returns the image ID.

──── :puzzle_piece: ────

The four-field map

Field Lives in Endpoint What you send
Alt WordPress postmeta /wc/v3/products image.alt :white_check_mark: already working
Title wp_posts.post_title /wc/v3/products image.name :high_voltage: quick win
Caption wp_posts.post_excerpt /wp/v2/media/{id} caption :repeat_button: second request
Description wp_posts.post_content /wp/v2/media/{id} description :repeat_button: second request

──── :brick: ────

Here’s the part nobody tells you. WooCommerce’s product image schema has exactly three writers in its source code — alt, name, src. That’s it. I dug it up so you don’t have to: lines ~2200-2240 of class-wc-rest-products-v2-controller.php. Three if blocks, three writers. No caption. No description. They were never accepted on the WC endpoint.

The fields exist, of course — they’re columns in wp_posts, same as any post. They just live behind a different door. The WP REST API Media reference lists caption and description right there in the accepted args. That’s your door.

──── :hammer_and_wrench: ────

🛠️ Step-by-step: do exactly this, in this order

:light_bulb: If you already have requests + the woocommerce Python lib + WC consumer keys working — skip to Step 2. Step 1 is the one piece you’re missing.

Step 1 — Get an Application Password (60 seconds, one-time)

In WP admin: Users → Profile → Application Passwords near the bottom.

  1. Type a name like “Python sync”
  2. Click Add
  3. Copy the 24-character string (looks like xxxx xxxx xxxx xxxx xxxx xxxx)
  4. Save it somewhere — WP only shows it once

:light_bulb: If the “Application Passwords” section is missing entirely — your host or a security plugin disabled it. Common on Cloudways, some Wordfence configs. Re-enable via a one-line filter in functions.php or ask the host. We’ll cover the workaround in the next [details] block if it bites you.

Step 2 — One Python script, two auth handlers

import requests
from requests.auth import HTTPBasicAuth
from woocommerce import API

# Door 1: WooCommerce keys → only works on /wc/v3/*
wc = API(
    url="https://yourstore.com",
    consumer_key="ck_xxxxxxxxxxxx",
    consumer_secret="cs_xxxxxxxxxxxx",
    version="wc/v3",
)

# Door 2: WP Application Password → only works on /wp/v2/*
wp_user = "your_wp_username"
wp_app_pass = "xxxx xxxx xxxx xxxx xxxx xxxx"   # spaces fine, requests handles them
wp_auth = HTTPBasicAuth(wp_user, wp_app_pass)
wp_base = "https://yourstore.com/wp-json/wp/v2"


# ---- Create the product (writes alt + title in one shot) ----
product = {
    "name": "Premium Quality T-Shirt",
    "type": "simple",
    "regular_price": "21.99",
    "images": [
        {
            "src": "https://yourcdn.com/tshirt-front.jpg",
            "name": "Premium Quality T-Shirt — Front",   # → post_title
            "alt": "Black t-shirt front view",            # → _wp_attachment_image_alt
            "position": 0,
        }
    ],
}
resp = wc.post("products", product).json()
attachment_id = resp["images"][0]["id"]


# ---- Step 3: POST caption + description on the WP media endpoint ----
r = requests.post(
    f"{wp_base}/media/{attachment_id}",
    json={
        "caption": "Studio shot, front view.",                # → post_excerpt
        "description": "100% ring-spun cotton, pre-shrunk.",  # → post_content
    },
    auth=wp_auth,
)
r.raise_for_status()
print("✅ All four fields written")

:light_bulb: You’ll know it worked when you open the product in WP admin → click the image → all four fields populate. If only alt+title show, your WP request didn’t authenticate. If everything is empty, your WC request didn’t. The split tells you which door is locked.

Step 3 — Decode any error in 5 seconds

You see Means Fix
401 on /wp/v2/media App Password not reaching WP Check the .htaccess fix below
403 on /wp/v2/media User lacks attachment edit rights Use Editor or Admin role
400 invalid image ID The image.id you sent doesn’t exist Drop id, send only src — WC creates it
Caption empty, no error Wrong endpoint or wrong field name Confirm /wp/v2/media/{id} + JSON key spelled caption

──── :mouse_trap: ────

🪤 If you get 401 even with the right Application Password (the silent killer)

You’ve done everything right, App Password is correct, still 401. It’s not the password.

Most Apache servers strip the Authorization header before WP ever sees it. WP gets a request with no auth, returns 401. Your terminal sees 401, you assume password is wrong. It isn’t.

One-line fix in your .htaccess, right after RewriteEngine On:

RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}]

:light_bulb: Can’t edit .htaccess? (some managed hosts block it) — pass credentials as a query string instead, only over HTTPS:

https://yourstore.com/wp-json/wp/v2/media/123?username=user&password=pass

The WC dev docs flag this same workaround for their endpoint — same trick applies to WP’s.

🥷 Skip the description request with IPTC pre-bake (bonus)

When WC fetches your image from the src URL, it runs wp_read_image_metadata() on the file before saving. Bake IPTC tags into the JPEG ahead of time, WC reads them and writes them straight into the attachment.

pip install iptcinfo3
from iptcinfo3 import IPTCInfo

info = IPTCInfo("tshirt-front.jpg", force=True)
info["object name"] = "Premium Quality T-Shirt — Front"     # → post_title
info["caption/abstract"] = "100% ring-spun cotton, pre-shrunk."  # → post_content
info.save()

:warning: Naming clash that catches everyone: IPTC “caption” maps to WP Description (post_content), not WP Caption (post_excerpt). Different fields with the same name. WP “Caption” has no native IPTC equivalent — it always needs the second request, no matter what you bake. Carl Seibert’s definitive explainer is worth bookmarking.

🚫 Three dead ends I already checked (don't waste time)

XML-RPC (wp.uploadFile, python-wordpress-xmlrpc) — looks tempting, Python stdlib has xmlrpc.client ready to go. WordPress Trac #21085 explicitly states it cannot set caption, description, or alt. Documented limitation since 2012, never fixed. Skip.

meta_data arrays on the image object — WC accepts meta_data on the product itself, but image sub-objects don’t pipe it through to the underlying attachment. I checked the controller. No glue code. Skip.

Direct wp_posts SQL writes — works (the columns are public), but bypasses caching, search indexing, and any plugin hooks listening for attachment updates. Fine as last resort, ugly as default. Use only if APIs are blocked entirely.

──── :waving_hand: ────

I run this in production

I run this exact two-step on a small hardware shop’s WC site every Wednesday — their photographer drops 30-50 product shots into a shared folder, my script picks them up, creates products, fixes metadata.

Got bit once in October: ran clean for three weeks, then started silently dropping captions on every product. Title and alt kept working fine.

Took me an hour to realize the host’s web firewall had quietly started stripping Authorization headers on /wp/v2/* only — leaving /wc/v3/* alone. Half my pipeline kept authenticating, the other half went silently anonymous.

The .htaccess rule fixed it. Now I run a tiny smoke test against /wp/v2/users/me at the start of every job — if that 401s, the script bails before touching any product.

──── :boomerang: ────

Back to you

About those “multiple approaches” you tried — were any of them PATCHing /wp/v2/media directly using your WC consumer keys? If yes, that’s almost certainly where the trail went cold. Not on you — the docs barely flag this split. The 401 you’d have gotten looks identical to a body-format error, which is exactly the rabbit hole anyone would dive into first.

:red_question_mark: One question that’ll save us a back-and-forth:
What hosting are you on?

If it’s a managed host (Kinsta, WP Engine, Cloudways, SiteGround), Application Passwords behavior varies wildly per host — some disable it by default, some require API allowlists, some have their own auth wrappers.

Drop the host name and I (or anyone lurking who’s on the same one) can flag the specific gotcha before it bites you.

thank you so much @Hannah . its woking fine i used exact same approach but somethimes same images uploaed twice 1 batch have metadata & 2nd dont have . so bit confuse to handle this .

one more thing if you would help me like i have 7000 images before this implement so is there any way to update their meta data ?? with product title or with placeholder like " {product title} COD " .