# SpankPay APIs

## Purchase

A purchase is a request for a payment. When created with the Button API or the JavaScript API, the user will be presented with a payment form prompting them to send cryptocurrency equivalent in value to the requested amount. Once the cryptocurrency has been received, the `callback` webhook will be called, and the Button/JavaScript `onPurchaseComplete` callback called.

| Attribute | Description |
| --------- | ----------- |

| `id` | The purchase's ID (assigned by SpankPay) |
| ---- | ---------------------------------------- |

| `timestamp` | The timestamp the purchase was created (ISO 8601 format, assigned by SpankPay) |
| ----------- | ------------------------------------------------------------------------------ |

| `apiKey` | The API key used to create this purchase. |
| -------- | ----------------------------------------- |

| `amount` | The amount of the purchase, in "currency". Must be positive and rounded to the appropriate number of decimal places for the purchase currency. |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |

| `currency` | The currency being requested. For valid values, see [Output Currencies](#output-currencies). |
| ---------- | -------------------------------------------------------------------------------------------- |

| `acceptedCurrencies` | <p>The currencies which should be accepted for this purchase. For valid values, see <a href="#input-currencies">Input Currencies</a>.</p><p>Must be a subset of the API Key's list of accepted currencies.</p><p>Optional.</p> |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

| `metadata` | Arbitrary metadata provided by the caller, stored and returned along with the purchase. We suggest including the order or invoice number, and an opaque customer ID. Limited to 128kb of JSON data. Optional. |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

### Input Currencies

The Input Currency for a Purchase is the currency used by the user to pay for the purchase.

Currently valid input currencies:

| Currency | Description |
| -------: | ----------- |
|    `BTC` | Bitcoin     |
|    `ETH` | Ethereum    |
|  `BOOTY` | BOOTY Coin  |
|   `DOGE` | Doge Coin   |
|    `LTC` | Litecoin    |

### Output Currencies

The Output Currency for a Purchase is the currency which will be displayed to the user, and the currency which will be credited to your account. For example, a US$10 Purchase will have `"amount": "10.00"` and `"currency": "USD"`. When paying, the user will be given the option to pay with (for example) 0.074 ETH or 0.0026 BTC.

Currently valid output currencies:

| Currency | Description |
| -------: | ----------- |
|    `USD` | US Dollars  |
|    `BTC` | Bitcoin     |
|    `ETH` | Ethereum    |

### Creating a Purchase with a Button

A SpankPay button is the simplest way to accept SpankPay on your site.

When the button is clicked, the user will be presented with the SpankPay purchase frame, and the `data-on-payment` callback will be called once the payment is complete and your callback has accepted the payment.

```markup
<script src="https://pay.spankchain.com/spankpay.js"></script>

<script>
function onSpankPayPayment(payment) {
  console.log(`Payment ${payment.status}`, payment)
}
</script>

<button
    data-spankpay-key="test_quickstart_key"
    data-amount="69.69"
    data-currency="USD"
    data-accept-currencies="BOOTY,ETH,BTC,DOGE"
    data-metadata="{&quot;orderId&quot;: &quot;sc696969&quot;}"
    data-callback="https://pay.spankchain.com/api/quickstart/callback"
    data-on-payment="onSpankPayPayment">
  Pay with SpankPay!
</button>
```

See also:

* [Purchase parameter](#purchase)
* [`payment` Event](#payment-event)
* [Webhook Callbacks](#webhook-callbacks)

### Creating a Purchase with the SpankPay JavaScript API

For complete control over the user's SpankPay experience, the API can be called directly.

The `SpankPay.showPurchase(...)` method can be used to show the SpankPay purchase frame.

```javascript
const { SpankPay } = require('spankpay')

const frame = SpankPay.showPurchase({
  key: 'test_quickstart_key',
  amount: '69.69',
  currency: 'USD',
  acceptCurrencies: ['BOOTY', 'ETH', 'BTC', 'DOGE'],
  metadata: {
    orderId: 'sc696969',
  },
  callback: 'https://pay.spankchain.com/api/quickstart/callback',
})

frame.on('payment', payment => {
  console.log(`Payment ${payment.status}`, payment)
})

frame.on('stateChange', state => {
  console.log('Frame state:', state)
})

frame.on('close', () => {
  console.log('Frame was closed!')
})
```

See also:

* [Purchase parameters](#purchase)
* [`payment` Event](#payment-event)
* [Webhook Callbacks](#webhook-callbacks)

### Purchase Frame Events

Events can be handled either by passing a callback to `SpankPay.showPurchase(...)`, or binding to the event on the returned frame object:

```javascript
const frame = SpankPay.showPurchase({
  onStateChange: state => { ... },
})
frame.on('payment', payment => { ... })
```

|         Event | Description                                                                                                                      |
| ------------: | -------------------------------------------------------------------------------------------------------------------------------- |
|       `close` | Triggered when the frame is closed.                                                                                              |
| `stateChange` | Triggered when the frame's state changes. See also: [Frame State](#frame-state)                                                  |
|     `payment` | Triggered when a payment is received, after the `callback` URL has accepted the payment. See: [`payment` Event](#payment-event). |

### Frame State

The purchase frame state, exposed through `frame.state` and the `stateChange` event, has the following fields:

| Attribute | Description                                                                                                                                |
| --------: | ------------------------------------------------------------------------------------------------------------------------------------------ |
|  `isOpen` | `true` or `false`, depending on whether or not the frame is showing.                                                                       |
|  `status` | The `status` will be one of: `"awaiting-payment"`, `"confirming-payment"`, or `"complete"`.                                                |
| `payment` | When the `status` is `"complete"` the `payment` field will contain a Payment object with details of the payment. See: [Payment](#payment). |

### `payment` Event

The `payment` event will be triggered when a payment has been received and the `callback` url has accepted the payment.

The `payment` argument will be a [Payment object](#payment), and the `status` should be checked to ensure the payment has succeeded. Note, however, that the payment will only fail if the `callback` rejects the payment (see: [Webhook Expected Response](#expected-response)).

For example:

```javascript
function onPayment(payment) {
  console.log(`Payment ${payment.status}:`, payment)
  if (payment.status == "succeeded") {
    window.location.href = '/order-complete'
  } else {
    window.location.href = '/order-failed'
  }
}
```

## Payment

A payment is created when SpankPay receives a user's payment in response to a Purchase.

| Attribute | Description |
| --------- | ----------- |

| `id` | The payment's ID, assigned by SpankPay. |
| ---- | --------------------------------------- |

| `timestamp` | The timestamp when the payment was first received. ISO 8601 format. |
| ----------- | ------------------------------------------------------------------- |

| `status` | <p>One of <code>"pending"</code>, <code>"failed"</code>, or <code>"succeeded"</code>.</p><p><code>"pending"</code> if the payment is still being verified (either waiting for an onchain transaction, or waiting for result of the callback).</p><p><code>"failed"</code> if the webhook callback returns a failure, or if the user navigates away from the payment page before completing the payment.</p><p> <code>"succeeded"</code> if the payment has been confirmed onchain and the callback has returned success.</p> |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| `purchaseId` | The ID of the corresponding Purchase. |
| ------------ | ------------------------------------- |

| `purchase` | The corresponding Purchase object (see above) |
| ---------- | --------------------------------------------- |

| `amount` | The Purchase amount |
| -------- | ------------------- |

| `currency` | The Purchase's currency |
| ---------- | ----------------------- |

| `inputAmount` | The amount of the input currency that was paid (ex, "0.6969"). |
| ------------- | -------------------------------------------------------------- |

| `inputCurrency` | The input currency selected by the user (ex, "ETH"). |
| --------------- | ---------------------------------------------------- |

| `inputTx` | An object describing the input transaction |
| --------- | ------------------------------------------ |

| `inputTx.hash` | The transaction hash. |
| -------------- | --------------------- |

| `receipt` | The result of the webhook callback. |
| --------- | ----------------------------------- |

| `receipt.type` | Always `"webhook"` |
| -------------- | ------------------ |

| `receipt.url` | The URL which was called |
| ------------- | ------------------------ |

| `receipt.status` | One of `"pending"`, `"failed"`, or `"succeeded"`. |
| ---------------- | ------------------------------------------------- |

| `receipt.calledOn` | The timestamp of the last call. ISO 8601 format. |
| ------------------ | ------------------------------------------------ |

| `receipt.responseStatus` | <p>The HTTP status code of the last request.</p><p>The request will be considered successful if the status code is <code>2XX</code>, permanently failed if it is <code>4XX</code>, and otherwise the callback will be retried.</p><p>A <code>responseStatus</code> of <code>999</code> indicates a network or other non-HTTP error.</p> |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| `receipt.response` | <p>The content of the HTTP response.</p><p>A JSON object if the response has <code>Content-Type: application/json</code> , and a string otherwise.</p><p>Truncated to 128kb.</p> |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

The `callback` URL is provided when the [Purchase](#purchase) is created, and we recommend including some metadata in the URL which your application can use to credit the appropriate order.

For example, if you assign each order an ID, the `callback` URL might be `https://your-site.com/api/spankpay/callback?order-id=sc696969`.

### Webhook Format

Webhook messages will take the following format:

```http
POST /api/quickstart/callback
Content-Type: text/plain
X-SpankPay-Key: test_quickstart_key
X-SpankPay-Signature: t=1551389518&s=b613679a0814d9ec…

{
    "type": "payment",
    "payment_id": "pay_c493715653c",
    "timestamp": "1969-06-09T06:09:06.969Z",
    "purchaseId": "pur_f95d778c35f",
    "purchase": { ... },
    "amount": "69.69",
    "amountCurrency": "USD",
    "inputAmount": "0.6969",
    "inputCurrency": "ETH",
    "inputTx": "0x2144292c5ad…",
    "receipt": {
        "type": "webhook",
        "url": "https://yoursite.com/yourserver/callback",
        "status": "called",
        "calledOn": "1969-06-09T06:09:06.969Z",
        "responseStatusCode": 200,
        "response": ...,
        ...
    }
    ...
}
```

The `type` field will always be `"payment"`, although there may be other types in the future. The rest of the body will be a [Payment object](#payment).

Note: the `Content-Type` will be `text/plain` instead of `application/json` as might be expected. This is to ensure that web frameworks like Express do not attempt to parse the request body as JSON, and instead make the raw string available to the request handler so it can more easy check the signature.

### Expected Response

The webhook endpoint must return an HTTP 200 response with a JSON object containing `{ "received": true }`. Other metadata may optionally be included in the JSON object, and it will be returned verbatim in the Payment's `receipt.response` field.

A payment will be considered failed if the response contains either `{ "received": false }`, or an HTTP status code of `4XX`. On a failed payment, the user will be refunded their cryptocurrency (less standard transaction fees) and the payment will be marked `"failed"`.

If the webhook endpoint returns a non-200 response, or a body that does not contain `{ "received": true }`, the webhook will be retried according to the following rules:

* 10 times, each 30 seconds apart (ie, every 30 seconds for 5 minutes)
* 10 times, each 5 minutes apart (ie, every 5 minutes for 50 minutes)
* 10 times, each 60 minutes apart (ie, every 1 hour for 10 hours)

If all retries fail, the API key administrator will be notified. A webhook can be manually retried at any point through the administrative UI.

### Testing Webhooks

The Webhook Test Page (not yet available) can be used to send simulated webhooks.

(this will be a page with inputs for URL, public key, secret key, various payment fields, and a button which - when clicked - will trigger a webhook call to the provided URL).

Note: the webhook test page can only be used with testing keys (ie, keys prefixed with "test\_"), and all currencies will be "TEST-" currencies.

Additionally, we recommend that developers use [ngrok](https://ngrok.com/) to create a public URL which can route to their local development server. During development, your application can be configured to automatically query ngrok for the developer's current public URL:

```javascript
async function getPublicUrl() {
  if (config.PUBLIC_URL)
    return config.PUBLIC_URL

  if (!config.ENVIRONMENT != 'development')
    throw new Error('config.PUBLIC_URL has not been defined!')

  try {
    const res = await fetch('http://localhost:4040/api/tunnels')
  } catch (e) {
    throw new Error(
      'Error connecting to ngrok to fetch public URL ' +
      '(hint: did you run "ngrok"?). Original error: ' + e
    )
  }

  const obj = await res.json()
  for (const tun of obj.tunnels) {
    return tun.public_url
  }

  throw new Error(
    'Unexpected response from ngrok (tunnels found): ' +
    JSON.stringify(obj)
  )
}
```

### Security

To verify that webhooks are authentically from SpankPay, the content can be verified using the `X-SpankPay-Signature` header.

We *strongly* recommend validating webhook signatures, otherwise it could be possible for an attacker to create fake payment confirmations.

#### Validating Webhook Signatures

{% tabs %}
{% tab title="Node + Express" %}

```javascript
const { SpankPay } = require('spankpay')

app.post('/spankpay/callback', (req, res) => {
    const key = req.headers['x-spankpay-key']
    if (key != process.env.SPANKPAY_API_KEY) {
        console.error(
            `Unexpected SpankPay API key: ` +
            `${key} != ${process.env.SPANKPAY_API_KEY}`
        )
        return res.status(400)
    }

    const [data, timestamp, err] = SpankPay.decodeWebhook(
        req.body,
        req.headers['x-spankpay-signature'],
        process.env.SPANKPAY_API_SECRET,
    )
    if (err) {
        console.error('Error decoding SpankPay webhook:', err)
        return res.status(400)
    }

    const age = (Date.now() / 1000) - timestamp
    if (age > 60 * 10) {
        console.error(`SpankPay webhook too old (was created ${age}s ago)`)
        return res.status(400)
    }

    // ... handle webhook ...

    return res.json({ received: true })
})
```

{% endtab %}

{% tab title="JavaScript (manually)" %}

```javascript
const crypto = require('crypto')

/**
 * Decodes a SpankPay webhook, returning a triple of:
 *   [data, timestamp, error]
 *
 * Where `data` is the webhook object, and `timestamp` is the
 * call's timestamp (integer seconds since epoch, UTC).
 *
 * If an error is encountered (for example, because the
 * signature is invalid), `error` will be a non-null
 * string describing the error.
 *
 * For example:
 *   const [data, timestamp, error] = decodeSpankPayWebhook(
 *     process.env.SPANKPAY_API_SECRET,
 *     req.headers['x-spankpay-signature'],
 *     req.body,
 *   )
 */
function decodeSpankPayWebhook(data, sig, secret) {
    const sigData = {}
    sig.split('&').forEach(bit => {
        const [key, val] = bit.split('=')
        sigData[key] = val
    })

    const timestamp = parseInt(sigData.t)
    if (!isFinite(timestamp))
        return [null, null, `Invalid or missing timestamp: ${sig}`]

    const hash = crypto.createHmac('sha256', secret)
    hash.update(`${timestamp}.${data}`)
    const actualSig = hash.digest('hex')
    if (sigData.s !== actualSig)
        return [null, null, `Invalid signature. ${sigData.s} != ${actualSig}`]

    let dataObj
    try {
        dataObj = JSON.parse(data)
    } catch (e) {
        return [null, null, `Error decoding JSON: ${'' + e}`]
    }

    return [dataObj, timestamp, null]
}


const secret = 'sk_spankpay'
const sig = 't=1552944138459&s=9157a7bf673ea425674a7cba92122293a127cc80c93da754b1113765dbd5aebe'
const data = '{"SpankPay": "BOOTY"}'
console.log(decodeSpankPayWebhook(secret, sig, data))
// [ { SpankPay: 'BOOTY' }, 1552944138459, null ]
```

{% endtab %}

{% tab title="Python (manually)" %}

```python
from __future__ import print_function

import hmac
import time
import json
import hashlib

def decode_spankpay_webhook(secret, sig, data):
    """ Decodes a SpankPay webhook, returning a triple of: `(data, timestamp,
        error)`

        Where `data` is the webhook object, and `timestamp` is the call's
        timestamp (integer seconds since epoch, UTC).

        If an error is encountered (for example, because the signature is
        invalid), `error` will be a non-null string describing the error.

        For example::

            (data, timestamp, error) = decodeSpankPayWebhook(
                request.data,
                request.headers['x-spankpay-signature'],
                app.config.SPANKPAY_API_SECRET,
            )
    """
    sig_data = dict(bit.split("=") for bit in sig.split("&"))

    try:
        timestamp = int(sig_data.get("t"))
    except (ValueError, TypeError):
        return (None, None, "Invalid or missing timestamp: %r" %(sig, ))

    to_sign="%s.%s" %(timestamp, data)
    actual_sig = hmac.new(secret, to_sign, hashlib.sha256).hexdigest()
    if sig_data.get("s") != actual_sig:
        return (None, None, "Invalid signature. %r != %r" %(sig_data["s"], actual_sig))

    try:
        data_obj = json.loads(data)
    except ValueError as e:
        return (None, None, "Error decoding JSON: %s" %(e, ))

    return (data_obj, timestamp, None)

secret = 'sk_spankpay'
sig = 't=1552944138459&s=9157a7bf673ea425674a7cba92122293a127cc80c93da754b1113765dbd5aebe'
data = '{"SpankPay": "BOOTY"}'
print(decode_spankpay_webhook(secret, sig, data))
# ({u'SpankPay': u'BOOTY'}, 1552944138459, None)
```

{% endtab %}
{% endtabs %}

#### Preventing Replay Attacks

To ensure your application only processes each webhook once, we recommend using the signature as a nonce. For example:

```javascript
app.post('/spankpay/callback', async (req, res) => {
    const sig = req.headers['x-spankpay-signature']
    // ... validate signature ...

    try {
        const firstUse = await redis.set(`spankpay-webhook:${sig}`, '1', {
            // The nx - Not Exists - flag ensures the key can only be set once
            nx: true,
            // The ex - EXpire - flag ensures the key will expire after an hour
            ex: 60 * 60,
        })
        if (!firstUse)
            return res.json({ received: true })

        // ... handle webhook ...
    } catch (e) {
        // If there is an error, clear the flag so that the webhook
        // will be processed on a subsequent request.
        // NOTE: your application must be careful not to leave the
        //       webhook in a partially processed state, otherwise
        //       there may be inconsistencies when it is retried.
        await redis.del(`spankpay-webhook:${sig}`)
        throw e
    }

    return res.json({ received: true })
})
```
