Skip to content

Pay-per-access

Create a payment, redirect the customer to pay, and unlock content only after the webhook confirms payment on-chain. Works for file downloads, gated articles, or API trial keys.


Flow

Customer     →  Clicks "Unlock" on your site
Your server  →  POST /payments/requests (stores payment_id -> content mapping)
Customer     →  Redirected to DropPay pay page
Customer     →  Pays with XRP wallet
DropPay      →  Redirects customer to your redirect_url
DropPay      →  Sends payment.confirmed webhook
Your server  →  Marks content as unlocked for this payment
Customer     →  Accesses content

Step 1 - Create the payment

Generate a short-lived access token and store it with the payment so you can identify what to unlock in the webhook.

unlock.py
import uuid
import requests
from flask import Flask, redirect, session

app = Flask(__name__)
BASE = "https://droppayxrp.com"
API_KEY = "dp_live_your_api_key_here"

CONTENT = {
    "report_q3": {
        "title": "Q3 Financial Report (PDF)",
        "price_xrp": 5.0,
        "file_path": "/downloads/q3-report.pdf"
    }
}

@app.route("/unlock/<content_id>", methods=["POST"])
def unlock(content_id):
    item = CONTENT[content_id]
    access_token = str(uuid.uuid4())

    response = requests.post(
        f"{BASE}/payments/requests",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "amount": item["price_xrp"],
            "currency": "XRP",
            "description": item["title"],
            "order_id": f"{content_id}:{access_token}",
            "redirect_url": f"https://yoursite.com/access/{access_token}"
        }
    )

    payment = response.json()
    return redirect(BASE + payment["pay_url"])
unlock.js
import { randomUUID } from "crypto";

const BASE = "https://droppayxrp.com";
const API_KEY = "dp_live_your_api_key_here";

const CONTENT = {
  report_q3: {
    title: "Q3 Financial Report (PDF)",
    priceXrp: 5.0,
    filePath: "/downloads/q3-report.pdf"
  }
};

router.post("/unlock/:contentId", async (req, res) => {
  const { contentId } = req.params;
  const item = CONTENT[contentId];
  const accessToken = randomUUID();

  const response = await fetch(`${BASE}/payments/requests`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      amount: item.priceXrp,
      currency: "XRP",
      description: item.title,
      order_id: `${contentId}:${accessToken}`,
      redirect_url: `https://yoursite.com/access/${accessToken}`
    })
  });

  const payment = await response.json();
  res.redirect(BASE + payment.pay_url);
});

Step 2 - Unlock in the webhook

Parse order_id to extract the content ID and access token. Mark the token as paid in your database before the customer returns.

webhook.py
import hmac, hashlib, time
from flask import request, abort

WEBHOOK_SECRET = "your_webhook_secret"

@app.route("/webhook/droppay", methods=["POST"])
def droppay_webhook():
    timestamp = request.headers.get("X-DropPay-Timestamp", "")
    signature = request.headers.get("X-DropPay-Signature", "")
    body = request.get_data()

    if abs(int(time.time()) - int(timestamp)) > 300:
        abort(400)

    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        timestamp.encode() + b"." + body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(400)

    event = request.json
    if event["event"] == "payment.confirmed":
        order_id = event["data"]["order_id"]
        content_id, access_token = order_id.split(":", 1)
        db.mark_access_token_paid(access_token, content_id)

    return "", 200
webhook.js
import crypto from "crypto";

router.post("/webhook/droppay",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const timestamp = req.headers["x-droppay-timestamp"];
    const signature = req.headers["x-droppay-signature"];

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(400).end();
    }

    const expected = "sha256=" + crypto
      .createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(timestamp + "." + req.body)
      .digest("hex");

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
      return res.status(400).end();
    }

    const event = JSON.parse(req.body);
    if (event.event === "payment.confirmed") {
      const [contentId, accessToken] = event.data.order_id.split(":", 2);
      await db.markAccessTokenPaid(accessToken, contentId);
    }

    res.status(200).end();
  }
);

Step 3 - Serve the content

The redirect_url includes the access_token in the path. Serve the file only if the token is marked as paid.

access.py
from flask import send_file, abort

@app.route("/access/<access_token>")
def serve_content(access_token):
    record = db.get_access_token(access_token)
    if not record or not record["paid"]:
        abort(403)

    item = CONTENT[record["content_id"]]
    return send_file(item["file_path"], as_attachment=True)
access.js
router.get("/access/:accessToken", async (req, res) => {
  const record = await db.getAccessToken(req.params.accessToken);
  if (!record?.paid) {
    return res.status(403).send("Payment required");
  }

  const item = CONTENT[record.contentId];
  res.download(item.filePath);
});

Notes

  • Store access tokens in your database with a paid boolean column and an expiry (e.g. 24 hours after payment)
  • Webhooks can arrive before the customer returns to your site - the access check handles both orders gracefully
  • DropPay payment links expire 30 minutes after the customer opens the pay page; set expires_in_hours if you need a shorter window

Next steps