Checkout API

Embed Soldi Now’s hosted checkout in your website. When a buyer clicks “Checkout,” you redirect them to a signed Soldi Now payment URL; the buyer pays by ACH, card, or passkey; Soldi Now redirects them back and posts a webhook to your endpoint. No line items, no SDKs — you sign a URL, redirect, and listen for a webhook.

Flow

  1. Buyer clicks “Checkout” on your site
  2. Your backend signs a /external/pay URL with your merchant signing key
  3. You redirect the buyer to that URL
  4. Soldi Now renders the hosted payment page; buyer pays (ACH, card, or passkey)
  5. Soldi Now redirects the buyer to your successUrl or failureUrl
  6. Soldi Now POSTs a webhook to your registered endpoint with the result

The webhook is the source of truth — never grant value based on the redirect alone.

Credentials

From your merchant dashboard: Settings → Developer Keys. You receive a Merchant ID (identifies you on every signed URL), a Merchant Signing Key (HMAC-SHA256 secret you sign checkout URLs with), and a Soldi Now Signing Key (used to verify webhooks). Never expose signing keys in client-side code.

The checkout URL

GET https://web.soldinow.com/external/pay?<signed query parameters>

Staging: https://web-staging.soldinow.com/external/pay. Signed URLs are valid for 5 minutes; generate fresh on every checkout click.

ParameterRequiredDescription
merchantIdYesYour merchant ID
amountYesDollars, two decimals (e.g. 49.99)
externalResourceIdYesYour order/cart ID — used for duplicate-payment detection, returned in the webhook
successUrl / failureUrlYesRedirect targets after payment success / cancel-failure
timestampYesUnix seconds at signing time
nonceYesUnique random string per request (16-byte hex recommended)
signatureYesBase64 HMAC-SHA256 of the canonical parameter string
externalUserIdNoYour stable buyer ID; returned in the webhook. Omit for guest checkout.
externalResourceDescriptionNoShown on the payment page (e.g. “Order #1234”)
taxRateNoTax rate percent as decimal string (e.g. 8.25)
metadataNoURL-encoded JSON, round-tripped to your webhook

Signing

  1. Generate timestamp (Unix seconds) and nonce (16-byte random hex)
  2. Sort all parameters (excluding signature) alphabetically by key
  3. Join as key=value&key=value using raw values (not URL-encoded)
  4. Compute HMAC-SHA256 with your merchant signing key; Base64-encode → signature
  5. Append signature, URL-encode the final redirect URL
// Node.js
const crypto = require('crypto');

function signCheckoutURL(merchantId, signingKey, baseURL, params) {
  params.merchantId = merchantId;
  params.timestamp = Math.floor(Date.now() / 1000).toString();
  params.nonce = crypto.randomBytes(16).toString('hex');

  const canonical = Object.keys(params)
    .filter(k => k !== 'signature')
    .sort()
    .map(k => k + '=' + params[k])
    .join('&');

  params.signature = crypto
    .createHmac('sha256', signingKey)
    .update(canonical)
    .digest('base64');

  return baseURL + '?' + new URLSearchParams(params).toString();
}
// Go
import (
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "net/url"
    "sort"
    "strconv"
    "strings"
    "time"
)

func signCheckoutURL(merchantID, signingKey, baseURL string, params url.Values) string {
    nonceBytes := make([]byte, 16)
    rand.Read(nonceBytes)
    params.Set("merchantId", merchantID)
    params.Set("timestamp", strconv.FormatInt(time.Now().Unix(), 10))
    params.Set("nonce", hex.EncodeToString(nonceBytes))

    var keys []string
    for k := range params {
        if k != "signature" {
            keys = append(keys, k)
        }
    }
    sort.Strings(keys)

    var parts []string
    for _, k := range keys {
        parts = append(parts, fmt.Sprintf("%s=%s", k, params.Get(k)))
    }
    canonical := strings.Join(parts, "&")

    h := hmac.New(sha256.New, []byte(signingKey))
    h.Write([]byte(canonical))
    params.Set("signature", base64.StdEncoding.EncodeToString(h.Sum(nil)))

    return fmt.Sprintf("%s?%s", baseURL, params.Encode())
}
# Python
import base64, hashlib, hmac, secrets, time
from urllib.parse import urlencode

def sign_checkout_url(merchant_id, signing_key, base_url, params):
    params['merchantId'] = merchant_id
    params['timestamp'] = str(int(time.time()))
    params['nonce'] = secrets.token_hex(16)

    canonical = '&'.join(
        f"{k}={params[k]}" for k in sorted(params) if k != 'signature'
    )
    sig = hmac.new(
        signing_key.encode(), canonical.encode(), hashlib.sha256
    ).digest()
    params['signature'] = base64.b64encode(sig).decode()

    return f"{base_url}?{urlencode(params)}"

Worked example

Merchant mch_abc123 charges $49.99 for order order-12345:

  1. Buyer clicks “Checkout” on merchantsite.com/cart.
  2. Backend builds the parameter set:
    merchantId=mch_abc123
    amount=49.99
    externalResourceId=order-12345
    externalResourceDescription=Order #12345
    successUrl=https://merchantsite.com/checkout/success
    failureUrl=https://merchantsite.com/checkout/cancel
    timestamp=1761000000
    nonce=a1b2c3d4e5f67890a1b2c3d4e5f67890
    Canonical string (alphabetical, raw values):
    amount=49.99&externalResourceDescription=Order #12345&externalResourceId=order-12345&failureUrl=https://merchantsite.com/checkout/cancel&merchantId=mch_abc123&nonce=a1b2c3d4e5f67890a1b2c3d4e5f67890&successUrl=https://merchantsite.com/checkout/success&timestamp=1761000000
    HMAC-SHA256 with the merchant signing key, Base64-encoded → signature.
  3. Backend redirects the buyer to the URL-encoded final URL; buyer pays on Soldi Now’s hosted page.
  4. Soldi Now redirects to https://merchantsite.com/checkout/success?status=paid&external_transaction_id=....
  5. Soldi Now POSTs the webhook; the merchant verifies the signature, looks up metadata.external_resource_id (= order-12345), and marks the order paid.

Redirect behavior

After the buyer completes or abandons payment, Soldi Now redirects to successUrl (payment captured) or failureUrl (buyer canceled or payment failed), and may append query parameters such as status and external_transaction_id. The redirect is for user experience only — redirect parameters are unsigned; do not fulfill the order based on the redirect. Wait for the webhook.

Webhook

Register your HTTPS webhook URL under Settings → Developer Keys → Webhooks. On completed payment Soldi Now POSTs:

{
  "event": "external_payment.completed",
  "external_transaction_id": "8f2c9b7a-...",
  "amount": "49.99",
  "merchant_id": "your-merchant-id",
  "metadata": {
    "external_resource_id": "order-12345",
    "external_user_id": "user-9876",
    "payer_email": "buyer@example.com",
    "payment_method": "card"
  },
  "timestamp": "2026-04-26T15:23:01Z"
}

Every webhook is signed with your Soldi Now Signing Key — compute HMAC-SHA256 of the raw body and compare to the X-Webhook-Signature header in constant time. Reject mismatches, reject timestamps more than ~5 minutes stale, and deduplicate by external_transaction_id (Soldi Now retries on non-2xx).

// Node.js webhook verification
const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, soldinowSigningKey) {
  const expected = crypto
    .createHmac('sha256', soldinowSigningKey)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

If the buyer already paid for this externalResourceId, Soldi Now redirects to successUrl with ?status=already_purchased without charging again.

List transactions

Fetch your merchant’s payment history — reconcile your order DB against completed payments, backfill missed webhooks, or look up a transaction for a support ticket.

GET https://web.soldinow.com/api/v1/get-transactions

Authentication headers as on the Invoice API: X-API-Key, X-Merchant-ID, X-Signature.

ParameterDescriptionDefault
external_resource_idFilter to one of your order IDs (the externalResourceId passed at checkout)
external_transaction_idFilter to a specific Soldi Now transaction ID (from a webhook or redirect)
transaction_stateFilter by state, e.g. TransferCompleted, TransferFailed, Initiated
since / untilRFC3339 bounds on creation time
limit / offsetPagination (limit 1–100)50 / 0

Returns transaction objects newest-first: internal_transaction_id, external_transaction_id, external_resource_id (your order ID), amount, transaction_state (Initiated, TransferCreated, TransferRequested, TransferCompleted, TransferFailed, TransferReturned), display_message, payer_email, payer_name, bank_return_code/bank_return_comment (ACH returns, e.g. R01), created_at, updated_at, plus total_count for pagination.

Common patterns: webhook backfill (query since = start of your downtime, reconcile against your payments table), order lookup (external_resource_id=<order> shows all attempts including failures), daily reconciliation (since/until bracketing yesterday, compare totals).

Testing

Always build and verify your integration against staging before pointing at production.

EnvironmentBase URL
Staginghttps://web-staging.soldinow.com
Productionhttps://web.soldinow.com

Use staging to:

Staging uses separate API credentials and a separate webhook URL — create staging keys from the staging dashboard and store them separately from production keys. No real money moves on staging. To go live, update three things: the base URL, your merchant ID, and your signing keys.

Errors

When the hosted page rejects a request before rendering, Soldi Now returns an HTTP error with a JSON body { "success": false, "error": { "code", "message" } }:

CodeHTTPCause
MISSING_REQUIRED_FIELDS400merchantId or amount missing
INVALID_AMOUNT400Amount not parseable as decimal
MISSING_SIGNATURE_PARAMETERS401signature, timestamp, or nonce missing
INVALID_SIGNATURE401Signature does not match canonical string
SIGNATURE_EXPIRED401timestamp older than 5 minutes
MERCHANT_NOT_FOUND400Unknown or uncertified merchantId

Common pitfalls

Support

Email support@soldinow.com with the external_transaction_id for payment-specific questions.