Skip to main content
v2 splits publishing into a quote you can read at any time and a publish call that commits to it. A draft survey costs nothing until you publish; publish pays for it in one call.

Quotes

Every GET /v2/surveys/{id} (and every POST /v2/surveys / PATCH /v2/surveys/{id} response) includes:
  • amount is the total USD price if you paid entirely on card.
  • credits is the same price expressed in credits (1 credit = $2 USD).
  • discounts includes plan-level, country-level, and API discounts already applied — the same pricing engine as the PickFu app.
  • quotedAt is when the price was calculated. The most recent quote you fetched is what publish will honour.
The quote is a contract. Publish compares the last quote it recorded against the current price and refuses if they diverge — see quote_changed below.

Publishing

POST /v2/surveys/{id}/publish requires the spend scope and an Idempotency-Key header. In one call it:
  1. Confirms the recorded quote still matches the current price (else 409 quote_changed).
  2. Spends credits first, oldest expiry first, from the team’s credit balance.
  3. 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.
  4. Marks the order paid and starts collection asynchronously.
A 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.
To recover:
  1. Buy the suggested pack: POST /v2/orders with { "pack": "credits_20" } and a fresh Idempotency-Key.
  2. Send the buyer to the returned checkoutUrl to pay by card on Stripe’s hosted page.
  3. Wait for the order to move to status: "paid" — poll GET /v2/orders/{id} or listen for the order.paid webhook.
  4. Retry POST /v2/surveys/{id}/publish with 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:
Send the buyer to 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:
Re-fetch the survey (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.
The response includes a 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:
The response includes a portal URL. Send the buyer there to add, update, or remove saved cards. The 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.