Here’s the thing with Carthage – most of their armies were mercenaries, outside of the officer corps and some of their infantry. Their cavalry, which so outmatched the Romans, were from a neighboring country, Numidia. While Hannibal was rampaging up and down Italy, trying to force the Romans to give in, or the Italians to join his anti-Roman coalition, and generally being unable to take Rome in a siege, Publius Cornelius Scipio (the future Scipio Africanus) was winning a bunch of battles in Spain against Carthage’s B-team.
When Scipio was elected consul due to his victories in Spain, he had the bright idea to go and bribe the Numidians into switching sides. He wasn’t wholly successful, but it meant that when he finally came to grips with Hannibal at Zama, he actually had more cavalry (6,000 to 4,000) than Hannibal did. Scipio’s cavalry beat Hannibal’s and pursued them off the field, the Roman infantry began to slowly grind down the Carthaginian infantry, and then Scipio’s cavalry rallied and hit Hannibal’s slowly retreating army in the rear, wiping out Hannibal’s army and leading to a “Roman Cannae.”
That’s the military/tactical side. There’s also a logistical side: Rome could raise far more men than the Carthaginians could, which meant that even though Hannibal kept wiping out entire Roman armies, they kept recruiting new ones. At that point, all Rome had to do was win once.