Consuming Interactions data via Pub/Sub
Connecting to Pub/Sub
Downstream destinations can choose to build a custom application to pull and process Interactions data from Pub/Sub or utilize Google's cloud-native solutions.
Building your own subscriber
Google provides client libraries in many languages with documentation and examples as well as documentation around best practices. Be sure to review this documentation if building custom application logic to subscribe to Interactions Pub/Sub data.
ℹ️ DDx will need the GCP service account name that will be used to pull from our GCP Pub/Sub subscription so we can grant the appropriate permissions.
Useful Links
Dataflow
Google also offers a fully-managed, scalable streaming platform called Dataflow that natively connects to Pub/Sub for comprehensive data streaming solutions.
Dataflow can be used to build comprehensive pipelines that can help you with:
- Ingesting streaming data from Pub/Sub, transforming it, and loading it into BigQuery, Cloud Storage, or other sinks.
- Consuming event data from Pub/Sub and performing analytics or triggering workflows.
Useful Links
Note: Google recommends specific Pub/Sub subscription settings when using Dataflow. Please let the DDx API Team know if you're planning on using Dataflow so subscriptions can be set up accordingly.
BigQuery
DDx can configure your destination subscription to write data to a BigQuery table in your GCP project.
Information needed for setup:
- GCP project name
- BigQuery dataset name
- BigQuery table name
ℹ️ Note: destinations will need to grant DDx's GCP service account specific permissions to the target table as detailed in GCP documentation here.
BigQuery table structure
If you have a BigQuery table ingesting messages from the Interactions API using a BigQuery subscription, it is recommended to set the column accepting the message body (data) or JSON objects within the message body to the STRING data type to prevent potential data loss. data may contain special characters which cannot be parsed by BigQuery's JSON column type; messages that fail to parse will bypass the specified table and instead be forwarded to the associated deadletter topic.
Google Cloud Storage (GCS)
DDx can configure your destination subscription to write data to a Google Cloud Storage (GCS) bucket in your GCP project.
Information needed for setup:
- Destination project and bucket name
- Format (text or avro)
- Any desired prefixes or suffixes (optional)
- Any preferences around batch delivery, if any (docs). Otherwise we'll use our defaults.
ℹ️ Note: destinations will need to grant DDx's GCP service account specific permissions to the target GCS bucket as detailed in GCP documentation here.
General notes on Pub/Sub
Pub/Sub Subscription configuration
There are many configuration options available in Pub/Sub. Retry policies, deadlettering, and message acknowledgement deadlines are some examples. Please reach out to DDx API Support at [email protected] or in a joint Slack channel with questions or requests regarding Pub/Sub configuration.
Data Format in Pub/Sub
Data is sent to destinations via Pub/Sub in format identical to the Interactions API's /interactions POST endpoint with a light wrapper that includes useful metadata about the data within.
Note that the jsonMetadata property is percent-encoded as a string and is not standard JSON.
{
"source": {
"publicKeyId": "32",
"name": "Voter Tool XYZ",
"workspace": {
"workspaceId": 12345,
"displayName": "Poppy for Governor",
"type": "Organization"
}
},
"correlationId": "11534196688560387915",
"messageIngressTimeUtc": "2025-09-19T15:11:22.199771Z",
"interaction": {
"person": [
{
"id": "123456789",
"type": "VAN"
},
{
"id": "12345",
"type": "rallyPersonId"
},
{
"id": "89076",
"type": "dwid"
}
],
"channel": {
"type": "phone_number",
"value": "\u002B14155552671"
},
"stateCode": "CO",
"attemptDateTime": "2025-09-10T22:11:48.662Z",
"method": "text",
"committee": [
{
"type": "MyCRM",
"id": "poppy-for-governor"
}
],
"canvasser": [
{
"type": "rallyUserId",
"id": "99999"
}
],
"vendorSource": "Blue Text",
"outcome": "successful_contact",
"outcomesDetailed": [
{
"type": "survey_response",
"value": {
"questionId": null,
"questionText": "Are you able to attend the Weekend Rally in Cambridge?",
"responseId": null,
"responseText": "yeah"
}
},
{
"type": "activist_code",
"value": {
"activistCodeId": null,
"text": "123"
}
},
{
"type":"communication_consent",
"value":{
"consentStatus": "opted_out"
}
},
{
"type": "other_type",
"value":
{
"id": "123",
"moreInfo":
{
"dayOfWeek": "wed",
"otherProp": "yes"
}
}
}
],
"threadId": null,
"jsonMetadata": "{\n \u0022event\u0022: \u0022weekend canvass cambridge\u0022,\n \u0022rsvp\u0022: \u0022no\u0022\n}",
"vanFields": {
"contactTypeId": "1",
"resultCodeId": "14"
},
}
}{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "InteractionPublishRequest",
"type": "object",
"description": "Request object for publishing an interaction to Pub/Sub.",
"additionalProperties": false,
"properties": {
"source": {
"x-nullable": true,
"$ref": "#/definitions/InteractionSource"
},
"correlationId": {
"type": "string"
},
"messageIngressTimeUtc": {
"type": "string",
"format": "date-time"
},
"interaction": {
"x-nullable": true,
"$ref": "#/definitions/InteractionDto"
}
},
"definitions": {
"InteractionSource": {
"type": "object",
"additionalProperties": false,
"properties": {
"publicKeyId": {
"type": "string"
},
"name": {
"type": "string"
},
"workspace": {
"$ref": "#/definitions/SimpleWorkspaceDto"
}
}
},
"SimpleWorkspaceDto": {
"type": "object",
"additionalProperties": false,
"properties": {
"workspaceId": {
"type": "integer",
"format": "int32"
},
"displayName": {
"type": "string"
},
"type": {
"$ref": "#/definitions/WorkspaceTypeDto"
}
}
},
"WorkspaceTypeDto": {
"type": "string",
"description": "Indicates the type of workspace.",
"enum": [
"Unknown",
"Personal",
"Organization"
]
},
"InteractionDto": {
"type": "object",
"description": "Represents a single interaction record containing voter outreach attempt data.",
"additionalProperties": false,
"required": [
"person",
"stateCode",
"attemptDateTime",
"method",
"committee",
"vendorSource",
"outcome"
],
"properties": {
"interactionId": {
"type": "string",
"description": "Optional ID used to identify a specific interaction.\nThis ID should only be provided in the initial request when re-sending data, and must use DDx-provided\ninteractionId value. If an existing ID is not provided to mark the interaction as a retry, Interactions API\nwill populate this ID field and include it in the response for future use if the interaction needs to be re-sent.",
"format": "guid"
},
"person": {
"type": "array",
"description": "Array of person identifiers representing all IDs from any vendor system for the person interacted with. Each identifier can\ncontain an ID and type of ID indicating source system. At least one ID is required, maximum of 100 IDs per interaction.\nAn ID of type 'VAN' must be present if VAN is included as a destination. Each person identifier type can appear at most once\nin the array (i.e. no duplicate types are allowed).",
"items": {
"$ref": "#/definitions/PersonIdentifier"
}
},
"channel": {
"description": "Channel-of-contact details used during the outreach attempt.\nIf the `type` is recognized as an Address Channel (type = \"address\"), the `value` will be deserialized to that object type.\nOther string-based types have specific validation documented in the \"Channel\" section below, like \"phone_number\".\nIf the `type` is unrecognized, the payload will be deserialized to an unvalidated, loosely typed object for flexibility.",
"x-nullable": true,
"$ref": "#/definitions/IChannelDetails"
},
"stateCode": {
"type": "string",
"description": "2-character US state postal abbreviation (e.g., AL, WY).\nMust be a valid uppercase state code.",
"maxLength": 2,
"minLength": 2
},
"attemptDateTime": {
"type": "string",
"description": "The date and time when the outreach attempt occurred, as reported by the vendor\nand normalized to Coordinated Universal Time (UTC).",
"format": "date-time",
"minLength": 1
},
"method": {
"description": "The standardized communication channel used to reach a constituent during an outreach attempt.\nExamples include phone, door, SMS, email, etc.",
"$ref": "#/definitions/ContactMethod"
},
"committee": {
"type": "array",
"description": "Array of entities (campaign, state party, etc.) that conducted or logged the outreach attempt.\nCommittee objects consist of a type and an identifier, and at least one committee must be provided. ",
"items": {
"$ref": "#/definitions/CommitteeDetails"
}
},
"canvasser": {
"type": "array",
"description": "Array of canvasser details for the individual who conducted the outreach attempt, as reported by the vendor.\nOptional object containing identifier and identifier type.",
"x-nullable": true,
"items": {
"$ref": "#/definitions/CanvasserDetails"
}
},
"vendorSource": {
"type": "string",
"description": "Canonical name of the vendor or tool used to facilitate the outreach attempt.\nThis field reflects the platform through which the contact was made, not necessarily where the data was ingested from.",
"minLength": 1
},
"outcome": {
"description": "Standardized result of the outreach attempt, representing what happened during the interaction.\nExamples include successful_contact, not_home, refused, etc.",
"$ref": "#/definitions/Outcome"
},
"outcomesDetailed": {
"type": "array",
"description": "Optional array of objects that provide additional context around the results of an interaction, such as activist codes or survey responses.\nIf the type is recognized as an Activist Code Outcome (type = \"activist_code\"),\nCommunication Consent Outcome (type = \"communication_consent\"), or Survey Response Outcome\n(type = \"survey_response\") it will be deserialized to that type.\nIf the type is unrecognized, it will be deserialized to an unvalidated, loosely typed object for flexibility.",
"x-nullable": true,
"items": {
"$ref": "#/definitions/IOutcomeDetails"
}
},
"threadId": {
"type": "string",
"description": "Unique identifier for interactions that took place during the same conversation or set of conversations.\nUsed to group related interactions together. Maximum length of 100 characters.",
"maxLength": 100,
"minLength": 0,
"x-nullable": true
},
"jsonMetadata": {
"type": "string",
"description": "Additional metadata in JSON format related to the interaction.\nMust be valid JSON when provided. Can contain arbitrary structured data relevant to the interaction.",
"x-nullable": true
},
"vanFields": {
"description": "VAN-specific data for an interaction. Must be provided if the Interactions API Key used on the request\nis configured with VAN as a destination.",
"x-nullable": true,
"$ref": "#/definitions/InteractionVanFieldsDto"
}
}
},
"PersonIdentifier": {
"type": "object",
"description": "Represents a person involved in an interaction, including their identifier and optional contact information.",
"additionalProperties": false,
"required": [
"id",
"type"
],
"properties": {
"id": {
"type": "string",
"description": "A primary identifier for tracking individual voters. This can be an integer (e.g. for DNC person_ids),\na string (for UUIDs, phone numbers), or other identifier formats depending on the source system.\nUsed to link voter records across different data sources and track in-state voter history over time.",
"minLength": 1
},
"type": {
"type": "string",
"description": "Type of identifier or source system that generated the id.\nExamples include \"VAN\", \"phone\", \"slimCRM\", \"DNC\", \"SOS\", etc. \nThis field indicates the origin or authority of the person identifier.",
"minLength": 1
}
}
},
"IChannelDetails": {
"type": "object",
"discriminator": {
"propertyName": "type"
},
"x-abstract": true,
"additionalProperties": false,
"required": [
"value",
"type"
],
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string",
"description": "Optional; unique identifier for any known id for the channel `type`",
"x-nullable": true
},
"value": {}
}
},
"AddressChannelDetails": {
"allOf": [
{
"$ref": "#/definitions/ChannelDetailsOfAddress"
},
{
"type": "object",
"description": "Represents that an address was used for an interaction.",
"additionalProperties": {},
"required": [
"type",
"value"
],
"properties": {
"type": {
"type": "string",
"description": "Specifies that the `type` of channel used was an `address` and a full address must be provided as the `value`.",
"minLength": 1
},
"value": {
"description": "Represents a postal address used for identifying a specific residence or mailing destination. ",
"$ref": "#/definitions/Address"
}
}
}
]
},
"Address": {
"type": "object",
"additionalProperties": false,
"properties": {
"addressLine1": {
"type": "string",
"description": "The primary street address (e.g., house number and street name)."
},
"addressLine2": {
"type": "string",
"description": "The secondary address information (e.g., apartment, suite, unit number).",
"x-nullable": true
},
"city": {
"type": "string",
"description": "The city of the address."
},
"state": {
"type": "string",
"description": "The state or region of the address."
},
"postalCode": {
"type": "string",
"description": "The postal (ZIP) code for the address. Can accept ZIP5, ZIP+4, or ZIP9 format."
}
}
},
"ChannelDetailsOfAddress": {
"type": "object",
"x-abstract": true,
"additionalProperties": {},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"minLength": 1
},
"id": {
"type": "string",
"x-nullable": true
},
"value": {
"$ref": "#/definitions/Address"
}
}
},
"ContactMethod": {
"type": "string",
"description": "",
"enum": [
"unknown",
"mail",
"letter",
"digital_ad",
"email",
"text",
"text_broadcast",
"robo_call",
"dialer_call",
"phone_call",
"door_knock",
"Event",
"hot_spot",
"one_on_one",
"web_interaction"
]
},
"CommitteeDetails": {
"type": "object",
"description": "Represents details for the entities that conducted or logged the outreach attempt.",
"additionalProperties": false,
"required": [
"type",
"id"
],
"properties": {
"type": {
"type": "string",
"description": "Type of the committee identifier specified in 'id', representing the source context of the interaction.",
"minLength": 1
},
"id": {
"type": "string",
"description": "The actual committee identifier itself.",
"minLength": 1
}
}
},
"CanvasserDetails": {
"type": "object",
"description": "Represents canvasser details for the individual who conducted the outreach attempt.",
"additionalProperties": false,
"required": [
"type",
"id"
],
"properties": {
"type": {
"type": "string",
"description": "Type of the canvasser identifier specified in 'id'.\nRequired when Canvasser is provided.",
"minLength": 1
},
"id": {
"type": "string",
"description": "The actual canvasser identifier used during the outreach attempt.\nExamples: user ID, employee number, volunteer ID, etc.\nRequired when Canvasser is provided.",
"minLength": 1
}
}
},
"Outcome": {
"type": "string",
"description": "",
"enum": [
"unknown",
"clicked_link",
"email_opened",
"submitted_form",
"successful_contact",
"deceased",
"deliverability_error",
"permanently_undeliverable",
"inaccessible",
"language_barrier",
"moved",
"no_answer",
"wrong_number",
"hostile",
"marked_spam",
"refused",
"delivered",
"lit_drop",
"left_message",
"other"
]
},
"IOutcomeDetails": {
"type": "object",
"discriminator": {
"propertyName": "type"
},
"x-abstract": true,
"additionalProperties": false,
"required": [
"value",
"type"
],
"properties": {
"type": {
"type": "string",
"description": "This field categorizes the type of detailed outcome information being provided."
},
"value": {}
}
},
"ActivistCodeOutcomeDetails": {
"allOf": [
{
"$ref": "#/definitions/OutcomeDetailsOfActivistCode"
},
{
"type": "object",
"description": "Represents an activist code (e.g. affiliation, activity, or interest) acquired during an interaction.",
"additionalProperties": {},
"required": [
"value"
],
"properties": {
"type": {
"type": "string"
},
"value": {
"$ref": "#/definitions/ActivistCode"
}
}
}
]
},
"ActivistCode": {
"type": "object",
"additionalProperties": false,
"required": [
"text"
],
"properties": {
"activistCodeId": {
"type": "string",
"description": "Unique identifier for an activist code;\nthe value may be retrieved from the system that sourced the activist code ",
"x-nullable": true
},
"text": {
"type": "string",
"description": "The question or information prompted during the interaction",
"minLength": 1
}
}
},
"OutcomeDetailsOfActivistCode": {
"type": "object",
"x-abstract": true,
"additionalProperties": {},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"description": "This field categorizes the type of detailed outcome information being provided.",
"minLength": 1
},
"value": {
"$ref": "#/definitions/ActivistCode"
}
}
},
"SurveyResponseOutcomeDetails": {
"allOf": [
{
"$ref": "#/definitions/OutcomeDetailsOfSurveyResponse"
},
{
"type": "object",
"description": "Represents a survey question and response obtained from an interaction.",
"additionalProperties": {},
"required": [
"value"
],
"properties": {
"type": {
"type": "string"
},
"value": {
"$ref": "#/definitions/SurveyResponse"
}
}
}
]
},
"SurveyResponse": {
"type": "object",
"additionalProperties": false,
"required": [
"questionText",
"responseText"
],
"properties": {
"questionId": {
"type": "string",
"description": "Unique identifier for the survey question being asked;\nthe value may be retrieved from the system that sourced the question ",
"x-nullable": true
},
"questionText": {
"type": "string",
"description": "The question text presented during an interaction",
"minLength": 1
},
"responseId": {
"type": "string",
"description": "Unique identifier for the survey response associated with the question;\nthe value may be retrieved from the system that sourced the question ",
"x-nullable": true
},
"responseText": {
"type": "string",
"description": "An answer to the question.\nRequired when Survey Question text is provided.",
"minLength": 1
}
}
},
"OutcomeDetailsOfSurveyResponse": {
"type": "object",
"x-abstract": true,
"additionalProperties": {},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"description": "This field categorizes the type of detailed outcome information being provided.",
"minLength": 1
},
"value": {
"$ref": "#/definitions/SurveyResponse"
}
}
},
"CommunicationConsentOutcomeDetails": {
"allOf": [
{
"$ref": "#/definitions/OutcomeDetailsOfCommunicationConsent"
},
{
"type": "object",
"description": "Represents a communication consent response (\"opted_in\" or \"opted_out\").",
"additionalProperties": {},
"required": [
"value"
],
"properties": {
"type": {
"type": "string"
},
"value": {
"$ref": "#/definitions/CommunicationConsent"
}
}
}
]
},
"CommunicationConsent": {
"type": "object",
"additionalProperties": false,
"required": [
"consentStatus"
],
"properties": {
"consentStatus": {
"description": "Communication consent status (\"opted_in\" or \"opted_out\").",
"$ref": "#/definitions/CommunicationConsentStatus"
}
}
},
"CommunicationConsentStatus": {
"type": "string",
"description": "",
"enum": [
"opted_in",
"opted_out"
]
},
"OutcomeDetailsOfCommunicationConsent": {
"type": "object",
"x-abstract": true,
"additionalProperties": {},
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"description": "This field categorizes the type of detailed outcome information being provided.",
"minLength": 1
},
"value": {
"$ref": "#/definitions/CommunicationConsent"
}
}
},
"InteractionVanFieldsDto": {
"type": "object",
"description": "VAN-specific data for an interaction. Must be provided if if the Interactions API Key used on the request\nis configured with VAN as a destination.",
"additionalProperties": false,
"properties": {
"contactTypeId": {
"type": "string",
"description": "VAN contact type ID. Must match a Contact Type accessible to the destination VAN committee.\nSee https://docs.ngpvan.com/reference/canvassresponsescontacttypes for details on retrieving accessible contact types."
},
"resultCodeId": {
"type": "string",
"description": "VAN contact attempt result code, representing the result of that contact attempt. Result code availibility varies based on\ncontact type, so this value must represent a code that is available to the Contact Type ID specified above.\nSee https://docs.ngpvan.com/reference/canvassresponsesresultcodes to view Result Codes accessible to the destination VAN committee.",
"x-nullable": true
}
}
}
}
}Updated 10 days ago
