← SoloPayLink

Developers

Connect a SoloPayLink business to its own website or system. Bookings and payments always happen on SoloPayLink; your website receives a signed message every time something changes. Website connections are being switched on for businesses step by step.

Book now button

The owner copies this from Your website in the SoloPayLink app (with their own booking link name):

<a href="https://solopay.link/your-link" data-solopaylink="your-link">Book now</a>
<script src="https://solopaylink.com/embed/v1.js" async></script>

Booking webhooks

The business owner opens Your website in the SoloPayLink app, enters your receiving address (https:// only) and receives a signing secret that starts with whsec_. From then on SoloPayLink sends an HTTPS POST with a JSON body to that address for each booking change. Reply with any 2xx status within 10 seconds; do slow work after replying.

Webhooks follow the open Standard Webhooks specification, so its ready-made libraries work unchanged.

HeaderMeaning
webhook-idEvent id (evt_…). The same on every retry: store it and ignore repeats.
webhook-timestampUnix seconds when this attempt was signed. Reject anything more than 5 minutes away from your clock.
webhook-signaturev1,<base64> — HMAC-SHA256 of id.timestamp.body. During a secret change two signatures are sent, separated by a space; accept either.
solopaylink-eventThe event type, for logging.

Event types

TypeWhen
booking.confirmedA booking is confirmed (paid, deposit paid, or pay later).
booking.rescheduledThe time changed. data.previous holds the old local date and time.
booking.cancelledThe booking was cancelled.
booking.completedThe appointment took place.
payment.deposit_paid, payment.paidA deposit or the full amount was paid.
payment.refund_pending, payment.partially_refunded, payment.refundedRefund progress.
payment.disputedThe customer opened a card dispute.
test.pingThe owner pressed Send test. The body has "test": true and a fake booking — do not store it.

Events can arrive out of order (for example after retries). Use created_at to keep the newest state, and treat SoloPayLink as the owner of the customer's emails and calendar event: your system should not send its own booking confirmation or create a second calendar entry.

Payload

{
  "id": "evt_3f1c…",
  "type": "booking.confirmed",
  "api_version": "2026-11-01",
  "created_at": "2028-01-01T02:15:00.000Z",
  "livemode": true,
  "business": { "slug": "harbour-learner-driving" },
  "data": {
    "booking": {
      "id": "bk_9a0c51d2e4f6a8b0c1d2e3f4",
      "reference": "E5F6G7H8",
      "status": "CONFIRMED",
      "payment_status": "DEPOSIT_PAID",
      "service": { "id": "svc_60", "name": "Driving lesson 60 min", "location_type": "at_customer" },
      "start": "2028-01-03T23:00:00.000Z",
      "end": "2028-01-04T00:00:00.000Z",
      "timezone": "Australia/Brisbane",
      "local_date": "2028-01-04",
      "local_time": "09:00",
      "duration_minutes": 60,
      "customer": { "name": "Sam Learner", "email": "sam@example.com", "phone": null, "address": null, "note": null },
      "answers": [],
      "amounts": { "currency": "AUD", "total_cents": 7500, "paid_cents": 750, "refunded_cents": 0, "balance_due_cents": 6750 },
      "payment": { "type": "deposit" },
      "created_at": "2028-01-01T02:14:58.000Z"
    }
  }
}

Money is always in the smallest currency unit (cents). status is one of AWAITING_PAYMENT, CONFIRMED, COMPLETED, CANCELLED; payment_status one of UNPAID, DEPOSIT_PAID, PAID, REFUND_PENDING, PARTIALLY_REFUNDED, REFUNDED, DISPUTED. By default only the customer's name and email are included; the owner can choose to add phone, address, note and answers to their booking questions.

Verify the signature

Always verify before trusting a message. Use the raw request body exactly as received (before any JSON parsing).

Node.js

import crypto from "node:crypto";

export function verifySoloPayLink(rawBody, headers, secret) {
  const id = headers["webhook-id"], ts = Number(headers["webhook-timestamp"]);
  if (!id || !Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
  return String(headers["webhook-signature"] || "").split(" ").some((part) => {
    const [v, sig] = part.split(",");
    const given = Buffer.from(sig || "", "base64");
    return v === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

PHP

function verify_solopaylink(string $raw, array $h, string $secret): bool {
  $id = $h['webhook-id'] ?? ''; $ts = (int)($h['webhook-timestamp'] ?? 0);
  if ($id === '' || abs(time() - $ts) > 300) return false;
  $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
  $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$raw", $key, true));
  foreach (explode(' ', $h['webhook-signature'] ?? '') as $part) {
    [$v, $sig] = array_pad(explode(',', $part, 2), 2, '');
    if ($v === 'v1' && hash_equals($expected, $sig)) return true;
  }
  return false;
}

Python

import base64, hashlib, hmac, time

def verify_solopaylink(raw: bytes, headers: dict, secret: str) -> bool:
    msg_id, ts = headers.get("webhook-id", ""), int(headers.get("webhook-timestamp", "0"))
    if not msg_id or abs(time.time() - ts) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + raw, hashlib.sha256).digest()).decode()
    return any(p.split(",", 1)[0] == "v1" and hmac.compare_digest(p.split(",", 1)[1], expected)
               for p in headers.get("webhook-signature", "").split() if "," in p)

Retries and pausing

AI agent booking API

AI assistants can find a SoloPayLink business, read its services and open times, and start a booking for a person through GET https://solopaylink.com/agent/v1 (REST) or POST https://solopaylink.com/agent/mcp (Model Context Protocol). The customer confirms with a code sent to their email and pays on the secure Stripe page. Start at /agent/v1 for the list of endpoints.

Questions? Contact support.