Create a subscriber
One person, with an email or a phone. Status is stored as active. Tags, groups and consent are not part of this call.
curl -X POST https://api.connect.carriercrow.com/subscribers \
-H "content-type: application/json" \
-H "x-api-key: $CCC_API_KEY" \
-d '{"email":"ada@example.com","firstName":"Ada","lastName":"Lovelace"}'
Import subscribers
One request with every row in data. The UI’s chunked upload is a different call; this is the shape an API key uses. Each row needs an email or a phone.
curl -X POST https://api.connect.carriercrow.com/subscriber-imports \
-H "content-type: application/json" \
-H "x-api-key: $CCC_API_KEY" \
-d '{"data":[{"email":"ada@example.com","firstName":"Ada"},{"email":"grace@example.com","tags":["launch"]}]}'
Create a campaign draft
Leave status off and the API stores a draft. Nothing in this request sends the campaign. templateId, when you send one, has to belong to the same project.
curl -X POST https://api.connect.carriercrow.com/campaigns \
-H "content-type: application/json" \
-H "x-api-key: $CCC_API_KEY" \
-d '{"name":"October note","type":"standard","subject":"A short note","templateId":"TEMPLATE_ID"}'
Verify a webhook signature
When the channel has a secret, the request carries X-CarrierCrow-Signature. Recompute HMAC-SHA256 over the raw body bytes, prefix sha256=, and compare in constant time. Re-encoding the JSON will not match.
import { createHmac, timingSafeEqual } from 'node:crypto'
export const verifyCarrierCrowSignature = (rawBody, header, secret) => {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(String(header || ''))
return a.length === b.length && timingSafeEqual(a, b)
}