Skip to main content

Webhooks

Configure webhook URLs to have thirdweb notify your backend when successful or failed events occur.

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.

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_createdThis is only emitted if capturePaymentLater is set. A 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.

Request format

Paper will call your backend with an HTTPS POST request:

Headers

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

Request body

{
"event": "transfer:succeeded",
"result": {
"id": "5bbbada7-e864-4dac-ae4b-0ee4967f55d8",
"checkoutId": "70e08b7f-c528-46af-8b17-76b0e0ade641",
"walletAddress": "0x2086Fcd5b0B8F4aFAc376873E861DE00c67D7B83",
"walletType": "Preset",
"email": "buyer@example.com",
"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": "buyer@example.com",
"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": { ... },
}
}

Usage

Provide a webhook handler URL

Webhooks are configured separately for testnet and production checkout on the dashboard. 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 serve a temporary public URL.

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

Verify the signature header

To ensure the request came from thirdweb, 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 Secret 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 thirdweb API Secret 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.

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.