Printing credits
Use your balance for printing, with customer shipping and tax paid through Stripe.
Printing credit + customer shipping checkout
Automatic API and widget checkout: keep your existing API key and the normal create → poll → embedded checkout integration. Both server-API carts and standalone widget carts automatically use available partner credit for the full printing cost. The customer pays shipping and fulfillment tax through Stripe. If there is not enough eligible credit, checkout charges the customer for printing, shipping and tax normally. No additional prepaid API calls or demo-app changes are needed.
Accept the prepaid terms and keep enough available credit to cover printing. The platform chooses the payment path when the customer continues to payment, saves it, and reuses it on retries. It never partially spends credit or switches a previously created payment to another funding path. A payment setup error does not silently trigger a second full-price charge. Existing widget snippets use the same automatic credit selection; no API key or frontend change is needed. Test-mode widget previews never spend credit.
Buy printing credit in your partner platform under Buy credits. Accept the prepaid terms, choose an amount and continue to Stripe. Your balance updates after payment confirmation.
You charge the customer for cards in your store. Your USD credit pays TCGPlaytest’s printing cost only. The customer then pays shipping and quoted fulfillment tax directly to TCGPlaytest through Stripe. Printing starts once both are funded. No Stripe Connect account is needed.
Rate limits: all printing-credit endpoints share the 60 requests per API key per minute allowance with uploads, cart creation and order reads. Quote creation, quote GET polling, reserve, release, prepaid orders, balance/ledger reads and retries all count. Only authenticated cart-status GETs are exempt. Poll quotes every five seconds or slower (12 requests/minute per quote at five seconds), budget across concurrent orders, and honor Retry-After on HTTP 429. Retry mutations with their original body and idempotency key where required.
Optional advanced integration: reserve credit yourself
Use the endpoints below only when your own store needs to reserve credit before taking a separate card payment. The automatic hosted flow above does not require these steps. Your store remains responsible for any separate retail charge; automatic credit selection does not collect your retail card price for you.
- Accept the current prepaid terms in the dashboard and buy $10–$5,000 credit. Keep your existing live partner API key; it works with both Stripe test and live credentials.
- Create a cart with
settlement: "prepaid". Upload and validate artwork as usual. Poll untilopen; the cart itself has no checkout URL. - Quote the final shipping address.
credit_centsis the printing cost;customer_payment_centsis shipping plus fulfillment tax. Amounts are integer USD cents at current prices, without coupons or affiliate rewards. - Reserve printing credit before charging for cards. The quote and unused reservation expire at
expires_at, up to 30 minutes; retries do not extend it. Stop on insufficient credit. Do not also collect the quoted shipping/fulfillment tax in your store. - Collect the rights statement and quality acknowledgment. Confirm your store’s card-sale payment on your server. Submit the reserved quote with saved acceptance and a stable idempotency key.
- Submission spends printing credit once and returns 202,
order_id: null, a quote status URL and acheckout_url. Redirect that customer to the returned Stripe URL for shipping and tax. Checkout lasts one hour after submission. - Poll the quote through your server every five seconds or slower, honoring rate limits. Once both payments are confirmed,
order_ididentifies the manufacturing order. Use the existing order API and signed order webhooks to track printing/shipping.
Example: $20 printing, $6.95 shipping and $2 fulfillment tax spends $20 partner credit and collects $8.95 from the customer through Stripe. This tax calculation covers the quoted fulfillment amounts; it does not calculate your retail markup or replace your own retail tax setup.
I confirm I own these images or have permission to reproduce them, and I accept responsibility for the content of this order.
acceptance.accepted_at is the actual customer acceptance time, on/after cart creation. Use terms_version: "2026-10-04-v2", the shipping email and image_quality_accepted: true when warnings were shown and acknowledged. Persist the exact body/key; do not invent acceptance on the customer’s behalf.
| Endpoint | Purpose |
|---|---|
| GET /v1/prepaid/balance | Available/reserved/total credit and latest 50 ledger entries. Use next_before as the before query parameter for older entries. |
| POST /v1/prepaid/quotes | JSON {cart_id, shipping}. Requires Idempotency-Key; immutable address and pricing. |
| GET /v1/prepaid/quotes/{id} | Read customer_payment_status, checkout_url while open and order_id once manufacturing is funded. |
| POST /v1/prepaid/quotes/{id}/reserve | No body. Reserve printing cost once. Repeated calls are safe. |
| POST /v1/prepaid/quotes/{id}/release | No body. Release unused pre-submission credit. Released quotes cannot be reserved again. |
| POST /v1/prepaid/orders | JSON {quote_id, acceptance}; requires Idempotency-Key. Spends printing credit and returns customer shipping/tax checkout. Manufacturing waits for verified payment. |
Shipping: required name, email, line1, city, postal_code, country; optional line2, phone. State is required for US/CA/AU. Use a supported two-letter country. Changing the address requires a new quote/key before submission.
Payment states: pending/open: wait or resume the same checkout; paid with order_id: funded; expired: no manufacturing and printing credit returned; review/refund_pending/refunded: stop and contact support. Quote status consumed means printing credit was spent, not that manufacturing is paid.
Retries and expiry: retry ambiguous failures with the saved body/key or read the quote. Never create a second checkout because of a timeout. Submitting requires more than 65 minutes of cart life remaining. After submission, credit cannot be released manually; Stripe-confirmed unpaid expiry returns it automatically. A fresh attempt needs a new cart. Your store must handle its separate card-sale refund or continuation. An uncertain checkout creation that cannot be recovered safely needs support reconciliation before another charge.
Payment confirmation: order.paid confirms printing credit and customer shipping/tax payment. It does not verify the card purchase charged by your store. Production delivery retries if commerce is temporarily unavailable. Both hosted payment paths automatically return to the cart’s validated return_url after payment and order confirmation. A short confirmation screen waits for delayed payment notifications. Your custom success page must still verify the order API or signed webhook.
Refunds: contact support to cancel/review fulfillment. An approved full cancellation refund returns printing credit and separately queues the customer’s shipping/tax refund. The partner refunds its own card sale. Reversals put unshipped manufacturing on hold; partial customer refunds require operator review and tax reconciliation. Cancellation alone does not refund credit. Previously funded all-credit orders keep their original refund amounts. No partial credit-refund or cash-withdrawal API is provided.
Testing with Stripe
The platform uses its server-side STRIPE_SECRET_KEY automatically. Keep the same tcgp_live_ partner API key when switching Stripe credentials. Both environments use the same wallet tables and accepted terms; test payments never add real printing credit. Change the Stripe keys and matching webhook secrets on the partner and commerce deployments, then redeploy. No database switch or repeated SQL setup is needed.
After a verified test payment, the quote and order API return mode: "test" and simulated: true. The Test order webhook can receive a simulated order.paid event with data.simulated: true. No production or shipment events are generated. Hosted test carts and the standalone widget still open a non-paying preview.
Platform setup: use Stripe credentials from the same account and environment on both the partner and commerce apps, and set STRIPE_CREDIT_WEBHOOK_SECRET to that environment’s credit webhook secret. Live quotes require Stripe Tax setup. With the current commerce quote handler, prepaid quotes using Stripe test credentials return zero tax without calling Stripe Tax; this does not change live calculations or ordinary checkout tax settings. Keep PARTNER_PREPAID_ENABLED=true. No extra mode variable is needed.
To go live, finish pending test checkouts, switch both apps to live Stripe credentials and the live credit webhook secret, configure live Stripe Tax, and redeploy. Use a tcgp_live_ partner key and buy real credit. Test credit never becomes real credit; any existing live balance stays intact. Each deployment runs one Stripe environment at a time. Use separate deployments if both must remain active.
cURL: complete prepaid flow
# YOUR SERVER only. Store TCGP_API_KEY in an environment variable.
API_BASE="https://partner.tcgplaytest.com"
# Keep the same tcgp_live_ partner key when switching Stripe credentials.
# 1. Create artwork cart; poll it using GET /v1/carts/{id} until open.
curl --fail-with-body "$API_BASE/v1/carts" \
-H "Authorization: Bearer $TCGP_API_KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: shop-order-1042-artwork-v1' \
-d '{"settlement":"prepaid","external_ref":"shop-order-1042","items":[{"image_url":"https://your-shop.example/front.png","quantity":10}]}'
# Replace CART_ID with the returned cart ID after validation is open.
curl --fail-with-body "$API_BASE/v1/prepaid/quotes" \
-H "Authorization: Bearer $TCGP_API_KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: shop-order-1042-quote-v1' \
-d '{"cart_id":"CART_ID","shipping":{"name":"Customer Name","email":"buyer@example.com","line1":"123 Main St","city":"Seattle","state":"WA","postal_code":"98101","country":"US"}}'
# Save the returned quote ID and expires_at. Reserve BEFORE charging customer.
QUOTE_ID="REPLACE_WITH_QUOTE_UUID"
curl --fail-with-body -X POST "$API_BASE/v1/prepaid/quotes/$QUOTE_ID/reserve" \
-H "Authorization: Bearer $TCGP_API_KEY"
# Confirm payment for CARDS in your store (do not also collect the quoted
# shipping/fulfillment tax). Then prepare and persist order.json:
# {"quote_id":"the saved UUID","acceptance":{"accepted":true,
# "terms_version":"2026-10-04-v2","email":"buyer@example.com",
# "accepted_at":"the actual saved RFC3339 customer acceptance timestamp",
# "image_quality_accepted":true}}
# Show the exact rights statement and any warnings before recording acceptance.
curl --fail-with-body "$API_BASE/v1/prepaid/orders" \
-H "Authorization: Bearer $TCGP_API_KEY" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: shop-order-1042-submit-v1' --data-binary @order.json
# HTTP 202: send the customer to checkout_url to pay shipping + fulfillment tax.
# order_id stays null until Stripe confirms payment. Poll the quote on your server:
curl --fail-with-body "$API_BASE/v1/prepaid/quotes/$QUOTE_ID" \
-H "Authorization: Bearer $TCGP_API_KEY"
# If still open, honor checkout_expires_at; do not create another payment.
# In live mode only, order_id + confirmed funding or signed order.paid confirms manufacturing.
# In test mode these identify a simulated order; mode:test and simulated:true are returned.
# BEFORE submission, if the customer's card purchase fails, release the reservation:
# curl --fail-with-body -X POST "$API_BASE/v1/prepaid/quotes/$QUOTE_ID/release" \
# -H "Authorization: Bearer $TCGP_API_KEY"
# AFTER submission, release is unavailable. Verified unpaid Stripe expiry returns
# printing credit automatically. Use a new cart for a fresh attempt; handle your
# own card-sale refund separately. Never charge your customer twice.These snippets replace the submission step. Complete validation, reservation, rights acceptance and your store’s card-sale payment first. Accept HTTP 202, redirect the owning customer to checkout_url, then wait for both payments before showing manufacturing success.
// savedOrder is loaded from YOUR database, owned by this customer.
// Its body and key were saved once, after verified customer payment.
const response = await fetch(process.env.TCGP_API_BASE + '/v1/prepaid/orders', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.TCGP_API_KEY,
'Content-Type': 'application/json', 'Idempotency-Key': savedOrder.submissionKey },
body: JSON.stringify(savedOrder.submissionBody)
});
const result = await response.json();
if (!response.ok) throw new Error(result.error.message);
// Save result.quote.id and result.status_url. HTTP 202 is a successful submission.
// If result.checkout_url exists, redirect this customer there for shipping/tax.
// Poll the quote on YOUR server; save order_id only once it is non-null.
// On timeout, retry this exact body and key.import os, requests
# Load saved_body and saved_submission_key from your database.
# Confirm your customer payment before reaching this step.
response = requests.post(
os.environ['TCGP_API_BASE'] + '/v1/prepaid/orders',
headers={'Authorization': 'Bearer ' + os.environ['TCGP_API_KEY'],
'Idempotency-Key': saved_submission_key},
json=saved_body, timeout=30)
response.raise_for_status()
order = response.json()
# Save order['quote']['id'] and order['status_url']; 202 means customer payment pending.
# Redirect this customer to order['checkout_url'] when present.
# Poll the quote server-side until order_id exists or payment expires/needs review.
# Reuse the same body/key after network errors.<?php
// Load $savedBody and $savedSubmissionKey from your database.
// Call only after your server confirms the customer's payment.
$curl = curl_init(getenv('TCGP_API_BASE') . '/v1/prepaid/orders');
curl_setopt_array($curl, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('TCGP_API_KEY'),
'Content-Type: application/json', 'Idempotency-Key: ' . $savedSubmissionKey],
CURLOPT_POSTFIELDS => json_encode($savedBody, JSON_THROW_ON_ERROR)
]);
$raw = curl_exec($curl);
if ($raw === false) throw new RuntimeException('Network error; retry the saved request.');
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) throw new RuntimeException($result['error']['message']);
// Persist $result['quote']['id'] and $result['status_url']; accept HTTP 202.
// Redirect the owning customer to $result['checkout_url'] if present.
// Poll the quote through your server. order_id is null until both payments clear.
// Do not generate new acceptance timestamps on retry.Errors: 402 insufficient_credit: top up before charging for cards. 403 prepaid_terms_required: accept current terms in the dashboard. 403 prepaid_mode_mismatch: use a live partner API key for real payments; that same key also works with Stripe test credentials. 409 reservation_required / quote_expired: obtain a new quote. 409 credit_blocked / payment_reconciliation_required: contact support. 409 idempotency_conflict: instructions changed. Honor Retry-After on 429. Retry network/5xx failures with the same body/key.