Base URL: https://v2.api.finerworks.com · Routes: /v3/<endpoint> · Body: JSON
Quick checklist
Work through these in order before opening a support request.
- Call
GET /v3/test_my_credentials. If it succeeds, your keys, account and profile are fine and the problem is in the specific call. - 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.
- Read the whole response, not just the HTTP code. Several failures return HTTP 200 with
success: trueand the real problem instatus.messageor an empty result. - Run
submit_orders_v2withvalidate_only: truebefore a real submit. It catches most address, shipping-code, duplicate-PO and SKU errors without creating an order. - Keep the
reference_idfrom the responsestatusblock 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.
| Shape | Looks like | Typical 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
ModelStatekey is often a .NET type name such asSystem.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
GETbut take a JSON body (for exampletest_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://. Plainhttp://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 / message | Cause | Fix |
|---|---|---|
Missing or unauthorized api credentials | Wrong, missing or unapproved key pair; only one of the two sent; header name misspelled | Send 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 gateway | Some proxies and API gateways drop headers that contain underscores | Send the keys as query-string parameters instead, or configure the proxy to pass underscore headers. |
Missing or unauthorized api account | Keys are valid but resolve to no usable account | Confirm 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 too | A 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 incomplete | Same, for the business profile | Complete your business profile. |
account_key seems ignored | The account_key body field is only honored for FinerWorks-internal accounts | Don'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).
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.
| Symptom | Cause | Fix |
|---|---|---|
You sent test_mode: false but the order is a test order, or the email says [TEST MODE] | The app key is not live | Switch 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 order | Use a production token. Tokens are not interchangeable between environments. |
| A batch of live orders ended up as test orders | One order in the batch was in test mode | Don'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.
| Message | Cause | Fix |
|---|---|---|
Must be an array of the correct object type. (on orders) | orders is empty or null | Send at least one order. |
Exceeded max order submission count of 150 | More than 150 orders in one call | Batch 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 characters | Clean the PO. Characters such as # are rejected. |
Duplicate order_po submissions detected: X,Y | The same PO appears twice in the request, or an approved order with that PO already exists on your account | Use unique POs. Test-mode orders don't count as duplicates. |
Invalid shipping_code for order_po X | Unknown shipping code | Call 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 state | Use 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 address | Leave state_code empty and use province. |
Must contain a valid 2 digit ISO 3166 country code. | Missing or unknown country_code | Use an ISO two-letter country code. |
<field> must be no more than <n> characters | Field too long. Limits: names 50, address lines 100, city 50, zip 10, phone 50, email 50 | Shorten the value. |
<field> cannot contain '.' or '@' characters. (on phone) | Dots or @ in the phone number | Use 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 template | Send product_sku (or product_code). |
<sku> has some missing or invalid info. | A print product with no image | Provide 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 empty | Add at least one item. |
<code> appears to be invalid. | The SKU or product code isn't recognised | Build it with build_product_code and check it with validate_product. |
AP...: <description> | A virtual-inventory (AP…) SKU that isn't on your account | Check list_virtual_inventory. |
Orders failed to submit. Please check your order details and try again. (HTTP 400, envelope) | No order rows could be created | Re-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_codeis 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_codeis ignored rather than rejected. customs_tax_infois optional and is not enforced for international orders.
Payments
| Situation | What you see | What to do |
|---|---|---|
| Card declined | HTTP 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 invoicing | Payment fails (the value is sent to the card gateway) | Ask FinerWorks to enable invoice billing, or use a card token. |
payment_token is empty | The account's default saved card is used | If 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 order | Normal. |
| Credits and failed payments | Credits are applied to the total first, but only recorded as withdrawn if payment succeeds | If 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: 0with the labelOrder ID Incorrect or Not Found. - Lookup by PO returns at most the 5 most recent matches, and is ignored if
order_idsis also sent. GET /v3/list_order_status_definitionslists the status names.
| Id | Meaning |
|---|---|
| 2 | Payment pending |
| 3 | Order received (cart, paid) |
| 14 | Cancelled |
| 25 | Submitted by app |
| 29 | Cancel requested |
| 30 | On hold |
| 49 | Payment 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_widthandpixel_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
| Symptom | Cause | Fix |
|---|---|---|
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 library | Send a session id of at least 8 characters. |
| You get temporary images (or nothing) instead of your inventory | library.name must be exactly lowercase "inventory". Any other value ("Inventory", "temp") silently goes to the temporary library | Send "inventory". |
| Empty inventory | Your account really has none, or active: true filters them out | Check the dashboard and try without active. |
The session_id values geogalleries and collections change the query | Reserved for FinerWorks properties | Don't use these as your own session id. |
Empty result with success: true after a search | A search_filter containing an apostrophe (for example O'Brien) breaks the query, which fails quietly | Avoid apostrophes and %, _, [ in search text. |
| A multi-word title search finds nothing | Titles are stored URL-encoded (my+image) | Search a single word, or search the file name. |
Empty result with page_number: 0 or per_page: 0 | These produce an invalid query, which fails quietly | Use 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 page | per_page echoes the number of rows returned, and count is read from the last row | Track page size and the total from your first page. |
400 on guid_filter | A malformed GUID fails JSON binding | Send 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_imagesis not available to external accounts (it returns 403You 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 omittedtitleresets to the file name, an omitteddescriptionis cleared,assigned_gallery_subcategory_idsreplaces all prior assignments, and arankof 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. Omittingdescriptionclears it, omittingasking_pricesets it to 0, andtrack_inventoryandquantity_in_stockdefault to false and 0. Always send the full record. The 400Must 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 inproduct_code_filter, or more than 50 codes, gives an empty result. More than 50sku_filterentries are ignored.
Products, SKUs & pricing
Most of these endpoints return HTTP 200 for invalid input. Always check the field noted below.
| Endpoint | Invalid input looks like | Check this |
|---|---|---|
get_prices | HTTP 200, total_price: 0, info: "<sku> is not valid or recognized." | total_price and info |
get_product_details | HTTP 200, description_short: "Not Available", with a long description beginning Invalid product_sku... | description_short |
validate_product | Always HTTP 404, even when the product is valid | product_validations[].valid and validation_message |
build_product_code | Always HTTP 200 | status.message (Success or Failed) and product_code |
list_* (products, mats, glazing, and so on) | 200 with an empty list, or 404 when nothing matches | The list itself |
get_prices/validate_productlimits: 1–50 products per call. Outside that range,get_pricesreturnsMust be an array of the correct object type.(the message doesn't mention the limit).validate_productprocesses only the first 50 and drops failures silently.get_pricestotal:total_priceis the sum of product, color correction, frame, glazing and mats for one unit. Quantity is not multiplied in — multiply it yourself.validate_productis 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
Cmeans color correction;DDmeans a decimal point;AP<acct>P<id>is a virtual-inventory SKU (any "AP" inside a SKU makes it be treated that way). Usebuild_product_coderather than hand-building codes. - Image vs product shape: orientation comes from the
pixel_width/pixel_heightyou send. For rotatable products an8X10SKU with a landscape image becomes10X8. 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., andWidth and/or height must be provided.Mat-window problems are not reported. The glazing min/max values returned bylist_glazingare generic values, not per-frame limits.list_product_typesreturns a bare array (nostatuswrapper) and only "create-a-print" types, so frames are not listed.list_media_typesneeds a request body that includessite_id.update_user: requiresaccount_key(otherwise 403Account cannot be updated. Please check the account_key.). Twosocial_linkskeys that map to the same field cause a 500. Alogo_urlthat can't be downloaded is skipped and the call still returns 200.- Promo codes:
find_promo_codeandapply_promo_codeare not available to external accounts (400 with an empty error body).
Limits and platform behavior
| Item | Behavior |
|---|---|
Orders per submit_orders_v2 | 150 |
Products per get_prices, validate_product and update_virtual_inventory | 50 |
product_qty | 1–999 |
| Rate limiting | None enforced, but please keep to a sensible request rate |
| Request size / timeout | Standard ASP.NET defaults (about 4 MB request, about 110 seconds) |
| Unhandled errors | HTTP 500 |
Still having issues? Contact support
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_idfrom 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_poandorder_id.