Webhook Notifications

How to use SphereOne webhooks for real-time payment status updates.

SphereOne uses webhooks to notify your application of payment status updates. By creating and configuring an endpoint for our notifications to be sent to, your application will be able to handle real-time updates regarding users' payments. This webhook URL can be configured on the SphereOne Merchant Dashboard settings page.

We'll send a JSON payload containing a paymentStatus and a transactionId (the same ID received after creating a charge) to your configured webhook URL when we begin processing the payment. The paymentStatus can be one of the following values: PROCESSING, SUCCESS, or FAILURE. In case of a payment failure, we'll also include an error property containing the reason for the failure.

Webhook Payload Examples

{
  "paymentStatus": "PROCESSING",  
  "transactionId": "0UzDE7FWzI1yGdMTQsIQ",  
  "error": null
}
{
  "paymentStatus": "SUCCESS",  
  "transactionId": "0UzDE7FWzI1yGdMTQsIQ",  
  "error": null
}
{
  "paymentStatus": "FAILURE",  
  "transactionId": "67wCM3w9eJb2wN9VS6db",  
  "error": "The minimum amount to transfer is $1"
}

Secure Webhooks

We will include an HMAC signature in the request header x-sphereone-signature, which will allow you to confirm that the request is indeed from us. To verify this, you will need to create a signature by matching the payload of the request sent by the webhook with your webhook secret. You can obtain your webhook secret from the SphereOne Merchant Dashboard in the same section where the webhook URL is set.

To verify the webhook signature, you can refer to the following example.

import crypto from 'crypto';

app.post('/webhook-endpoint', (req, res) => {
  const payload = req.body;
  const receivedSignature = req.headers['x-sphereone-signature'];

  const secret = 'your_secret_key'; // Replace with the webhook secret from the merchant dashboard
  const calculatedSignature = crypto.createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex');

  if (receivedSignature === calculatedSignature) {
    // The webhook is verified
  } else {
    // The webhook is not verified
  }
});

Webhook Endpoint Implementation Examples

Below, we provide two examples of how you can set up a webhook endpoint to receive payment status updates in real time. In each example, we use the paymentStatus received from the webhook payload to determine the appropriate action, and the transactionId to fetch and update the corresponding transaction record in our database. The first example uses a Firebase Cloud Function with a Firestore database, while the second example utilizes an Express server with Prisma ORM for a PostgreSQL database.

// FIREBASE CLOUD FUNCTION - FIRESTORE
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import cors from "cors";

const db = admin.firestore();

export const webhooks = functions.https.onRequest(async (req, res) => {
  cors({origin: true})(req, res, async () => {
    try {
      if (req.method !== "POST") throw new Error("Method not allowed");

      const {paymentStatus, transactionId, error} = req.body;
      if (!paymentStatus || !transactionId) res.status(400).json({msg: "Missing information"});

      if (paymentStatus === "SUCCESS") {
      // Get transaction details from database
        const transaction = (await db.doc(`transactions/${transactionId}`).get()).data();
        if (transaction === undefined) throw new Error("Transaction not found");
        const nftUid = transaction.nftUid;
        const userUid = transaction.userUid;

        // Update transaction status
        await db.doc(`transactions/${transactionId}`).set({
          status: "SUCCESS",
        }, {merge: true});

        // Assign nft to user
        await db.doc(`nfts/${nftUid}`).set({
          ownerUid: userUid,
        }, {merge: true});
      } else if (paymentStatus === "FAILURE") {
        // Handle error
        await db.doc(`transactions/${transactionId}`).set({
          status: "FAILURE",
          error: error ?? null,
        }, {merge: true});
      } else {
        // Just update status
        await db.doc(`transactions/${transactionId}`).set({
          status: paymentStatus,
          error: error?? null,
        }, {merge: true});
      }

      return res.status(200).send("OK");
    } catch (error) {
      return res.status(400).send(error?.message);
    }
  });
});

// EXPRESS - PRISMA POSTGRESQL
import express from "express";
import cors from "cors";
import { PrismaClient } from "@prisma/client";

const app = express();
const prisma = new PrismaClient();

app.use(cors());
app.use(express.json());

app.post("/webhooks", async (req, res) => {
  try {
    const { paymentStatus, transactionId, error } = req.body;
    if (!paymentStatus || !transactionId)
      res.status(400).json({ msg: "Missing information" });

    if (paymentStatus === "SUCCESS") {
      // Get transaction details from database
      const transaction = await prisma.transaction.findUnique({
        where: { id: transactionId },
      });

      if (!transaction) throw new Error("Transaction not found");

      const { nftUid, userUid } = transaction;

      // Update transaction status
      await prisma.transaction.update({
        where: { id: transactionId },
        data: { status: "SUCCESS" },
      });

      // Assign nft to user
      await prisma.nft.update({
        where: { id: nftUid },
        data: { ownerUid: userUid },
      });
    } else if (paymentStatus === "FAILURE") {
      // Handle error
      await prisma.transaction.update({
        where: { id: transactionId },
        data: { status: "FAILURE", error: error ?? null },
      });
    } else {
      // Just update status
      await prisma.transaction.update({
        where: { id: transactionId },
        data: { status: paymentStatus, error: error ?? null },
      });
    }

    return res.status(200).send("OK");
  } catch (error) {
    return res.status(400).send(error?.message);
  }
});