Skip to content

Webhooks

Rach sends signed HTTP POST requests to your configured webhook URL when payment and wallet events occur.

Base URL: https://payments-api-dev-966260606560.europe-west2.run.app


Setup

Configure Your Webhook URL

POST /api/v1/settings/webhook

http
POST /api/v1/settings/webhook
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "webhook_url": "https://yoursite.com/webhooks/rach",
  "wallet_webhook_enabled": true
}
FieldTypeRequiredDescription
webhook_urlstring (URI)Your HTTPS endpoint
wallet_webhook_enabledbooleanEnable WaaS deposit events (default false)

Response 200 OK (first-time setup)

json
{
  "webhook_url": "https://yoursite.com/webhooks/rach",
  "webhook_secret": "whsec_AbCdEfGh1234...",
  "wallet_webhook_enabled": true,
  "message": "Webhook configured"
}

⚠️ The webhook_secret is only returned on first setup. Store it immediately in your secrets manager. To get a new secret use the rotate endpoint.

Get Current Configuration

GET /api/v1/settings/webhook

The secret is never included in the GET response.

Rotate Webhook Secret

POST /api/v1/settings/webhook/rotate-secret

Generates a new whsec_* secret. The old secret is immediately invalidated — update your verification code before rotating.

http
POST /api/v1/settings/webhook/rotate-secret
Authorization: Bearer <jwt>

Response:

json
{
  "webhook_secret": "whsec_NewSecretHere...",
  "message": "Secret rotated — update your verification code now"
}

Send a Test Event

POST /api/v1/settings/webhook/test

Sends a webhook.test event to verify connectivity and that your signature verification logic is working.

http
POST /api/v1/settings/webhook/test
Authorization: Bearer <jwt>

Signature Verification

Every webhook request includes an X-Webhook-Signature header containing the HMAC-SHA256 of the raw request body, signed with your whsec_* secret.

Always verify the signature before processing an event.

javascript
const crypto = require('crypto');

app.post('/webhooks/rach', express.raw({ type: '*/*' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  const expectedSig = crypto
    .createHmac('sha256', process.env.RACH_WEBHOOK_SECRET)
    .update(req.body) // raw Buffer — do NOT parse before this step
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  handleEvent(event);
  res.sendStatus(200);
});
python
import hmac, hashlib
from flask import request, abort

@app.route('/webhooks/rach', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    raw_body = request.get_data()

    expected = hmac.new(
        RACH_WEBHOOK_SECRET.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = request.get_json(force=True)
    handle_event(event)
    return '', 200
go
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig := r.Header.Get("X-Webhook-Signature")

    mac := hmac.New(sha256.New, []byte(os.Getenv("RACH_WEBHOOK_SECRET")))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(sig), []byte(expected)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var event map[string]interface{}
    json.Unmarshal(body, &event)
    handleEvent(event)
    w.WriteHeader(http.StatusOK)
}

Event Types

Checkout / Payment Gateway Events

EventFired When
payment.detectedDeposit seen on-chain — not yet confirmed
payment.confirmedDeposit fully confirmed — safe to fulfill order
payment.failedDeposit detected but failed to confirm
payment.expiredSession timed out before payment received
webhook.testTest event from dashboard or /settings/webhook/test

WaaS (Wallet-as-a-Service) Events

Enabled by setting wallet_webhook_enabled: true in your webhook config.

EventFired When
wallet.deposit.detectedIncoming deposit seen on-chain (not confirmed)
wallet.deposit.confirmedDeposit confirmed — safe to credit customer balance

Payload Shapes

payment.confirmed

json
{
  "event": "payment.confirmed",
  "data": {
    "session_id": "a1b2c3d4-e5f6-...",
    "reference": "ORDER-001",
    "amount": 100.00,
    "currency": "USD",
    "amount_crypto": "100.00",
    "crypto_currency": "USDT",
    "network": "BSC",
    "tx_hash": "0xabc123...",
    "customer_email": "customer@example.com",
    "paid_at": "2026-06-25T14:12:33Z",
    "metadata": {
      "order_id": "ord_abc123"
    }
  }
}

wallet.deposit.detected / wallet.deposit.confirmed

json
{
  "event": "wallet.deposit.confirmed",
  "data": {
    "customer_id": "cus_abc123",
    "network": "ETH",
    "address": "0x1a2b3c...",
    "amount": "100.00",
    "currency": "USDT",
    "tx_hash": "0xabc...",
    "confirmations": 12,
    "status": "confirmed",
    "detected_at": "2026-01-01T00:00:00Z",
    "confirmed_at": "2026-01-01T00:02:00Z"
  }
}

TRX, SOL, LTC, BCH, XRP: These networks use a settlement delay. The confirmations field will always be 0 even when status is "confirmed". Use status === "confirmed" as the authoritative signal, not the confirmations count.

webhook.test

json
{
  "event": "webhook.test",
  "data": {
    "message": "This is a test webhook event",
    "timestamp": "2026-06-25T14:00:00Z"
  }
}

Retry Policy

If your endpoint does not return a 2xx response, Rach will retry delivery with exponential backoff:

AttemptDelay
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry12 hours
Final24 hours

After the final attempt, the event is marked as failed. You can view delivery logs in the dashboard.


Best Practices

  • Always verify the signature before processing any event
  • Use payment.confirmed (not payment.detected) to fulfill orders
  • Respond quickly — return 200 immediately and process async. If processing takes >5s, the delivery is marked as failed
  • Be idempotent — you may receive the same event more than once; use session_id or tx_hash as a deduplication key
  • Validate the amount — confirm it matches your order total before fulfilling

Rach Payments API