build Developer Guide

Troubleshooting

The most common problems developers run into with the FinerWorks API, the exact messages you'll see, and how to fix them.

Base URL: https://v2.api.finerworks.com · Routes: /v3/<endpoint> · Body: JSON

Quick checklist

Work through these in order before opening a support request.

  1. Call GET /v3/test_my_credentials. If it succeeds, your keys, account and profile are fine and the problem is in the specific call.
  2. Check which mode your app is in. An app key in test mode silently turns every order into a test order (see Test mode vs live mode). This is the most common surprise.
  3. Read the whole response, not just the HTTP code. Several failures return HTTP 200 with success: true and the real problem in status.message or an empty result.
  4. Run submit_orders_v2 with validate_only: true before a real submit. It catches most address, shipping-code, duplicate-PO and SKU errors without creating an order.
  5. Keep the reference_id from the response status block when you contact support.

If the problem persists after these checks, contact our customer service team (email only).

Reading responses

Responses come in three shapes. Your client needs to handle all of them.

ShapeLooks likeTypical cause
Validation error (HTTP 400){"Message":"The request is invalid.","ModelState":{"<key>":["<message>"]}}Bad credentials, missing or invalid fields, bad address, bad SKU
Envelope (HTTP 200 or 400){"status":{"success":false,"status_code":400,"message":"...","reference_id":"..."}, ...}Business failures such as payment failed or nothing saved
Empty "success" (HTTP 200){"status":{"success":true,...},"images":[],"count":0}The query matched nothing, or silently failed (see Images & inventory)
  • In validation errors the ModelState key is often a .NET type name such as System.Collections.Generic.List`1[ApiC.Models.order_details]. Ignore the key and read the message.
  • Validation runs in stages, so you may fix one error and then see a different one. Errors appear in this order: JSON binding, credentials, account billing/business profile, duplicate PO, shipping code, product/SKU, order count.
  • Validation errors have no reference_id. Only envelope responses do.
  • Send Content-Type: application/json. XML and form-encoded bodies are not supported.
  • JSON property names are snake_case (order_po, product_qty). Matching is case-insensitive and unknown properties are ignored, so a typo in an optional field is silently dropped rather than rejected.
  • Some endpoints are GET but take a JSON body (for example test_my_credentials, get_payment_tokens, list_order_status_definitions). Many HTTP clients and proxies strip bodies on GET. The API Reference shows the method for each endpoint.
  • Always call https://. Plain http:// gets a 301 redirect, and many clients drop the body and headers (or turn a POST into a GET) when following it.

Authentication & account setup

Credentials are two GUIDs, web_api_key and app_key, sent as request headers (or as query-string parameters; if both are present the query string wins). Get them from the FinerWorks account dashboard under API credentials.

Symptom / messageCauseFix
Missing or unauthorized api credentialsWrong, missing or unapproved key pair; only one of the two sent; header name misspelledSend both. Header names are exactly web_api_key and app_key (lowercase, with underscores). Keys must be hyphenated GUIDs with no braces.
Headers "disappear" behind a proxy or gatewaySome proxies and API gateways drop headers that contain underscoresSend the keys as query-string parameters instead, or configure the proxy to pass underscore headers.
Missing or unauthorized api accountKeys are valid but resolve to no usable accountConfirm the account is active. An inactive account (for example one that never confirmed its email) gets the same message as a wrong key.
Keys worked, then stopped (or a new key still fails)Credential lookups are cached on the server for up to 24 hours, and failed lookups are cached tooA key tried before it was activated can keep failing while you keep retrying. Stop retrying for a while, or contact support to refresh. Regenerated keys are new values, so they aren't affected.
Account billing incomplete. (Quick Checkout)The account's own billing profile is missing name, address, city, zip or country (US also needs a state)Complete your billing profile. This is not about the recipient address.
Account business profile is incompleteSame, for the business profileComplete your business profile.
account_key seems ignoredThe account_key body field is only honored for FinerWorks-internal accountsDon't send it. You always act as the account that owns your keys.

test_my_credentials runs the credential check and the billing/business checks together, so it surfaces all of the problems above in one call.

Test mode vs live mode

Each app key has a live/test setting, which you change in the FinerWorks dashboard (or with PUT /v3/update_app_details).

warning

If the app key is not live, every order is forced to test_mode = true, regardless of the test_mode value in your request. The API still returns a normal-looking response.

Other things that force test mode: a payment_token of xxxx, and any test order in a batch (one test order puts the whole batch in test mode).

What test mode does:

  • Orders are not approved or produced, and are hidden from list_orders.
  • Account credits are not withdrawn.
  • Emails are prefixed [TEST MODE].
  • Payments go to the Braintree sandbox, which only accepts sandbox payment tokens.
SymptomCauseFix
You sent test_mode: false but the order is a test order, or the email says [TEST MODE]The app key is not liveSwitch the app key to live in the dashboard, then resubmit.
Failure email: Payment method token is invalid. followed by Note: the payment token was not recognized in the sandbox environment used for this order (test_mode=True)...A live payment token was sent while the order ran in the sandbox (usually because the app is in test mode)Make the app live, or use a sandbox token.
The same note, but ...in the production environment used for this order (test_mode=False)...A sandbox token was sent on a live orderUse a production token. Tokens are not interchangeable between environments.
A batch of live orders ended up as test ordersOne order in the batch was in test modeDon't mix test and live orders in one call.

Get payment tokens from GET /v3/get_payment_tokens (add ?sandbox=true for sandbox tokens). It returns 404 if the account has none on file for that environment.

Submitting orders (submit_orders_v2)

Run with validate_only: true first. It checks everything in this table except payment, price and image reachability.

MessageCauseFix
Must be an array of the correct object type. (on orders)orders is empty or nullSend at least one order.
Exceeded max order submission count of 150More than 150 orders in one callBatch in groups of 150 or fewer.
Must be between 1-50 characters. Only alphanumeric characters, -, and _ signs are allowed.order_po has characters other than letters, digits, _, - or ., or is over 50 charactersClean the PO. Characters such as # are rejected.
Duplicate order_po submissions detected: X,YThe same PO appears twice in the request, or an approved order with that PO already exists on your accountUse unique POs. Test-mode orders don't count as duplicates.
Invalid shipping_code for order_po XUnknown shipping codeCall get_shipping_options_ids (or list_shipping_options_multiple for real options) and use a returned code.
Must contain a valid 2 digit ISO 3166-2 state code.country_code is us and state_code isn't a valid two-letter US stateUse USPS two-letter state codes.
Must be left blank when country_code does not equal 'us'A state_code was sent on a non-US addressLeave state_code empty and use province.
Must contain a valid 2 digit ISO 3166 country code.Missing or unknown country_codeUse an ISO two-letter country code.
<field> must be no more than <n> charactersField too long. Limits: names 50, address lines 100, city 50, zip 10, phone 50, email 50Shorten the value.
<field> cannot contain '.' or '@' characters. (on phone)Dots or @ in the phone numberUse digits and dashes only, for example 123-456-7890.
<field> must be at least 1. (on product_qty)Quantity outside 1–999. The message is misleading for values over 999.Send a quantity from 1 to 999.
order: <po> - sku: <sku> has some missing or invalid info.product_sku is empty and there is no templateSend product_sku (or product_code).
<sku> has some missing or invalid info.A print product with no imageProvide product_image.product_url_file (or library_file.id, or a template).
Invalid product item supplied. Please check data and sku is valid.order_items is emptyAdd at least one item.
<code> appears to be invalid.The SKU or product code isn't recognisedBuild it with build_product_code and check it with validate_product.
AP...: <description>A virtual-inventory (AP…) SKU that isn't on your accountCheck list_virtual_inventory.
Orders failed to submit. Please check your order details and try again. (HTTP 400, envelope)No order rows could be createdRe-run with validate_only and check the item details.

These are not validated at submit time, so they won't fail here but may cause problems later in production: whether the image URL is reachable, its file type, its real pixel size or DPI, and the zip-code format. See Images & inventory.

Behavior to know about:

  • If your shipping_code is valid but not offered for that parcel and destination, the order is priced with the first available rate instead of being rejected.
  • An invalid or zero-discount promo_code is ignored rather than rejected.
  • customs_tax_info is optional and is not enforced for international orders.

Payments

SituationWhat you seeWhat to do
Card declinedHTTP 200 with success: true, but status.message is Payment failed. Orders have been received but placed in a payment pending status. The order status is 2. A "FINERWORKS FULFILLMENT SUBMITTED - FAILED" email goes to the account email with the processor response.This is expected: the order was received but not paid. Don't treat HTTP 200 as paid — check status.message and the returned order_status_id. Orders are held; complete payment within 30 days or they may be cancelled.
payment_token is invoice but the account isn't enabled for invoicingPayment fails (the value is sent to the card gateway)Ask FinerWorks to enable invoice billing, or use a card token.
payment_token is emptyThe account's default saved card is usedIf there isn't one the charge fails. Pass a token explicitly.
Total is $0 (covered by credits or a promo)No gateway call; the order is stored as a credit orderNormal.
Credits and failed paymentsCredits are applied to the total first, but only recorded as withdrawn if payment succeedsIf a payment fails, check your credit balance before resubmitting.

On success, a "FINERWORKS FULFILLMENT SUBMITTED - SUCCESS" email is sent to the account email.

After you submit: status, changes, webhooks

Response on success: orders[] with order_id, order_po, order_guid and order_status{order_status_id, order_status_label}, plus the message Orders have been successfully submitted.

Checking status: POST /v3/fetch_order_status with order_ids or order_pos.

  • It always returns HTTP 200. An unknown id comes back as order_status_id: 0 with the label Order ID Incorrect or Not Found.
  • Lookup by PO returns at most the 5 most recent matches, and is ignored if order_ids is also sent.
  • GET /v3/list_order_status_definitions lists the status names.
IdMeaning
2Payment pending
3Order received (cart, paid)
14Cancelled
25Submitted by app
29Cancel requested
30On hold
49Payment failed (cart)

list_orders hides test orders, unapproved orders, and cancelled orders (unless show_cancelled is true). get_order requires both order_guid and order_email, and the email must match the order's billing email; otherwise it returns 404.

Cancel / hold / resume: PUT /v3/update_order with an update_command of pending, hold or cancel. It only works while the order's current status allows it; otherwise you'll get 403 Due to where this order is within our processing, you will need to contact customer service to request any change to this order. Treat cancel as a request: it notifies customer care and isn't guaranteed.

Webhooks: you can supply webhook_order_status_url (alias webhook_url) on an order. FinerWorks stores it with the order and handles delivery.

Images & inventory

The API does not download or inspect your image when you submit. product_image.product_url_file and pixel_width/pixel_height are stored exactly as sent. Reachability, file type, size and resolution are checked later in production, so a bad image does not fail at submit time.

To avoid problems downstream:

  • Use a public, stable HTTPS URL that returns the file directly — no login, no redirect to a web page, and no URL that expires before production picks it up.
  • Send accurate pixel_width and pixel_height. They drive orientation and crop, so a wrong value can rotate or crop the product unexpectedly.
  • URL-encode characters such as ' in the URL. An apostrophe can silently break the order's item record.
  • Crop-to-fill is the default. Send custom_data_1: "fit" on the item to avoid cropping.

list_images

SymptomCauseFix
400: The session_id must be at least 8 characters long.library.session_id is required and must be 8–50 characters, even for the inventory librarySend a session id of at least 8 characters.
You get temporary images (or nothing) instead of your inventorylibrary.name must be exactly lowercase "inventory". Any other value ("Inventory", "temp") silently goes to the temporary librarySend "inventory".
Empty inventoryYour account really has none, or active: true filters them outCheck the dashboard and try without active.
The session_id values geogalleries and collections change the queryReserved for FinerWorks propertiesDon't use these as your own session id.
Empty result with success: true after a searchA search_filter containing an apostrophe (for example O'Brien) breaks the query, which fails quietlyAvoid apostrophes and %, _, [ in search text.
A multi-word title search finds nothingTitles are stored URL-encoded (my+image)Search a single word, or search the file name.
Empty result with page_number: 0 or per_page: 0These produce an invalid query, which fails quietlyUse page_number ≥ 1 and per_page ≥ 1. (A maximum of 50 per page is documented but not enforced.)
Response per_page is 0 and count is 0 on a later pageper_page echoes the number of rows returned, and count is read from the last rowTrack page size and the total from your first page.
400 on guid_filterA malformed GUID fails JSON bindingSend valid GUIDs. An empty list is ignored.

sort_field accepts title, file_size, date_added, date_updated, file_name and (inventory only) rank. Anything else sorts by id. sort_direction is descending unless you send ASC.

Other image and inventory calls

  • add_images is not available to external accounts (it returns 403 You do not have proper permissions to run this request). Supply images inline on the order item instead.
  • update_images: returns 200 even if no GUID matched. An omitted title resets to the file name, an omitted description is cleared, assigned_gallery_subcategory_ids replaces all prior assignments, and a rank of 0 or omitted keeps the old rank.
  • delete_images: a soft delete. Returns 200 even if the GUID wasn't found.
  • update_virtual_inventory: overwrites every field on every call. Omitting description clears it, omitting asking_price sets it to 0, and track_inventory and quantity_in_stock default to false and 0. Always send the full record. The 400 Must be an array of the correct object type and count. means 0 or more than 50 entries. SKUs that don't belong to your account are skipped without an error.
  • list_virtual_inventory: an apostrophe in product_code_filter, or more than 50 codes, gives an empty result. More than 50 sku_filter entries are ignored.

Products, SKUs & pricing

warning

Most of these endpoints return HTTP 200 for invalid input. Always check the field noted below.

EndpointInvalid input looks likeCheck this
get_pricesHTTP 200, total_price: 0, info: "<sku> is not valid or recognized."total_price and info
get_product_detailsHTTP 200, description_short: "Not Available", with a long description beginning Invalid product_sku...description_short
validate_productAlways HTTP 404, even when the product is validproduct_validations[].valid and validation_message
build_product_codeAlways HTTP 200status.message (Success or Failed) and product_code
list_* (products, mats, glazing, and so on)200 with an empty list, or 404 when nothing matchesThe list itself
  • get_prices / validate_product limits: 1–50 products per call. Outside that range, get_prices returns Must be an array of the correct object type. (the message doesn't mention the limit). validate_product processes only the first 50 and drops failures silently.
  • get_prices total: total_price is the sum of product, color correction, frame, glazing and mats for one unit. Quantity is not multiplied in — multiply it yourself.
  • validate_product is structural only. It confirms the code is well formed, not that the materials exist, are active, or fit together. Messages: Product appears to be valid., The product_sku appears to be invalid, The product_code appears to be incorrectly formatted.
  • SKU format: codes are case-insensitive; a trailing C means color correction; DD means a decimal point; AP<acct>P<id> is a virtual-inventory SKU (any "AP" inside a SKU makes it be treated that way). Use build_product_code rather than hand-building codes.
  • Image vs product shape: orientation comes from the pixel_width/pixel_height you send. For rotatable products an 8X10 SKU with a landscape image becomes 10X8.
  • frame_builder: errors arrive as validation messages: The width or height is outside of this framing choice's min/max size., Glazing cannot be used with this type of framing option., Matting cannot be used with this type of framing option., and Width and/or height must be provided. Mat-window problems are not reported. The glazing min/max values returned by list_glazing are generic values, not per-frame limits.
  • list_product_types returns a bare array (no status wrapper) and only "create-a-print" types, so frames are not listed. list_media_types needs a request body that includes site_id.
  • update_user: requires account_key (otherwise 403 Account cannot be updated. Please check the account_key.). Two social_links keys that map to the same field cause a 500. A logo_url that can't be downloaded is skipped and the call still returns 200.
  • Promo codes: find_promo_code and apply_promo_code are not available to external accounts (400 with an empty error body).

Limits and platform behavior

ItemBehavior
Orders per submit_orders_v2150
Products per get_prices, validate_product and update_virtual_inventory50
product_qty1–999
Rate limitingNone enforced, but please keep to a sensible request rate
Request size / timeoutStandard ASP.NET defaults (about 4 MB request, about 110 seconds)
Unhandled errorsHTTP 500

Still having issues? Contact support

mail

If issues continue to persist, reach out to our customer service team. Support is by email only — use the FinerWorks contact page to send us a message.

To help us resolve it quickly, include the following:

  • The status.reference_id from the response (if there is one), and the time of the call with its time zone.
  • The endpoint, the HTTP method, and the request body with payment tokens and keys removed.
  • The full response body.
  • Your account username, which app you called from, and whether that app is live or in test mode.
  • For order problems: the order_po and order_id.