Payment Endpoint¶
The payment endpoint is a server-to-server webhook URL configured on your SecPaid account. When any of your payment links is paid, SecPaid sends a POST request to this URL with the payment details.
How It Differs from callback_url¶
| Aspect | payment_endpoint | callback_url |
|---|---|---|
| Type | Server-to-server HTTP POST | Browser redirect (HTTP 302) |
| Reliability | High — independent of customer's browser | Low — customer can close tab |
| Configuration | Once, account-wide | Per link (or account default) |
| Payload | JSON body with payment details | Query parameters on URL |
| Security | Can be encrypted (AES) | Easily spoofable by end users |
| Triggers on cancel | No | Yes (with status=cancel) |
| Use for | Order fulfillment, backend logic | Customer-facing UI feedback |
Configuration¶
Setting Your Payment Endpoint¶
- Log in to your SecPaid account
- Navigate to Settings → API & Integrations
- Enter your webhook URL in the Payment Endpoint field
- Click Save Settings
This is your account-level default — all payment links you create will use this endpoint unless overridden per-link via the API.
Requirements¶
Your endpoint URL must:
- Be publicly accessible from SecPaid's servers
- Accept POST requests with JSON body
- Return an HTTP 2xx response
- Use HTTPS in production
Request Details¶
HTTP Request¶
POST /api/secpaid/webhook HTTP/1.1
Host: yourserver.com
Content-Type: application/json
Accept: application/json
{
"ResponseCode": 1,
"data": {
"pay_id": 12345,
"note": "Invoice #1234",
"amount": 49.99,
"user_id": "usr-abc-def-123",
"status": "Success"
}
}
Payload Fields¶
| Field | Type | Always Present | Description |
|---|---|---|---|
ResponseCode |
integer | Yes | Always 1 |
data.pay_id |
integer | Yes | The linktopay_id identifying the payment |
data.note |
string | Yes | The recipient_note set during link creation (may be empty string) |
data.amount |
number | Yes | Amount paid by the customer |
data.user_id |
string | Yes | Your internal user ID (the merchant who created the link) |
data.status |
string | Yes | Always "Success" (endpoint only fires on successful payment) |
Split Link Webhooks¶
For split links, the payment endpoint is called for each recipient in the split. Each recipient's configured payment_endpoint receives the webhook with:
pay_id— same across all recipients (the link's ID)amount— the recipient's share, not the total amountuser_id— the recipient's user IDnote— therecipient_notefrom the link
Encryption¶
If your account has encryption enabled (is_encryption = Yes in SecPaid account attributes), the webhook payload is encrypted before sending:
Encrypted Request¶
POST /api/secpaid/webhook HTTP/1.1
Host: yourserver.com
Content-Type: application/json
{
"data": "U2FsdGVkX1+0Hk8Q7z...base64_encrypted_string..."
}
The entire {ResponseCode, data: {...}} object is:
- JSON-encoded
- Encrypted with AES-256-CBC using your
EncryptionKey - Base64-encoded
- Sent as the
datafield value
See Encryption for decryption instructions.
Trigger Conditions¶
| Event | payment_endpoint Called? | callback_url Called? |
|---|---|---|
| Customer pays via card or bank transfer | ✅ Yes | ✅ Yes (redirect) |
| Admin confirms bank transfer | ✅ Yes | ❌ No (no browser session) |
| Customer cancels link | ❌ No | ✅ Yes (status=cancel) |
| Link deleted via API | ❌ No | ❌ No |
| Refund processed | ❌ No | ❌ No |
Implementation Examples¶
Route::post('/api/secpaid/webhook', function (Request $request) {
$payload = $request->all();
// If encryption is enabled, decrypt first
if (isset($payload['data']) && is_string($payload['data'])) {
$decrypted = openssl_decrypt(
$payload['data'],
'AES-256-CBC',
env('SECPAID_ENCRYPTION_KEY'),
0,
substr(env('SECPAID_ENCRYPTION_KEY'), 0, 16)
);
$payload = json_decode($decrypted, true);
}
$payId = $payload['data']['pay_id'];
$amount = $payload['data']['amount'];
$status = $payload['data']['status'];
// Process the payment
$order = Order::where('payment_link_id', $payId)->first();
if ($order && $status === 'Success') {
$order->update(['status' => 'paid', 'paid_amount' => $amount]);
// Send confirmation email, fulfill order, etc.
}
return response()->json(['received' => true], 200);
});
app.post('/api/secpaid/webhook', (req, res) => {
let payload = req.body;
// If encryption is enabled, decrypt first
if (typeof payload.data === 'string') {
const crypto = require('crypto');
const key = process.env.SECPAID_ENCRYPTION_KEY;
const iv = key.substring(0, 16);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(payload.data, 'base64', 'utf8');
decrypted += decipher.final('utf8');
payload = JSON.parse(decrypted);
}
const { pay_id, amount, status } = payload.data;
// Process the payment
if (status === 'Success') {
fulfillOrder(pay_id, amount);
}
res.status(200).json({ received: true });
});
@app.route('/api/secpaid/webhook', methods=['POST'])
def secpaid_webhook():
payload = request.get_json()
# If encryption is enabled, decrypt first
if isinstance(payload.get('data'), str):
from Crypto.Cipher import AES
import base64
key = os.environ['SECPAID_ENCRYPTION_KEY'].encode()
iv = key[:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(base64.b64decode(payload['data']))
payload = json.loads(decrypted.rstrip(b'\x00').decode())
pay_id = payload['data']['pay_id']
amount = payload['data']['amount']
status = payload['data']['status']
if status == 'Success':
fulfill_order(pay_id, amount)
return jsonify({'received': True}), 200
Testing Webhooks¶
Development Environment¶
Use the development environment (app.dev.secpaid.com) with a tool like ngrok or webhook.site to receive webhooks locally:
- Start ngrok:
ngrok http 8000 - Set your
paymentEndpointto the ngrok URL - Create a test link and pay with a test card
- Observe the webhook hitting your local server
Re-sending Webhooks¶
If you missed a webhook, you can manually re-trigger it:
curl -X POST https://app.secpaid.com/api/v2/sendWebhookViaPayId \
-H "Content-Type: application/json" \
-H "token: YOUR_TOKEN" \
-d '{"pay_ids": "12345"}'
Best Practices¶
- Respond quickly — Return 200 immediately, process asynchronously if needed
- Idempotency — Handle duplicate webhooks gracefully (same
pay_idmay arrive multiple times) - Verify the payment — Check that
pay_idmatches an order you created - Use HTTPS — Always use TLS in production
- Log everything — Store raw webhook payloads for debugging
- Don't rely on order — The webhook may arrive before or after the
callback_urlredirect