Made for agents.
Grounded in the real world.
Assistants list products, save a customer’s image as a draft, show a preview on the actual product, and start a Stripe checkout, through a REST API or an MCP server with the same operations.
One service. Two ways in.
The website and the connector share the same customer, draft and order records. Wall art (photo print, matte poster, framed print, canvas gallery wrap and metal print, up to 24×36 inches) and merchandise (T-shirt, hoodie, mug, stickers, blanket and playing cards), one image each. listProducts returns each product’s artwork rules and live price. Every preview is drawn from the exact file that gets printed.
Connection
Assistants connect to the MCP server at https://gettangibl.com/api/mcp (Streamable HTTP) and register themselves through OAuth dynamic client registration, so customers only add the address and sign in. /connect describes setup for assistants; /connect/usage explains day-to-day use. Connector configuration:
mcp_server_url: https://gettangibl.com/api/mcp
transport: Streamable HTTP
auth_scheme: oauth2_code (PKCE S256)
authorization_url: https://gettangibl.com/oauth/authorize
token_url: https://gettangibl.com/oauth/token
registration_url: https://gettangibl.com/oauth/register
revoke_url: https://gettangibl.com/oauth/revoke
token_endpoint_auth_method: none
scopes: draftsThe MCP tools are the operations below under the same names. The REST API is described by an OpenAPI document at /api/v1/openapi and uses the same OAuth sign-in. Instead of file uploads, MCP clients save new images with uploadDraftImage (a file the app passes, such as ChatGPT’s openai/fileParams), uploadDraftFromUrl (a public https link) or createUploadLink, a single-use page where the customer picks the image (getUploadLink returns the result).
Accounts & permissions
Customers sign in to Tangibl with Google, Facebook or email and approve the connection. They can disconnect it from My Tangibl at any time.
- Customer identity comes from the access token, never from an agent-supplied customer ID.
- Every draft and order is checked against the signed-in owner.
- Supplier credentials, payment secrets and print files stay server-side.
- Disconnecting stops future access; it does not cancel paid orders.
Operations
| Operation | Request | Purpose |
|---|---|---|
listProducts | GET /api/v1/products | Products, artwork rules and generation hints |
uploadDraft | POST /api/v1/uploads | multipart/form-data: file (JPEG/PNG), product_id, name, optional occasion and note; header Idempotency-Key (new UUID); optional ?parent_id= |
uploadCanvasPrintFile | POST /api/v1/canvas/uploads | multipart/form-data: file (3300×3900), product_id=canvas-print, name, optional occasion and note; header Idempotency-Key |
listMyDrafts | GET /api/v1/drafts | Recent drafts with their names, occasions and notes |
reviseDraftProduct | POST /api/v1/drafts | application/json: {"parent_id", "product_id", optional "name", "occasion", "note"}; header Idempotency-Key. Same artwork, another product; not for new images |
getDraft | GET /api/v1/drafts/{id} | One draft |
deleteDraft | DELETE /api/v1/drafts/{id} | Permanently delete a creation (not possible once it has an order). Only when the customer clearly asks, after confirming which one |
updateDraftDetails | PATCH /api/v1/drafts/{id} | application/json: any of {"name", "occasion", "note"}; empty string clears |
getDraftPreview | GET /api/v1/drafts/{id}/preview | Artwork image (JPEG) |
getDraftProof | GET /api/v1/drafts/{id}/proof | Preview status |
getDraftProofImage | GET /api/v1/drafts/{id}/proof/image | Product preview image (JPEG) |
requestSupplierMockup | POST /api/v1/drafts/{id}/supplier-mockup | Retry a failed canvas or poster preview |
startCheckout | POST /api/v1/drafts/{id}/checkout | application/json: optional {"quantity"} (1–5), {"size"} (required for T-shirt and hoodie: ask the customer) and {"address_id"} (where it ships, from listAddresses; required once the customer has saved addresses); header Idempotency-Key (new UUID per order). Returns total, ship_to and a Stripe checkout_url |
getCart | GET /api/v1/cart | The saved cart: items, prices, one shipping line, total |
addToCart | POST /api/v1/cart/items | application/json: {"draft_id", optional "size" (required for T-shirt and hoodie), optional "quantity" 1–5}; same creation and size again sets its quantity. Returns the cart |
updateCartItem | PATCH /api/v1/cart/items/{cart_item_id} | application/json: {"quantity"} 1–5. Returns the cart |
removeCartItem | DELETE /api/v1/cart/items/{cart_item_id} | Remove one line (POST …/delete also works). Returns the cart |
checkoutCart | POST /api/v1/cart/checkout | application/json: {"address_id"} (where it ships; required once the customer has saved addresses); header Idempotency-Key (new UUID per order). One order and one Stripe checkout_url for the whole cart; the cart empties once paid |
listAddresses | GET /api/v1/addresses | The address book: labelled US and Colombian addresses ("Me", "Mom") with address_id and the default |
createAddressLink | POST /api/v1/addresses/links | application/json: {"label"}. A single-use page (address_url, 30 minutes) where the customer enters the address; same label replaces a saved one |
getAddressLink | GET /api/v1/addresses/links/{address_link_id} | waiting, saved (with the address and address_id) or expired |
updateAddress | PATCH /api/v1/addresses/{address_id} | application/json: {"label"} and/or {"is_default": true} |
deleteAddress | DELETE /api/v1/addresses/{address_id} | Remove an address (POST …/delete also works). Only when the customer clearly asks |
listMyOrders | GET /api/v1/orders | Recent orders and their status |
getOrder | GET /api/v1/orders/{id} | One order: payment status, total, tracking; report_problem_url (paid orders) is where the customer reports a misprint with photos |
sendFeedback | POST /api/feedback | application/json: {"message", "topic", "agent_name", "context"} |
Write operations take an Idempotency-Key. Prices come from the server; an agent cannot set a total or mark an order as paid.
Examples
Save an image from a public link as an 11 oz mug. Over MCP, send a JSON-RPC tools/call to https://gettangibl.com/api/mcp with an access token issued for that endpoint:
POST /api/mcp
Authorization: Bearer <access token>
Content-Type: application/json
MCP-Protocol-Version: 2025-11-25
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "uploadDraftFromUrl",
"arguments": {"image_url": "https://example.com/art.png", "product_id": "mug", "name": "Review Mug"}}}The result carries the saved draft in structuredContent, and the same JSON as text in content. Selected fields (the full draft also includes image, proof and message):
{
"draft_id": "3f2b8c1e-5d4a-4c7b-9e2f-1a6d0b9c8e7f",
"name": "Review Mug",
"product_id": "mug",
"size": null,
"review_url": "https://gettangibl.com/account/drafts/3f2b8c1e-5d4a-4c7b-9e2f-1a6d0b9c8e7f",
"preview_url": "https://gettangibl.com/api/v1/drafts/3f2b8c1e-5d4a-4c7b-9e2f-1a6d0b9c8e7f/preview",
"preview_requires_bearer": true
}Then call getDraftProofImage with {"draft_id": "…"}; it returns the product preview as an image content block (JPEG). MCP calls take no Idempotency-Key: the server derives one from the connection, tool, arguments and a fixed 15-minute window, so an identical call on the same connection within that window returns the same draft. A retry that falls in the next window can create a new one.
Tool execution errors return isError: true with {"error": "<code>", "message": "…"} as text; the message says what to change. A missing or invalid token gets HTTP 401 with a WWW-Authenticate header, and a malformed message gets a JSON-RPC error.
The same upload over REST is a multipart request. Generate the key once and send the same key when retrying, so a retry does not save a second draft:
KEY=$(uuidgen)
curl https://gettangibl.com/api/v1/uploads \
-H "Authorization: Bearer <access token>" \
-H "Idempotency-Key: $KEY" \
-F file=@art.png -F product_id=mug -F name="Review Mug"REST errors use HTTP status codes with a {"error": "<code>", "message": "…"} body.
A token requested for one endpoint with the OAuth resource parameter (https://gettangibl.com/api/mcp or https://gettangibl.com/api/v1) is rejected by the other with HTTP 401. A token issued for https://gettangibl.com, or without a resource, works on both.
Limits
- Images: JPEG, PNG or WebP, up to 4 MB.
listProductsgives each product’s shape and minimum resolution. - MCP messages up to 64 KiB, batches up to 20 messages, 60 seconds per request.
- Upload pages (
createUploadLink): 30 per customer per hour. Address pages (createAddressLink): 20 per customer per hour. Both expire after 30 minutes. sendFeedback: 5 per customer per hour.- Client registration (
/oauth/register): 500 per hour.
Over a limit, the API returns HTTP 429 (over MCP, a tool error) with temporarily_unavailable or a specific code and a message saying when to try again. Make calls one at a time and back off on 429 or 503 rather than retrying immediately.
Orders
- The customer saves an image as a draft and sees the product preview.
startCheckoutreturns the total and a Stripe checkout link. The customer pays on Stripe’s page, never in the chat.- After payment, Tangibl makes and ships the order, and its status appears in My Tangibl and through
listMyOrders.
Prices include US shipping. Made to order, so no change-of-mind returns; misprints are reprinted.
Integration questions →