Webhook Configurations

Configure webhook URLs in the Developer Dashboard to have Paper call your backend service when notable user events happen. Webhooks are configured for the entire organization and separated by testnet and production checkouts.

Webhook Events

The following webhook events are emitted from our backend.

There currently is not a way to filter which events are sent to your webhook URLs, and Paper may add new webhook event types without notice.

Please handle unexpected or unused event types by returning a 2xx response code so our backend does not retry.

payment:succeededA buyer successfully paid with a supported payment method.
payment:failedA buyer paid but their payment method was rejected.

Extra data fields may be available with information from our payment processor on failure reason.
payment:refundedA buyer's payment is refunded because Paper is unable to mint the NFT for some reason.

Extra data fields may be available with the reason for the refund.
payment:hold_createdA buyer's payment method has a pre-authorization hold created for the given amount. They are not charged yet.

You can capture or cancel this transaction hold later.
transfer:succeededA buyer successfully received their purchase to their wallet.
transfer:failedA buyer failed to receive their purchase to their wallet.

Note: Paper's engineering team is notified when this occurs to debug and attempt to resolve the issue manually (e.g. retry the transaction or refund the buyer).

HTTP Request Format

Paper will call your backend with an HTTPS request:


// headers
Content-Type: application/json
X-Paper-Signature: <signature>

// body
  "event": "transfer:succeeded",
  "result": {
    "id": "5bbbada7-e864-4dac-ae4b-0ee4967f55d8",
    "checkoutId": "70e08b7f-c528-46af-8b17-76b0e0ade641",
    "walletAddress": "0x2086Fcd5b0B8F4aFAc376873E861DE00c67D7B83",
    "walletType": "Preset",
    "email": "[email protected]",
    "quantity": 1,
    "paymentMethod": "BUY_WITH_CARD",
    "networkFeeUsd": 0.02,
    "networkFeeUsd": 1.79,
    "totalPriceUsd": 45.99,
    "createdAt": "2022-08-22T19:15:09.755375+00:00",
    "paymentCompletedAt": "2022-08-22T19:16:01.673+00:00",
    "transferCompletedAt": "2022-08-22T19:16:18.024+00:00",
    "claimedTokens": {
      "collectionAddress": "0x965550329b91b7c703a527347b613E175f38872d",
      "collectionTitle": "My First NFT",
      "tokens": [
          "transferHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
          "transferExplorerUrl": "https://polygonscan.com/tx/0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
          "tokenId": "262",
          "quantity": 1
    "title": "My First Paper Checkout",
    "transactionHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
    "valueInCurrency": "0.05",
    "currency": "ETH",
    "metadata": {
      "myAppUserId": "23a9fj2930gya0"
    "mintMethod": { ... },
    "eligibilityMethod": { ... },
    "contractArgs": { ... }, 


Webhooks will retry every 5 minutes for one hour until it receives a 2xx response code (13 attempts total).
Webhooks will originate from the IP address

View Recent Events

View the 20 most recent webhook events emitted along with the request payload, response status code, and response body returned by your backend. This log is useful to debug missing or mishandled webhook handlers.


List webhook events

Test Your Webhook Integration

Paper will send a dummy payload to your webhook URL and display the response status and response body.


Test webhook URL

Verify the Signature Header

Paper signs each webhook request with a signature in the X-Paper-Signature header. This signature ensures you can trust that the request comes from Paper.

To verify the signature, create a SHA-256 HMAC hash with the API key as the secret and the body payload as the message (as a JSON-encoded string).
Here's an example in Node with Typescript (simplified for clarity):

import { createHmac, timingSafeEqual } from 'crypto';

// In your webhook HTTP handler
const signature = req.headers['X-Paper-Signature'];
const hash = createHmac('sha256', YOUR_API_KEY)
  .update(JSON.stringify(req.body)) // {"event":"transfer:succeeded","result":{"id":...

if (!timingSafeEqual(Buffer.from(signature), Buffer.from(hash))) {
  // Signature mismatch: Reject this request
} else {
  // Signature match: Continue processing this request


Here are common issues if you're not processing webhook calls successfully.

  • Check if your server framework is reading the header properly. Some frameworks like Next.js lowercase all header names.
  • Make sure you're passing the entire body as the message in the HMAC signature. Some frameworks require you to configure the middleware to not parse the request body.
  • Make sure the API key you're using matches the one shown in the Developer Dashboard.
  • Webhook URLs must be https endpoints that are internet accessible. Do not provide a localhost URL to test your local server. Instead use a service like ngrok to make your local server internet accessible.