Custom Callbacks

Run your own code when a paywall asks for it, then send back success or failure so the paywall can pick its next step. Web SDK 0.3.4+.

Beta

The Web SDK is in beta and its API may change between releases.

A Custom Callback action in the paywall editor asks your page to run some code and answer with success or failure. The paywall then runs its On Success or On Failure actions. Use it to check a code the user typed or look something up in your backend.

This works the same way as onCustomCallback in the iOS and Android SDKs. You answer callbacks per presentation, on the handler you pass to register(), not on the delegate.

Requirements

  • Web SDK 0.3.4 or later (@superwall/paywalls-js, and @superwall/paywalls-react for React).
  • The Web SDK must present the paywall. A paywall opened on its own, outside the SDK, has nothing to answer the callback, so it always gets failure.
  • The paywall must use the default iframe presenter or a custom presenter. Paywalls you render yourself with the paywall option or useCustomPaywall don't receive callbacks.

Add a callback

  1. In the paywall editor, select the element that should trigger the callback.
  2. Under Tap Behavior, click + Add Action and choose Custom Callback.
  3. Enter a Name, such as validateCode.
  4. Choose a Behavior. See Blocking and non-blocking.
  5. To send values to your page, click + Add Variable.
  6. Add follow-up actions under On Success and On Failure.
  7. Publish the paywall.
  8. In your page, pass handler.onCustomCallback to register():
await sw.register({
  placement: "campaign_trigger",
  handler: {
    onCustomCallback: async ({ name, variables }) => {
      if (name !== "validateCode") return { status: "failure" };

      const ok = await api.validate(variables?.["params.code"]);
      return { status: ok ? "success" : "failure", data: { ok } };
    },
  },
});

See Tap behaviors for the rest of the editor's actions.

What your handler receives

onCustomCallback receives a CustomCallback:

Prop

Type

What to return

Return a CustomCallbackResult, or a promise of one:

Prop

Type

For example, a callback named validateCode that returns { status: "success", data: { ok: true } } sets callbacks.validateCode.data.ok in the paywall. The paywall also sets callbacks.validateCode.result to the status.

The SDK always answers the paywall. It sends failure when:

  • You pass neither onCustomCallback nor a matching entry in customCallbacks.
  • Your handler throws or its promise rejects.
  • Your handler returns something without a status of "success" or "failure".

A throw or a bad return is logged through the SDK's logger. If the paywall closes before your handler finishes, the answer is dropped.

Typed callbacks

For type-checked handlers, declare your callbacks on the CustomCallbacks interface and answer them by name with handler.customCallbacks. Each handler gets that callback's input as its variables, and a success result's data is checked against output.

declare module "@superwall/paywalls-js" {
  interface CustomCallbacks {
    validateCode: { input: { "params.code": string }; output: { ok: boolean } };
  }
}

await sw.register({
  placement: "campaign_trigger",
  handler: {
    customCallbacks: {
      validateCode: async (vars) => ({
        status: "success",
        data: { ok: await api.validate(vars["params.code"]) },
      }),
    },
  },
});

The SDK checks customCallbacks first. Any callback name that isn't in the map goes to onCustomCallback, so you can pass both. Typed handlers come with a few catches:

  • Write input and output as inline object types or type aliases. An interface doesn't fit Record<string, JsonValue>, so its handler is typed as never.
  • The types are not checked at runtime. Callback names and variables come from the paywall editor and can drift from your declaration.
  • A typed handler receives variables as its first argument and { name } as its second.

React

Pass onCustomCallback or customCallbacks to usePlacement:

const { register } = usePlacement({
  onCustomCallback: async ({ name, variables }) => {
    if (name !== "validateCode") return { status: "failure" };
    const ok = await api.validate(variables?.["params.code"]);
    return { status: ok ? "success" : "failure" };
  },
});

usePlacement always calls the latest onCustomCallback you passed, so it can use current state. A throw still reaches the SDK and becomes failure.

<SuperwallPaywall> accepts onCustomCallback and customCallbacks props without a type error but ignores them in 0.3.4. Use usePlacement or call register() yourself.

Blocking and non-blocking

The Behavior you choose in the editor controls only whether the paywall waits. Your handler runs and the SDK answers either way.

  • Blocking: the paywall waits for your answer before it runs the rest of the action chain. Use it when the next step depends on the result, such as validating a code.
  • Non-blocking: the paywall continues right away. On Success or On Failure still runs when your answer arrives.

The SDK has no timeout. A blocking callback waits as long as your handler takes, so keep it fast.

Custom presenters

If you pass your own presenter to register(), the SDK hands it the callback runner as ctx.onCustomCallback on the PresentationContext. It runs customCallbacks and onCustomCallback for you and never rejects. A handler that throws resolves failure.

ctx.onCustomCallback is set only when the caller passed onCustomCallback or customCallbacks. When it's absent, answer failure, as the default presenter does.

Limitations

  • Only paywalls the Web SDK presents receive callbacks. A standalone paywall link always gets a failure.
  • Paywalls rendered through a custom paywall renderer or useCustomPaywall don't receive callbacks.
  • <SuperwallPaywall> ignores callback props in 0.3.4.
  • There's no timeout. A blocking callback holds the paywall until your handler answers.

How is this guide?

On this page