Cove sends HTTP POST requests to your configured webhook_url when bureau submission statuses change.
Event types
| Event | When Fired | Key Data Fields |
|---|
submission.pending | Metro 2 file generated, queued for delivery | tradeline_id, bureau, submission_date |
submission.submitted | File uploaded to bureau via SFTP | tradeline_id, bureau, submission_date |
submission.accepted | Bureau confirmed acceptance | tradeline_id, bureau, response_code, response_message |
submission.rejected | Bureau rejected the record | tradeline_id, bureau, response_code, response_message |
tradeline.updated | Tradeline status changed | tradeline_id, changes |
{
"id": "550e8400-e29b-41d4-a716-446655440099",
"event": "submission.accepted",
"created_at": "2026-02-06T14:30:00Z",
"data": {
"tradeline_id": "660e8400-e29b-41d4-a716-446655440001",
"bureau": "equifax",
"submission_date": "2026-02-05",
"status": "accepted",
"response_code": "00",
"response_message": "Record accepted successfully"
}
}
Every webhook POST includes these headers:
| Header | Value |
|---|
Content-Type | application/json |
X-Cove-Signature | sha256=<HMAC-SHA256 hex digest> |
X-Cove-Event | Event type (e.g., submission.accepted) |
X-Cove-Delivery | Unique delivery UUID |
Signature verification
The X-Cove-Signature header contains an HMAC-SHA256 digest of the raw request body, signed with your webhook_secret.
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
const rawBody = req.body; // raw string, not parsed JSON
const signature = req.headers['x-cove-signature'];
if (!verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
Retry behavior
| Attempt | Delay | Notes |
|---|
| 1 | Immediate | First delivery attempt |
| 2 | 1 minute | After first failure |
| 3 | 10 minutes | After second failure |
| 4 | 1 hour | Final attempt |
- Each attempt has a 10-second timeout.
- After all retries are exhausted, the event is marked as
failed.
Fallback: Poll GET /submissions to check status if webhooks are missed.
Best practices
- Return 2xx quickly — process webhook data asynchronously. The 10-second timeout is strict.
- Handle duplicates — webhooks may be delivered more than once. Use the
id field for idempotency.
- Use
GET /submissions for reconciliation — don’t rely solely on webhooks for critical business logic.