Appearance
Drip integration
Preview contract
This site describes the current candidate contract. Confirm rollout status with Dinodial before using it against a production campaign.
Drip is the boundary between your contact workflow and Dinodial's calling system. Your application submits contacts to a running process. Pulse schedules and places the calls, then reports one terminal outcome to the callback URL supplied with each contact.
This guide explains the contract and the decisions an integration must make. Use the Vox CLI or the Dinodial skill for setup, manual testing, and recovery operations; use the HTTP API from your application in production.
The ownership model
The integration is reliable when each side owns a small, explicit responsibility.
| Owner | Responsibility |
|---|---|
| Your application | Choose stable contact IDs, submit contacts, expose an HTTPS callback endpoint, verify signatures, persist outcomes idempotently, and reconcile failed deliveries. |
| Pulse | Validate and deduplicate submissions, schedule calls, determine terminal outcomes, sign callbacks, and retain delivery failures for reconciliation. |
| Vox CLI and skill | Configure workspaces, validate manual input, exercise the API safely, and perform explicit recovery actions. |
The CLI is the preferred interface for human operations. The HTTP contract exists for application-to-application integration; the documentation does not duplicate every CLI flag or confirmation flow.
The identity model
Three identifiers answer different questions:
| Identifier | Owned by | Question it answers |
|---|---|---|
process_id | Dinodial | Which running Drip process owns this contact? |
unique_id | You | Which record in your system is this, and is this submission a retry? |
dd_pulse_id | Dinodial | Which exact Pulse contact produced this callback? |
unique_id is scoped to one process. Retrying the same ID with identical contact data returns the original dd_pulse_id and does not place another call. Reusing it with different data is a conflict.
Persist dd_pulse_id when the contact is accepted. Use it as the callback deduplication key. Keep unique_id as the correlation key into your own system.
There is no event_id. A callback also has no separate termination_reason; status is the terminal reason.
The outcome model
Every callback status is terminal. Your application should not infer additional intermediate states from callback timing.
| Status | Meaning |
|---|---|
answered | The receiver answered and the call concluded. |
no_answer | The call rang but nobody engaged before termination. |
busy | The destination line was busy. |
failure_t | A transport or carrier-side failure, such as a connection timeout or rejected number. |
failure_p | A Dinodial platform, partner, infrastructure, or control-plane failure. |
failed | An unclassified failure when no more specific reason is available. |
dnd_blocked | The contact was blocked by the DND check before a call was placed. |
Only an answered call can include a Lens review URL. Post-call tool output is included when the agent emitted it; otherwise the field is absent.
Delivery and reconciliation
Callback handling is deliberately idempotent:
- Pulse sends a signed JSON
POSTto the contact's callback URL. - Your endpoint verifies the signature on the raw body.
- Your application records the outcome using
dd_pulse_idas the deduplication key. - Your endpoint returns a
2xxonly after that record is durable.
Pulse makes one HTTP attempt for a callback duty. A recovered duty may be observed more than once, so a callback endpoint must tolerate duplicates even when the normal case is one delivery.
A network failure or non-2xx response is retained for explicit reconciliation. Reconciliation exists because an HTTP sender cannot know that your application durably stored an outcome merely because it attempted delivery. Pulse therefore preserves the failed attempt until your application explicitly confirms that it has applied the payload.
Reading failures and acknowledging them are separate operations by design. A failed read, worker crash, or database rollback cannot remove an outcome before your application is ready.
Reconciliation procedure
Run this procedure independently for each Drip process:
- Read unacknowledged failures with
GET /failed-callbacks. - If
failuresis empty, the process is currently caught up. - For each entry, apply its embedded
payloadusing the same validation anddd_pulse_iddeduplication used by the normal webhook handler. - Commit those outcomes to your database before acknowledging anything.
- Send only the successfully committed
failure_idvalues toPOST /failed-callbacks/ack. - Repeat until a read returns an empty
failuresarray.
Each read returns at most 100 entries, oldest first. Reads are non-destructive, so the same entries remain visible until acknowledged. Acknowledgement is idempotent: retrying IDs that were already acknowledged is a successful no-op.
The embedded payload arrives through the authenticated Pulse API; it is not a second webhook request and has no callback signature header. Treat it as the outcome Pulse attempted to deliver, while retaining the same schema validation and idempotent persistence rules.
Never acknowledge an entry before its outcome is committed. Otherwise a crash between acknowledgement and persistence would permanently discard the only recoverable copy.
Signature reasoning
The callback secret and the API key have different roles:
- The API key authenticates requests your application sends to Pulse.
- The per-process callback secret authenticates callbacks Pulse sends to you.
Pulse sends:
http
X-Dinodial-Pulse-Signature: t=<unix-seconds>,v1=<lowercase-hex-hmac>The signed input is:
text
HMAC-SHA256(callback_secret, timestamp + "." + raw_request_body)Verify the exact raw bytes before parsing JSON. Compare the HMAC in constant time, reject future timestamps, and enforce a short freshness window appropriate for your clock synchronization and network path. The skill can generate framework-specific verification code; the wire rule above remains the source of truth.
Never expose the callback secret in browser code, logs, examples, or support messages.
Protocol reference
The Pulse API base URL is:
text
https://pulse.dinodial.techAPI requests use:
http
X-Api-Key: <account-api-key>
Content-Type: application/jsonDinodial provisions the running process and transfers its process_id and callback secret securely.
Endpoints
| Method and path | Request body | Success response |
|---|---|---|
POST /api/drip-processes/{process_id}/contacts | Array of 1 to 500 contact objects | 200 with one accepted-contact object per input, in the same order |
GET /api/drip-processes/{process_id}/failed-callbacks | None | 200 with { "failures": [...] } |
POST /api/drip-processes/{process_id}/failed-callbacks/ack | { "failure_ids": [...] }, containing 1 to 100 IDs | 200 with { "acknowledged": number } |
POST /api/drip-processes/{process_id}/close | Empty | 204 No Content |
Contact shape
The contact endpoint accepts a JSON array:
json
[
{
"unique_id": "crm-contact-000123",
"phone_number": "+12025550123",
"customer_callback_url": "https://callbacks.example.test/dinodial/outcomes",
"placeholders": {
"first_name": "Taylor"
}
}
]| Field | Meaning | Requirement |
|---|---|---|
unique_id | Your record key and the idempotency key for submission | Required, trimmed, non-empty, at most 128 characters, and unique within this request |
phone_number | Destination Pulse should call | Required valid E.164 number with an explicit country code in a supported campaign region |
customer_callback_url | Endpoint that receives this contact's terminal outcome | Required URL; use HTTPS |
placeholders | Per-contact values available to the agent during the call | Optional object keyed by the agent's exact declared placeholder names |
The body is an array because one request may submit multiple independent contacts. Each contact can use a different callback URL and placeholder set.
The response order matches the request order:
json
[
{
"unique_id": "crm-contact-000123",
"dd_pulse_id": "0198d6ca-65f3-7c11-9b9a-7c3a326e041a"
}
]unique_id is echoed so you can match responses without depending only on array position. Store the returned dd_pulse_id; it is the contact identity used in callbacks and reconciliation.
Callback shape
json
{
"process_id": "0198d6c4-1d83-73fe-b83b-14ebc6172169",
"unique_id": "crm-contact-000123",
"dd_pulse_id": "0198d6ca-65f3-7c11-9b9a-7c3a326e041a",
"phone_number": "+12025550123",
"status": "answered",
"post_tools": [
{
"name": "call_summary",
"args": {
"summary": "The customer requested a follow-up."
},
"flow": {
"stage": "follow_up",
"outcome": "completed",
"post": "summary"
}
}
],
"lens_url": "https://lens.example.test/review/example-token"
}| Field | Meaning |
|---|---|
process_id | Drip process that owns the contact |
unique_id | Your original correlation and submission-idempotency key |
dd_pulse_id | Pulse contact identity and callback deduplication key |
phone_number | Destination associated with the outcome |
status | Terminal outcome and reason |
post_tools | Optional agent-emitted structured results from the terminal flow step |
lens_url | Optional signed review URL; only available for answered calls with an archive |
Each post_tools entry contains the tool name, its JSON args, and a flow context. flow.stage identifies the agent stage; optional flow.outcome and flow.post identify the terminal branch and post step that emitted it.
post_tools is omitted when empty. lens_url is present only for answered calls with a review archive.
Failed-delivery response
json
{
"failures": [
{
"failure_id": "0198d6df-4fba-7ab7-a270-b986b06a4d39",
"dd_pulse_id": "0198d6ca-65f3-7c11-9b9a-7c3a326e041a",
"call_id": "0198d6d4-2573-7c58-a6c6-6d0837284c39",
"http_status": 503,
"created_at": "2026-08-08T10:20:00Z",
"payload": {
"process_id": "0198d6c4-1d83-73fe-b83b-14ebc6172169",
"unique_id": "crm-contact-000123",
"dd_pulse_id": "0198d6ca-65f3-7c11-9b9a-7c3a326e041a",
"phone_number": "+12025550123",
"status": "no_answer"
}
}
]
}| Field | Meaning |
|---|---|
failure_id | Reconciliation-queue identity; acknowledge this after committing the payload |
dd_pulse_id | Contact identity; use this to deduplicate the outcome |
call_id | Diagnostic call reference; do not use it as the reconciliation key |
http_status | Non-2xx status returned by your callback endpoint |
error | Network error when no HTTP response was received |
created_at | Time Pulse recorded the failed delivery |
payload | Exact callback JSON Pulse attempted to deliver |
http_status and error are mutually exclusive: Pulse either received an HTTP response or encountered a network failure before receiving one.
Acknowledgement payload
After the corresponding outcomes are committed, send their failure IDs:
json
{
"failure_ids": [
"0198d6df-4fba-7ab7-a270-b986b06a4d39"
]
}The response reports how many previously unacknowledged rows changed state:
json
{
"acknowledged": 1
}Repeating the same request returns acknowledged: 0. IDs already acknowledged, unknown IDs, and IDs belonging to another process are ignored.
Use the CLI for manual operations
The CLI owns workspace authentication, typed validation, file loading, confirmations, and structured output. Discover the current command contract from the installed binary:
sh
vox process place-call --help
vox process add-drip-contacts --help
vox process failed-callbacks --help
vox process ack-failed-callbacks --help
vox process close-drip --helpUse place-call for one manual contact and add-drip-contacts for a validated JSON file. Use the failed-callback commands as a pair: read first, apply outcomes durably, then acknowledge only the applied failure IDs.