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 whole journey
One eligible contact crosses the integration boundary twice: first as a request to call, then as one terminal outcome coming back.
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 strict per-process contact IDs, 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? |
dd_pulse_id | Dinodial | Which exact Pulse contact produced this callback? |
unique_id is scoped to one process. Any value already present in that process rejects the entire insertion request with 409 Conflict, even when the submitted contact data is identical. The same value may be used in a different process.
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.
A terminal outcome without a call
DND is a pre-dial gate. A blocked contact skips the calling runtime but still completes through the same customer outcome channel.
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.
The durability boundary
The replay isolates the ordering rule: read, save durably, then acknowledge.
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.
Both reconciliation operations are metered at the minimum catalog rate of 100,000 CU per record unit. A successful read uses one unit for each returned failure, with a one-unit minimum when the result is empty. A successful acknowledgement uses one unit for each submitted failure_id; idempotent retries carry the same submitted-ID count.
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-Process-Id: <process-uuid>
X-Dinodial-Pulse-Signature: t=<unix-seconds>,v1=<lowercase-hex-hmac>The signed input is:
text
HMAC-SHA256(callback_secret, timestamp + "." + raw_request_body)Use the process-ID header only to select the candidate process secret. Verify the exact raw bytes before parsing JSON, compare the HMAC in constant time, reject future timestamps, and enforce a short freshness window. After verification, require the signed body's process_id to equal the header value. 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 OK with an empty body |
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 process-scoped record key | Required, trimmed, non-empty, at most 128 characters, and not previously used in this process or repeated 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. Insertion is atomic: one missing, duplicate, conflicting, or otherwise invalid contact rejects the full array and inserts nothing. 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 process-scoped correlation 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"
]
}Success returns 200 OK with an empty body. Repeating the same request also returns 200 OK; IDs already acknowledged, unknown IDs, and IDs belonging to another process are ignored. Confirm that reconciliation is caught up with the next GET /failed-callbacks response rather than an acknowledgement count.
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.
Integrate outcome callbacks
The Drip callback integration guide covers receiver deployment, secret handling, process creation, a rehearsal call, live verification, failed-delivery recovery, and production operations. A downloadable reference receiver is linked from the guide; its source is not reproduced in the documentation.