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.
Event | Description |
---|---|
transfer:succeeded | The NFT has been delivered to the buyer's wallet. |
transfer:failed | The NFT was unable to be delivered after multiple retries. Paper's engineering team is notified to resolve or refund this transaction. |
payment:succeeded | A buyer's payment has been successfully completed. |
payment:failed | A buyer's payment attempt has been rejected. Extra data fields may be available with information from our payment processor on the failure reason. |
payment:refunded | A 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_created | 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. |
Flow
This diagram explains when each event is sent.

A normal checkout purchase.

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,
"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": { ... },
}
}
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:

- As a Slack admin, select the Add to Slack button in Incoming WebHooks.
- Select the channel you'd like to post your message to.
- select Add Incoming Webhooks.
- Copy the webhook URL that starts with
https://hooks.slack.com/services/...
. - Add this new webhook URL on Developer Dashboard: Developers.
Discord
Get notified on Discord when a buyer completes a purchase:

- As a Discord admin, select Server Settings.
- Select Integrations on the left menu.
- Select Webhooks.
- Select New Webhook.
- Select Copy Webhook URL.
- 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.