Strimz
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 { Strimz } from '@strimz/sdk'
 
const strimz = new Strimz({ 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 strimz.finance/pay/<sessionId>, picks a wallet (Reown handles the picker for every major one), and signs two typed-data messages: the token authorization and the Strimz PayIntent.

The signature is an EIP-3009 ReceiveWithAuthorization. Strimz's relayer turns it into the on-chain payWithAuthorization call, pays the gas itself, and settles the transfer. The customer never sees a second prompt or 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.

webhooks-handler.ts
import { verifyWebhookSignature } from '@strimz/sdk'
 
app.post('/webhooks/strimz', async (req, res) => {
  const ok = verifyWebhookSignature({
    secret: process.env.STRIMZ_WEBHOOK_SECRET!,
    body: req.rawBody,
    signatureHeader: req.headers['strimz-signature'] as string,
  })
  if (!ok) return res.status(401).end()
 
  const event = JSON.parse(req.rawBody.toString())
  const { type, data } = event
 
  switch (type) {
    case 'payment.completed': {
      const orderId = data.metadata?.orderId
      if (orderId) await fulfillOrder(orderId, data.id, data.amount)
      break
    }
    case 'payment.failed':
      await markOrderFailed(data.metadata?.orderId)
      break
  }
  res.status(200).end()
})

Edge cases to know about

  • Session expiry: 24 hours by default. If the customer doesn't pay in time, the session goes to cancelled 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 sits in pending until expiry.
  • Customer pays the same session twice: impossible. Each session is one on-chain pay() call and the contract enforces the ref is single-use.
  • Network reorg: the indexer waits 5 confirmations before flipping confirmed. You won't get a flapping "confirmed to pending to confirmed" sequence.

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