API documentation
A single REST API for transactional email and SMS. Every endpoint returns JSON with a request id, so you can trace any send end to end.
Quickstart
Create a workspace, generate a test API key from Developers → API keys, then send your first message. Test keys never touch a real provider — messages are simulated and appear in your logs marked as test.
curl -X POST https://api.getquiver.app/api/public/v1/email/send \
-H "Authorization: Bearer qv_test_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"subject": "Your order is confirmed",
"html": "<p>Thanks for your order!</p>"
}'Authentication
Pass your API key as a bearer token on every request. Keys are scoped to one workspace and one environment.
Authorization: Bearer qv_live_xxxxxxxxxxxxxxxxqv_test_*— simulated delivery, no credits consumed.qv_live_*— real delivery through Resend and Twilio.- Keys are shown once at creation and stored only as a hash. Rotate freely.
Send email
/api/public/v1/email/sendProvide html, text, or a template_id. Template variables use {{variable}} syntax.
{
"to": "customer@example.com",
"from": "orders@yourdomain.com",
"template_id": "order-confirmed",
"variables": {
"first_name": "Maria",
"order_id": "1042",
"order_total": "$38.50"
},
"idempotency_key": "order-1042-confirmed"
}{
"success": true,
"request_id": "req_8f2c1d4b9a7e",
"data": {
"message": {
"id": "5c0f...",
"status": "sent",
"channel": "email",
"environment": "test"
}
}
}Send SMS
/api/public/v1/sms/sendNumbers must be E.164 formatted. Contacts without SMS opt-in and numbers on your suppression list are rejected before the provider is called.
{
"to": "+14155550142",
"body": "Hi {{first_name}}, order {{order_id}} is ready for pickup.",
"variables": { "first_name": "Maria", "order_id": "1042" }
}Bulk sending
/api/public/v1/email/bulk/api/public/v1/sms/bulkUp to 1,000 recipients per request, each with its own variables. Sends are queued and retried with exponential backoff.
{
"template_id": "weekly-specials",
"subject": "This week at Bella's",
"messages": [
{ "to": "maria@example.com", "variables": { "first_name": "Maria" } },
{ "to": "sam@example.com", "variables": { "first_name": "Sam" } }
]
}Order notifications
/api/public/v1/notifications/orderOne call fans out to email and SMS using your saved templates for that event, with a built-in fallback if no template exists yet.
{
"event": "order_ready",
"channels": ["email", "sms"],
"customer": {
"name": "Maria Lopez",
"email": "maria@example.com",
"phone": "+14155550142"
},
"order": { "id": "1042", "total": "$38.50" },
"business_name": "Bella's Kitchen",
"idempotency_key": "order-1042-ready"
}Supported events: order_received, order_confirmed, order_processing, order_ready, order_shipped, order_delivered, payment_received, booking_confirmed, booking_cancelled, password_reset, welcome.
Contacts
/api/public/v1/contactsUpserts by email or phone. Use it to keep your GetQuiver audience in sync with your own database.
{
"email": "maria@example.com",
"phone": "+14155550142",
"first_name": "Maria",
"tags": ["vip", "pickup"],
"sms_opt_in": true
}Webhooks
Delivery events from Resend and Twilio are ingested automatically and applied to the matching message, updating status and campaign counters.
/api/public/v1/webhooks/resend/api/public/v1/webhooks/twilioSignatures are verified with your provider secret, timestamps older than five minutes are rejected, and duplicate event ids are ignored — so retries are safe.
Errors & limits
Errors use a consistent envelope with a machine-readable code.
{
"success": false,
"request_id": "req_8f2c1d4b9a7e",
"error": {
"code": "validation_error",
"message": "One or more fields are invalid.",
"details": [{ "field": "to", "message": "Required" }]
}
}| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key | No bearer token supplied |
| 401 | invalid_api_key | Key is unknown or revoked |
| 402 | insufficient_credits | Workspace is out of credits |
| 422 | validation_error | Request body failed validation |
| 429 | rate_limited | Over 120 requests per minute |
| 500 | internal_error | Unexpected failure — safe to retry |
Rate limit: 120 requests per minute per API key. Retry after the window resets, or batch through the bulk endpoints.