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.
/external/pay URL with your merchant signing keysuccessUrl or failureUrlThe webhook is the source of truth — never grant value based on the redirect alone.
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.
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.
| Parameter | Required | Description |
|---|---|---|
merchantId | Yes | Your merchant ID |
amount | Yes | Dollars, two decimals (e.g. 49.99) |
externalResourceId | Yes | Your order/cart ID — used for duplicate-payment detection, returned in the webhook |
successUrl / failureUrl | Yes | Redirect targets after payment success / cancel-failure |
timestamp | Yes | Unix seconds at signing time |
nonce | Yes | Unique random string per request (16-byte hex recommended) |
signature | Yes | Base64 HMAC-SHA256 of the canonical parameter string |
externalUserId | No | Your stable buyer ID; returned in the webhook. Omit for guest checkout. |
externalResourceDescription | No | Shown on the payment page (e.g. “Order #1234”) |
taxRate | No | Tax rate percent as decimal string (e.g. 8.25) |
metadata | No | URL-encoded JSON, round-tripped to your webhook |
timestamp (Unix seconds) and nonce (16-byte random hex)signature) alphabetically by keykey=value&key=value using raw values (not URL-encoded)signaturesignature, 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)}"
Merchant mch_abc123 charges $49.99 for order order-12345:
merchantsite.com/cart.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×tamp=1761000000
HMAC-SHA256 with the merchant signing key, Base64-encoded → signature.https://merchantsite.com/checkout/success?status=paid&external_transaction_id=....metadata.external_resource_id (= order-12345), and marks the order paid.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.
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.
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.
| Parameter | Description | Default |
|---|---|---|
external_resource_id | Filter to one of your order IDs (the externalResourceId passed at checkout) | — |
external_transaction_id | Filter to a specific Soldi Now transaction ID (from a webhook or redirect) | — |
transaction_state | Filter by state, e.g. TransferCompleted, TransferFailed, Initiated | — |
since / until | RFC3339 bounds on creation time | — |
limit / offset | Pagination (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).
Always build and verify your integration against staging before pointing at production.
| Environment | Base URL |
|---|---|
| Staging | https://web-staging.soldinow.com |
| Production | https://web.soldinow.com |
Use staging to:
externalResourceId, network failures during webhook deliveryStaging 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.
When the hosted page rejects a request before rendering, Soldi Now returns an HTTP error with a JSON body { "success": false, "error": { "code", "message" } }:
| Code | HTTP | Cause |
|---|---|---|
MISSING_REQUIRED_FIELDS | 400 | merchantId or amount missing |
INVALID_AMOUNT | 400 | Amount not parseable as decimal |
MISSING_SIGNATURE_PARAMETERS | 401 | signature, timestamp, or nonce missing |
INVALID_SIGNATURE | 401 | Signature does not match canonical string |
SIGNATURE_EXPIRED | 401 | timestamp older than 5 minutes |
MERCHANT_NOT_FOUND | 400 | Unknown or uncertified merchantId |
&, exclude signatureEmail support@soldinow.com with the external_transaction_id for payment-specific questions.