Skip to main content
This guide is for teams that deploy the apps/web stack themselves. Integrating merchants never touch CRON_SECRET — they only receive signed webhooks (Webhooks).

How retries work

Failed webhook deliveries retry up to five times (1 min → 5 min → 30 min → 2 h → 24 h). Most of the time, retries run inline: whenever Payoes dispatches a new payment event, it processes any due retries first. That covers busy deployments without extra infrastructure. On a quiet deployment, a failed delivery might sit until the next payment event triggers a retry pass. To avoid that gap, call POST /api/cron/webhook-retries on a schedule.

CRON_SECRET

Set a bearer token in apps/web/.env.local or your production environment:
CRON_SECRET=<openssl rand -base64 32>
Restart the app after changing env vars. The token protects the retry endpoint — requests without it get 401. You don’t need a scheduler for local dev. To smoke-test the endpoint:
curl -X POST http://localhost:3000/api/cron/webhook-retries \
  -H "Authorization: Bearer $CRON_SECRET"
Without CRON_SECRET set, the route returns 503.

Production endpoint

curl -X POST https://your-payoes-host/api/cron/webhook-retries \
  -H "Authorization: Bearer $CRON_SECRET"
X-Cron-Secret works as an alternative to the Authorization header. Response:
{ "processed": 3 }
processed is how many due deliveries were attempted in that run (capped at 25).

Scheduling

Next.js doesn’t run background jobs. Pick a scheduler and hit the endpoint every 1–5 minutes.

Vercel Cron

Add vercel.json at the repository root (or app root, depending on your Vercel project):
{
  "crons": [
    {
      "path": "/api/cron/webhook-retries",
      "schedule": "*/5 * * * *"
    }
  ]
}
Add CRON_SECRET to your Vercel environment variables. Some Vercel plans don’t attach Authorization automatically — if auth fails, trigger the endpoint from GitHub Actions or another scheduler instead.

GitHub Actions

# .github/workflows/webhook-retries.yml
name: Webhook retries
on:
  schedule:
    - cron: "*/5 * * * *"
  workflow_dispatch:

jobs:
  retry:
    runs-on: ubuntu-latest
    steps:
      - name: Process due webhook retries
        run: |
          curl -fsS -X POST "${{ vars.PAYOES_URL }}/api/cron/webhook-retries" \
            -H "Authorization: Bearer ${{ secrets.CRON_SECRET }}"
Put CRON_SECRET in repository secrets and PAYOES_URL in repository variables.

Other schedulers

cron-job.org, Railway cron, Render cron, or a plain crontab on a VPS all work — any service that can POST with the bearer token.

Troubleshooting

ProblemWhat to check
503 CRON_SECRET is not configuredEnv var missing in the deployment
401 UnauthorizedToken mismatch or wrong header
Retries stall in productionNo scheduler and no payment events to trigger inline processing
processed: 0Nothing was due at that moment