Merchant Integration Quick Start

Prerequisites

  • Basic programming skills
  • You can edit server side code of your web store or service
  • You have CypherpunkPay installed and running

Render checkout form

This is the form that creates a new charge in CypherpunkPay and redirects user to the CypherpunkPay payment flow.

Replace action with your CypherpunkPay instance and replace total and currency values with real data of the order.

<form method="post" action="https://cypherpunkpay.YOURWEBSITE.COM/cypherpunkpay/charge">
    <input name="merchant_order_id" value="RENDER_ORDER_ID_HERE" type="hidden">
    <input name="total" value="49.90" type="hidden">
    <input name="currency" value="usd" type="hidden">
    <button>Pay $49.90 with cryptocurrency</button>
</form>

This will simply render a button:

What about authentication and request integrity?

User can edit the form in the browser and create arbitrary charges in CypherpunkPay.

A popular way to solve this is to sign the whole request with a server-side secret. While best security practice, this proven to be pretty tedious to implement and an integration burden. We propose hopefully a more pragmatic alternative:

You MUST validate order values in the payment completed callback, as they essentially come from an untrusted user.

Configure a callback URL

Open /etc/cypherpunkpay.conf and set these values in the [merchant] chapter.

[merchant]

merchant_enabled = true

payment_completed_notification_url = https://YOURWEBSITE.COM/api/cypherpunkpay_payment_completed
back_to_merchant_url = https://YOURWEBSITE.COM/order/{merchant_order_id}

cypherpunkpay_to_merchant_auth_token = REPLACE_CYPHERPUNKPAY_TO_MERCHANT_AUTH_TOKEN

Obviously, adjust both URL-s.

Remember to restart CypherpunkPay: sudo systemctl restart cypherpunkpay

Handle payment completed callback

“Payment Completed” is the final, successful state assigned by CypherpunkPay. It means you can ship the order. It means cryptocurrency transaction received enough network confirmations to be assumed final and irreversible. Specific number of confirmations is implementation detail encapsulated by CypherpunkPay. It depends on cryptocurrency, total amount and technical transaction details.

In your webapp, add a new route accepting a POST request at /api/cypherpunkpay_payment_completed or whatever path you configured as your payment_completed_notification_url.

1. Authenticate CypherpunkPay request

You MUST verify the request actually comes from CypherpunkPay. Proceed only if the request has expected header:

Authorization: Bearer <cypherpunkpay_to_merchant_auth_token>

Example (obviously, adjust the value to your cypherpunkpay_to_merchant_auth_token):

Authorization: Bearer 9b2qtb5j8q8v2yyeww59mv7kkc72378m

2. Parse body as JSON

The request body will have the following structure:

{
  "untrusted": {
    "merchant_order_id": "whatever you rendered in the checkout form",
    "total": "49.90",
    "currency": "usd"
  },
  
  "status": "completed",    
  "cc_total": "0.000998",
  "cc_currency": "btc"
}
3. Verify untrusted order values

Read order from your database by merchant_order_id.

If the order was found, you MUST verify untrusted values from CypherpunkPay request against expected values from your database.

If there is any discrepancy, it means user tampered with value. It could also mean you parse or compare values incorrectly. Mind amounts are represented as strings to avoid parsing as floats and losing precision.

In either case, return 200 OK. If you return an error (4** or 5**), CypherpunkPay will attempt to call you in perpetuity, to make sure your received the callback.

4. Ship!

Mark your order as paid and initiate shipping.

Python Example

Simplified Python pseudo code:

    # POST /api/cypherpunkpay_payment_completed
    def cypherpunkpay_payment_completed(self):
        log.info(f'Received notification from CypherpunkPay: {self.request.body}')

        # Authenticate CypherpunkPay
        authorization_header = self.request.headers.get('Authorization')
        if not authorization_header == 'Bearer nsrzukv53xjhmw4w5ituyk5cre':  # do not store secrets in the source code
            return HTTPForbidden()  # 403

        # Parse body as JSON
        import json
        try:
            cypherpunkpay = json.loads(self.request.body)
        except json.decoder.JSONDecodeError as e:
            log.error(e)
            return HTTPBadRequest()

        untrusted = cypherpunkpay.get('untrusted', {})

        # Read order from database
        untrusted_order_id = untrusted.get('merchant_order_id')
        order = App().db().get_order_by_uid(untrusted_order_id)

        # Validate order data were not tampered with
        # Return 200 OK regardless to acknowledge callback reception so it won't get repeated
        if order is None:
            log.warning(f'Invalid merchant_order_id={untrusted_order_id}')
            return HTTPOk()
        if Decimal(untrusted.get('total', 0)) != order.total:
            log.warning(f'Invalid order total={untrusted.get("total")}')
            return HTTPOk()
        if untrusted.get('currency', '').casefold() != order.currency.casefold():
            log.warning(f'Invalid order currency={untrusted.get("currency")}')
            return HTTPOk()

        # Mark order as paid and initiate shipping
        order.payment_completed(cypherpunkpay['cc_total'], cypherpunkpay['cc_currency'])
        order.ship()
        self.db().save(order)

        return HTTPOk()

Congratulations!

What we have achieved here:

  • Minimal production ready integration of your store with CypherpunkPay.

  • You do not call CypherpunkPay API. User browser creates a charge in CypherpunkPay by submitting a form your backend rendered.

  • CypherpunkPay does call your API to notify about payment completion. In the handler your verify order was not tampered with and initiate the shipping.