Recur Docs

Cancel

Cancel subscriptions — graceful request, immediate exit, or permissionless finalization.

Cancel

Recur supports three cancel flows:

FlowInstructionWho callsEffect
Request cancelrequest_cancelSubscriber or merchantMarks cancel pending; prepaid period honored
Subscriber cancelsubscriber_cancelSubscriber onlyImmediate close; forfeits remaining prepaid time
Finalize cancelfinalize_cancelAnyone (permissionless)Closes PDA after prepaid period elapses

buildCancelTransaction

Builds a request_cancel instruction. The subscription stays active until the prepaid period ends.

const { instructions } = recur.buildCancelTransaction(
  wallet.publicKey, // authority (subscriber or merchant)
  {
    merchantWallet: "MERCHANTxxx...",
    planSeed: "0100000000000000",
  },
);
 
const tx = new Transaction().add(...instructions);
const signed = await wallet.signTransaction(tx);
await connection.sendRawTransaction(signed.serialize());

buildSubscriberCancelTransaction

Immediate exit. The subscriber forfeits any remaining prepaid time and the PDA is closed on-chain.

const { instructions } = recur.buildSubscriberCancelTransaction(
  wallet.publicKey,
  {
    merchantWallet: "MERCHANTxxx...",
    planSeed: "0100000000000000",
  },
);
 
const tx = new Transaction().add(...instructions);
const signed = await wallet.signTransaction(tx);
await connection.sendRawTransaction(signed.serialize());

buildFinalizeCancelTransaction

After a request_cancel, once the prepaid period elapses, anyone can call finalize_cancel to close the PDA and reclaim rent. This is permissionless — typically called by the keeper.

const { instructions } = recur.buildFinalizeCancelTransaction({
  subscriberWallet: "SUBSCRIBERxxx...",
  merchantWallet: "MERCHANTxxx...",
  planSeed: "0100000000000000",
});
 
const tx = new Transaction().add(...instructions);
// No specific signer required — anyone can submit
await connection.sendRawTransaction(tx.serialize());

cancel (high-level)

Combines build, sign, send, and confirm in one call.

// Graceful cancel (prepaid period honored)
await recur.cancel(wallet, {
  merchantWallet: "MERCHANTxxx...",
  planSeed: "0100000000000000",
  mode: "request",
});
 
// Immediate cancel (forfeits prepaid time)
await recur.cancel(wallet, {
  merchantWallet: "MERCHANTxxx...",
  planSeed: "0100000000000000",
  mode: "instant",
});
modeInstruction usedBehavior
"request"request_cancelMarks pending; no more payments pulled; PDA closed after period ends
"instant"subscriber_cancelCloses PDA immediately

Cancel lifecycle

  1. Subscriber calls request_cancel → status becomes cancel_pending
  2. Keeper stops pulling payments for this subscription
  3. Webhook cancel_requested fires to merchant
  4. When last_payment_timestamp + interval passes, anyone calls finalize_cancel
  5. PDA is closed, rent reclaimed, webhook cancel_finalized fires

On this page