Skip to main content
POST
/
api
/
campaigns
/
{id}
/
sync-stats
Sync latest delivery stats for a campaign
curl --request POST \
  --url https://app.puffle.ai/api/campaigns/{id}/sync-stats \
  --header 'Authorization: Bearer <token>'
{
  "campaign": {
    "id": "8c2b2d4e-e29b-41d4-a716-446655440000",
    "user_id": "11111111-1111-1111-1111-111111111111",
    "type": "email",
    "name": "Q2 Enterprise Outreach",
    "status": "active",
    "daily_limit": 50,
    "stop_on_reply": true,
    "open_tracking": true,
    "signature": "Best,\n%first_name%",
    "skip_other_campaigns": null,
    "operating_hours": null,
    "stats": {
      "total": 120,
      "sent": 34,
      "replied": 4,
      "bounced": 1
    },
    "created_at": "2026-04-18T15:12:00Z",
    "updated_at": "2026-04-22T11:30:00Z",
    "sender_accounts": []
  },
  "prospects_by_status": {
    "pending": 81,
    "sent": 34,
    "replied": 4,
    "bounced": 1
  }
}

Overview

Re-aggregates campaigns.stats from the ground truth of campaign_prospects statuses via the recalculate_campaign_stats RPC, stamps last_synced_at, and returns the fresh campaign row plus a prospects_by_status count map. Works for both LinkedIn and email campaigns. Request body is empty. For email campaigns, also fires the checkEmailBounceRate guard in the background — best-effort, errors are logged and do not fail the request. If the rolling bounce rate crosses the guard’s threshold (≥ 5% with ≥ 100 sent), the bounce guard pauses the campaign on its own; the sync response still returns 200 with the updated stats reflecting the auto-pause.

AI agent notes

You usually don’t need this. The Trigger.dev engine updates campaigns.stats atomically through the update_prospect_with_stat RPC on every status transition, so the stored stats are already live. Call sync-stats only when you suspect drift — after a manual DB edit, after a crashed run, or for a periodic reconciliation.Side effect on email campaigns. The bounce-rate guard runs after re-aggregation. If your campaign has been silently stuck with high bounces, a sync call can be the event that triggers the auto-pause — plan for the campaign to possibly land in paused after this call.Related endpoints. For a live snapshot of the campaign without re-aggregating, prefer GET /api/campaigns/{id} — it’s cheaper and never mutates.

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

id
string<uuid>
required

Campaign UUID

Pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$

Response

Stats re-aggregated. Response includes the fresh campaign and (when present) a prospect-status breakdown.

Fresh snapshot after re-aggregating campaign_prospects statuses via the recalculate_campaign_stats RPC.

campaign
object
required

The refreshed campaign row. stats and last_synced_at reflect the re-aggregated ground truth.

prospects_by_status
object

Map of prospect status → count, e.g. { "pending": 42, "sent": 34, "replied": 2 }. Omitted when the campaign has no prospects yet.