You can configure your checkout with up to three webhook URLs. When a user successfully purchases and receives an NFT through your checkout, each webhook URL will be called.

Only HTTPS webhook URLs are supported.

A webhook is typically called immediately after a successful purchase but may be delayed by up to a few minutes depending on network traffic.

HTTPS Request

Paper's server will make the following HTTPS request to each webhook URL you define. Please ensure your backend handles this route, asserts the correct HTTP method, and decodes the signature.

fetch("YOUR_WEBHOOK_URL", {
    headers: {
    "Content-Type": "application/json",
    // to be verified by your backend
    "X-Paper-Signature": <signature>
  }
  body: JSON.stringify({
    "event": "transfer:succeeded",
    "result": {
      "id": "8e2b245b-7be7-4f25-a406-cc9a3c905f9f",
      "checkoutId": "d2ccf22b-a3d5-46b8-9da5-55a28016ab52",
      "walletAddress": "0x86Bd910E7d7D84C0F8C8DE418B877609196Fb719",
      "walletType": "MetaMask",
      "email": "[email protected]",
      "quantity": 1,
      "paymentMethod": "BUY_WITH_CARD",
      "networkFeeUsd": 0.01,
      "totalPriceUsd": 45.50,
      "createdAt": "2022-03-08T12:28:07.958+00:00",
      "paymentCompletedAt": "2022-03-08T12:28:32.528+00:00",
      "transferCompletedAt": "2022-03-08T12:28:52.265+00:00",
      "claimedTokens": {
        "tokenIds": ["0"],
        "transactionHashes": {
          "0x4c76e13a67e4ce1a03ae111f862f82d69383f6ac7aaf7f248644d9818d33cf19": [
            "0"
          ]
        }
      }
    }
  })
})

Transaction Data Model

The "results" field contain the following fields (i.e. the transaction id is requestBody.result.id).

Field

Type & Example

Notes & Example

id

string
773c9754-6ec1-4241-ba52-309f80b901d7

The unique ID of the transaction.

checkoutId

string
70e08b7f-c528-46af-8b17-76b0e0ade641

The ID of the checkout.

walletAddress

string
0xdf8d13972902A6FCc5Ad61b3ec86737d6F16fb29

The buyer's wallet where the NFT was minted or transferred to.

walletType

string, one of
Preset
Paper Wallet
(for ERC-20 chains)
MetaMask
WalletConnect
Coinbase Wallet
(for Solana)
Phantom

The type of the buyer's wallet.

email

string, optional
[email protected]

The buyer's email, if provided. This may not be set if the user connected an external wallet and did not provide an email on the "Purchase confirmed" page.

quantity

number
1

The number of NFTs purchased.

networkFeeUsd

number, in USD
33.50

The amount paid for network fees by the user.

This value is not set for native mints.

totalPriceUsd

number, in USD
33.50

The total amount paid by the user.

paymentCompletedAt

string, in RFC3339
2022-02-04T02:32:45.260325+00:00

The time when the user completed their payment.

transferCompletedAt

string, in RFC3339
2022-02-04T02:32:45.260325+00:00

The time when the user received their purchased NFT.

paymentMethod

string, one of
NATIVE_MINT
BUY_WITH_CARD
BUY_WITH_CRYPTO

The buyer's payment method.

claimedTokens

object in the following format

tokenIds": ["0"],
  "transactionHashes": {
    "0x4c76e13a67e4ce1a03ae111f862f82d69383f6ac7aaf7f248644d9818d33cf19": [
      "0"
    ]
  }

The blockchain transaction hashes of the tokens claimed by this purchase, and the list of all tokens claimed

Event Types

Event

Description

payment:succeeded

The purchase is confirmed. Paper has received payment from the buyer.
This event is only sent for BUY_WITH_CARD and BUY_WITH_CRYPTO payment methods.

transfer:succeeded

The purchase is completed and the NFT(s) has been transferred to the buyer's wallet.

transfer:failed

The purchase did not complete after multiple attempts, and the NFT(s) was not transferred to the buyer's wallet.

The Paper team is alerted when this happens and will manually retry this job.

Verify the Signature Header

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

To verify the signature, create a SHA-256 HMAC hash with the API key as the secret and the body payload (as a JSON-encoded string) as the message.

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":...
  .digest('hex');

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

Troubleshooting

Here are some common problems if your webhook call is not successful.

  • 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 dashboard.

​## Retries

Each webhook will be attempted until a 2xx status code is returned. If a 2xx is not returned, the webhook will retry once every 5 minutes for one hour (13 attempts in total).