Skip to content

Pre-confirmation hook

A capacity check the partner controls — fires after the customer commits but BEFORE Stripe is charged. Use it for inventory-tight commerce: bookings, ticketing, classes, time-slotted appointments.

await fetch(
`https://api.swft.co.uk/v2/agencies/${ws}/merchants/${mid}/pre-confirmation`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${SWFT_PARTNER_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': uuid(),
},
body: JSON.stringify({
url: 'https://api.unstack.co.uk/swft/capacity-check',
fail_mode: 'closed', // default — 503 to client on partner timeout
}),
},
)
// → returns { url, secret, fail_mode } — SAVE the secret, never recoverable.

Swft POSTs to your url with Standard Webhooks-signed headers:

POST /swft/capacity-check
webhook-id: msg_01HZ...
webhook-timestamp: 1717000000
webhook-signature: v1,<base64-hmac-sha256(secret, "${id}.${ts}.${body}")>
content-type: application/json
{
"session_id": "sess_...",
"merchant_id": "mer_...",
"extensions": { "booking": { ... } },
"cart": {
"items": [...],
"total_pence": 2500,
"currency": "GBP"
}
}

Verify the signature, look up the class, and answer:

// Available
return Response.json({ available: true })
// Not available — pass a `reason` string; Swft echoes it in
// `order.failed.data.reason` and to the API caller as
// `error.meta.reason`.
return Response.json({ available: false, reason: 'class_full' })

Reference implementation (Next.js + Supabase)

Section titled “Reference implementation (Next.js + Supabase)”
app/api/swft/capacity-check/route.ts
import { verifyStandardWebhook } from '@swft-checkout/js/webhooks'
export async function POST(req: Request) {
const id = req.headers.get('webhook-id') ?? ''
const timestamp = req.headers.get('webhook-timestamp') ?? ''
const sig = req.headers.get('webhook-signature') ?? ''
const body = await req.text()
if (!verifyStandardWebhook({ id, timestamp, body, signatureHeader: sig, candidateSecrets: [process.env.SWFT_PRECONFIRM_SECRET!] })) {
return new Response('bad signature', { status: 401 })
}
const { session_id, extensions } = JSON.parse(body)
const classId = extensions.booking?.class_schedule_id
const { data: cls } = await supabase
.from('class_schedules')
.select('booked_count, capacity')
.eq('id', classId)
.single()
if (!cls || cls.booked_count >= cls.capacity) {
return Response.json({ available: false, reason: 'class_full' })
}
return Response.json({ available: true })
}
Partner responseAPI behaviour
{ available: true }Stripe PI is created; checkout proceeds.
{ available: false, reason }409 pre_confirmation_rejected; session marked failed; order.failed fires with reason.
Timeout (>5s) / 5xx + fail_mode='closed'503 pre_confirmation_unavailable.
Timeout / 5xx + fail_mode='open'Proceed with payment; warning logged.

Doing the check before Stripe is charged means we never have to refund. Stripe idempotency holds the partner’s commitment until the page either confirms or fails — no double-charge risk on retry.