Skip to content
Merged
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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,23 @@ await TurboSign.resend(documentId, ['recipient-uuid']);
| `initials` | Initials field |
| `text` | Free-form text input |
| `date` | Date stamp |
| `checkbox` | Checkbox / agreement |
| `checkbox` | Checkbox / agreement — also the **controlling** field for conditional (IF/THEN) logic |
| `full_name` | Full name |
| `first_name` | First name |
| `last_name` | Last name |
| `email` | Email address |
| `title` | Job title |
| `company` | Company name |

#### Conditional (IF/THEN) fields

Any field accepts an optional `metadata` object for conditional logic. Set `metadata.fieldKey` on a
controlling `checkbox`, then set `metadata.conditional` (`controllingFieldKey`, `operator`:
`is_checked` | `is_not_checked`, `action`: `show` | `unlock`) on a dependent field that references
that key. `show` hides the dependent field until the condition is met; `unlock` keeps it visible
but read-only until met. See each SDK's README "Conditional (IF/THEN) Fields" section for the
language-specific form.

---

## Requirements
Expand Down
40 changes: 40 additions & 0 deletions packages/go-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,45 @@ if err == nil {
| `title` | Job title |
| `company` | Company name |

The `checkbox` type doubles as the **controlling** field for conditional logic (see below).

### Conditional (IF/THEN) Fields

Any field may carry an optional `Metadata` (`*FieldMetadata`) that drives conditional logic. Set
`FieldKey` on a **controlling** `checkbox` to give it a stable id, then set `Conditional` on a
**dependent** field that references that id:

```go
Fields: []turbodocx.Field{
// Controlling checkbox — carries the FieldKey dependents reference
{
Type: "checkbox",
RecipientEmail: "john@example.com",
Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"},
},
// Dependent text field — hidden until the checkbox is checked ("If checked, explain")
{
Type: "text",
RecipientEmail: "john@example.com",
IsMultiline: true,
Metadata: &turbodocx.FieldMetadata{
Conditional: &turbodocx.FieldConditional{
ControllingFieldKey: "request_changes", // must equal the checkbox's FieldKey
Operator: turbodocx.ConditionalOperatorIsChecked, // IsChecked | IsNotChecked
Action: turbodocx.ConditionalActionShow, // Show (hidden until met) | Unlock (read-only until met)
},
},
},
}
```

| `Metadata` field | Set on | Meaning |
|:-----------------|:-------|:--------|
| `FieldKey` | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference |
| `Conditional.ControllingFieldKey` | dependent field | Must equal the controlling checkbox's `FieldKey` |
| `Conditional.Operator` | dependent field | `ConditionalOperatorIsChecked` (`"is_checked"`) or `ConditionalOperatorIsNotChecked` (`"is_not_checked"`) |
| `Conditional.Action` | dependent field | `ConditionalActionShow` (hidden until met) or `ConditionalActionUnlock` (visible but read-only until met) |

---

## Type Reference
Expand Down Expand Up @@ -982,6 +1021,7 @@ type Field struct {
Required bool `json:"required,omitempty"` // Field is required
BackgroundColor string `json:"backgroundColor,omitempty"` // Background color (hex)
Template *TemplateAnchor `json:"template,omitempty"` // Template anchor for dynamic positioning
Metadata *FieldMetadata `json:"metadata,omitempty"` // Conditional (IF/THEN) logic — see "Conditional Fields"
}
```

Expand Down
30 changes: 30 additions & 0 deletions packages/go-sdk/examples/turbosign_advanced.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,36 @@ func main() {
Size: &turbodocx.Size{Width: 200, Height: 50},
},
},
// Conditional (IF/THEN) fields
// Controlling checkbox: carries a stable FieldKey that dependents reference.
{
Type: "checkbox",
RecipientEmail: "john@example.com",
Template: &turbodocx.TemplateAnchor{
Anchor: "{request_changes}",
Placement: "replace",
Size: &turbodocx.Size{Width: 20, Height: 20},
},
Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"},
},
// Dependent text field: hidden until the checkbox above is checked ("If checked, explain").
{
Type: "text",
RecipientEmail: "john@example.com",
IsMultiline: true,
Template: &turbodocx.TemplateAnchor{
Anchor: "{change_details}",
Placement: "replace",
Size: &turbodocx.Size{Width: 200, Height: 50},
},
Metadata: &turbodocx.FieldMetadata{
Conditional: &turbodocx.FieldConditional{
ControllingFieldKey: "request_changes",
Operator: turbodocx.ConditionalOperatorIsChecked,
Action: turbodocx.ConditionalActionShow,
},
},
},
},
})

Expand Down
98 changes: 98 additions & 0 deletions packages/go-sdk/examples/turbosign_conditional_fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//go:build ignore
// +build ignore

// Example: Conditional (IF/THEN) Fields
//
// A checkbox can control other fields so signers only see what applies to them:
// - Give a "checkbox" field a stable Metadata.FieldKey.
// - Give a dependent field a Metadata.Conditional rule that references that key.
// Operator: "is_checked" | "is_not_checked" -- when the rule fires.
// Action: "show" (hidden until the rule fires)
// "unlock" (visible but read-only until the rule fires).
//
// One checkbox can drive any number of dependent fields -- give them the same
// ControllingFieldKey. Uses CreateSignatureReviewLink (no emails are sent).

package main

import (
"context"
"fmt"
"os"

turbodocx "github.com/TurboDocx/SDK/packages/go-sdk"
)

func main() {
client, err := turbodocx.NewClientWithConfig(turbodocx.ClientConfig{
APIKey: getEnv("TURBODOCX_API_KEY", "your-api-key-here"),
OrgID: getEnv("TURBODOCX_ORG_ID", "your-org-id-here"),
SenderEmail: getEnv("TURBODOCX_SENDER_EMAIL", "support@yourcompany.com"),
SenderName: getEnv("TURBODOCX_SENDER_NAME", "Your Company Name"),
})
if err != nil {
fmt.Printf("Error creating client: %v\n", err)
return
}

pdfFile, err := os.ReadFile("../../ExampleAssets/advanced-contract.pdf")
if err != nil {
fmt.Printf("Error reading file: %v\n", err)
return
}

fmt.Println("Creating a review link with conditional fields...")

ctx := context.Background()
result, err := client.TurboSign.CreateSignatureReviewLink(ctx, &turbodocx.CreateSignatureReviewLinkRequest{
File: pdfFile,
FileName: "advanced-contract.pdf",
DocumentName: "Conditional Fields Demo",
Recipients: []turbodocx.Recipient{
{Name: "John Doe", Email: "john@example.com", SigningOrder: 1},
},
Fields: []turbodocx.Field{
// Controlling checkboxes -- each carries a stable FieldKey.
{Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 120, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "request_changes"}},
{Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 300, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "override_amount"}},
{Type: "checkbox", RecipientEmail: "john@example.com", Page: 1, X: 60, Y: 480, Width: 20, Height: 20, Metadata: &turbodocx.FieldMetadata{FieldKey: "consent"}},

// show + is_checked -- HIDDEN until "request_changes" is checked.
{Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 120, Width: 260, Height: 40,
Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "request_changes", Operator: "is_checked", Action: "show"}}},
// ONE checkbox driving a SECOND dependent (same ControllingFieldKey) -- a signature.
{Type: "signature", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 180, Width: 200, Height: 50,
Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "request_changes", Operator: "is_checked", Action: "show"}}},

// unlock + is_checked -- VISIBLE but locked until "override_amount" is checked.
{Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 300, Width: 150, Height: 30, DefaultValue: "1000.00",
Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "override_amount", Operator: "is_checked", Action: "unlock"}}},

// show + is_not_checked -- a "please explain" box shown only while consent is WITHHELD.
{Type: "text", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 480, Width: 260, Height: 40,
Metadata: &turbodocx.FieldMetadata{Conditional: &turbodocx.FieldConditional{ControllingFieldKey: "consent", Operator: "is_not_checked", Action: "show"}}},

// A normal required signature with no rule -- always visible, always required.
{Type: "signature", RecipientEmail: "john@example.com", Page: 1, X: 120, Y: 620, Width: 200, Height: 50, Required: true},
},
})
if err != nil {
// A malformed rule is rejected here with a 400 and code "InvalidConditionalRule".
fmt.Printf("Error: %v\n", err)
return
}

fmt.Println("✅ Review link created!")
fmt.Printf("Document ID: %s\n", result.DocumentID)
fmt.Printf("Preview URL: %s\n", result.PreviewURL)

// Fail-open: a well-formed rule whose ControllingFieldKey matches NO checkbox is NOT an
// error -- the dependent field stays visible/editable. Double-check your keys match exactly.
}

func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
49 changes: 49 additions & 0 deletions packages/go-sdk/turbosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,55 @@ type Field struct {
Required bool `json:"required,omitempty"`
BackgroundColor string `json:"backgroundColor,omitempty"`
Template *TemplateAnchor `json:"template,omitempty"`
// Metadata carries optional conditional (IF/THEN) logic — a FieldKey on a controlling
// checkbox, or a Conditional rule on a dependent field. Omitted when nil.
Metadata *FieldMetadata `json:"metadata,omitempty"`
}

// ConditionalOperator is how a dependent field's controlling checkbox is tested.
type ConditionalOperator string

const (
// ConditionalOperatorIsChecked fires while the controlling checkbox is checked.
ConditionalOperatorIsChecked ConditionalOperator = "is_checked"
// ConditionalOperatorIsNotChecked fires while the controlling checkbox is unchecked.
ConditionalOperatorIsNotChecked ConditionalOperator = "is_not_checked"
)

// ConditionalAction is what happens to a dependent field until its condition is met.
type ConditionalAction string

const (
// ConditionalActionShow keeps the field hidden until the condition is met.
ConditionalActionShow ConditionalAction = "show"
// ConditionalActionUnlock keeps the field visible but read-only until the condition is met.
ConditionalActionUnlock ConditionalAction = "unlock"
)

// FieldConditional is a conditional (IF/THEN) rule set on a DEPENDENT field.
//
// The dependent field reacts to a CONTROLLING checkbox elsewhere in the same Fields slice.
// The controlling field must be Type "checkbox" and carry Metadata.FieldKey; this rule
// references it by that exact key.
type FieldConditional struct {
// ControllingFieldKey must equal the controlling checkbox's Metadata.FieldKey.
ControllingFieldKey string `json:"controllingFieldKey"`
// Operator is whether the rule fires when the checkbox is checked or unchecked.
Operator ConditionalOperator `json:"operator"`
// Action is whether the dependent field is hidden ("show") or read-only ("unlock") until met.
Action ConditionalAction `json:"action"`
}

// FieldMetadata is optional per-field metadata for conditional (IF/THEN) logic.
//
// Set FieldKey on a CONTROLLING checkbox to give it a stable client id; set Conditional on a
// DEPENDENT field to make it react to that checkbox. Both sides are authored by the caller in
// the same payload.
type FieldMetadata struct {
// FieldKey is a stable client id (<=100 chars) for a controlling checkbox, referenced by dependents.
FieldKey string `json:"fieldKey,omitempty"`
// Conditional is the rule set on a dependent field.
Conditional *FieldConditional `json:"conditional,omitempty"`
}

// CreateSignatureReviewLinkRequest is the request for CreateSignatureReviewLink
Expand Down
68 changes: 68 additions & 0 deletions packages/go-sdk/turbosign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,74 @@ func TestTurboSignClient_SendSignature(t *testing.T) {
})
}

func TestTurboSignClient_SendSignatureConditionalMetadata(t *testing.T) {
// The whole Fields slice is JSON-marshaled into the "fields" form value, so conditional
// (IF/THEN) metadata must ride along untouched.
var capturedFields string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]string
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
capturedFields = body["fields"]

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"documentId": "doc-conditional",
"status": "UNDER_REVIEW",
"message": "Document sent for signing",
})
}))
defer server.Close()

client, _ := NewClientWithConfig(ClientConfig{
APIKey: "test-api-key",
OrgID: "test-org-id",
BaseURL: server.URL,
SenderEmail: "test@example.com",
})

result, err := client.TurboSign.SendSignature(context.Background(), &SendSignatureRequest{
FileLink: "https://example.com/doc.pdf",
Recipients: []Recipient{
{Name: "John Doe", Email: "john@example.com", SigningOrder: 1},
},
Fields: []Field{
// Controlling checkbox
{
Type: "checkbox",
RecipientEmail: "john@example.com",
Metadata: &FieldMetadata{FieldKey: "request_changes"},
},
// Dependent field
{
Type: "text",
RecipientEmail: "john@example.com",
Metadata: &FieldMetadata{
Conditional: &FieldConditional{
ControllingFieldKey: "request_changes",
Operator: ConditionalOperatorIsChecked,
Action: ConditionalActionShow,
},
},
},
},
})

require.NoError(t, err)
assert.Equal(t, "doc-conditional", result.DocumentID)

var sentFields []Field
require.NoError(t, json.Unmarshal([]byte(capturedFields), &sentFields))
require.Len(t, sentFields, 2)
require.NotNil(t, sentFields[0].Metadata)
assert.Equal(t, "request_changes", sentFields[0].Metadata.FieldKey)
require.NotNil(t, sentFields[1].Metadata)
require.NotNil(t, sentFields[1].Metadata.Conditional)
assert.Equal(t, "request_changes", sentFields[1].Metadata.Conditional.ControllingFieldKey)
assert.Equal(t, ConditionalOperatorIsChecked, sentFields[1].Metadata.Conditional.Operator)
assert.Equal(t, ConditionalActionShow, sentFields[1].Metadata.Conditional.Action)
}

func TestTurboSignClient_GetStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/turbosign/documents/doc-123/status", r.URL.Path)
Expand Down
42 changes: 42 additions & 0 deletions packages/java-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,48 @@ if (quote.getPreparedBy() != null) {
| `title` | Job title |
| `company` | Company name |

The `checkbox` type doubles as the **controlling** field for conditional logic (see below).

### Conditional (IF/THEN) Fields

Any field may carry an optional `FieldMetadata` that drives conditional logic. Set a `fieldKey` on
a **controlling** `checkbox` to give it a stable id, then set a `FieldConditional` on a
**dependent** field that references that id:

```java
import com.turbodocx.models.Field;
import com.turbodocx.models.FieldMetadata;
import com.turbodocx.models.FieldConditional;

List<Field> fields = Arrays.asList(
// Controlling checkbox — carries the fieldKey dependents reference
new Field.Builder()
.type("checkbox")
.recipientEmail("john@example.com")
.template(new Field.TemplateAnchor.Builder().anchor("{request_changes}").placement("replace").size(new Field.Size(20, 20)).build())
.metadata(FieldMetadata.forFieldKey("request_changes"))
.build(),
// Dependent text field — hidden until the checkbox is checked ("If checked, explain")
new Field.Builder()
.type("text")
.recipientEmail("john@example.com")
.isMultiline(true)
.template(new Field.TemplateAnchor.Builder().anchor("{change_details}").placement("replace").size(new Field.Size(200, 50)).build())
.metadata(FieldMetadata.forConditional(
new FieldConditional("request_changes", "is_checked", "show")))
.build()
);
```

`FieldConditional(controllingFieldKey, operator, action)`:

| Argument | Set on | Meaning |
|:---------|:-------|:--------|
| `fieldKey` (on `FieldMetadata`) | controlling `checkbox` | Stable client id (≤100 chars) that dependents reference |
| `controllingFieldKey` | dependent field | Must equal the controlling checkbox's `fieldKey` |
| `operator` | dependent field | `"is_checked"` or `"is_not_checked"` |
| `action` | dependent field | `"show"` (hidden until met) or `"unlock"` (visible but read-only until met) |

---

## Examples
Expand Down
Loading
Loading