Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
*.md
LICENSE
example/
target/
.env
.env.*
*.log
58 changes: 58 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Publish to Maven Central

on:
push:
tags:
- 'release/v*'
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'

- name: Import GPG key
run: echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

- name: Configure Maven settings
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'EOF'
<settings>
<servers>
<server>
<id>central</id>
<username>${env.MAVEN_CENTRAL_USERNAME}</username>
<password>${env.MAVEN_CENTRAL_TOKEN}</password>
</server>
</servers>
<profiles>
<profile>
<id>gpg</id>
<properties>
<gpg.passphrase>${env.GPG_PASSPHRASE}</gpg.passphrase>
</properties>
</profile>
</profiles>
<activeProfiles>
<activeProfile>gpg</activeProfile>
</activeProfiles>
</settings>
EOF

- name: Build and publish
run: ./mvnw deploy -DskipTests
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,6 @@ yarn-error.log*

# Test files
test-image.jpg

# Claude
.claude/
223 changes: 206 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ A Kotlin/Java client library for interacting with the [CloudContactAI](https://c
- Send SMS messages to single or multiple recipients
- Send MMS messages with images to single or multiple recipients
- Send Email campaigns to single or multiple recipients
- Brand registration and management for TCR verification
- Campaign registration and management for TCR carrier vetting
- Manage webhooks for event notifications
- Webhook signature validation for security
- Validate email addresses (valid/invalid/risky) and phone numbers (valid/invalid/landline)
- Variable substitution in messages
- Test environment support
- Comprehensive error handling
Expand All @@ -29,7 +32,7 @@ Add this dependency to your `pom.xml`:
<dependency>
<groupId>com.cloudcontactai</groupId>
<artifactId>ccai-java-sdk</artifactId>
<version>1.0.5</version>
<version>1.0.9</version>
</dependency>
```

Expand All @@ -38,7 +41,7 @@ Add this dependency to your `pom.xml`:
Add this dependency to your `build.gradle`:

```gradle
implementation 'com.cloudcontactai:ccai-java-sdk:1.0.5'
implementation 'com.cloudcontactai:ccai-java-sdk:1.0.9'
```

## Configuration
Expand Down Expand Up @@ -245,15 +248,12 @@ For optimal MMS delivery and performance:

```kotlin
import com.cloudcontactai.sdk.mms.Account
import com.cloudcontactai.sdk.mms.SignedUploadUrlRequest
import java.io.File

// Send MMS with automatic image upload (recommended)
// ── Option A: All-in-one (recommended) ─────────────────────────────────────
val mmsAccounts = listOf(
Account(
firstName = "John",
lastName = "Doe",
phone = "+15551234567"
)
Account(firstName = "John", lastName = "Doe", phone = "+15551234567")
)

val imageFile = File("path/to/image.jpg")
Expand All @@ -262,17 +262,82 @@ val mmsResponse = ccai.mms.sendWithImage(
message = "Check out this image!",
title = "MMS Campaign",
imageFile = imageFile
// optional: senderPhone = "+15559990000"
)

// Response ID may be in campaignId or id field
val responseId = mmsResponse.campaignId ?: mmsResponse.id
println("MMS sent with ID: ${responseId}")

// ── Option B: Manual workflow (step-by-step) ────────────────────────────────

// Step 1 — Get a pre-signed S3 upload URL
val uploadRequest = SignedUploadUrlRequest(fileName = "image.jpg", fileType = "image/jpeg")
val uploadResponse = ccai.mms.getSignedUploadUrl(uploadRequest)

// Step 2 — Upload the image to S3
ccai.mms.uploadImageToSignedUrl(uploadResponse.signedS3Url, imageFile, "image/jpeg")

// Step 3 — (Optional) Confirm the file is available
val stored = ccai.mms.checkFileUploaded(uploadResponse.fileKey!!)
println("File URL: ${stored.storedUrl}")

// Step 4a — Send to multiple recipients using the uploaded fileKey
val bulkResponse = ccai.mms.send(
accounts = mmsAccounts,
message = "Hello ${firstName}!",
title = "MMS Campaign",
pictureFileKey = uploadResponse.fileKey!!
// optional: senderPhone = "+15559990000"
)

// Step 4b — Send to a single recipient
val singleResponse = ccai.mms.sendSingle(
firstName = "John",
lastName = "Doe",
phone = "+15551234567",
message = "Hello ${firstName}!",
title = "MMS Campaign",
pictureFileKey = uploadResponse.fileKey!!
// optional: senderPhone = "+15559990000"
)
```

#### Contact Validator

Validate email addresses and phone numbers.

```kotlin
import com.cloudcontactai.sdk.contactvalidator.PhoneInput

// Validate a single email
val emailResult = ccai.contactValidator.validateEmail("user@example.com")
println(emailResult.status) // "valid" | "invalid" | "risky"
println(emailResult.metadata["safe_to_send"]) // true | false

// Validate multiple emails (up to 50)
val bulkEmails = ccai.contactValidator.validateEmails(listOf(
"user@example.com",
"bad@invalid.xyz"
))
println(bulkEmails.summary) // ValidationSummary(total=2, valid=1, invalid=1, risky=0, landline=0)

// Validate a single phone number
val phoneResult = ccai.contactValidator.validatePhone("+15551234567", countryCode = "US")
println(phoneResult.status) // "valid" | "invalid" | "landline"
println(phoneResult.metadata["carrier_type"]) // "mobile" | "landline" | "voip"

// Validate multiple phone numbers (up to 50)
val bulkPhones = ccai.contactValidator.validatePhones(listOf(
PhoneInput(phone = "+15551234567"),
PhoneInput(phone = "+15559876543", countryCode = "US")
))
println(bulkPhones.summary) // ValidationSummary(total=2, valid=1, invalid=0, risky=0, landline=1)
```

#### Webhook Management

```kotlin
import com.cloudcontactai.sdk.webhook.WebhookRequest
import com.cloudcontactai.sdk.webhook.WebhookUpdateRequest

// Create a webhook (auto-generated secret)
val webhook = ccai.webhook.create(WebhookRequest("https://your-app.com/webhooks/ccai"))
Expand All @@ -286,20 +351,27 @@ val customWebhook = ccai.webhook.create(
)
println("Webhook created with custom secret!")

// Get the webhook
val webhookDetails = ccai.webhook.get()
webhookDetails?.let {
println("Current webhook URL: ${it.url}")
println("Method: ${it.method}")
println("Secret Key: ${it.secretKey}")
// Get all webhooks
val allWebhooks = ccai.webhook.getAll()
allWebhooks.forEach { wh ->
println("Webhook ID: ${wh.id}, URL: ${wh.url}")
}

// Get a specific webhook by ID
val webhookDetails = ccai.webhook.get(webhook.id)
println("Current webhook URL: ${webhookDetails.url}")
println("Method: ${webhookDetails.method}")
println("Secret Key: ${webhookDetails.secretKey}")

// Update webhook
val updated = ccai.webhook.update(
WebhookRequest("https://your-app.com/webhooks/ccai-updated", "my-custom-secret-32chars12345")
WebhookUpdateRequest(webhook.id, "https://your-app.com/webhooks/ccai-updated", "my-custom-secret-32chars12345")
)
println("Webhook updated to: ${updated.url}")

// Delete a webhook
ccai.webhook.delete(webhook.id)

// Validate CloudContactAI webhook signature (using eventHash)
val payload = """
{
Expand Down Expand Up @@ -330,6 +402,123 @@ if (isValid) {
}
```

#### Brand Registration

```kotlin
import com.cloudcontactai.sdk.brands.BrandRequest

// Create a brand
val brand = ccai.brands.create(BrandRequest(
legalCompanyName = "Collect.org Inc.",
dba = "Collect",
entityType = "NON_PROFIT",
taxId = "123456789",
taxIdCountry = "US",
country = "US",
verticalType = "NON_PROFIT",
websiteUrl = "https://www.collect.org",
street = "123 Main Street",
city = "San Francisco",
state = "CA",
postalCode = "94105",
contactFirstName = "Jane",
contactLastName = "Doe",
contactEmail = "jane@collect.org",
contactPhone = "+14155551234"
))
println("Brand created with ID: ${brand.id}")

// Get a brand by ID
val fetched = ccai.brands.get(brand.id)
println("Website match score: ${fetched.websiteMatchScore ?: "pending"}")

// List all brands for the account
val brands = ccai.brands.list()
println("Found ${brands.size} brand(s)")

// Update a brand (partial update)
val updated = ccai.brands.update(brand.id, BrandRequest(
street = "456 Oak Avenue",
city = "Los Angeles"
))

// Delete a brand
ccai.brands.delete(brand.id)
```

**Entity Types:** `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT`, `SOLE_PROPRIETOR`

> Note: `PUBLIC_PROFIT` entities require `stockSymbol` and `stockExchange` fields.

**Vertical Types:** `AUTOMOTIVE`, `AGRICULTURE`, `BANKING`, `COMMUNICATION`, `CONSTRUCTION`, `EDUCATION`, `ENERGY`, `ENTERTAINMENT`, `GOVERNMENT`, `HEALTHCARE`, `HOSPITALITY`, `INSURANCE`, `LEGAL`, `MANUFACTURING`, `NON_PROFIT`, `PROFESSIONAL`, `REAL_ESTATE`, `RETAIL`, `TECHNOLOGY`, `TRANSPORTATION`

#### Campaign Registration

```kotlin
import com.cloudcontactai.sdk.campaigns.CampaignRequest

// Create a campaign
val campaign = ccai.campaigns.create(CampaignRequest(
brandId = 1,
useCase = "MIXED",
subUseCases = listOf("CUSTOMER_CARE", "TWO_FACTOR_AUTHENTICATION", "ACCOUNT_NOTIFICATION"),
description = "Security codes and support messaging.",
messageFlow = "Users opt-in via signup form at https://example.com/signup",
hasEmbeddedLinks = true,
hasEmbeddedPhone = false,
isAgeGated = false,
isDirectLending = false,
optInKeywords = listOf("START"),
optInMessage = "Welcome! Reply STOP to cancel.",
optInProofUrl = "https://example.com/opt-in-proof.png",
helpKeywords = listOf("HELP"),
helpMessage = "For HELP email support@example.com.",
optOutKeywords = listOf("STOP"),
optOutMessage = "STOP received. You are unsubscribed.",
sampleMessages = listOf(
"Your code is 554321. Reply STOP to cancel.",
"Your ticket has been updated. Reply HELP for info."
)
))
println("Campaign created with ID: ${campaign.id}")

// Get a campaign by ID
val fetched = ccai.campaigns.get(campaign.id)

// List all campaigns for the account
val campaigns = ccai.campaigns.list()
println("Found ${campaigns.size} campaign(s)")

// Update a campaign (partial update)
val updated = ccai.campaigns.update(campaign.id, CampaignRequest(
description = "Updated description."
))

// Delete a campaign
ccai.campaigns.delete(campaign.id)
```

**Use Cases:** `TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `FRAUD_ALERT`, `HIGHER_EDUCATION`, `LOW_VOLUME_MIXED`, `MARKETING`, `MIXED`, `POLLING_VOTING`, `PUBLIC_SERVICE_ANNOUNCEMENT`, `SECURITY_ALERT`

> Note: `MIXED` and `LOW_VOLUME_MIXED` campaigns require 2–3 `subUseCases`.

**Sub-Use Cases:** `TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `FRAUD_ALERT`, `MARKETING`, `POLLING_VOTING`

#### Contact Management

```kotlin
import com.cloudcontactai.sdk.contact.ContactService

// Opt a contact out of text messages (by phone number)
ccai.contact.setDoNotText(phone = "+15551234567", doNotText = true)

// Opt a contact back in (by phone number)
ccai.contact.setDoNotText(phone = "+15551234567", doNotText = false)

// Opt out by contactId
ccai.contact.setDoNotText(contactId = "contact-abc-123", doNotText = true)
```

### Java Usage

```java
Expand Down
Loading