Skip to content

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

  1. Log in to your SecPaid account
  2. Navigate to SettingsAPI & Integrations
  3. Enter your webhook URL in the Payment Endpoint field
  4. Click Save Settings
Payment Endpoint: https://yourserver.com/api/secpaid/webhook

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)

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 amount
  • user_id — the recipient's user ID
  • note — the recipient_note from 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:

  1. JSON-encoded
  2. Encrypted with AES-256-CBC using your EncryptionKey
  3. Base64-encoded
  4. Sent as the data field 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:

  1. Start ngrok: ngrok http 8000
  2. Set your paymentEndpoint to the ngrok URL
  3. Create a test link and pay with a test card
  4. 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

  1. Respond quickly — Return 200 immediately, process asynchronously if needed
  2. Idempotency — Handle duplicate webhooks gracefully (same pay_id may arrive multiple times)
  3. Verify the payment — Check that pay_id matches an order you created
  4. Use HTTPS — Always use TLS in production
  5. Log everything — Store raw webhook payloads for debugging
  6. Don't rely on order — The webhook may arrive before or after the callback_url redirect