API documentation
OpenAPI 3.1.2

Personmade API

Organization-scoped CRUD for the canonical sellable catalog, Etsy/Shopify/Personmade listing projections, location inventory, immutable order lines, fulfillment, private assets, and append-only activity.

Quick start

API setup

  1. 1

    Generate a key

    Open your dashboard, go to Settings, name the integration, and generate an API key. Settings keeps it, so you can expand the key's row and copy it again later.

  2. 2

    Configure your process

    Keep the key in a secret manager or environment variable. Never commit it.

    export PERSONMADE_API_KEY='pms_…'
    export PERSONMADE_API_BASE='https://www.personmade.club/api/v1'
  3. 3

    Verify access

    The key determines the organization automatically; no organization ID is sent.

    curl --fail --silent --show-error \
      -H "Authorization: Bearer $PERSONMADE_API_KEY" \
      "$PERSONMADE_API_BASE/products"
  4. 4

    Create the first record

    curl --fail --silent --show-error \
      -X POST \
      -H "Authorization: Bearer $PERSONMADE_API_KEY" \
      -H "Content-Type: application/json" \
      --data '{"name":"Poster","description":"Archival print"}' \
      "$PERSONMADE_API_BASE/product-types"

    Approve the type with POST /product-types/{id}/approve before filing products under it.

Response shapeSuccessful reads and writes return {"data": …}. Validation and request errors return {"error": "…"}. Deletes return HTTP 204 with no body.
Contract

Endpoints

Generated from the same OpenAPI document served at /openapi.json.

Product types

GET/product-typesList product types

Returns up to 500 records in newest-first order.

operationId: listProductTypes
POST/product-typesCreate a product type
operationId: createProductType
GET/product-types/{id}Get a product type
operationId: getProductType
PATCH/product-types/{id}Update selected product type fields
operationId: updateProductType
PUT/product-types/{id}Update supplied product type fields

PUT currently has the same partial-update semantics as PATCH.

operationId: putProductType
DELETE/product-types/{id}Delete a product type
operationId: deleteProductType
POST/product-types/{id}/approveApprove a product type
operationId: approveProductType
GET/product-types/{productTypeId}/etsy-configurationRead reusable Etsy configuration

Returns the Etsy values inherited by this product type's mirrors, including per-shop shipping, return, and processing profile ids.

operationId: getEtsyProductTypeConfiguration
PUT/product-types/{productTypeId}/etsy-configurationCreate or replace reusable Etsy configuration

Validates the category and variation mappings with Etsy, saves all type-owned mirror values, and synchronizes existing automatic mirrors.

operationId: putEtsyProductTypeConfiguration
GET/connections/{connectionId}/etsy-policiesRead shipping, return, and processing profiles from Etsy

Returns the real shop policy and processing-profile ids used to configure an Etsy product type for this connection. It refreshes the Etsy access token when necessary.

operationId: readEtsyPolicies
GET/connections/{connectionId}/etsy-configuration-choicesRead Etsy categories and variation properties
operationId: readEtsyConfigurationChoices

Products

GET/productsList products

Returns up to 500 records in newest-first order.

operationId: listProducts
POST/productsCreate a product

A product type must be approved before products can be filed under it.

operationId: createProduct
GET/products/{id}Get a product
operationId: getProduct
PATCH/products/{id}Update selected product fields

A product type must be approved before products can be filed under it.

operationId: updateProduct
PUT/products/{id}Update supplied product fields

PUT currently has the same partial-update semantics as PATCH. A product type must be approved before products can be filed under it.

operationId: putProduct
DELETE/products/{id}Delete a product
operationId: deleteProduct
POST/products/{id}/approveApprove a product after it has at least 2 product photos
operationId: approveProduct
POST/products/{id}/unapproveReturn an approved product to draft

Clears the approval. The product keeps its photos, listings, and catalog data; a product that is already a draft is a 409.

operationId: unapproveProduct

Catalog

GET/products/{productId}/catalogRead the complete canonical product

Returns the product's return-policy reference, ordered options and values, explicit sparse variants with supply and shipping fields, personalization, reviewed media, and inventory items. A product without visible options still returns its one stable default variant.

operationId: getProductCatalog
POST/products/{productId}/optionsAdd an ordered option axis

A product can have zero to three axes. Existing variants atomically select the first supplied value, so no active variant is left invalid.

operationId: createProductOption
PATCH/product-options/{id}Rename or reorder an option axis

Reordering also rebuilds every canonical variant selection key atomically.

operationId: updateProductOption
DELETE/product-options/{id}Remove an option axis

Rejected if removing the axis would create duplicate variant combinations.

operationId: deleteProductOption
POST/product-options/{optionId}/valuesAdd an option value
operationId: createProductOptionValue
PATCH/product-option-values/{id}Rename or reorder an option value
operationId: updateProductOptionValue
DELETE/product-option-values/{id}Remove an unused option value

A value selected by any variant cannot be removed.

operationId: deleteProductOptionValue
POST/products/{productId}/variantsCreate one explicit purchasable variant

Sparse combinations are valid and no Cartesian product is generated. The complete selected combination must be unique within the product.

operationId: createProductVariant
PATCH/product-variants/{id}Update a variant or its complete selection

Variant UUID is stable identity; SKU is nullable, repeatable merchant data.

operationId: updateProductVariant
DELETE/product-variants/{id}Remove an unmapped variant

Every product must keep at least one explicit variant.

operationId: deleteProductVariant

Shop policies

GET/shipping-profilesList canonical shipping profiles
operationId: listShippingProfiles
POST/shipping-profilesCreate a canonical shipping profile

A profile owns its origin and processing window. Add destination prices through its rates collection.

operationId: createShippingProfile
PATCH/shipping-profiles/{id}Update a shipping profile
operationId: updateShippingProfile
DELETE/shipping-profiles/{id}Delete an unused shipping profile

A profile referenced by a variant cannot be deleted.

operationId: deleteShippingProfile
GET/shipping-profiles/{profileId}/ratesList a shipping profile's destination rates
operationId: listShippingRates
POST/shipping-profiles/{profileId}/ratesAdd a destination rate

A profile can have one everywhere rate and one rate for each country.

operationId: createShippingRate
DELETE/shipping-rates/{id}Delete a shipping rate
operationId: deleteShippingRate
GET/return-policiesList canonical return policies
operationId: listReturnPolicies
POST/return-policiesCreate a canonical return policy
operationId: createReturnPolicy
PATCH/return-policies/{id}Update a return policy
operationId: updateReturnPolicy
DELETE/return-policies/{id}Delete an unused return policy

A policy referenced by a product cannot be deleted.

operationId: deleteReturnPolicy
GET/shipping-profile-mappingsList shipping profile marketplace mappings
operationId: listShippingProfileMappings
PUT/shipping-profile-mappingsCreate or replace a shipping profile marketplace mapping

Upserts the remote id for one canonical policy and connection. Remote ids are read from the marketplace account, not invented per listing.

operationId: putShippingProfileMapping
DELETE/shipping-profile-mappings/{id}Delete a shipping-profile marketplace mapping
operationId: deleteShippingProfileMapping
GET/return-policy-mappingsList return policy marketplace mappings
operationId: listReturnPolicyMappings
PUT/return-policy-mappingsCreate or replace a return policy marketplace mapping

Upserts the remote id for one canonical policy and connection. Remote ids are read from the marketplace account, not invented per listing.

operationId: putReturnPolicyMapping
DELETE/return-policy-mappings/{id}Delete a return-policy marketplace mapping
operationId: deleteReturnPolicyMapping

Etsy mirrors

GET/products/{productId}/etsy-mirrorsList a product's Etsy mirrors
operationId: listEtsyMirrors
POST/products/{productId}/etsy-mirrorsCreate and synchronize an Etsy mirror

Persists the optional tag override and inherits all other Etsy choices from the product type. It adopts and updates a compatible Etsy listing already linked to this product; otherwise it creates a draft.

operationId: createEtsyMirror
PATCH/etsy-mirrors/{listingId}Change Etsy-only choices

Only tags and synchronization can differ from the product type.

operationId: updateEtsyMirror
POST/etsy-mirrors/{listingId}/syncSend canonical product state to Etsy now

Uses the latest product-type Etsy configuration and retains only the mirror's tag override and synchronization setting.

operationId: syncEtsyMirror

Personalization

POST/products/{productId}/customizationsAdd buyer personalization

Personalization does not create variants. Canonical kinds are text, single_select, and file; provider validators apply narrower provider limits.

operationId: createProductCustomization
PATCH/product-customizations/{id}Update buyer personalization
operationId: updateProductCustomization
DELETE/product-customizations/{id}Remove buyer personalization
operationId: deleteProductCustomization
POST/product-customizations/{customizationId}/choicesAdd a single-select choice
operationId: createCustomizationChoice
PATCH/product-customization-choices/{id}Update a single-select choice
operationId: updateCustomizationChoice
DELETE/product-customization-choices/{id}Remove a single-select choice
operationId: deleteCustomizationChoice

Catalog media

PUT/product-variants/{id}/mediaReplace a variant's ordered asset associations
operationId: replaceVariantMedia
GET/products/{productId}/mediaList reviewed product media
operationId: listProductMedia
POST/products/{productId}/mediaSelect reviewed product media

The source ETag or checksum pins the reviewed bytes.

operationId: createProductMedia
DELETE/product-media/{id}Remove product media selection
operationId: deleteProductMedia

Inventory

GET/inventory-locationsList inventory locations

Every organization has one required active default location.

operationId: listInventoryLocations
POST/inventory-locationsCreate an inventory location
operationId: createInventoryLocation
PATCH/inventory-locations/{id}Update an inventory location

The required default location cannot be deactivated or removed.

operationId: updateInventoryLocation
DELETE/inventory-locations/{id}Delete an unused non-default location
operationId: deleteInventoryLocation
GET/inventory-itemsList product inventory items
operationId: listInventoryItems
POST/inventory-itemsCreate a product inventory item

Several variants can reference one stock identity; this does not add bundle semantics.

operationId: createInventoryItem
PATCH/inventory-items/{id}Update inventory policy
operationId: updateInventoryItem
DELETE/inventory-items/{id}Delete an unused inventory item
operationId: deleteInventoryItem
GET/inventory-adjustmentsList the append-only inventory ledger
operationId: listInventoryAdjustments
POST/inventory-adjustmentsAppend an inventory quantity adjustment

Source plus idempotency_key is unique per organization.

operationId: createInventoryAdjustment
GET/inventory-reservationsList inventory reservations
operationId: listInventoryReservations
POST/inventory-reservationsHold available quantity for a checkout

Active, unexpired reservations reduce availability.

operationId: createInventoryReservation
PATCH/inventory-reservations/{id}Release or commit an active reservation
operationId: updateInventoryReservation
GET/inventory-availabilityRead ledger availability minus live reservations
operationId: listInventoryAvailability

Listing projections

GET/listings/{listingId}/revisionsList immutable desired revisions

Observed listing_data remains separate.

operationId: listListingRevisions
POST/listings/{listingId}/revisionsCreate a numbered provider-typed desired revision
operationId: createListingRevision
PATCH/listing-revisions/{id}Edit or approve a draft revision

Approved revisions cannot be changed or deleted.

operationId: updateListingRevision

Order lines

GET/orders/{orderId}/linesList immutable order-line snapshots
operationId: listOrderLines
POST/orders/{orderId}/linesCreate an immutable order-line snapshot

Selected options and buyer personalization are preserved separately from the stable variant reference.

operationId: createOrderLine

Fulfillment lines

GET/fulfillments/{fulfillmentId}/linesList fulfilled order-line quantities
operationId: listFulfillmentLines
POST/fulfillments/{fulfillmentId}/linesApply a fulfilled quantity to one order line

Total fulfilled quantity cannot exceed the order-line quantity.

operationId: createFulfillmentLine

Connections

GET/connectionsList organization marketplace connections

Returns safe connection metadata and a credentials_configured flag. Credential values are never returned.

operationId: listMarketplaceConnections
POST/connectionsCreate an organization marketplace connection

Accepts organization-owned credentials, encrypts them before D1 storage, and excludes them from the response and activity event.

operationId: createMarketplaceConnection
GET/connections/{id}Read safe marketplace connection metadata
operationId: getMarketplaceConnection
PATCH/connections/{id}Update connection metadata or replace credentials

Omit credentials to keep the encrypted values. Supplying credentials replaces the full encrypted credential object.

operationId: updateMarketplaceConnection
DELETE/connections/{id}Delete an unlinked marketplace connection

A connection still used by a listing cannot be deleted.

operationId: deleteMarketplaceConnection
POST/connections/{id}/disconnectDisconnect and erase organization credentials

Erases the encrypted credential material while preserving safe account identity and listing mappings for history and reconnection.

operationId: disconnectMarketplaceConnection

Listings

GET/listingsList listings

Returns up to 500 records in newest-first order.

operationId: listListings
POST/listingsCreate a listing
operationId: createListing
GET/listings/{id}Get a listing
operationId: getListing
PATCH/listings/{id}Update selected listing fields
operationId: updateListing
PUT/listings/{id}Update supplied listing fields

PUT currently has the same partial-update semantics as PATCH.

operationId: putListing
DELETE/listings/{id}Delete a listing
operationId: deleteListing

Orders

GET/ordersList orders

Returns up to 500 records in newest-first order.

operationId: listOrders
POST/ordersCreate an order
operationId: createOrder
GET/orders/{id}Get an order
operationId: getOrder
PATCH/orders/{id}Update selected order fields
operationId: updateOrder
PUT/orders/{id}Update supplied order fields

PUT currently has the same partial-update semantics as PATCH.

operationId: putOrder
DELETE/orders/{id}Delete an order
operationId: deleteOrder

Fulfillments

GET/fulfillmentsList fulfillments

Returns up to 500 records in newest-first order.

operationId: listFulfillments
POST/fulfillmentsCreate a fulfillment
operationId: createFulfillment
GET/fulfillments/{id}Get a fulfillment
operationId: getFulfillment
PATCH/fulfillments/{id}Update selected fulfillment fields
operationId: updateFulfillment
PUT/fulfillments/{id}Update supplied fulfillment fields

PUT currently has the same partial-update semantics as PATCH.

operationId: putFulfillment
DELETE/fulfillments/{id}Delete a fulfillment
operationId: deleteFulfillment

Assets

POST/products/{id}/assetsUpload a product asset

Streams a raw binary body to private storage. Maximum size is 100 MB, rejected from Content-Length before the body is read when the client declares it. Product photos are selected for storefront publishing by default; other asset kinds remain private.

operationId: uploadProductAsset
GET/assetsList asset metadata
operationId: listAssets
GET/assets/{id}Download an asset

Always an attachment of type application/octet-stream, whatever the upload claimed: the stored content type is catalogue metadata, never a rendering instruction. Use /assets/{id}/view to display an image or video instead.

operationId: downloadAsset
PATCH/assets/{id}Rename an asset

The filename is the only writable field. The stored object keeps the key it was written under.

operationId: renameAsset
DELETE/assets/{id}Delete an asset and its stored file
operationId: deleteAsset
GET/assets/{id}/viewView an asset inline

Serves the object under its own content type, for displaying it rather than saving it. Only PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, Ogg and QuickTime are served that way, always with nosniff and a sandboxing CSP; every other type falls back to the same attachment download as /assets/{id}. Answers Range requests so a video can seek, and conditional requests with a 304 so a preview is fetched once.

operationId: viewAsset
GET/assets/{id}/watermarkedDownload a watermarked copy

Asks Cloudflare Images to tile Personmade's proof mark over an eligible JPEG, PNG, or WebP and returns the WebP immediately. The private original remains unchanged and no derivative is stored.

operationId: downloadWatermarkedAsset

Activity

GET/eventsList the latest 500 app events

Returns the append-only activity stream in newest-first order.

operationId: listEvents

JSON fields

listing_data, order_data, and fulfillment_data are JSON objects, not encoded JSON strings. All timestamps represent UTC instants. The API returns Unix epoch milliseconds; order and fulfillment writes accept those or RFC 3339 UTC strings ending in Z.

Shop terms and supply

Shipping profiles, destination rates, and return policies are canonical shop records. Products reference one return policy, and physical variants reference one shipping profile. A variant's supply_policy is stocked, made_to_order, or made_to_request. Stocked variants need an inventory item; made-to-order variants need a positive made_to_order_limit; made-to-request variants are not directly purchasable.

Marketplace mapping endpoints store the remote ID that a connected shop uses for the same canonical policy.

Asset uploads

Send the file as the raw request body with its actual Content-Type, an X-Filename header, and a valid kind query parameter. Assets can be up to 100 MB.

curl -X POST \
  -H "Authorization: Bearer $PERSONMADE_API_KEY" \
  -H "Content-Type: image/png" \
  -H "X-Filename: artwork.png" \
  --data-binary @artwork.png \
  "$PERSONMADE_API_BASE/products/PRODUCT_ID/assets?kind=source"

Watermarked assets

Eligible JPEG, PNG, and WebP assets can be stamped on demand with your shop's proof mark, which carries your organization's name. Cloudflare Images returns the stamped copy immediately, usually as WebP and sometimes as JPEG or PNG when it judges that a better fit for the source; the private original remains unchanged and no derivative is stored. Each asset carries a watermarked_url pointing at /assets/ASSET_ID/watermarked, or null for an unsupported format. The route returns HTTP 200 with the copy, 422 for an unsupported format, 502 if Cloudflare cannot transform it, and 404 never reveals another organization's asset.