Webhooks

Configure webhook URLs on Developer Dashboard: Developers to have Paper notify your backend when payment and mint events happen.

Use cases

  • Update your database when a buyer purchases an NFT.
  • Send an email to a buyer after their purchase succeeds.
  • Inform your team in Slack/Discord when a payment or purchase failed.

Events

The following webhook events are supported.

Please return a 2xx response for unexpected or unused event types to prevent unnecessary retries.

EventDescription
transfer:succeededThe NFT has been delivered to the buyer's wallet.
transfer:failedThe NFT was unable to be delivered after multiple retries.

Paper's engineering team is notified to resolve or refund this transaction.
payment:succeededA buyer's payment has been successfully completed.
payment:failedA buyer's payment attempt has been rejected.

Extra data fields may be available with information from our payment processor on the failure reason.
payment:refundedA buyer's payment has been refunded because the mint failed multiple attempts.

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 have not been charged yet.

You can capture this hold to complete their purchase, or cancel it.

Flow

This diagram explains when each event is sent.

A normal checkout purchase.

A normal checkout purchase.

Using "Pre-pay" to charge a card later.

Using "Pre-pay" to charge a card later.

Request format

Paper will call your backend with an HTTPS POST request:

Headers

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

Request 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,
    "serviceFeeUsd": 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": { ... }, 
  }
}
{
  "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,
    "serviceFeeUsd": 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": {
      "tokens": [
        {
          "transferHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
          "transferExplorerUrl": "https://polygonscan.com/tx/0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
          "tokenId": "262",
          "quantity": 1,
          "from": "0xce6913CA121276E550b82844A08aCB4dfDc09178",
          "collectionAddress": "0x965550329b91b7c703a527347b613E175f38872d",
          "collectionTitle": "My First NFT"
        }
      ]
    },
    "title": "My First Paper Checkout",
    "transactionHash": "0x076d1b496152efd2a97d0db1d558c681188a1a76a8a2c271a33e4c34cc1fa467",
    "valueInCurrency": "0.05",
    "currency": "ETH",
    "metadata": {
      "myAppUserId": "23a9fj2930gya0"
    },
    "mintMethod": { ... },
    "eligibilityMethod": { ... },
    "contractArgs": { ... }, 
  }
}

Integration

Provide a webhook handler URL

Webhooks are configured separately for testnet and production checkout onDeveloper Dashboard: Developers. Webhook URLs must be publicly accessible https endpoints.

Do not provide a localhost URL to test your local server. We recommend testing your development server with a service like ngrok to get a temporary public URL.

Verify the signature header

To ensure the request came from Paper, each webhook request signs the payload and provides this signature in the X-Paper-Signature header.

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

Example implementation

Here's a simplified HTTP handler in Next.js:

import { createHmac, timingSafeEqual } from 'crypto';

const paperWebhookHandler = (req, res) => {
  const apiKey = '2483b84a-...'; // Your Paper API key

  // Get the provided signature.
  const signature = req.headers['x-paper-signature'];
  // Compute the expected signature.
  const hash = createHmac('sha256', apiKey)
    .update(JSON.stringify(req.body)) // {"event":"transfer:succeeded","result":{"id":...
    .digest('hex');
  // Confirm the provided signature matches.
  if (!timingSafeEqual(Buffer.from(signature), Buffer.from(hash))) {
    return res.status(400).send('Signature mismatch!');
  }

  switch (req.body.event) {
    case 'transfer:succeeded':
      // Handle when an NFT was delivered.
    case 'transfer:failed':
      // Handle when an NFT could not be delivered.
    default:
      // Ignore all other events and return 2xx.
  }

  return res.status(200).send('OK');
}

Test the webhook response

Use the Test webhook button to send a dummy payload to your webhook URL and see response status/body.

View recent webhook events

Select the List events button to view the recent webhook events, including the request body and response status/body from your backend. This view is useful to debug misconfigured webhook handlers.

Common integrations

Slack

Get notified on Slack when a buyer completes a purchase:

  1. As a Slack admin, select the Add to Slack button in Incoming WebHooks.
  2. Select the channel you'd like to post your message to.
  3. select Add Incoming Webhooks.
  4. Copy the webhook URL that starts with https://hooks.slack.com/services/....
  5. Add this new webhook URL on Developer Dashboard: Developers.

Discord

Get notified on Discord when a buyer completes a purchase:

  1. As a Discord admin, select Server Settings.
  2. Select Integrations on the left menu.
  3. Select Webhooks.
  4. Select New Webhook.
  5. Select Copy Webhook URL.
  6. Add this new webhook URL on Developer Dashboard: Developers.

You might notice /slack appended to your URL. This is expected since we call Discord's Slack-compatible Webhook.

FAQ

Why do I need to verify the signature header?

If your server is public, a bad actor can spoof a webhook request. Verifying the signature ensures the payload has not been changed. If a bad actor changes the webhook request body, the signature would not match the signed payload.

Why is my signature header mismatched?

Here are common reasons the signature header may be mismatched.

  • Check if the header is set lower-cased. Some server frameworks (e.g. Next.js) use lowercase request header names since they are case-insensitive (RFC 2616).
  • Make sure you're passing the entire body as the message in the HMAC signature. Some frameworks require you to configure the HTTP handler to not parse the request body (e.g. Next.js).
  • Make sure your API key is valid.

What IP address will webhook requests come from?

Webhooks will be sent from the IP address 44.225.232.73.

How often will webhook requests be retried?

Webhooks are retried every five minutes for up to one hour until a 2xx response is returned.

Can I filter which webhook events are sent?

Currently there is no way to filter which events are sent to your webhook URLs. Paper may add new webhook event types without notice. Please ignore events that you don't need by returning a 2xx response.