StrimzStrimz
Guides

Accept a one-shot payment

The full happy-path for charging a customer once.

You want to charge a customer one time for one thing. Here's the entire flow, with both server and client code.

Server: create the session

server.ts
import { StrimzClient } from '@strimz/sdk'
 
const strimz = new StrimzClient({ apiKey: process.env.STRIMZ_KEY! })
 
export async function createPaymentForOrder(orderId: string, amountUsd: number) {
  const session = await strimz.paymentSessions.create({
    amount: String(Math.round(amountUsd * 1_000_000)), // → smallest USDC units
    currency: 'USDC',
    description: `Order ${orderId}`,
    successUrl: `https://your-site.com/orders/${orderId}/thanks`,
    cancelUrl: `https://your-site.com/orders/${orderId}/cancelled`,
    metadata: { orderId },
  })
  return session
}

metadata is round-tripped to your webhooks unchanged. Use it to correlate Strimz sessions back to your own resources.

Client: redirect to checkout

checkout-button.tsx
'use client'
 
export function CheckoutButton({ orderId }: { orderId: string }) {
  return (
    <button
      onClick={async () => {
        const r = await fetch('/api/checkout', {
          method: 'POST',
          body: JSON.stringify({ orderId }),
        })
        const { checkoutUrl } = await r.json()
        window.location.href = checkoutUrl
      }}
    >
      Pay with USDC
    </button>
  )
}

That's the entire client side. The customer lands on https://checkout.strimz.finance/pay/<sessionId>, picks a wallet (Reown handles the picker for every major one), and approves two signature prompts: an EIP-3009 ReceiveWithAuthorization for the token, and the Strimz PayIntent.

Neither prompt is a transaction. Strimz's relayer turns the two signatures into the on-chain payWithAuthorization call, pays the gas itself, and settles the transfer. The customer never pays gas. The fee split (platform fee to FeeCollector, net to your payout) lands atomically in the same transaction.

The full security model lives on the meta-tx flow page: what the customer signs, why the relayer cannot move funds on its own, how the signature expires.

Webhook: react when it confirms

Don't trust the redirect to successUrl for state changes. Anyone can navigate to that URL. The webhook is the source of truth.

The handler runs on your server. It verifies the raw request body before parsing it, and it awaits verifyWebhookSignature, which resolves to { valid: true } or { valid: false, reason }:

webhooks-handler.ts
import express from 'express'
import { verifyWebhookSignature } from '@strimz/sdk'
 
const app = express()
 
app.post('/webhooks/strimz', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = req.body.toString('utf8')
  const result = await verifyWebhookSignature(
    payload,
    req.get('strimz-signature') ?? '',
    process.env.STRIMZ_WEBHOOK_SECRET!,
  )
  if (!result.valid) return res.status(401).end()
 
  const event = JSON.parse(payload)
  const { type, data } = event
 
  switch (type) {
    case 'payment.completed': {
      const orderId = data.session.metadata?.orderId
      if (orderId) await fulfillOrder(orderId, data.session.id, data.session.amount)
      break
    }
    case 'payment.failed':
      await markOrderFailed(data.session.metadata?.orderId)
      break
  }
  res.status(200).end()
})

Edge cases to know about

  • Session expiry: 30 minutes by default (set expiresInMinutes, up to one day). If the customer doesn't pay in time, the session goes to expired and you'll receive payment.failed. Create a new session.
  • Customer changes their mind mid-checkout: they hit the back button, you see no webhook. Don't fulfill until you receive payment.completed. The session stays created or awaiting_payment until it expires.
  • Customer pays the same session twice: the token authorization's nonce is derived from the session id, so the token contract rejects a second authorization for the same session.
  • Finality: Arc has deterministic finality. The indexer stays CONFIRMATIONS blocks behind the chain head (required; 1 is recommended), so a confirmed session doesn't flip back.

What just happened on-chain

The customer's wallet signed an EIP-3009 authorisation. Strimz's relayer wrapped it into a StrimzPayments.payWithAuthorization call and broadcast the transaction. Arc settled it in under a second. Your payoutAddress received amount - feeAmount USDC immediately. The indexer picked up the PaymentExecuted event, matched its ref to your PaymentSession.id, inserted a Transaction(kind=one_shot, status=confirmed), and fired payment.completed. End to end (the click to your webhook handler) takes about 20 seconds.

On this page