Set up Sonar fraud protection

Sonar is Frame's buy-side fraud engine. It runs on every charge automatically — there's no enable/disable, no rules surface to configure, no allow-list API. The work for an integrator isn't switching it on; it's giving Sonar enough signal to make good decisions, and handling the declines when it blocks a transaction.

This guide covers the four integration choices that materially affect Sonar's signal quality, then the merchant-side handling when Sonar blocks.

Why there's no config API

Sonar's decisioning lives server-side. The fraud model is shared across the whole Frame network — every merchant's outcomes (chargebacks, refunds, disputes) feed back into a single trained model, and per-merchant rule tuning happens through Frame's risk operations team rather than a self-service API. Velocity rules, threshold tuning, and merchant-specific block lists all sit behind that surface.

What you do control is the signal Sonar sees. The four levers below.

1. Load frame-js site-wide

<script src="https://js.framepayments.com/v1/frame.js"></script>

Drop the tag in your base layout so frame-js loads on every page, not just /checkout. Sonar's behavioral signals (time on page, navigation patterns, copy-paste behavior) need observation across the customer's whole session — loading only on checkout means Sonar gets the last few seconds of a multi-minute visit, which is much weaker context.

A few rules:

  • Load from js.framepayments.com, not a local copy. Sonar's tracking JavaScript updates continuously; a static copy decays in fraud-detection quality over time.
  • Initialize early. Frame.init(publishable_key) should run on page load so the Sonar session starts immediately.
  • Don't lazy-load it on the checkout page. Same problem as a local copy — Sonar misses the pre-checkout window.

2. Forward sonar_session_id on every charge

When frame-js initializes, it creates a Sonar session and stores its identifier in localStorage under frame_charge_session_id. On the client, retrieve it and forward it to your backend with the rest of the checkout payload:

const sonarSessionId = localStorage.getItem('frame_charge_session_id');

await fetch('/checkout', {
  method: 'POST',
  body: JSON.stringify({
    cart,
    paymentMethodId,
    sonar_session_id: sonarSessionId,
  }),
});

On your backend, include sonar_session_id on the charge creation call:

curl --request POST \
  --url https://api.framepayments.com/v1/transfers \
  --header "Authorization: Bearer $FRAME_SECRET_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": 5000,
    "currency": "USD",
    "account_id": "<account_id>",
    "payment_method_id": "<payment_method_id>",
    "sonar_session_id": "<sonar_session_id>"
  }'

Without sonar_session_id, Frame links the charge to a session-less context — Sonar still evaluates against issuer/network signals, but the device/behavioral half of its inputs is empty. Risk-scoring degrades accordingly.

3. Send customer profile data

Sonar correlates across customer identity. Pass everything your checkout collects — name, email, billing address, shipping address — on the charge or on the customer/account record. Each correlation point is a chance for Sonar to flag a mismatch (billing address geo-distant from device IP, email recently used by a flagged customer, etc.) that an anonymized charge would miss.

If you're using the Account primitive for the buyer, set profile fields on the account at create time and Sonar pulls them automatically. For one-off charges without an Account, pass profile fields directly on the transfer request.

4. Use 3D Secure where the issuer requests it

When the card issuer requests authentication, 3D Secure shifts the fraud-liability burden from the merchant to the issuer for that specific charge. Sonar's decision and 3DS are independent surfaces — both run, and both can fail a transaction independently — but a clean 3DS authentication is a strong positive signal for Sonar. See handle 3D Secure for the integration.

React to Sonar-flagged declines

When Sonar blocks, the transfer fails with failure_code: fraudulent (or merchant_blacklist for block-list matches, or one of the do_not_honor/generic_decline codes when the issuer declines for fraud reasons after Sonar passes the charge through). On webhook, listen for charge.failed and inspect failure_code.

// Webhook handler
if (event.type === 'charge.failed') {
  const failureCode = event.data.failure_code;
  if (['fraudulent', 'merchant_blacklist', 'do_not_honor'].includes(failureCode)) {
    // Sonar (or an issuer agreeing with Sonar) blocked this charge.
    // Don't disclose this to the customer. Surface a generic error.
    logFraudFlag({ charge_id: event.data.id, failure_code: failureCode });
  }
}

Customer-facing message: never tell the customer that fraud detection flagged them. Use a generic "we couldn't process this payment, please contact your card issuer or try another card" — disclosing the fraud flag tells legitimate bad actors how to game the next attempt, and offends legitimate customers caught in a false positive. The decline codes concept covers the merchant-policy rationale.

For the broader decline-handling playbook (what to do per failure_code, retry vs. block), see handle declines.

When to escalate to Frame support

Sonar is well-calibrated by default. The two cases that warrant escalation:

  • Persistently high false-positive rate. If you're seeing legitimate customers blocked at a rate that's hurting conversion (your own definition of "high"), Frame's risk team can review your traffic and adjust thresholds. Bring concrete examples — charge_ids of blocked transactions you believe were legitimate.
  • A specific bad actor you want to block. Frame supports merchant-specific block lists for cards, emails, and device fingerprints. Manage via the Frame Dashboard or by contacting support — there's no public API for block-list management today.

The wrong escalation: asking Frame to "turn Sonar off" for a specific charge or merchant. Sonar is the default fraud floor and there's no per-charge bypass. If a specific transaction needs to proceed despite a flag, the customer has to retry with a different card or use a payment link that may route differently.

Privacy disclosure

Sonar collects device and behavioral signals from your customers. Your privacy policy should disclose this — see the Sonar concept for Frame's suggested language. Not legal advice; consult counsel.

Gotchas

Symptom: Sonar's false-positive rate spiked after a code change. Why: most often, frame-js stopped loading on a checkout-adjacent page (a routing change, a CSP tightening, a lazy-load regression). Sonar lost behavioral context and started erring cautious on isolated charges. Fix: verify frame-js is loaded on every page in your funnel, not just /checkout. Open DevTools on a sample page and check localStorage.getItem('frame_charge_session_id') returns a value early in the session.

Symptom: you sent sonar_session_id but the charge still came back fraudulent. Why: sonar_session_id doesn't override Sonar's decision — it provides the signals Sonar evaluates. If the device/behavioral signals point at fraud, Sonar flags regardless. Fix: this is working as intended. If you believe the flag is wrong, log the charge_id and contact Frame support for review.

Symptom: you can't find a way to allowlist a specific customer in the API. Why: allowlist management isn't exposed via API in V1. Fix: use the Frame Dashboard, or contact support. Programmatic allowlist support is a candidate for a future API surface.

Next steps