Quotes
EveryGET /v2/surveys/{id} (and every POST /v2/surveys / PATCH /v2/surveys/{id} response) includes:
amountis the total USD price if you paid entirely on card.creditsis the same price expressed in credits (1 credit = $2 USD).discountsincludes plan-level, country-level, and API discounts already applied — the same pricing engine as the PickFu app.quotedAtis when the price was calculated. The most recent quote you fetched is what publish will honour.
quote_changed below.
Publishing
POST /v2/surveys/{id}/publish requires the spend scope and an Idempotency-Key header. In one call it:
- Confirms the recorded quote still matches the current price (else
409 quote_changed). - Spends credits first, oldest expiry first, from the team’s credit balance.
- Charges any shortfall off-session to the team’s default saved card, on the survey order itself. There is no separate credit-pack order in this path — the card charge is split-tendered on the survey.
- Marks the order paid and starts collection asynchronously.
200 response means paid, not live. Collection starts in the background; watch status on GET /v2/surveys/{id} (in-progress → completed) or listen for the survey.completed webhook.
Successful response:
402 insufficient_credits — buy a pack, then retry
If credits don’t cover the price and the team has no saved card, publish answers 402 insufficient_credits. Nothing is charged and the survey is still a draft.
- Buy the suggested pack:
POST /v2/orderswith{ "pack": "credits_20" }and a fresh Idempotency-Key. - Send the buyer to the returned
checkoutUrlto pay by card on Stripe’s hosted page. - Wait for the order to move to
status: "paid"— pollGET /v2/orders/{id}or listen for theorder.paidwebhook. - Retry
POST /v2/surveys/{id}/publishwith a new Idempotency-Key.
402 card_declined / card_authentication_required
If credits don’t cover the price and the saved card can’t be charged off-session, publish answers 402 card_declined or 402 card_authentication_required with a billingPortalUrl:
billingPortalUrl to add or update a card, then publish again with a new Idempotency-Key.
409 quote_changed
If the price moved between your last quote and the publish call, publish refuses to charge:
GET /v2/surveys/{id}) to record the new quote, show it to the buyer, and publish again with a new Idempotency-Key.
Credit packs
POST /v2/orders sells a credit pack on Stripe Hosted Checkout. PickFu never touches the card number.
checkoutUrl. Send the buyer to that URL; credits land on the team once Stripe confirms the payment. Then either poll GET /v2/orders/{id} until status: "paid" or listen for the order.paid webhook.
GET /v2/orders and GET /v2/orders/{id} list and read orders. Both credit-pack orders and survey orders appear; distinguish them by the resource fields returned per order.
An unknown pack value returns 400.
Managing saved cards
POST /v2/billing-portal-sessions opens a Stripe Billing Portal session for the team’s Stripe customer:
spend scope is sufficient — there is no separate billing-admin gate.
Credit balance
GET /v2/credit-balance returns the remaining credits and the underlying FIFO buckets (each with its expiry). Credits are spent oldest-expiry-first.