Skip to content

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.

OwnerResponsibility
Your applicationChoose stable contact IDs, submit contacts, expose an HTTPS callback endpoint, verify signatures, persist outcomes idempotently, and reconcile failed deliveries.
PulseValidate and deduplicate submissions, schedule calls, determine terminal outcomes, sign callbacks, and retain delivery failures for reconciliation.
Vox CLI and skillConfigure 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:

IdentifierOwned byQuestion it answers
process_idDinodialWhich running Drip process owns this contact?
unique_idYouWhich record in your system is this, and is this submission a retry?
dd_pulse_idDinodialWhich 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.

StatusMeaning
answeredThe receiver answered and the call concluded.
no_answerThe call rang but nobody engaged before termination.
busyThe destination line was busy.
failure_tA transport or carrier-side failure, such as a connection timeout or rejected number.
failure_pA Dinodial platform, partner, infrastructure, or control-plane failure.
failedAn unclassified failure when no more specific reason is available.
dnd_blockedThe 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:

  1. Pulse sends a signed JSON POST to the contact's callback URL.
  2. Your endpoint verifies the signature on the raw body.
  3. Your application records the outcome using dd_pulse_id as the deduplication key.
  4. Your endpoint returns a 2xx only 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:

  1. Read unacknowledged failures with GET /failed-callbacks.
  2. If failures is empty, the process is currently caught up.
  3. For each entry, apply its embedded payload using the same validation and dd_pulse_id deduplication used by the normal webhook handler.
  4. Commit those outcomes to your database before acknowledging anything.
  5. Send only the successfully committed failure_id values to POST /failed-callbacks/ack.
  6. Repeat until a read returns an empty failures array.

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.tech

API requests use:

http
X-Api-Key: <account-api-key>
Content-Type: application/json

Dinodial provisions the running process and transfers its process_id and callback secret securely.

Endpoints

Method and pathRequest bodySuccess response
POST /api/drip-processes/{process_id}/contactsArray of 1 to 500 contact objects200 with one accepted-contact object per input, in the same order
GET /api/drip-processes/{process_id}/failed-callbacksNone200 with { "failures": [...] }
POST /api/drip-processes/{process_id}/failed-callbacks/ack{ "failure_ids": [...] }, containing 1 to 100 IDs200 with { "acknowledged": number }
POST /api/drip-processes/{process_id}/closeEmpty204 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"
    }
  }
]
FieldMeaningRequirement
unique_idYour record key and the idempotency key for submissionRequired, trimmed, non-empty, at most 128 characters, and unique within this request
phone_numberDestination Pulse should callRequired valid E.164 number with an explicit country code in a supported campaign region
customer_callback_urlEndpoint that receives this contact's terminal outcomeRequired URL; use HTTPS
placeholdersPer-contact values available to the agent during the callOptional 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"
}
FieldMeaning
process_idDrip process that owns the contact
unique_idYour original correlation and submission-idempotency key
dd_pulse_idPulse contact identity and callback deduplication key
phone_numberDestination associated with the outcome
statusTerminal outcome and reason
post_toolsOptional agent-emitted structured results from the terminal flow step
lens_urlOptional 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"
      }
    }
  ]
}
FieldMeaning
failure_idReconciliation-queue identity; acknowledge this after committing the payload
dd_pulse_idContact identity; use this to deduplicate the outcome
call_idDiagnostic call reference; do not use it as the reconciliation key
http_statusNon-2xx status returned by your callback endpoint
errorNetwork error when no HTTP response was received
created_atTime Pulse recorded the failed delivery
payloadExact 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 --help

Use 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.

Dinodial customer documentation