CES Survey Implementation on Website
CES (Customer Effort Score) measures the effort a user exerted to achieve their goal: "How easy was it to resolve your issue?" Scale 1-7 (1 = very difficult, 7 = very easy). Low CES predicts churn.
Where to Apply CES
- After onboarding completion
- After support contact
- After complex form or multi-step process
- After first successful integration (B2B SaaS)
CES predicts repeat purchases in B2C and churn in B2B better than NPS.
API
// CesController
public function store(Request $request): JsonResponse
{
$request->validate([
'score' => 'required|integer|min:1|max:7',
'journey' => 'required|string|max:100', // 'onboarding', 'support', 'checkout'
'comment' => 'nullable|string|max:500',
]);
CesResponse::create([
'user_id' => auth()->id(),
'score' => $request->score,
'journey' => $request->journey,
'comment' => $request->comment,
]);
// If CES <= 3 — create task for support team
if ($request->score <= 3) {
SupportTask::create([
'type' => 'low_ces_followup',
'user_id' => auth()->id(),
'notes' => "CES {$request->score} for {$request->journey}. Comment: {$request->comment}",
]);
}
return response()->json(['success' => true]);
}
Frontend: 7-Point Scale Widget
const SCALE = [
{ value: 1, label: 'Very\nDifficult' },
{ value: 2, label: '' },
{ value: 3, label: 'Difficult' },
{ value: 4, label: 'Neutral' },
{ value: 5, label: 'Easy' },
{ value: 6, label: '' },
{ value: 7, label: 'Very\nEasy' },
];
export function CesWidget({ journey }: { journey: string }) {
const [selected, setSelected] = useState<number | null>(null);
const [done, setDone] = useState(false);
const submit = async (value: number) => {
setSelected(value);
await fetch('/api/ces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ score: value, journey }),
});
setDone(true);
};
if (done) return <p className="text-sm text-green-600">Thank you! Your feedback helps us improve.</p>;
return (
<div>
<p className="text-sm font-medium mb-3">How easy was it to complete this process?</p>
<div className="flex gap-2">
{SCALE.map(({ value, label }) => (
<button key={value} onClick={() => submit(value)}
className={`flex-1 py-2 rounded border text-sm font-medium transition-colors
${selected === value ? 'bg-blue-600 text-white border-blue-600' : 'border-gray-300 hover:border-blue-400'}`}>
{value}
{label && <span className="block text-xs text-gray-400 whitespace-pre-line leading-tight">{label}</span>}
</button>
))}
</div>
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>Very difficult</span><span>Very easy</span>
</div>
</div>
);
}
Calculate Average CES
SELECT
journey,
ROUND(AVG(score), 2) AS avg_ces,
COUNT(*) AS responses,
COUNT(*) FILTER (WHERE score <= 3) AS low_effort_count
FROM ces_responses
WHERE created_at >= now() - interval '30 days'
GROUP BY journey
ORDER BY avg_ces ASC;
Timeline
CES widget with low-score follow-up logic: 1-2 business days.







