From b1ddced051f72376d5767eb580b6474ab22bef57 Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 26 Nov 2025 15:43:52 +0000 Subject: [PATCH 1/3] feat: added routes for turbosign single step route integration --- docs/TurboSign/API Signatures.md | 1102 +++++++++++------ static/img/turbosign/api/types.svg | 19 + .../prepare-for-review/csharp/controller.cs | 146 +++ .../prepare-for-review/csharp/minimal.cs | 109 ++ .../api/single-step/prepare-for-review/go.go | 188 +++ .../single-step/prepare-for-review/java.java | 128 ++ .../prepare-for-review/nodejs/express.js | 102 ++ .../prepare-for-review/nodejs/fastify.js | 113 ++ .../single-step/prepare-for-review/php.php | 118 ++ .../prepare-for-review/powershell.ps1 | 138 +++ .../prepare-for-review/python/fastapi.py | 130 ++ .../prepare-for-review/python/flask.py | 120 ++ .../single-step/prepare-for-review/ruby.rb | 131 ++ .../prepare-for-signing/csharp/controller.cs | 149 +++ .../prepare-for-signing/csharp/minimal.cs | 107 ++ .../api/single-step/prepare-for-signing/go.go | 192 +++ .../single-step/prepare-for-signing/java.java | 134 ++ .../prepare-for-signing/nodejs/express.js | 101 ++ .../prepare-for-signing/nodejs/fastify.js | 111 ++ .../single-step/prepare-for-signing/php.php | 117 ++ .../prepare-for-signing/powershell.ps1 | 140 +++ .../prepare-for-signing/python/fastapi.py | 126 ++ .../prepare-for-signing/python/flask.py | 121 ++ .../single-step/prepare-for-signing/ruby.rb | 130 ++ 24 files changed, 3602 insertions(+), 370 deletions(-) create mode 100644 static/img/turbosign/api/types.svg create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/csharp/controller.cs create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/csharp/minimal.cs create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/go.go create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/java.java create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/express.js create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/php.php create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/powershell.ps1 create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/python/fastapi.py create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/python/flask.py create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-review/ruby.rb create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/controller.cs create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/minimal.cs create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/go.go create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/java.java create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/express.js create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/php.php create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/powershell.ps1 create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/python/fastapi.py create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/python/flask.py create mode 100644 static/scripts/turbosign/api/single-step/prepare-for-signing/ruby.rb diff --git a/docs/TurboSign/API Signatures.md b/docs/TurboSign/API Signatures.md index 0426deb..b73de02 100644 --- a/docs/TurboSign/API Signatures.md +++ b/docs/TurboSign/API Signatures.md @@ -1,12 +1,15 @@ --- title: TurboSign API Integration sidebar_position: 3 -description: Complete guide for integrating TurboSign API to upload documents, add recipients, and prepare documents for electronic signatures. Learn the 3-step process with detailed examples and code samples. +description: Complete guide for integrating TurboSign API using single-step document preparation. Send documents for electronic signatures in one API call with our simplified workflow. keywords: - turbosign api + - single-step signature api - document upload api - electronic signature api - - recipients api + - multipart form data + - prepare for review + - prepare for signing - signature preparation - api integration - document signing workflow @@ -35,26 +38,31 @@ import ScriptLoader from '@site/src/components/ScriptLoader'; # TurboSign API Integration -This comprehensive guide walks you through the TurboSign API integration process. Learn how to programmatically upload documents, add recipients, and prepare documents for electronic signatures using our RESTful API. +This comprehensive guide walks you through the TurboSign single-step API integration. Learn how to programmatically upload documents, configure recipients, set up signature fields, and send documents for electronic signatures using a single, streamlined API call. ![TurboSign API Integration Overview](/img/turbosign/api/api-illustration.png) ## Overview -The TurboSign API follows a simple 3-step process to prepare documents for electronic signatures: +The TurboSign API provides a simplified single-step process to prepare documents for electronic signatures. Instead of multiple API calls, you can now accomplish everything in one request. -1. **Upload Document** - Upload your PDF document to TurboSign -2. **Add Recipients** - Configure who needs to sign and their signing order -3. **Prepare for Signing** - Set up signature fields and send for signing +### Two Endpoint Options -![TurboSign API Integration Overview](/img/turbosign/api/steps.png) +TurboSign offers two single-step endpoints to fit different workflows: + +1. **Prepare for Review** - Upload and get preview URL (no emails sent) +2. **Prepare for Signing** - Upload and send immediately (emails sent) + +![TurboSign Single-Step Workflow](/img/turbosign/api/types.svg) ### Key Features -- **RESTful API**: Standard HTTP methods with JSON payloads +- **Single API Call**: Upload document, add recipients, and configure fields in one request +- **RESTful API**: Standard HTTP methods with multipart/form-data - **Bearer Token Authentication**: Secure API access using JWT tokens - **Multiple Recipients**: Support for multiple signers with custom signing order - **Flexible Field Placement**: Position signature fields using anchors or coordinates +- **Multiple File Sources**: Upload file, or reference deliverableId, templateId, or fileLink - **Real-time Status Updates**: Track document status throughout the signing process - **Webhook Integration**: Receive notifications when signing is complete @@ -64,58 +72,48 @@ Don't want to read all the details? Here's what you need to know: ### Available Field Types -| Type | Description | Use Case | -| ----------- | -------------------------- | ------------------------------------------------- | -| `signature` | Electronic signature field | Legal signatures | -| `initial` | Initial field | Document initials, paragraph acknowledgments | -| `date` | Date picker field | Signing date, agreement date | -| `full_name` | Full name field | Automatically fills signer's complete name | -| `first_name`| First name field | Automatically fills signer's first name | -| `last_name` | Last name field | Automatically fills signer's last name | -| `title` | Title/job title field | Professional title or position | -| `company` | Company name field | Organization or company name | -| `email` | Email address field | Signer's email address | -| `text` | Generic text input field | Custom text, notes, or any other text input | - -### Complete 3-Step Workflow +| Type | Description | Use Case | +| ------------ | -------------------------- | -------------------------------------------- | +| `signature` | Electronic signature field | Legal signatures | +| `initial` | Initial field | Document initials, paragraph acknowledgments | +| `date` | Date picker field | Signing date, agreement date | +| `full_name` | Full name field | Automatically fills signer's complete name | +| `first_name` | First name field | Automatically fills signer's first name | +| `last_name` | Last name field | Automatically fills signer's last name | +| `title` | Title/job title field | Professional title or position | +| `company` | Company name field | Organization or company name | +| `email` | Email address field | Signer's email address | +| `text` | Generic text input field | Custom text, notes, or any other text input | +| `checkbox` | Checkbox field | Acknowledgments, consent, agreements | + +### Quick Start: Prepare for Signing (Most Common) + +Use this endpoint to send documents immediately for signing: -### Quick Coordinate Example +### Alternative: Prepare for Review -If you prefer using exact coordinates instead of text anchors: +Use this endpoint when you need a preview URL to verify field placement: -```json -// Step 3 payload using coordinates instead of templates -[ - { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "signature", - "page": 1, - "x": 100, - "y": 200, - "width": 200, - "height": 80, - "pageWidth": 612, - "pageHeight": 792 - }, - { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "date", - "page": 1, - "x": 350, - "y": 300, - "width": 150, - "height": 30, - "pageWidth": 612, - "pageHeight": 792 - } -] -``` + + +### Quick Comparison + +| Feature | prepare-for-review | prepare-for-signing | +| -------------------- | ------------------------------ | ------------------------- | +| Sends emails? | ❌ No | ✅ Yes | +| Returns preview URL? | ✅ Yes | ❌ No | +| Final status | REVIEW_READY | UNDER_REVIEW | +| Use when | Need to verify field placement | Ready to send immediately | Now that you've seen the whole thing, let's dive into the details... @@ -134,8 +132,8 @@ Before you begin, ensure you have: 3. **API Keys Section**: Generate or retrieve your API access token 4. **Organization ID**: Copy your organization ID from the settings -![TurboSign API Integration Overview](/img/turbosign/api/api-key.png) -![TurboSign API Integration Overview](/img/turbosign/api/org-id.png) +![TurboSign API Key](/img/turbosign/api/api-key.png) +![TurboSign Organization ID](/img/turbosign/api/org-id.png) ## Authentication @@ -152,14 +150,40 @@ x-rapiddocx-org-id: YOUR_ORGANIZATION_ID User-Agent: TurboDocx API Client ``` -## Step 1: Upload Document +## Choosing Your Endpoint + +TurboSign offers two single-step endpoints to fit different workflows. Choose the one that best matches your use case. -The first step is to upload your PDF document to TurboSign. This creates a new document record and returns a document ID that you'll use in subsequent steps. +### When to Use prepare-for-review + +✅ **Use this endpoint when you want to:** + +- Verify field placement before sending to recipients +- Get a preview URL to review the document in TurboSign's interface +- Manually trigger email sending after verifying field placement +- Ensure correct field positioning before recipients receive emails + +**Workflow**: Upload → Get preview URL → Review in browser → Manually send when ready + +### When to Use prepare-for-signing + +✅ **Use this endpoint when you want to:** + +- Send documents immediately without preview step +- Automate the entire signature process end-to-end +- Use with verified templates or confident field positioning +- Skip manual review and send directly to recipients + +**Workflow**: Upload → Emails sent automatically → Recipients sign + +## Endpoint 1: Prepare for Review + +Creates a signature request and returns a preview URL. No emails are sent to recipients. ### Endpoint ```http -POST https://www.turbodocx.com/turbosign/documents/upload +POST https://api.turbodocx.com/turbosign/single/prepare-for-review ``` ### Headers @@ -171,379 +195,628 @@ x-rapiddocx-org-id: YOUR_ORGANIZATION_ID User-Agent: TurboDocx API Client ``` -### Request Body (Form Data) +### Request Body (multipart/form-data) + +⚠️ **Important**: Recipients and fields must be sent as JSON strings in form-data + +| Field | Type | Required | Description | +| ------------------- | -------------- | ------------- | ------------------------------------------ | +| file | File | Conditional\* | PDF, DOCX, or PPTX file to upload | +| deliverableId | String (UUID) | Conditional\* | Reference to existing deliverable | +| templateId | String (UUID) | Conditional\* | Reference to existing template | +| fileLink | String (URL) | Conditional\* | URL to download file from | +| documentName | String | No | Document name in TurboSign (max 255 chars) | +| documentDescription | String | No | Document description (max 1000 chars) | +| recipients | String (JSON) | **Yes** | JSON string array of recipient objects | +| fields | String (JSON) | **Yes** | JSON string array of field objects | +| senderName | String | No | Name of sender (max 255 chars) | +| senderEmail | String (email) | No | Email of sender | +| ccEmails | String (JSON) | No | JSON string array of CC email addresses | + +\* **File Source**: Must provide exactly ONE of: file, deliverableId, templateId, or fileLink + +### Recipients JSON Format + +Recipients must be stringified before adding to form-data: ```javascript -{ - "name": "Contract Agreement", - "file": [PDF_FILE_BINARY], - // Optional: triggerMeta for advanced configurations - // "triggerMeta": "{\"url\": \"callback_url\"}" -} +const recipients = JSON.stringify([ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1, + metadata: { + color: "hsl(200, 75%, 50%)", + lightColor: "hsl(200, 75%, 93%)", + }, + }, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2, + metadata: { + color: "hsl(270, 75%, 50%)", + lightColor: "hsl(270, 75%, 93%)", + }, + }, +]); +formData.append("recipients", recipients); +``` + +### Fields JSON Format + +Fields reference recipients by **email** (not recipientId) and must be stringified: + +#### Template-based (recommended): + +```javascript +const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + template: { + anchor: "{Signature1}", + placement: "replace", + size: { width: 200, height: 80 }, + offset: { x: 0, y: 0 }, + }, + required: true, + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + template: { + anchor: "{Date1}", + placement: "replace", + size: { width: 150, height: 30 }, + }, + required: true, + }, +]); +formData.append("fields", fields); +``` + +#### Coordinate-based: + +```javascript +const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + pageWidth: 612, + pageHeight: 792, + required: true, + }, +]); +formData.append("fields", fields); ``` ### Response ```json { - "data": { - "id": "4a20eca5-7944-430c-97d5-fcce4be24296", - "name": "Contract Agreement", - "description": "", - "status": "draft", - "createdOn": "2025-09-17T13:24:57.083Z" - } + "success": true, + "documentId": "4a20eca5-7944-430c-97d5-fcce4be24296", + "status": "REVIEW_READY", + "previewUrl": "https://www.turbodocx.com/sign/preview/abc123...", + "recipients": [ + { + "id": "5f673f37-9912-4e72-85aa-8f3649760f6b", + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1, + "metadata": { + "color": "hsl(200, 75%, 50%)", + "lightColor": "hsl(200, 75%, 93%)" + } + } + ], + "message": "Document prepared for review. Use the preview URL to review and assign fields." } ``` ### Response Fields -| Field | Type | Description | -| ------------------ | ------ | ----------------------------------------------------- | -| `data.id` | string | Unique document identifier (save this for next steps) | -| `data.name` | string | Document name as provided | -| `data.description` | string | Document description (empty by default) | -| `data.status` | string | Document status (`draft` after upload) | -| `data.createdOn` | string | ISO 8601 timestamp of document creation | - - +| Field | Type | Description | +| ---------- | ------------- | ---------------------------------------------- | +| success | Boolean | Request success status | +| documentId | String (UUID) | Unique document identifier - save for tracking | +| status | String | Document status (REVIEW_READY) | +| previewUrl | String (URL) | URL to preview and verify document | +| recipients | Array | Array of recipient objects with generated IDs | +| message | String | Human-readable success message | ### Code Examples - -## Step 2: Add Recipients +### Next Steps After Review + +Once you've reviewed the document via the preview URL click "Send for Signing" button on the preview page to send emails to recipients -After uploading your document, add the recipients who need to sign. You can specify multiple recipients with their signing order and customize their signature appearance. +## Endpoint 2: Prepare for Signing + +Creates a signature request and immediately sends emails to recipients. Use this for production workflows when you're confident in your field positioning. ### Endpoint ```http -POST https://www.turbodocx.com/turbosign/documents/{documentId}/update-with-recipients +POST https://api.turbodocx.com/turbosign/single/prepare-for-signing ``` ### Headers ```http -Content-Type: application/json +Content-Type: multipart/form-data Authorization: Bearer YOUR_API_TOKEN x-rapiddocx-org-id: YOUR_ORGANIZATION_ID User-Agent: TurboDocx API Client ``` -### Request Body +### Request Body (multipart/form-data) -```json -{ - "document": { - "name": "Contract Agreement - Updated", - "description": "This document requires electronic signatures from both parties. Please review all content carefully before signing." - }, - "recipients": [ - { - "name": "John Smith", - "email": "john.smith@company.com", - "signingOrder": 1, - "metadata": { - "color": "hsl(200, 75%, 50%)", - "lightColor": "hsl(200, 75%, 93%)" - }, - "documentId": "4a20eca5-7944-430c-97d5-fcce4be24296" - }, - { - "name": "Jane Doe", - "email": "jane.doe@partner.com", - "signingOrder": 2, - "metadata": { - "color": "hsl(270, 75%, 50%)", - "lightColor": "hsl(270, 75%, 93%)" - }, - "documentId": "4a20eca5-7944-430c-97d5-fcce4be24296" - } - ] -} -``` +The request format is **identical** to prepare-for-review. See the "Endpoint 1: Prepare for Review" section above for detailed field documentation. ### Response ```json { - "data": { - "document": { - "id": "4a20eca5-7944-430c-97d5-fcce4be24296", - "name": "Contract Agreement - Updated", - "description": "This document requires electronic signatures from both parties. Please review all content carefully before signing.", - "status": "setup_complete", - "updatedOn": "2025-09-17T13:26:10.000Z" - }, - "recipients": [ - { - "id": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "name": "John Smith", - "email": "john.smith@company.com", - "signingOrder": 1, - "metadata": { - "color": "hsl(200, 75%, 50%)", - "lightColor": "hsl(200, 75%, 93%)" - } - }, - { - "id": "a8b9c1d2-3456-7890-abcd-ef1234567890", - "name": "Jane Doe", - "email": "jane.doe@partner.com", - "signingOrder": 2, - "metadata": { - "color": "hsl(270, 75%, 50%)", - "lightColor": "hsl(270, 75%, 93%)" - } - } - ] - } + "success": true, + "documentId": "4a20eca5-7944-430c-97d5-fcce4be24296", + "message": "Document sent for signing. Emails are being sent to recipients." } ``` -### Request Fields +### Response Fields -| Field | Type | Required | Description | -| ---------------------------------- | ------ | -------- | --------------------------------------------------- | -| `document.name` | string | Yes | Updated document name | -| `document.description` | string | No | Document description for recipients | -| `recipients[].name` | string | Yes | Full name of the signer | -| `recipients[].email` | string | Yes | Email address for signing notifications | -| `recipients[].signingOrder` | number | Yes | Order in which recipients should sign (1, 2, 3...) | -| `recipients[].metadata.color` | string | No | Primary color for this recipient's signature fields | -| `recipients[].metadata.lightColor` | string | No | Light color variant for highlights | -| `recipients[].documentId` | string | Yes | Document ID from Step 1 | +| Field | Type | Description | +| ---------- | ------------- | ---------------------------------------------- | +| success | Boolean | Request success status | +| documentId | String (UUID) | Unique document identifier - save for tracking | +| message | String | Human-readable success message | - +⚠️ **Note**: This endpoint returns immediately after creating the document. Email sending happens asynchronously in the background. Use webhooks to receive notification when the document is fully signed. ### Code Examples - -## Step 3: Prepare for Signing +## Recipients Reference -The final step configures signature fields and sends the document to recipients for signing. You can position fields using text anchors or absolute coordinates. +### Recipient Properties -### Endpoint +Each recipient object in the `recipients` array should contain the following properties: -```http -POST https://www.turbodocx.com/turbosign/documents/{documentId}/prepare-for-signing -``` +| Property | Type | Required | Description | +| ------------ | -------------- | -------- | ---------------------------------------------------------- | +| name | String | Yes | Full name of the recipient/signer | +| email | String (email) | Yes | Email address of the recipient (must be unique) | +| signingOrder | Number | Yes | Order in which recipient should sign (starts at 1) | +| metadata | Object | No | Optional metadata for UI customization (color, lightColor) | -### Headers +### Metadata Object (Optional) -```http -Content-Type: application/json -Authorization: Bearer YOUR_API_TOKEN -x-rapiddocx-org-id: YOUR_ORGANIZATION_ID -User-Agent: TurboDocx API Client -``` +The `metadata` object allows you to customize the recipient's UI appearance: -### Request Body +| Property | Type | Description | Example | +| ---------- | ------ | -------------------------------------------------- | ---------------------- | +| color | String | Primary color for recipient in HSL format | `"hsl(200, 75%, 50%)"` | +| lightColor | String | Light background color for recipient in HSL format | `"hsl(200, 75%, 93%)"` | + +### Example Recipients Array ```json [ { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "signature", - "template": { - "anchor": "{Signature1}", - "placement": "replace", - "size": { "width": 200, "height": 80 }, - "offset": { "x": 0, "y": 0 }, - "caseSensitive": true, - "useRegex": false - }, - "defaultValue": "", - "required": true + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 }, { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "date", - "template": { - "anchor": "{Date1}", - "placement": "replace", - "size": { "width": 150, "height": 30 }, - "offset": { "x": 0, "y": 0 }, - "caseSensitive": true, - "useRegex": false - }, - "defaultValue": "", - "required": true - }, - { - "recipientId": "a8b9c1d2-3456-7890-abcd-ef1234567890", - "type": "signature", - "template": { - "anchor": "{Signature2}", - "placement": "replace", - "size": { "width": 200, "height": 80 }, - "offset": { "x": 0, "y": 0 }, - "caseSensitive": true, - "useRegex": false - }, - "defaultValue": "", - "required": true - }, + "name": "Jane Doe", + "email": "jane.doe@partner.com", + "signingOrder": 2 + } +] +``` + +### With Optional Metadata + +```json +[ { - "recipientId": "a8b9c1d2-3456-7890-abcd-ef1234567890", - "type": "text", - "template": { - "anchor": "{Title2}", - "placement": "replace", - "size": { "width": 200, "height": 30 }, - "offset": { "x": 0, "y": 0 }, - "caseSensitive": true, - "useRegex": false - }, - "defaultValue": "Business Partner", - "required": false + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1, + "metadata": { + "color": "hsl(200, 75%, 50%)", + "lightColor": "hsl(200, 75%, 93%)" + } } ] ``` -### Response +## Field Types Reference + +### Complete Field Type List + +| Type | Description | Auto-filled | Use Case | +| ------------ | -------------------------- | ----------- | -------------------------------------------- | +| `signature` | Electronic signature field | No | Legal signatures, agreements | +| `initial` | Initial field | No | Document initials, paragraph acknowledgments | +| `date` | Date picker field | No | Signing date, agreement date | +| `full_name` | Full name field | Yes | Automatically fills signer's complete name | +| `first_name` | First name field | Yes | Automatically fills signer's first name | +| `last_name` | Last name field | Yes | Automatically fills signer's last name | +| `title` | Title/job title field | No | Professional title or position | +| `company` | Company name field | No | Organization or company name | +| `email` | Email address field | Yes | Signer's email address | +| `text` | Generic text input field | No | Custom text, notes, or any other text input | +| `checkbox` | Checkbox field | No | Acknowledgments, consent, agreements | + +### Field Configuration Properties + +#### Common Properties (All Field Types) + +| Property | Type | Required | Description | +| --------------- | ------- | -------- | -------------------------------------------------------------- | +| recipientEmail | String | Yes | Email address of recipient (matches email in recipients array) | +| type | String | Yes | Field type (see table above) | +| required | Boolean | No | Whether field must be completed (default: true) | +| defaultValue | String | No | Pre-filled value for the field | +| isReadonly | Boolean | No | Makes field non-editable (for prefilled values) | +| backgroundColor | String | No | Custom background color (hex or rgba) | + +#### Template-based Properties + +| Property | Type | Required | Description | +| ---------------------- | ------- | -------- | --------------------------------------------------------------- | +| template.anchor | String | Yes | Text anchor to find in document (e.g., "{Signature1}") | +| template.placement | String | Yes | How to place field: "replace", "before", "after" | +| template.size | Object | Yes | Field dimensions: { width: number, height: number } | +| template.offset | Object | No | Position offset: { x: number, y: number } (default: {x:0, y:0}) | +| template.caseSensitive | Boolean | No | Whether anchor search is case-sensitive (default: true) | +| template.useRegex | Boolean | No | Whether to treat anchor as regex pattern (default: false) | + +#### Coordinate-based Properties + +| Property | Type | Required | Description | +| ---------- | ------ | -------- | ------------------------------------------------------------------ | +| page | Number | Yes | Page number (starts at 1) | +| x | Number | Yes | Horizontal position from left edge (pixels) | +| y | Number | Yes | Vertical position from top edge (pixels) | +| width | Number | Yes | Field width in pixels | +| height | Number | Yes | Field height in pixels | +| pageWidth | Number | No | Total page width in pixels (optional, for responsive positioning) | +| pageHeight | Number | No | Total page height in pixels (optional, for responsive positioning) | + +### Field Type Special Behaviors + +**signature & initial** + +- Draws a signature pad for user input +- Can be text-based or drawn +- Cryptographically signed and hashed for legal validity + +**date** + +- Shows date picker interface +- Format: MM/DD/YYYY (US) or DD/MM/YYYY (configurable) +- Can set defaultValue to "today" for auto-population + +**full_name, first_name, last_name, email** + +- Auto-populated from recipient profile +- Can be overridden by recipient if needed +- Useful for legal compliance and form filling + +**text** + +- Single-line text input by default +- Supports defaultValue for prefilled content +- Use for titles, company names, custom fields + +**checkbox** + +- Boolean true/false value +- Useful for acknowledgments and consent +- Can have label text next to checkbox + +## Field Positioning Methods + +TurboSign supports two methods for positioning signature fields in your documents. + +### Method 1: Template-based Positioning (Recommended) + +Uses text anchors in your PDF as placeholders. TurboSign searches for these anchors and places fields accordingly. + +#### Advantages + +✅ Easy to update field positions (just edit the PDF) +✅ No need to measure exact coordinates +✅ Works across different page sizes +✅ More maintainable for non-technical users +✅ Handles document variations gracefully + +#### How it Works + +1. **Add anchor text to your PDF**: Place text like `{Signature1}`, `{Date1}`, `{Initial1}` where you want fields +2. **Configure fields with anchor references**: Tell TurboSign what to search for +3. **TurboSign finds and replaces**: Anchors are found and replaced with interactive fields + +#### Anchor Configuration Example + +```json +{ + "recipientEmail": "john.smith@company.com", + "type": "signature", + "template": { + "anchor": "{Signature1}", + "placement": "replace", + "size": { "width": 200, "height": 80 }, + "offset": { "x": 0, "y": 0 }, + "caseSensitive": true, + "useRegex": false + }, + "required": true +} +``` + +#### Placement Options + +- **replace**: Removes the anchor text and places the field in its position +- **before**: Places field before the anchor text (anchor remains visible) +- **after**: Places field after the anchor text (anchor remains visible) + +#### Offset Usage + +Offset allows fine-tuning field position relative to the anchor: + +- `x`: Positive moves right, negative moves left (pixels) +- `y`: Positive moves down, negative moves up (pixels) ```json { - "success": true + "anchor": "{Signature1}", + "placement": "replace", + "size": { "width": 200, "height": 80 }, + "offset": { "x": 10, "y": -5 } // 10px right, 5px up from anchor } ``` -### Field Positioning Options +### Method 2: Coordinate-based Positioning -TurboSign supports two field positioning methods: +Uses exact pixel coordinates to position fields on specific pages. Best for precise control or when anchors aren't feasible. -1. **Template-based positioning** (recommended) - Uses text anchors in your PDF -2. **Coordinate-based positioning** - Uses exact pixel coordinates +#### Advantages -#### Template-based Field Configuration +✅ Pixel-perfect precision +✅ Works with PDFs that can't be edited +✅ Programmatically generated positions +✅ Useful for form-filling scenarios +✅ Consistent placement across documents -| Field | Type | Required | Description | -| ------------------------ | ------- | -------- | ------------------------------------------------------ | -| `recipientId` | string | Yes | Recipient ID from Step 2 | -| `type` | string | Yes | Field type - see Available Field Types in TLDR section | -| `template.anchor` | string | Yes | Text anchor to find in document (e.g., "{Signature1}") | -| `template.placement` | string | Yes | How to place field ("replace", "before", "after") | -| `template.size` | object | Yes | Field dimensions (width, height in pixels) | -| `template.offset` | object | No | Position offset from anchor (x, y in pixels) | -| `template.caseSensitive` | boolean | No | Whether anchor search is case-sensitive | -| `template.useRegex` | boolean | No | Whether to treat anchor as regex pattern | -| `defaultValue` | string | No | Pre-filled value for the field | -| `required` | boolean | No | Whether field must be completed | +#### How it Works -#### Coordinate-based Field Configuration +1. **Measure exact x,y coordinates** in your PDF (using PDF editor or viewer) +2. **Provide page number, coordinates, and dimensions** +3. **TurboSign places fields at exact positions** -For precise positioning when text anchors aren't suitable, use coordinate-based fields: +#### Coordinate Configuration Example ```json -[ - { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "signature", - "page": 1, - "x": 100, - "y": 200, - "width": 200, - "height": 80, - "pageWidth": 612, - "pageHeight": 792 - }, - { - "recipientId": "5f673f37-9912-4e72-85aa-8f3649760f6b", - "type": "date", - "page": 1, - "x": 350, - "y": 200, - "width": 150, - "height": 30, - "pageWidth": 612, - "pageHeight": 792 - } -] +{ + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "pageWidth": 612, + "pageHeight": 792, + "required": true +} ``` -| Field | Type | Required | Description | -| ------------ | ------ | -------- | ----------------------------------------------- | -| `recipientId`| string | Yes | Recipient ID from Step 2 | -| `type` | string | Yes | Field type - see Available Field Types section | -| `page` | number | Yes | Page number (starts at 1) | -| `x` | number | Yes | Horizontal position from left edge (pixels) | -| `y` | number | Yes | Vertical position from top edge (pixels) | -| `width` | number | Yes | Field width in pixels | -| `height` | number | Yes | Field height in pixels | -| `pageWidth` | number | Yes | Total page width in pixels | -| `pageHeight` | number | Yes | Total page height in pixels | - -**Coordinate System Notes:** -- Origin (0,0) is at the top-left corner of the page -- Standard US Letter page size is 612 x 792 pixels (8.5" x 11" at 72 DPI) -- Fields cannot extend beyond page boundaries: `x + width ≤ pageWidth` and `y + height ≤ pageHeight` -- All coordinate values must be non-negative numbers - -**When to use coordinate-based positioning:** -- Precise pixel-perfect placement needed -- PDF doesn't contain suitable text anchors -- Programmatically generated field positions -- Converting from existing coordinate-based systems - - +#### Coordinate System Reference -### Code Examples +- **Origin (0,0)**: Top-left corner of the page +- **X-axis**: Increases from left to right +- **Y-axis**: Increases from top to bottom +- **Standard US Letter**: 612 x 792 pixels (8.5" x 11" at 72 DPI) +- **Standard A4**: 595 x 842 pixels (210mm x 297mm at 72 DPI) - +#### Coordinate Validation + +Fields must stay within page boundaries: + +- `x ≥ 0` +- `y ≥ 0` +- `x + width ≤ pageWidth` +- `y + height ≤ pageHeight` + +#### Measuring Coordinates + +**Adobe Acrobat Pro**: + +1. View → Show/Hide → Rulers & Grids → Rulers +2. Hover over location to see coordinates + +**Browser Developer Tools**: + +1. Open PDF in browser +2. Right-click → Inspect +3. Use element inspector to measure positions + +**PDF Editing Software**: + +- Use built-in coordinate display +- Draw rectangles to measure dimensions + +#### Quick Coordinate Example + +Position a signature field at bottom-right of a US Letter page: + +```json +{ + "recipientEmail": "john@example.com", + "type": "signature", + "page": 1, + "x": 362, // 612 - 250 = 362 (right aligned with 50px margin) + "y": 662, // 792 - 130 = 662 (bottom aligned with 50px margin) + "width": 200, + "height": 80, + "pageWidth": 612, + "pageHeight": 792 +} +``` ## Best Practices +### Workflow Selection + +**When You Need Field Verification**: + +- ✅ Use `prepare-for-review` to get preview URLs +- ✅ Verify field placement in browser before sending +- ✅ Manually trigger sending after review +- ✅ Useful for new document templates or complex field layouts + +**When Field Placement Is Verified**: + +- ✅ Use `prepare-for-signing` to send immediately +- ✅ Implement webhook handlers for completion notifications +- ✅ Use proper error handling and retry logic +- ✅ Monitor API rate limits +- ✅ Log all document IDs for tracking + +**General Tips**: + +- ✅ Use deliverableId or templateId to avoid repeated uploads +- ✅ Test with your own email addresses first +- ✅ Both endpoints are production-ready + ### Security -- **Never expose API tokens**: Store tokens securely in environment variables -- **Use HTTPS only**: All API calls must use HTTPS in production -- **Validate inputs**: Always validate recipient emails and document names +- **Never expose API tokens**: Store tokens securely in environment variables or secrets management +- **Use HTTPS only**: All API calls must use HTTPS in production (API enforces this) +- **Validate inputs**: Always validate recipient emails and document names before submission - **Implement rate limiting**: Respect API rate limits to avoid throttling +- **Rotate tokens regularly**: Generate new API tokens periodically +- **Use webhook signatures**: Verify webhook payloads using HMAC signatures +- **Sanitize user inputs**: Validate and sanitize all user-provided data ### Error Handling - **Check HTTP status codes**: Always verify response status before processing -- **Handle timeouts**: Implement retry logic for network failures +- **Handle timeouts**: Implement retry logic with exponential backoff for network failures - **Log API responses**: Keep detailed logs for debugging and monitoring - **Validate responses**: Check response structure before accessing data +- **Graceful degradation**: Have fallback behavior for API failures +- **User-friendly errors**: Display helpful error messages to end users ### Performance -- **Upload optimization**: Compress PDFs when possible to reduce upload time -- **Batch operations**: Group multiple recipients in single API calls -- **Async processing**: Use webhooks instead of polling for status updates -- **Connection pooling**: Reuse HTTP connections for multiple requests +**File Upload Optimization**: + +- Compress PDFs when possible (aim for <5MB) +- Use fileLink for files already in cloud storage (S3, GCS, etc.) +- Use deliverableId/templateId to reference existing documents +- Avoid uploading the same document multiple times + +**API Efficiency**: + +- Single-step endpoints reduce API calls from 3 to 1 (3x faster) +- Batch multiple documents in parallel requests when possible +- Use connection pooling for multiple requests +- Implement exponential backoff for retries +- Cache responses when appropriate + +**Network Optimization**: + +- Use CDN for document hosting when using fileLink +- Enable gzip compression for API requests +- Minimize payload sizes by only including required fields ### Document Preparation -- **Use text anchors**: Place anchor text like `{Signature1}` in your PDFs for precise field positioning -- **Consistent naming**: Use consistent anchor naming conventions across documents -- **Test coordinates**: Verify field positions with test documents before production -- **Document validation**: Ensure PDFs are not password-protected or corrupted +**Text Anchors (Template-based)**: + +- Use consistent anchor naming: `{FieldType}{Number}` (e.g., `{Signature1}`, `{Date1}`) +- Place anchors exactly where you want fields +- Use unique anchors (avoid duplicates) +- Test anchor placement with prepare-for-review first +- Document your anchor naming convention + +**Coordinate-based**: + +- Verify coordinates work across different PDF viewers +- Account for page margins and headers/footers +- Use standard page sizes when possible +- Test on actual page dimensions (don't assume) +- Validate boundaries before submission + +**Document Validation**: -### Coordinate-based Positioning Tips +- Ensure PDFs are not password-protected or corrupted +- Verify all pages are readable +- Test with actual documents before production +- Keep backup copies of source documents -- **Measure accurately**: Use PDF viewers with coordinate display to find exact positions -- **Account for margins**: Consider document margins when calculating field positions -- **Test on different devices**: Verify coordinates work across different PDF viewers -- **Use standard page sizes**: Stick to common page dimensions (612x792 for US Letter) -- **Validate boundaries**: Ensure fields don't extend beyond page edges -- **Consider scaling**: Be aware that different DPI settings may affect coordinates +### JSON String Formatting + +⚠️ **Critical**: Recipients and fields must be valid JSON strings when added to form-data. + +**Correct**: + +```javascript +const recipients = JSON.stringify([ + { name: "John", email: "john@example.com", signingOrder: 1 }, +]); +formData.append("recipients", recipients); +``` + +**Incorrect**: + +```javascript +// Don't send object/array directly! +formData.append("recipients", recipientsArray); // ❌ Wrong +formData.append("recipients", "[{...}]"); // ❌ Wrong (string literal, not stringified) +``` + +**Python Example**: + +```python +import json +recipients = json.dumps([ + {"name": "John", "email": "john@example.com", "signingOrder": 1} +]) +form_data['recipients'] = recipients +``` + +**C# Example**: + +```csharp +using System.Text.Json; +var recipients = JsonSerializer.Serialize(new[] { + new { name = "John", email = "john@example.com", signingOrder = 1 } +}); +formData.Add(new StringContent(recipients), "recipients"); +``` ## Error Handling & Troubleshooting @@ -555,61 +828,132 @@ For precise positioning when text anchors aren't suitable, use coordinate-based | `400` | Bad Request | Check request body format and required fields | | `401` | Unauthorized | Verify API token and headers | | `403` | Forbidden | Check organization ID and permissions | -| `404` | Not Found | Verify document ID and endpoint URLs | +| `404` | Not Found | Verify endpoint URLs are correct | | `422` | Unprocessable Entity | Validate field values and constraints | | `429` | Too Many Requests | Implement rate limiting and retry logic | | `500` | Internal Server Error | Contact support if persistent | ### Common Issues -#### Authentication Failures +#### JSON String Formatting Errors -**Symptoms**: 401 Unauthorized responses +**Symptoms**: 400 Bad Request with message "Invalid JSON in recipients/fields" **Solutions**: -- Verify API token is correct and not expired -- Check that `x-rapiddocx-org-id` header matches your organization -- Ensure Bearer token format: `Bearer YOUR_TOKEN` +- ✅ Verify JSON.stringify() or equivalent is used for recipients, fields, ccEmails +- ✅ Check JSON is valid using JSONLint or similar validator +- ✅ Ensure proper escaping of quotes in JSON strings +- ✅ Test with minimal example first (1 recipient, 1 field) -#### Document Upload Failures +**Example Error Response**: -**Symptoms**: Upload returns error or times out +```json +{ + "error": "Invalid JSON string in recipients field", + "code": "JSONParseError", + "details": "Unexpected token at position 45" +} +``` + +**Debug Steps**: + +1. Log the JSON string before sending +2. Validate JSON with online validator +3. Check for special characters or unescaped quotes +4. Test with hardcoded valid JSON first + +#### File Source Errors + +**Symptoms**: 400 Bad Request with message about file source **Solutions**: -- Verify PDF file is not corrupted or password-protected -- Check file size is under the maximum limit (typically 10MB) -- Ensure file is actually a PDF (check MIME type) -- Verify network connection and try again +- ✅ Provide exactly ONE of: file, deliverableId, templateId, fileId, fileLink +- ✅ Verify UUIDs are valid format (8-4-4-4-12 characters) +- ✅ Check file upload isn't corrupted or empty +- ✅ Ensure fileLink is accessible (not behind auth) -#### Recipient Configuration Issues +**Example Error Response**: -**Symptoms**: Recipients not receiving signing invitations +```json +{ + "error": "Must provide exactly one file source", + "code": "InvalidFileSource" +} +``` + +#### Recipients/Fields Mismatch + +**Symptoms**: 400 Bad Request about missing recipient or email mismatch + +**Solutions**: + +- ✅ Verify recipientEmail in fields matches email in recipients array **exactly** +- ✅ Check for typos in email addresses +- ✅ Ensure all fields reference valid recipients +- ✅ Email matching is case-sensitive + +**Example**: + +```javascript +// Recipients array +[{ email: "john.smith@company.com", ... }] + +// Fields array - must match exactly +[{ recipientEmail: "john.smith@company.com", ... }] // ✅ Correct +[{ recipientEmail: "John.Smith@company.com", ... }] // ❌ Wrong (case mismatch) +``` + +#### Authentication Failures + +**Symptoms**: 401 Unauthorized responses + +**Solutions**: + +- ✅ Verify API token is correct and not expired +- ✅ Check that `x-rapiddocx-org-id` header matches your organization +- ✅ Ensure Bearer token format: `Bearer YOUR_TOKEN` (with space) +- ✅ Confirm token has necessary permissions + +**Example Correct Headers**: + +```http +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +x-rapiddocx-org-id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +``` + +#### Document Upload Failures + +**Symptoms**: Upload returns error or times out **Solutions**: -- Verify email addresses are valid and correctly formatted -- Check signing order numbers are sequential (1, 2, 3...) -- Ensure document ID from Step 1 is used correctly -- Verify recipient metadata format is correct +- ✅ Verify PDF file is not corrupted or password-protected +- ✅ Check file size is under maximum limit (typically 10MB) +- ✅ Ensure file is actually a PDF (check MIME type) +- ✅ Verify network connection and try again +- ✅ For fileLink, ensure URL is accessible #### Field Positioning Problems -**Symptoms**: Signature fields appear in wrong locations +**Symptoms**: Signature fields appear in wrong locations or not at all **Template-based Solutions**: -- Verify anchor text exists in the PDF document -- Check anchor text matches exactly (case-sensitive by default) -- Test with `caseSensitive: false` if having matching issues -- Use PDF coordinates as fallback if anchors don't work + +- ✅ Verify anchor text exists in the PDF document +- ✅ Check anchor text matches exactly (case-sensitive by default) +- ✅ Test with `caseSensitive: false` if having matching issues +- ✅ Try different placement options (replace, before, after) +- ✅ Use prepare-for-review to visually verify placement **Coordinate-based Solutions**: -- Verify page dimensions match your PDF's actual size -- Check that x,y coordinates are within page boundaries -- Ensure coordinates account for any PDF margins or headers -- Test with different page numbers if multi-page document -- Validate that `x + width ≤ pageWidth` and `y + height ≤ pageHeight` + +- ✅ Verify page dimensions match your PDF's actual size +- ✅ Check that x,y coordinates are within page boundaries +- ✅ Ensure coordinates account for any PDF margins or headers +- ✅ Test with different page numbers if multi-page document +- ✅ Validate that `x + width ≤ pageWidth` and `y + height ≤ pageHeight` #### Webhook Integration Issues @@ -617,26 +961,44 @@ For precise positioning when text anchors aren't suitable, use coordinate-based **Solutions**: -- Verify webhook URLs are accessible and return 200 OK -- Check webhook configuration in organization settings -- Review webhook delivery history for error details -- Test webhook endpoints with external tools +- ✅ Verify webhook URLs are accessible and return 200 OK +- ✅ Check webhook configuration in organization settings +- ✅ Review webhook delivery history for error details +- ✅ Test webhook endpoints with external tools (webhook.site, ngrok) +- ✅ Implement HMAC signature verification ### Debugging Tips -1. **Enable request logging**: Log all API requests and responses -2. **Test step by step**: Isolate issues by testing each step individually -3. **Use Postman**: Import examples and test manually before coding -4. **Check network**: Verify connectivity to `turbodocx.com` -5. **Validate JSON**: Ensure request bodies are valid JSON format - - +1. **Test with prepare-for-review first**: Visual confirmation before sending emails +2. **Use preview URLs**: Verify field placement and document appearance +3. **Check response documentId**: Save this for tracking and debugging +4. **Enable request logging**: Log all requests and responses with timestamps +5. **Test with minimal payloads**: Start simple (1 recipient, 1 field), add complexity incrementally +6. **Validate JSON before sending**: Use JSON validators to check format +7. **Use Postman/Insomnia**: Test manually before writing code +8. **Check API status page**: Verify TurboDocx services are operational +9. **Review error messages carefully**: Error responses include specific details +10. **Monitor rate limits**: Track API usage to avoid throttling + +### Example Debug Request + +```bash +# Test with curl to isolate issues +curl -X POST https://api.turbodocx.com/turbosign/single/prepare-for-review \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "x-rapiddocx-org-id: YOUR_ORG_ID" \ + -F "file=@document.pdf" \ + -F "documentName=Test Document" \ + -F 'recipients=[{"name":"Test User","email":"test@example.com","signingOrder":1}]' \ + -F 'fields=[{"recipientEmail":"test@example.com","type":"signature","page":1,"x":100,"y":200,"width":200,"height":80,"pageWidth":612,"pageHeight":792}]' \ + -v +``` ## Next Steps ### Webhooks - The Next Logical Step -Now that you've integrated the basic signing flow, the next step is setting up webhooks to receive real-time notifications when documents are signed. This eliminates the need for polling and provides instant updates about document status changes. +Now that you've integrated the single-step signing flow, the next step is setting up webhooks to receive real-time notifications when documents are signed. This eliminates the need for polling and provides instant updates about document status changes. 📖 **[Learn how to configure Webhooks →](/docs/TurboSign/Webhooks)** @@ -656,4 +1018,4 @@ Need help with your integration? --- -Ready to get started? Follow the step-by-step guide above to integrate TurboSign API into your application and start collecting electronic signatures programmatically! +Ready to get started? Follow the guide above to integrate TurboSign single-step API into your application and start collecting electronic signatures programmatically with a single API call! diff --git a/static/img/turbosign/api/types.svg b/static/img/turbosign/api/types.svg new file mode 100644 index 0000000..5486791 --- /dev/null +++ b/static/img/turbosign/api/types.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/controller.cs b/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/controller.cs new file mode 100644 index 0000000..3d5f49d --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/controller.cs @@ -0,0 +1,146 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace TurboSign.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SignatureController : ControllerBase + { + private const string API_TOKEN = "YOUR_API_TOKEN"; + private const string ORG_ID = "YOUR_ORGANIZATION_ID"; + private const string BASE_URL = "https://api.turbodocx.com"; + private readonly HttpClient _httpClient; + + public SignatureController(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(); + } + + [HttpPost("prepare-for-review")] + public async Task PrepareForReview(IFormFile file) + { + try + { + using var formData = new MultipartFormDataContent(); + + // Add file + var fileContent = new StreamContent(file.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + formData.Add(fileContent, "file", file.FileName); + + // Add document metadata + formData.Add(new StringContent("Contract Agreement"), "documentName"); + formData.Add(new StringContent("Please review and sign this contract"), "documentDescription"); + formData.Add(new StringContent("Your Company"), "senderName"); + formData.Add(new StringContent("sender@company.com"), "senderEmail"); + + // Add recipients (as JSON string) + var recipients = JsonSerializer.Serialize(new[] + { + new + { + name = "John Smith", + email = "john.smith@company.com", + signingOrder = 1}, + new + { + name = "Jane Doe", + email = "jane.doe@partner.com", + signingOrder = 2} + }); + formData.Add(new StringContent(recipients), "recipients"); + + // Add fields (as JSON string) - Coordinate-based + var fields = JsonSerializer.Serialize(new[] + { + new + { + recipientEmail = "john.smith@company.com", + type = "signature", + page = 1, + x = 100, + y = 200, + width = 200, + height = 80, + required = true + }, + new + { + recipientEmail = "john.smith@company.com", + type = "date", + page = 1, + x = 100, + y = 300, + width = 150, + height = 30, + required = true + }, + new + { + recipientEmail = "jane.doe@partner.com", + type = "signature", + page = 1, + x = 350, + y = 200, + width = 200, + height = 80, + required = true + } + }); + formData.Add(new StringContent(fields), "fields"); + + // Set headers + var request = new HttpRequestMessage(HttpMethod.Post, $"{BASE_URL}/turbosign/single/prepare-for-review") + { + Content = formData + }; + request.Headers.Add("Authorization", $"Bearer {API_TOKEN}"); + request.Headers.Add("x-rapiddocx-org-id", ORG_ID); + request.Headers.Add("User-Agent", "TurboDocx API Client"); + + // Send request + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseContent); + + if (result.GetProperty("success").GetBoolean()) + { + return Ok(new + { + success = true, + documentId = result.GetProperty("documentId").GetString(), + status = result.GetProperty("status").GetString(), + previewUrl = result.GetProperty("previewUrl").GetString(), + recipients = result.GetProperty("recipients"), + message = "Document prepared for review successfully" + }); + } + else + { + return BadRequest(new + { + success = false, + error = result.GetProperty("error").GetString(), + code = result.TryGetProperty("code", out var code) ? code.GetString() : null + }); + } + } + catch (Exception ex) + { + return StatusCode(500, new + { + success = false, + error = "Internal server error", + message = ex.Message + }); + } + } + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/minimal.cs b/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/minimal.cs new file mode 100644 index 0000000..c22b3fc --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/csharp/minimal.cs @@ -0,0 +1,109 @@ +using System.Net.Http.Headers; +using System.Text.Json; + +// Configuration - Update these values +const string API_TOKEN = "YOUR_API_TOKEN"; +const string ORG_ID = "YOUR_ORGANIZATION_ID"; +const string BASE_URL = "https://api.turbodocx.com"; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddHttpClient(); + +var app = builder.Build(); + +app.MapPost("/prepare-for-review", async (IFormFile file, IHttpClientFactory httpClientFactory) => +{ + try + { + var httpClient = httpClientFactory.CreateClient(); + using var formData = new MultipartFormDataContent(); + + // Add file + var fileContent = new StreamContent(file.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + formData.Add(fileContent, "file", file.FileName); + + // Add document metadata + formData.Add(new StringContent("Contract Agreement"), "documentName"); + formData.Add(new StringContent("Please review and sign this contract"), "documentDescription"); + + // Add recipients (as JSON string) + var recipients = JsonSerializer.Serialize(new[] + { + new + { + name = "John Smith", + email = "john.smith@company.com", + signingOrder = 1} + }); + formData.Add(new StringContent(recipients), "recipients"); + + // Add fields (as JSON string) - Coordinate-based + var fields = JsonSerializer.Serialize(new[] + { + new + { + recipientEmail = "john.smith@company.com", + type = "signature", + page = 1, + x = 100, + y = 200, + width = 200, + height = 80, + required = true + }, + new + { + recipientEmail = "john.smith@company.com", + type = "date", + page = 1, + x = 350, + y = 200, + width = 150, + height = 30, + required = true + } + }); + formData.Add(new StringContent(fields), "fields"); + + // Set headers + var request = new HttpRequestMessage(HttpMethod.Post, $"{BASE_URL}/turbosign/single/prepare-for-review") + { + Content = formData + }; + request.Headers.Add("Authorization", $"Bearer {API_TOKEN}"); + request.Headers.Add("x-rapiddocx-org-id", ORG_ID); + request.Headers.Add("User-Agent", "TurboDocx API Client"); + + // Send request + var response = await httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseContent); + + if (result.GetProperty("success").GetBoolean()) + { + return Results.Ok(new + { + success = true, + documentId = result.GetProperty("documentId").GetString(), + status = result.GetProperty("status").GetString(), + previewUrl = result.GetProperty("previewUrl").GetString(), + message = "Document prepared for review successfully" + }); + } + else + { + return Results.BadRequest(new + { + success = false, + error = result.GetProperty("error").GetString() + }); + } + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +app.Run(); diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/go.go b/static/scripts/turbosign/api/single-step/prepare-for-review/go.go new file mode 100644 index 0000000..d385c30 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/go.go @@ -0,0 +1,188 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" +) + +// Configuration - Update these values +const ( + API_TOKEN = "YOUR_API_TOKEN" + ORG_ID = "YOUR_ORGANIZATION_ID" + BASE_URL = "https://api.turbodocx.com" +) + +type Recipient struct { + Name string `json:"name"` + Email string `json:"email"` + SigningOrder int `json:"signingOrder"` +} + +type Field struct { + RecipientEmail string `json:"recipientEmail"` + Type string `json:"type"` + Page int `json:"page,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Required bool `json:"required"` +} + +type Response struct { + Success bool `json:"success"` + DocumentID string `json:"documentId"` + Status string `json:"status"` + PreviewURL string `json:"previewUrl"` + Recipients []map[string]interface{} `json:"recipients"` + Message string `json:"message"` + Error string `json:"error,omitempty"` + Code string `json:"code,omitempty"` +} + +func prepareDocumentForReview() error { + // Prepare recipients + recipients := []Recipient{ + { + Name: "John Smith", + Email: "john.smith@company.com", + SigningOrder: 1, + }, + { + Name: "Jane Doe", + Email: "jane.doe@partner.com", + SigningOrder: 2, + }, + } + + recipientsJSON, err := json.Marshal(recipients) + if err != nil { + return fmt.Errorf("failed to marshal recipients: %w", err) + } + + // Prepare fields - Coordinate-based + fields := []Field{ + { + RecipientEmail: "john.smith@company.com", + Type: "signature", + Page: 1, + X: 100, + Y: 200, + Width: 200, + Height: 80, + Required: true, + }, + { + RecipientEmail: "john.smith@company.com", + Type: "date", + Page: 1, + X: 100, + Y: 300, + Width: 150, + Height: 30, + Required: true, + }, + { + RecipientEmail: "jane.doe@partner.com", + Type: "signature", + Page: 1, + X: 350, + Y: 200, + Width: 200, + Height: 80, + Required: true, + }, + } + + fieldsJSON, err := json.Marshal(fields) + if err != nil { + return fmt.Errorf("failed to marshal fields: %w", err) + } + + // Create multipart form + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add file + file, err := os.Open("./contract.pdf") + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + part, err := writer.CreateFormFile("file", "contract.pdf") + if err != nil { + return fmt.Errorf("failed to create form file: %w", err) + } + if _, err := io.Copy(part, file); err != nil { + return fmt.Errorf("failed to copy file: %w", err) + } + + // Add form fields + writer.WriteField("documentName", "Contract Agreement") + writer.WriteField("documentDescription", "Please review and sign this contract") + writer.WriteField("senderName", "Your Company") + writer.WriteField("senderEmail", "sender@company.com") + writer.WriteField("recipients", string(recipientsJSON)) + writer.WriteField("fields", string(fieldsJSON)) + + err = writer.Close() + if err != nil { + return fmt.Errorf("failed to close writer: %w", err) + } + + // Create request + req, err := http.NewRequest("POST", BASE_URL+"/turbosign/single/prepare-for-review", body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+API_TOKEN) + req.Header.Set("x-rapiddocx-org-id", ORG_ID) + req.Header.Set("User-Agent", "TurboDocx API Client") + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Send request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Parse response + var result Response + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + if result.Success { + fmt.Println("✅ Document prepared for review") + fmt.Printf("Document ID: %s\n", result.DocumentID) + fmt.Printf("Status: %s\n", result.Status) + fmt.Printf("Preview URL: %s\n", result.PreviewURL) + fmt.Println("\n🔍 Open this URL to preview the document:") + fmt.Println(result.PreviewURL) + } else { + fmt.Printf("❌ Error: %s\n", result.Error) + if result.Code != "" { + fmt.Printf("Error Code: %s\n", result.Code) + } + return fmt.Errorf("API error: %s", result.Error) + } + + return nil +} + +func main() { + if err := prepareDocumentForReview(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/java.java b/static/scripts/turbosign/api/single-step/prepare-for-review/java.java new file mode 100644 index 0000000..8550ebe --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/java.java @@ -0,0 +1,128 @@ +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; + +public class TurboSignPrepareForReview { + + // Configuration - Update these values + private static final String API_TOKEN = "YOUR_API_TOKEN"; + private static final String ORG_ID = "YOUR_ORGANIZATION_ID"; + private static final String BASE_URL = "https://api.turbodocx.com"; + + private final OkHttpClient client = new OkHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public void prepareDocumentForReview(File pdfFile) throws IOException { + // Prepare recipients + List> recipients = new ArrayList<>(); + Map recipient1 = new HashMap<>(); + recipient1.put("name", "John Smith"); + recipient1.put("email", "john.smith@company.com"); + recipient1.put("signingOrder", 1); + Map recipient2 = new HashMap<>(); + recipient2.put("name", "Jane Doe"); + recipient2.put("email", "jane.doe@partner.com"); + recipient2.put("signingOrder", 2); + String recipientsJson = objectMapper.writeValueAsString(recipients); + + // Prepare fields - Coordinate-based + List> fields = new ArrayList<>(); + + Map field1 = new HashMap<>(); + field1.put("recipientEmail", "john.smith@company.com"); + field1.put("type", "signature"); + field1.put("page", 1); + field1.put("x", 100); + field1.put("y", 200); + field1.put("width", 200); + field1.put("height", 80); + field1.put("required", true); + fields.add(field1); + + Map field2 = new HashMap<>(); + field2.put("recipientEmail", "john.smith@company.com"); + field2.put("type", "date"); + field2.put("page", 1); + field2.put("x", 100); + field2.put("y", 300); + field2.put("width", 150); + field2.put("height", 30); + field2.put("required", true); + fields.add(field2); + + Map field3 = new HashMap<>(); + field3.put("recipientEmail", "jane.doe@partner.com"); + field3.put("type", "signature"); + field3.put("page", 1); + field3.put("x", 350); + field3.put("y", 200); + field3.put("width", 200); + field3.put("height", 80); + field3.put("required", true); + fields.add(field3); + + String fieldsJson = objectMapper.writeValueAsString(fields); + + // Build multipart request + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", pdfFile.getName(), + RequestBody.create(pdfFile, MediaType.parse("application/pdf"))) + .addFormDataPart("documentName", "Contract Agreement") + .addFormDataPart("documentDescription", "Please review and sign this contract") + .addFormDataPart("senderName", "Your Company") + .addFormDataPart("senderEmail", "sender@company.com") + .addFormDataPart("recipients", recipientsJson) + .addFormDataPart("fields", fieldsJson) + .build(); + + // Build request + Request request = new Request.Builder() + .url(BASE_URL + "/turbosign/single/prepare-for-review") + .addHeader("Authorization", "Bearer " + API_TOKEN) + .addHeader("x-rapiddocx-org-id", ORG_ID) + .addHeader("User-Agent", "TurboDocx API Client") + .post(requestBody) + .build(); + + // Execute request + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected response: " + response); + } + + String responseBody = response.body().string(); + Map result = objectMapper.readValue(responseBody, Map.class); + + if ((Boolean) result.get("success")) { + System.out.println("✅ Document prepared for review"); + System.out.println("Document ID: " + result.get("documentId")); + System.out.println("Status: " + result.get("status")); + System.out.println("Preview URL: " + result.get("previewUrl")); + System.out.println("\n🔍 Open this URL to preview the document:"); + System.out.println(result.get("previewUrl")); + } else { + System.err.println("❌ Error: " + result.get("error")); + if (result.containsKey("code")) { + System.err.println("Error Code: " + result.get("code")); + } + } + } + } + + public static void main(String[] args) { + try { + TurboSignPrepareForReview service = new TurboSignPrepareForReview(); + File pdfFile = new File("./contract.pdf"); + service.prepareDocumentForReview(pdfFile); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/express.js b/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/express.js new file mode 100644 index 0000000..26f254e --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/express.js @@ -0,0 +1,102 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Prepare form data +const formData = new FormData(); + +// Add file +formData.append('file', fs.createReadStream('./contract.pdf')); + +// Add document metadata +formData.append('documentName', 'Contract Agreement'); +formData.append('documentDescription', 'Please review and sign this contract'); +formData.append('senderName', 'Your Company'); +formData.append('senderEmail', 'sender@company.com'); + +// Add recipients (as JSON string) +const recipients = JSON.stringify([ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1 + }, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2 + } +]); +formData.append('recipients', recipients); + +// Add fields (as JSON string) - Coordinate-based positioning +const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } +]); +formData.append('fields', fields); + +// Send request to prepare-for-review endpoint +const response = await fetch(`${BASE_URL}/turbosign/single/prepare-for-review`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData +}); + +const result = await response.json(); + +if (result.success) { + console.log('✅ Document prepared for review'); + console.log('Document ID:', result.documentId); + console.log('Status:', result.status); + console.log('Preview URL:', result.previewUrl); + console.log('\n📋 Recipients:'); + result.recipients.forEach(r => { + console.log(` - ${r.name} (${r.email}) - ID: ${r.id}`); + }); + console.log('\n🔍 Open this URL to preview the document:'); + console.log(result.previewUrl); +} else { + console.error('❌ Error:', result.error || result.message); + if (result.code) { + console.error('Error Code:', result.code); + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/fastify.js b/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/fastify.js new file mode 100644 index 0000000..232109d --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/nodejs/fastify.js @@ -0,0 +1,113 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Fastify route handler example +async function prepareDocumentForReview(request, reply) { + try { + // Prepare form data + const formData = new FormData(); + + // Add file + formData.append('file', fs.createReadStream('./contract.pdf')); + + // Add document metadata + formData.append('documentName', 'Contract Agreement'); + formData.append('documentDescription', 'Please review and sign this contract'); + + // Add recipients (as JSON string) + const recipients = JSON.stringify([ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1 + }, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2 + } + ]); + formData.append('recipients', recipients); + + // Add fields (as JSON string) - Coordinate-based positioning + const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } + ]); + formData.append('fields', fields); + + // Send request + const response = await fetch(`${BASE_URL}/turbosign/single/prepare-for-review`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData + }); + + const result = await response.json(); + + if (result.success) { + return reply.send({ + success: true, + documentId: result.documentId, + status: result.status, + previewUrl: result.previewUrl, + message: 'Document prepared for review successfully' + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || 'Failed to prepare document', + code: result.code + }); + } + } catch (error) { + console.error('Error preparing document:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { prepareDocumentForReview }; diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/php.php b/static/scripts/turbosign/api/single-step/prepare-for-review/php.php new file mode 100644 index 0000000..4bf6bb5 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/php.php @@ -0,0 +1,118 @@ + "John Smith", + "email" => "john.smith@company.com", + "signingOrder" => 1 + ], + [ + "name" => "Jane Doe", + "email" => "jane.doe@partner.com", + "signingOrder" => 2 + ] +]); + +// Prepare fields (as JSON string) - Coordinate-based positioning +$fields = json_encode([ + [ + "recipientEmail" => "john.smith@company.com", + "type" => "signature", + "page" => 1, + "x" => 100, + "y" => 200, + "width" => 200, + "height" => 80, + "required" => true + ], + [ + "recipientEmail" => "john.smith@company.com", + "type" => "date", + "page" => 1, + "x" => 100, + "y" => 300, + "width" => 150, + "height" => 30, + "required" => true + ], + [ + "recipientEmail" => "jane.doe@partner.com", + "type" => "signature", + "page" => 1, + "x" => 350, + "y" => 200, + "width" => 200, + "height" => 80, + "required" => true + ] +]); + +// Prepare POST fields +$postFields = [ + 'file' => new CURLFile('./contract.pdf', 'application/pdf', 'contract.pdf'), + 'documentName' => 'Contract Agreement', + 'documentDescription' => 'Please review and sign this contract', + 'senderName' => 'Your Company', + 'senderEmail' => 'sender@company.com', + 'recipients' => $recipients, + 'fields' => $fields +]; + +// Set cURL options +curl_setopt_array($ch, [ + CURLOPT_URL => $BASE_URL . '/turbosign/single/prepare-for-review', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postFields, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $API_TOKEN, + 'x-rapiddocx-org-id: ' . $ORG_ID, + 'User-Agent: TurboDocx API Client' + ] +]); + +// Execute request +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + +// Check for errors +if (curl_errno($ch)) { + echo 'Error: ' . curl_error($ch) . "\n"; + curl_close($ch); + exit(1); +} + +curl_close($ch); + +// Parse response +$result = json_decode($response, true); + +if ($result['success']) { + echo "✅ Document prepared for review\n"; + echo "Document ID: " . $result['documentId'] . "\n"; + echo "Status: " . $result['status'] . "\n"; + echo "Preview URL: " . $result['previewUrl'] . "\n"; + echo "\n📋 Recipients:\n"; + foreach ($result['recipients'] as $recipient) { + echo " - {$recipient['name']} ({$recipient['email']}) - ID: {$recipient['id']}\n"; + } + echo "\n🔍 Open this URL to preview the document:\n"; + echo $result['previewUrl'] . "\n"; +} else { + echo "❌ Error: " . ($result['error'] ?? $result['message']) . "\n"; + if (isset($result['code'])) { + echo "Error Code: " . $result['code'] . "\n"; + } + exit(1); +} + +?> diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/powershell.ps1 b/static/scripts/turbosign/api/single-step/prepare-for-review/powershell.ps1 new file mode 100644 index 0000000..d1c637b --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/powershell.ps1 @@ -0,0 +1,138 @@ +# Configuration - Update these values +$API_TOKEN = "YOUR_API_TOKEN" +$ORG_ID = "YOUR_ORGANIZATION_ID" +$BASE_URL = "https://api.turbodocx.com" + +# Prepare recipients (as JSON string) +$recipients = @( + @{ + name = "John Smith" + email = "john.smith@company.com" + signingOrder = 1 + }, + @{ + name = "Jane Doe" + email = "jane.doe@partner.com" + signingOrder = 2 + } +) | ConvertTo-Json -Compress + +# Prepare fields (as JSON string) - Coordinate-based +$fields = @( + @{ + recipientEmail = "john.smith@company.com" + type = "signature" + page = 1 + x = 100 + y = 200 + width = 200 + height = 80 + required = $true + }, + @{ + recipientEmail = "john.smith@company.com" + type = "date" + page = 1 + x = 100 + y = 300 + width = 150 + height = 30 + required = $true + }, + @{ + recipientEmail = "jane.doe@partner.com" + type = "signature" + page = 1 + x = 350 + y = 200 + width = 200 + height = 80 + required = $true + } +) | ConvertTo-Json -Compress -Depth 10 + +# Prepare file +$filePath = "./contract.pdf" +$fileName = [System.IO.Path]::GetFileName($filePath) +$fileBytes = [System.IO.File]::ReadAllBytes($filePath) +$fileContent = [System.Text.Encoding]::GetEncoding('iso-8859-1').GetString($fileBytes) + +# Create boundary +$boundary = [System.Guid]::NewGuid().ToString() + +# Build multipart form data +$bodyLines = @( + "--$boundary", + "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", + "Content-Type: application/pdf", + "", + $fileContent, + "--$boundary", + "Content-Disposition: form-data; name=`"documentName`"", + "", + "Contract Agreement", + "--$boundary", + "Content-Disposition: form-data; name=`"documentDescription`"", + "", + "Please review and sign this contract", + "--$boundary", + "Content-Disposition: form-data; name=`"senderName`"", + "", + "Your Company", + "--$boundary", + "Content-Disposition: form-data; name=`"senderEmail`"", + "", + "sender@company.com", + "--$boundary", + "Content-Disposition: form-data; name=`"recipients`"", + "", + $recipients, + "--$boundary", + "Content-Disposition: form-data; name=`"fields`"", + "", + $fields, + "--$boundary--" +) + +$body = $bodyLines -join "`r`n" + +# Prepare headers +$headers = @{ + "Authorization" = "Bearer $API_TOKEN" + "x-rapiddocx-org-id" = $ORG_ID + "User-Agent" = "TurboDocx API Client" + "Content-Type" = "multipart/form-data; boundary=$boundary" +} + +try { + # Send request + $response = Invoke-RestMethod ` + -Uri "$BASE_URL/turbosign/single/prepare-for-review" ` + -Method Post ` + -Headers $headers ` + -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) + + if ($response.success) { + Write-Host "✅ Document prepared for review" -ForegroundColor Green + Write-Host "Document ID: $($response.documentId)" + Write-Host "Status: $($response.status)" + Write-Host "Preview URL: $($response.previewUrl)" + Write-Host "`n📋 Recipients:" + foreach ($recipient in $response.recipients) { + Write-Host " - $($recipient.name) ($($recipient.email)) - ID: $($recipient.id)" + } + Write-Host "`n🔍 Open this URL to preview the document:" + Write-Host $response.previewUrl + } else { + Write-Host "❌ Error: $($response.error)" -ForegroundColor Red + if ($response.code) { + Write-Host "Error Code: $($response.code)" -ForegroundColor Red + } + exit 1 + } +} +catch { + Write-Host "❌ Exception: $_" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/python/fastapi.py b/static/scripts/turbosign/api/single-step/prepare-for-review/python/fastapi.py new file mode 100644 index 0000000..82b2717 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/python/fastapi.py @@ -0,0 +1,130 @@ +import json +import requests +from fastapi import FastAPI, HTTPException, UploadFile, File +from pydantic import BaseModel + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = FastAPI() + +class PrepareResponse(BaseModel): + success: bool + documentId: str + status: str + previewUrl: str + message: str + +@app.post('/prepare-for-review', response_model=PrepareResponse) +async def prepare_document_for_review(file: UploadFile = File(...)): + try: + # Prepare form data + files = { + 'file': (file.filename, file.file, file.content_type) + } + + # Prepare form fields + data = { + 'documentName': 'Contract Agreement', + 'documentDescription': 'Please review and sign this contract', + 'senderName': 'Your Company', + 'senderEmail': 'sender@company.com' + } + + # Add recipients (as JSON string) + recipients = json.dumps([ + { + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 + }, + { + "name": "Jane Doe", + "email": "jane.doe@partner.com", + "signingOrder": 2 + } + ]) + data['recipients'] = recipients + + # Add fields (as JSON string) - Coordinate-based positioning + fields = json.dumps([ + { + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": True + }, + { + "recipientEmail": "john.smith@company.com", + "type": "date", + "page": 1, + "x": 100, + "y": 300, + "width": 150, + "height": 30, + "required": True + }, + { + "recipientEmail": "jane.doe@partner.com", + "type": "signature", + "page": 1, + "x": 350, + "y": 200, + "width": 200, + "height": 80, + "required": True + } + ]) + data['fields'] = fields + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/single/prepare-for-review', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return PrepareResponse( + success=True, + documentId=result['documentId'], + status=result['status'], + previewUrl=result['previewUrl'], + message='Document prepared for review successfully' + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to prepare document'), + 'code': result.get('code') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error preparing document: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/python/flask.py b/static/scripts/turbosign/api/single-step/prepare-for-review/python/flask.py new file mode 100644 index 0000000..a0534ee --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/python/flask.py @@ -0,0 +1,120 @@ +import json +import requests +from flask import Flask, jsonify + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = Flask(__name__) + +@app.route('/prepare-for-review', methods=['POST']) +def prepare_document_for_review(): + try: + # Prepare form data + files = { + 'file': ('contract.pdf', open('./contract.pdf', 'rb'), 'application/pdf') + } + + # Prepare form fields + data = { + 'documentName': 'Contract Agreement', + 'documentDescription': 'Please review and sign this contract', + 'senderName': 'Your Company', + 'senderEmail': 'sender@company.com' + } + + # Add recipients (as JSON string) + recipients = json.dumps([ + { + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 + }, + { + "name": "Jane Doe", + "email": "jane.doe@partner.com", + "signingOrder": 2 + } + ]) + data['recipients'] = recipients + + # Add fields (as JSON string) - Coordinate-based positioning + fields = json.dumps([ + { + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": True + }, + { + "recipientEmail": "john.smith@company.com", + "type": "date", + "page": 1, + "x": 100, + "y": 300, + "width": 150, + "height": 30, + "required": True + }, + { + "recipientEmail": "jane.doe@partner.com", + "type": "signature", + "page": 1, + "x": 350, + "y": 200, + "width": 200, + "height": 80, + "required": True + } + ]) + data['fields'] = fields + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/single/prepare-for-review', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return jsonify({ + 'success': True, + 'documentId': result['documentId'], + 'status': result['status'], + 'previewUrl': result['previewUrl'], + 'recipients': result['recipients'], + 'message': 'Document prepared for review successfully' + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to prepare document'), + 'code': result.get('code') + }), 400 + + except Exception as error: + print(f'Error preparing document: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/scripts/turbosign/api/single-step/prepare-for-review/ruby.rb b/static/scripts/turbosign/api/single-step/prepare-for-review/ruby.rb new file mode 100644 index 0000000..5fc0eb7 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-review/ruby.rb @@ -0,0 +1,131 @@ +require 'net/http' +require 'uri' +require 'json' +require 'mime/types' + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +def prepare_document_for_review + # Prepare recipients + recipients = [ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1}, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2} + ] + + # Prepare fields - Coordinate-based + fields = [ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } + ] + + # Create multipart form data + uri = URI.parse("#{BASE_URL}/turbosign/single/prepare-for-review") + + boundary = "----WebKitFormBoundary#{rand(1000000000)}" + + post_body = [] + + # Add file + file_path = "./contract.pdf" + file_content = File.read(file_path) + post_body << "--#{boundary}\r\n" + post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"contract.pdf\"\r\n" + post_body << "Content-Type: application/pdf\r\n\r\n" + post_body << file_content + post_body << "\r\n" + + # Add form fields + form_fields = { + 'documentName' => 'Contract Agreement', + 'documentDescription' => 'Please review and sign this contract', + 'senderName' => 'Your Company', + 'senderEmail' => 'sender@company.com', + 'recipients' => recipients.to_json, + 'fields' => fields.to_json + } + + form_fields.each do |key, value| + post_body << "--#{boundary}\r\n" + post_body << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + post_body << value.to_s + post_body << "\r\n" + end + + post_body << "--#{boundary}--\r\n" + + # Create HTTP request + request = Net::HTTP::Post.new(uri.request_uri) + request["Authorization"] = "Bearer #{API_TOKEN}" + request["x-rapiddocx-org-id"] = ORG_ID + request["User-Agent"] = "TurboDocx API Client" + request["Content-Type"] = "multipart/form-data; boundary=#{boundary}" + request.body = post_body.join + + # Send request + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + + response = http.request(request) + result = JSON.parse(response.body) + + if result['success'] + puts "✅ Document prepared for review" + puts "Document ID: #{result['documentId']}" + puts "Status: #{result['status']}" + puts "Preview URL: #{result['previewUrl']}" + puts "\n📋 Recipients:" + result['recipients'].each do |recipient| + puts " - #{recipient['name']} (#{recipient['email']}) - ID: #{recipient['id']}" + end + puts "\n🔍 Open this URL to preview the document:" + puts result['previewUrl'] + else + puts "❌ Error: #{result['error'] || result['message']}" + puts "Error Code: #{result['code']}" if result['code'] + exit 1 + end +rescue => e + puts "❌ Exception: #{e.message}" + puts e.backtrace + exit 1 +end + +# Run the function +prepare_document_for_review diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/controller.cs b/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/controller.cs new file mode 100644 index 0000000..6d468d6 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/controller.cs @@ -0,0 +1,149 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace TurboSign.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class SignatureController : ControllerBase + { + private const string API_TOKEN = "YOUR_API_TOKEN"; + private const string ORG_ID = "YOUR_ORGANIZATION_ID"; + private const string BASE_URL = "https://api.turbodocx.com"; + private readonly HttpClient _httpClient; + + public SignatureController(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(); + } + + [HttpPost("prepare-for-signing")] + public async Task PrepareForSigning(IFormFile file) + { + try + { + using var formData = new MultipartFormDataContent(); + + // Add file + var fileContent = new StreamContent(file.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + formData.Add(fileContent, "file", file.FileName); + + // Add document metadata + formData.Add(new StringContent("Contract Agreement"), "documentName"); + formData.Add(new StringContent("Please review and sign this contract"), "documentDescription"); + formData.Add(new StringContent("Your Company"), "senderName"); + formData.Add(new StringContent("sender@company.com"), "senderEmail"); + + // Add recipients (as JSON string) + var recipients = JsonSerializer.Serialize(new[] + { + new + { + name = "John Smith", + email = "john.smith@company.com", + signingOrder = 1 + }, + new + { + name = "Jane Doe", + email = "jane.doe@partner.com", + signingOrder = 2 + } + }); + formData.Add(new StringContent(recipients), "recipients"); + + // Add fields (as JSON string) - Coordinate-based positioning + var fields = JsonSerializer.Serialize(new[] + { + new + { + recipientEmail = "john.smith@company.com", + type = "signature", + page = 1, + x = 100, + y = 200, + width = 200, + height = 80, + required = true + }, + new + { + recipientEmail = "john.smith@company.com", + type = "date", + page = 1, + x = 100, + y = 300, + width = 150, + height = 30, + required = true + }, + new + { + recipientEmail = "jane.doe@partner.com", + type = "signature", + page = 1, + x = 350, + y = 200, + width = 200, + height = 80, + required = true + } + }); + formData.Add(new StringContent(fields), "fields"); + + // Optional: Add CC emails + var ccEmails = JsonSerializer.Serialize(new[] { "manager@company.com", "legal@company.com" }); + formData.Add(new StringContent(ccEmails), "ccEmails"); + + // Set headers + var request = new HttpRequestMessage(HttpMethod.Post, $"{BASE_URL}/turbosign/single/prepare-for-signing") + { + Content = formData + }; + request.Headers.Add("Authorization", $"Bearer {API_TOKEN}"); + request.Headers.Add("x-rapiddocx-org-id", ORG_ID); + request.Headers.Add("User-Agent", "TurboDocx API Client"); + + // Send request + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseContent); + + if (result.GetProperty("success").GetBoolean()) + { + return Ok(new + { + success = true, + documentId = result.GetProperty("documentId").GetString(), + message = result.GetProperty("message").GetString() + }); + } + else + { + return BadRequest(new + { + success = false, + error = result.GetProperty("error").GetString(), + code = result.TryGetProperty("code", out var code) ? code.GetString() : null + }); + } + } + catch (Exception ex) + { + return StatusCode(500, new + { + success = false, + error = "Internal server error", + message = ex.Message + }); + } + } + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/minimal.cs b/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/minimal.cs new file mode 100644 index 0000000..084e203 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/csharp/minimal.cs @@ -0,0 +1,107 @@ +using System.Net.Http.Headers; +using System.Text.Json; + +// Configuration - Update these values +const string API_TOKEN = "YOUR_API_TOKEN"; +const string ORG_ID = "YOUR_ORGANIZATION_ID"; +const string BASE_URL = "https://api.turbodocx.com"; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddHttpClient(); + +var app = builder.Build(); + +app.MapPost("/prepare-for-signing", async (IFormFile file, IHttpClientFactory httpClientFactory) => +{ + try + { + var httpClient = httpClientFactory.CreateClient(); + using var formData = new MultipartFormDataContent(); + + // Add file + var fileContent = new StreamContent(file.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + formData.Add(fileContent, "file", file.FileName); + + // Add document metadata + formData.Add(new StringContent("Contract Agreement"), "documentName"); + formData.Add(new StringContent("Please review and sign this contract"), "documentDescription"); + + // Add recipients (as JSON string) + var recipients = JsonSerializer.Serialize(new[] + { + new + { + name = "John Smith", + email = "john.smith@company.com", + signingOrder = 1} + }); + formData.Add(new StringContent(recipients), "recipients"); + + // Add fields (as JSON string) - Coordinate-based + var fields = JsonSerializer.Serialize(new[] + { + new + { + recipientEmail = "john.smith@company.com", + type = "signature", + page = 1, + x = 100, + y = 200, + width = 200, + height = 80, + required = true + }, + new + { + recipientEmail = "john.smith@company.com", + type = "date", + page = 1, + x = 350, + y = 200, + width = 150, + height = 30, + required = true + } + }); + formData.Add(new StringContent(fields), "fields"); + + // Set headers + var request = new HttpRequestMessage(HttpMethod.Post, $"{BASE_URL}/turbosign/single/prepare-for-signing") + { + Content = formData + }; + request.Headers.Add("Authorization", $"Bearer {API_TOKEN}"); + request.Headers.Add("x-rapiddocx-org-id", ORG_ID); + request.Headers.Add("User-Agent", "TurboDocx API Client"); + + // Send request + var response = await httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(responseContent); + + if (result.GetProperty("success").GetBoolean()) + { + return Results.Ok(new + { + success = true, + documentId = result.GetProperty("documentId").GetString(), + message = result.GetProperty("message").GetString() + }); + } + else + { + return Results.BadRequest(new + { + success = false, + error = result.GetProperty("error").GetString() + }); + } + } + catch (Exception ex) + { + return Results.Problem(ex.Message); + } +}); + +app.Run(); diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/go.go b/static/scripts/turbosign/api/single-step/prepare-for-signing/go.go new file mode 100644 index 0000000..1caeaa5 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/go.go @@ -0,0 +1,192 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" +) + +// Configuration - Update these values +const ( + API_TOKEN = "YOUR_API_TOKEN" + ORG_ID = "YOUR_ORGANIZATION_ID" + BASE_URL = "https://api.turbodocx.com" +) + +type Recipient struct { + Name string `json:"name"` + Email string `json:"email"` + SigningOrder int `json:"signingOrder"` +} + +type Field struct { + RecipientEmail string `json:"recipientEmail"` + Type string `json:"type"` + Page int `json:"page,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Required bool `json:"required"` +} + +type Response struct { + Success bool `json:"success"` + DocumentID string `json:"documentId"` + Message string `json:"message"` + Error string `json:"error,omitempty"` + Code string `json:"code,omitempty"` +} + +func prepareDocumentForSigning() error { + // Prepare recipients + recipients := []Recipient{ + { + Name: "John Smith", + Email: "john.smith@company.com", + SigningOrder: 1, + }, + { + Name: "Jane Doe", + Email: "jane.doe@partner.com", + SigningOrder: 2, + }, + } + + recipientsJSON, err := json.Marshal(recipients) + if err != nil { + return fmt.Errorf("failed to marshal recipients: %w", err) + } + + // Prepare fields - Coordinate-based + fields := []Field{ + { + RecipientEmail: "john.smith@company.com", + Type: "signature", + Page: 1, + X: 100, + Y: 200, + Width: 200, + Height: 80, + Required: true, + }, + { + RecipientEmail: "john.smith@company.com", + Type: "date", + Page: 1, + X: 100, + Y: 300, + Width: 150, + Height: 30, + Required: true, + }, + { + RecipientEmail: "jane.doe@partner.com", + Type: "signature", + Page: 1, + X: 350, + Y: 200, + Width: 200, + Height: 80, + Required: true, + }, + } + + fieldsJSON, err := json.Marshal(fields) + if err != nil { + return fmt.Errorf("failed to marshal fields: %w", err) + } + + // Optional: CC emails + ccEmails := []string{"manager@company.com", "legal@company.com"} + ccEmailsJSON, err := json.Marshal(ccEmails) + if err != nil { + return fmt.Errorf("failed to marshal ccEmails: %w", err) + } + + // Create multipart form + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add file + file, err := os.Open("./contract.pdf") + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + part, err := writer.CreateFormFile("file", "contract.pdf") + if err != nil { + return fmt.Errorf("failed to create form file: %w", err) + } + if _, err := io.Copy(part, file); err != nil { + return fmt.Errorf("failed to copy file: %w", err) + } + + // Add form fields + writer.WriteField("documentName", "Contract Agreement") + writer.WriteField("documentDescription", "Please review and sign this contract") + writer.WriteField("senderName", "Your Company") + writer.WriteField("senderEmail", "sender@company.com") + writer.WriteField("recipients", string(recipientsJSON)) + writer.WriteField("fields", string(fieldsJSON)) + writer.WriteField("ccEmails", string(ccEmailsJSON)) + + err = writer.Close() + if err != nil { + return fmt.Errorf("failed to close writer: %w", err) + } + + // Create request + req, err := http.NewRequest("POST", BASE_URL+"/turbosign/single/prepare-for-signing", body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+API_TOKEN) + req.Header.Set("x-rapiddocx-org-id", ORG_ID) + req.Header.Set("User-Agent", "TurboDocx API Client") + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // Send request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + // Parse response + var result Response + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("failed to decode response: %w", err) + } + + if result.Success { + fmt.Println("✅ Document sent for signing") + fmt.Printf("Document ID: %s\n", result.DocumentID) + fmt.Printf("Message: %s\n", result.Message) + fmt.Println("\n📧 Emails are being sent to recipients asynchronously") + fmt.Println("💡 Tip: Set up webhooks to receive notifications when signing is complete") + } else { + fmt.Printf("❌ Error: %s\n", result.Error) + if result.Code != "" { + fmt.Printf("Error Code: %s\n", result.Code) + } + return fmt.Errorf("API error: %s", result.Error) + } + + return nil +} + +func main() { + if err := prepareDocumentForSigning(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/java.java b/static/scripts/turbosign/api/single-step/prepare-for-signing/java.java new file mode 100644 index 0000000..ed82934 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/java.java @@ -0,0 +1,134 @@ +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; + +public class TurboSignPrepareForSigning { + + // Configuration - Update these values + private static final String API_TOKEN = "YOUR_API_TOKEN"; + private static final String ORG_ID = "YOUR_ORGANIZATION_ID"; + private static final String BASE_URL = "https://api.turbodocx.com"; + + private final OkHttpClient client = new OkHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public void prepareDocumentForSigning(File pdfFile) throws IOException { + // Prepare recipients + List> recipients = new ArrayList<>(); + Map recipient1 = new HashMap<>(); + recipient1.put("name", "John Smith"); + recipient1.put("email", "john.smith@company.com"); + recipient1.put("signingOrder", 1); + Map recipient2 = new HashMap<>(); + recipient2.put("name", "Jane Doe"); + recipient2.put("email", "jane.doe@partner.com"); + recipient2.put("signingOrder", 2); + String recipientsJson = objectMapper.writeValueAsString(recipients); + + // Prepare fields - Coordinate-based + List> fields = new ArrayList<>(); + + Map field1 = new HashMap<>(); + field1.put("recipientEmail", "john.smith@company.com"); + field1.put("type", "signature"); + field1.put("page", 1); + field1.put("x", 100); + field1.put("y", 200); + field1.put("width", 200); + field1.put("height", 80); + field1.put("required", true); + fields.add(field1); + + Map field2 = new HashMap<>(); + field2.put("recipientEmail", "john.smith@company.com"); + field2.put("type", "date"); + field2.put("page", 1); + field2.put("x", 100); + field2.put("y", 300); + field2.put("width", 150); + field2.put("height", 30); + field2.put("required", true); + fields.add(field2); + + Map field3 = new HashMap<>(); + field3.put("recipientEmail", "jane.doe@partner.com"); + field3.put("type", "signature"); + field3.put("page", 1); + field3.put("x", 350); + field3.put("y", 200); + field3.put("width", 200); + field3.put("height", 80); + field3.put("required", true); + fields.add(field3); + + String fieldsJson = objectMapper.writeValueAsString(fields); + + // Optional: Add CC emails + List ccEmails = new ArrayList<>(); + ccEmails.add("manager@company.com"); + ccEmails.add("legal@company.com"); + String ccEmailsJson = objectMapper.writeValueAsString(ccEmails); + + // Build multipart request + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", pdfFile.getName(), + RequestBody.create(pdfFile, MediaType.parse("application/pdf"))) + .addFormDataPart("documentName", "Contract Agreement") + .addFormDataPart("documentDescription", "Please review and sign this contract") + .addFormDataPart("senderName", "Your Company") + .addFormDataPart("senderEmail", "sender@company.com") + .addFormDataPart("recipients", recipientsJson) + .addFormDataPart("fields", fieldsJson) + .addFormDataPart("ccEmails", ccEmailsJson) + .build(); + + // Build request + Request request = new Request.Builder() + .url(BASE_URL + "/turbosign/single/prepare-for-signing") + .addHeader("Authorization", "Bearer " + API_TOKEN) + .addHeader("x-rapiddocx-org-id", ORG_ID) + .addHeader("User-Agent", "TurboDocx API Client") + .post(requestBody) + .build(); + + // Execute request + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected response: " + response); + } + + String responseBody = response.body().string(); + Map result = objectMapper.readValue(responseBody, Map.class); + + if ((Boolean) result.get("success")) { + System.out.println("✅ Document sent for signing"); + System.out.println("Document ID: " + result.get("documentId")); + System.out.println("Message: " + result.get("message")); + System.out.println("\n📧 Emails are being sent to recipients asynchronously"); + System.out.println("💡 Tip: Set up webhooks to receive notifications when signing is complete"); + } else { + System.err.println("❌ Error: " + result.get("error")); + if (result.containsKey("code")) { + System.err.println("Error Code: " + result.get("code")); + } + } + } + } + + public static void main(String[] args) { + try { + TurboSignPrepareForSigning service = new TurboSignPrepareForSigning(); + File pdfFile = new File("./contract.pdf"); + service.prepareDocumentForSigning(pdfFile); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/express.js b/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/express.js new file mode 100644 index 0000000..fa1bbfd --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/express.js @@ -0,0 +1,101 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Prepare form data +const formData = new FormData(); + +// Add file +formData.append('file', fs.createReadStream('./contract.pdf')); + +// Add document metadata +formData.append('documentName', 'Contract Agreement'); +formData.append('documentDescription', 'Please review and sign this contract'); +formData.append('senderName', 'Your Company'); +formData.append('senderEmail', 'sender@company.com'); + +// Add recipients (as JSON string) +const recipients = JSON.stringify([ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1 + }, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2 + } +]); +formData.append('recipients', recipients); + +// Add fields (as JSON string) - Coordinate-based positioning +const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } +]); +formData.append('fields', fields); + +// Optional: Add CC emails +const ccEmails = JSON.stringify(["manager@company.com", "legal@company.com"]); +formData.append('ccEmails', ccEmails); + +// Send request to prepare-for-signing endpoint +const response = await fetch(`${BASE_URL}/turbosign/single/prepare-for-signing`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData +}); + +const result = await response.json(); + +if (result.success) { + console.log('✅ Document sent for signing'); + console.log('Document ID:', result.documentId); + console.log('Message:', result.message); + console.log('\n📧 Emails are being sent to recipients asynchronously'); + console.log('💡 Tip: Set up webhooks to receive notifications when signing is complete'); +} else { + console.error('❌ Error:', result.error || result.message); + if (result.code) { + console.error('Error Code:', result.code); + } +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/fastify.js b/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/fastify.js new file mode 100644 index 0000000..93ae813 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/nodejs/fastify.js @@ -0,0 +1,111 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Fastify route handler example +async function prepareDocumentForSigning(request, reply) { + try { + // Prepare form data + const formData = new FormData(); + + // Add file + formData.append('file', fs.createReadStream('./contract.pdf')); + + // Add document metadata + formData.append('documentName', 'Contract Agreement'); + formData.append('documentDescription', 'Please review and sign this contract'); + + // Add recipients (as JSON string) + const recipients = JSON.stringify([ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1 + }, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2 + } + ]); + formData.append('recipients', recipients); + + // Add fields (as JSON string) - Coordinate-based positioning + const fields = JSON.stringify([ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } + ]); + formData.append('fields', fields); + + // Send request + const response = await fetch(`${BASE_URL}/turbosign/single/prepare-for-signing`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData + }); + + const result = await response.json(); + + if (result.success) { + return reply.send({ + success: true, + documentId: result.documentId, + message: 'Document sent for signing. Emails are being sent to recipients.' + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || 'Failed to prepare document', + code: result.code + }); + } + } catch (error) { + console.error('Error preparing document:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { prepareDocumentForSigning }; diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/php.php b/static/scripts/turbosign/api/single-step/prepare-for-signing/php.php new file mode 100644 index 0000000..4c47e36 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/php.php @@ -0,0 +1,117 @@ + "John Smith", + "email" => "john.smith@company.com", + "signingOrder" => 1 + ], + [ + "name" => "Jane Doe", + "email" => "jane.doe@partner.com", + "signingOrder" => 2 + ] +]); + +// Prepare fields (as JSON string) - Coordinate-based positioning +$fields = json_encode([ + [ + "recipientEmail" => "john.smith@company.com", + "type" => "signature", + "page" => 1, + "x" => 100, + "y" => 200, + "width" => 200, + "height" => 80, + "required" => true + ], + [ + "recipientEmail" => "john.smith@company.com", + "type" => "date", + "page" => 1, + "x" => 100, + "y" => 300, + "width" => 150, + "height" => 30, + "required" => true + ], + [ + "recipientEmail" => "jane.doe@partner.com", + "type" => "signature", + "page" => 1, + "x" => 350, + "y" => 200, + "width" => 200, + "height" => 80, + "required" => true + ] +]); + +// Optional: Add CC emails +$ccEmails = json_encode(["manager@company.com", "legal@company.com"]); + +// Prepare POST fields +$postFields = [ + 'file' => new CURLFile('./contract.pdf', 'application/pdf', 'contract.pdf'), + 'documentName' => 'Contract Agreement', + 'documentDescription' => 'Please review and sign this contract', + 'senderName' => 'Your Company', + 'senderEmail' => 'sender@company.com', + 'recipients' => $recipients, + 'fields' => $fields, + 'ccEmails' => $ccEmails +]; + +// Set cURL options +curl_setopt_array($ch, [ + CURLOPT_URL => $BASE_URL . '/turbosign/single/prepare-for-signing', + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postFields, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $API_TOKEN, + 'x-rapiddocx-org-id: ' . $ORG_ID, + 'User-Agent: TurboDocx API Client' + ] +]); + +// Execute request +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + +// Check for errors +if (curl_errno($ch)) { + echo 'Error: ' . curl_error($ch) . "\n"; + curl_close($ch); + exit(1); +} + +curl_close($ch); + +// Parse response +$result = json_decode($response, true); + +if ($result['success']) { + echo "✅ Document sent for signing\n"; + echo "Document ID: " . $result['documentId'] . "\n"; + echo "Message: " . $result['message'] . "\n"; + echo "\n📧 Emails are being sent to recipients asynchronously\n"; + echo "💡 Tip: Set up webhooks to receive notifications when signing is complete\n"; +} else { + echo "❌ Error: " . ($result['error'] ?? $result['message']) . "\n"; + if (isset($result['code'])) { + echo "Error Code: " . $result['code'] . "\n"; + } + exit(1); +} + +?> diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/powershell.ps1 b/static/scripts/turbosign/api/single-step/prepare-for-signing/powershell.ps1 new file mode 100644 index 0000000..1f8d446 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/powershell.ps1 @@ -0,0 +1,140 @@ +# Configuration - Update these values +$API_TOKEN = "YOUR_API_TOKEN" +$ORG_ID = "YOUR_ORGANIZATION_ID" +$BASE_URL = "https://api.turbodocx.com" + +# Prepare recipients (as JSON string) +$recipients = @( + @{ + name = "John Smith" + email = "john.smith@company.com" + signingOrder = 1 + }, + @{ + name = "Jane Doe" + email = "jane.doe@partner.com" + signingOrder = 2 + } +) | ConvertTo-Json -Compress + +# Prepare fields (as JSON string) - Coordinate-based +$fields = @( + @{ + recipientEmail = "john.smith@company.com" + type = "signature" + page = 1 + x = 100 + y = 200 + width = 200 + height = 80 + required = $true + }, + @{ + recipientEmail = "john.smith@company.com" + type = "date" + page = 1 + x = 100 + y = 300 + width = 150 + height = 30 + required = $true + }, + @{ + recipientEmail = "jane.doe@partner.com" + type = "signature" + page = 1 + x = 350 + y = 200 + width = 200 + height = 80 + required = $true + } +) | ConvertTo-Json -Compress -Depth 10 + +# Optional: CC emails +$ccEmails = @("manager@company.com", "legal@company.com") | ConvertTo-Json -Compress + +# Prepare file +$filePath = "./contract.pdf" +$fileName = [System.IO.Path]::GetFileName($filePath) +$fileBytes = [System.IO.File]::ReadAllBytes($filePath) +$fileContent = [System.Text.Encoding]::GetEncoding('iso-8859-1').GetString($fileBytes) + +# Create boundary +$boundary = [System.Guid]::NewGuid().ToString() + +# Build multipart form data +$bodyLines = @( + "--$boundary", + "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"", + "Content-Type: application/pdf", + "", + $fileContent, + "--$boundary", + "Content-Disposition: form-data; name=`"documentName`"", + "", + "Contract Agreement", + "--$boundary", + "Content-Disposition: form-data; name=`"documentDescription`"", + "", + "Please review and sign this contract", + "--$boundary", + "Content-Disposition: form-data; name=`"senderName`"", + "", + "Your Company", + "--$boundary", + "Content-Disposition: form-data; name=`"senderEmail`"", + "", + "sender@company.com", + "--$boundary", + "Content-Disposition: form-data; name=`"recipients`"", + "", + $recipients, + "--$boundary", + "Content-Disposition: form-data; name=`"fields`"", + "", + $fields, + "--$boundary", + "Content-Disposition: form-data; name=`"ccEmails`"", + "", + $ccEmails, + "--$boundary--" +) + +$body = $bodyLines -join "`r`n" + +# Prepare headers +$headers = @{ + "Authorization" = "Bearer $API_TOKEN" + "x-rapiddocx-org-id" = $ORG_ID + "User-Agent" = "TurboDocx API Client" + "Content-Type" = "multipart/form-data; boundary=$boundary" +} + +try { + # Send request + $response = Invoke-RestMethod ` + -Uri "$BASE_URL/turbosign/single/prepare-for-signing" ` + -Method Post ` + -Headers $headers ` + -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) + + if ($response.success) { + Write-Host "✅ Document sent for signing" -ForegroundColor Green + Write-Host "Document ID: $($response.documentId)" + Write-Host "Message: $($response.message)" + Write-Host "`n📧 Emails are being sent to recipients asynchronously" + Write-Host "💡 Tip: Set up webhooks to receive notifications when signing is complete" + } else { + Write-Host "❌ Error: $($response.error)" -ForegroundColor Red + if ($response.code) { + Write-Host "Error Code: $($response.code)" -ForegroundColor Red + } + exit 1 + } +} +catch { + Write-Host "❌ Exception: $_" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/python/fastapi.py b/static/scripts/turbosign/api/single-step/prepare-for-signing/python/fastapi.py new file mode 100644 index 0000000..bbf8919 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/python/fastapi.py @@ -0,0 +1,126 @@ +import json +import requests +from fastapi import FastAPI, HTTPException, UploadFile, File +from pydantic import BaseModel + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = FastAPI() + +class SigningResponse(BaseModel): + success: bool + documentId: str + message: str + +@app.post('/prepare-for-signing', response_model=SigningResponse) +async def prepare_document_for_signing(file: UploadFile = File(...)): + try: + # Prepare form data + files = { + 'file': (file.filename, file.file, file.content_type) + } + + # Prepare form fields + data = { + 'documentName': 'Contract Agreement', + 'documentDescription': 'Please review and sign this contract', + 'senderName': 'Your Company', + 'senderEmail': 'sender@company.com' + } + + # Add recipients (as JSON string) + recipients = json.dumps([ + { + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 + }, + { + "name": "Jane Doe", + "email": "jane.doe@partner.com", + "signingOrder": 2 + } + ]) + data['recipients'] = recipients + + # Add fields (as JSON string) - Coordinate-based positioning + fields = json.dumps([ + { + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": True + }, + { + "recipientEmail": "john.smith@company.com", + "type": "date", + "page": 1, + "x": 100, + "y": 300, + "width": 150, + "height": 30, + "required": True + }, + { + "recipientEmail": "jane.doe@partner.com", + "type": "signature", + "page": 1, + "x": 350, + "y": 200, + "width": 200, + "height": 80, + "required": True + } + ]) + data['fields'] = fields + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/single/prepare-for-signing', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return SigningResponse( + success=True, + documentId=result['documentId'], + message=result['message'] + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to prepare document'), + 'code': result.get('code') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error preparing document: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/python/flask.py b/static/scripts/turbosign/api/single-step/prepare-for-signing/python/flask.py new file mode 100644 index 0000000..6937c02 --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/python/flask.py @@ -0,0 +1,121 @@ +import json +import requests +from flask import Flask, jsonify + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = Flask(__name__) + +@app.route('/prepare-for-signing', methods=['POST']) +def prepare_document_for_signing(): + try: + # Prepare form data + files = { + 'file': ('contract.pdf', open('./contract.pdf', 'rb'), 'application/pdf') + } + + # Prepare form fields + data = { + 'documentName': 'Contract Agreement', + 'documentDescription': 'Please review and sign this contract', + 'senderName': 'Your Company', + 'senderEmail': 'sender@company.com' + } + + # Add recipients (as JSON string) + recipients = json.dumps([ + { + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 + }, + { + "name": "Jane Doe", + "email": "jane.doe@partner.com", + "signingOrder": 2 + } + ]) + data['recipients'] = recipients + + # Add fields (as JSON string) - Coordinate-based positioning + fields = json.dumps([ + { + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": True + }, + { + "recipientEmail": "john.smith@company.com", + "type": "date", + "page": 1, + "x": 100, + "y": 300, + "width": 150, + "height": 30, + "required": True + }, + { + "recipientEmail": "jane.doe@partner.com", + "type": "signature", + "page": 1, + "x": 350, + "y": 200, + "width": 200, + "height": 80, + "required": True + } + ]) + data['fields'] = fields + + # Optional: Add CC emails + ccEmails = json.dumps(["manager@company.com", "legal@company.com"]) + data['ccEmails'] = ccEmails + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/single/prepare-for-signing', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return jsonify({ + 'success': True, + 'documentId': result['documentId'], + 'message': result['message'] + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to prepare document'), + 'code': result.get('code') + }), 400 + + except Exception as error: + print(f'Error preparing document: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/scripts/turbosign/api/single-step/prepare-for-signing/ruby.rb b/static/scripts/turbosign/api/single-step/prepare-for-signing/ruby.rb new file mode 100644 index 0000000..17825ac --- /dev/null +++ b/static/scripts/turbosign/api/single-step/prepare-for-signing/ruby.rb @@ -0,0 +1,130 @@ +require 'net/http' +require 'uri' +require 'json' +require 'mime/types' + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +def prepare_document_for_signing + # Prepare recipients + recipients = [ + { + name: "John Smith", + email: "john.smith@company.com", + signingOrder: 1}, + { + name: "Jane Doe", + email: "jane.doe@partner.com", + signingOrder: 2} + ] + + # Prepare fields - Coordinate-based + fields = [ + { + recipientEmail: "john.smith@company.com", + type: "signature", + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: "john.smith@company.com", + type: "date", + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + }, + { + recipientEmail: "jane.doe@partner.com", + type: "signature", + page: 1, + x: 350, + y: 200, + width: 200, + height: 80, + required: true + } + ] + + # Optional: CC emails + cc_emails = ["manager@company.com", "legal@company.com"] + + # Create multipart form data + uri = URI.parse("#{BASE_URL}/turbosign/single/prepare-for-signing") + + boundary = "----WebKitFormBoundary#{rand(1000000000)}" + + post_body = [] + + # Add file + file_path = "./contract.pdf" + file_content = File.read(file_path) + post_body << "--#{boundary}\r\n" + post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"contract.pdf\"\r\n" + post_body << "Content-Type: application/pdf\r\n\r\n" + post_body << file_content + post_body << "\r\n" + + # Add form fields + form_fields = { + 'documentName' => 'Contract Agreement', + 'documentDescription' => 'Please review and sign this contract', + 'senderName' => 'Your Company', + 'senderEmail' => 'sender@company.com', + 'recipients' => recipients.to_json, + 'fields' => fields.to_json, + 'ccEmails' => cc_emails.to_json + } + + form_fields.each do |key, value| + post_body << "--#{boundary}\r\n" + post_body << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + post_body << value.to_s + post_body << "\r\n" + end + + post_body << "--#{boundary}--\r\n" + + # Create HTTP request + request = Net::HTTP::Post.new(uri.request_uri) + request["Authorization"] = "Bearer #{API_TOKEN}" + request["x-rapiddocx-org-id"] = ORG_ID + request["User-Agent"] = "TurboDocx API Client" + request["Content-Type"] = "multipart/form-data; boundary=#{boundary}" + request.body = post_body.join + + # Send request + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + + response = http.request(request) + result = JSON.parse(response.body) + + if result['success'] + puts "✅ Document sent for signing" + puts "Document ID: #{result['documentId']}" + puts "Message: #{result['message']}" + puts "\n📧 Emails are being sent to recipients asynchronously" + puts "💡 Tip: Set up webhooks to receive notifications when signing is complete" + else + puts "❌ Error: #{result['error'] || result['message']}" + puts "Error Code: #{result['code']}" if result['code'] + exit 1 + end +rescue => e + puts "❌ Exception: #{e.message}" + puts e.backtrace + exit 1 +end + +# Run the function +prepare_document_for_signing From 09217240d42b4840b59de5c32c43b1ef90b225d0 Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 26 Nov 2025 16:06:26 +0000 Subject: [PATCH 2/3] feat: added CC email optional section to the setting up turboSign doc --- docs/TurboSign/Setting up TurboSign.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TurboSign/Setting up TurboSign.md b/docs/TurboSign/Setting up TurboSign.md index 24e6cdf..d55c08c 100644 --- a/docs/TurboSign/Setting up TurboSign.md +++ b/docs/TurboSign/Setting up TurboSign.md @@ -199,6 +199,8 @@ For each recipient, you'll need: ![Add recipients interface showing all options](/img/turbosign/AddRecipients.png) +**CC Emails (Optional):** You can also add CC email addresses for people who should receive a copy of the document. These individuals will get the final signed copy when everyone has completed signing, but they won't need to sign the document themselves. +
:::tip Recipient Best Practices From 101089c0f3c2f59d0c0e16a0b6c6b928133c6123 Mon Sep 17 00:00:00 2001 From: Amit Sharma Date: Wed, 26 Nov 2025 17:37:16 +0000 Subject: [PATCH 3/3] feat: added turbosign bulk api integration --- docs/TurboSign/API Bulk Signatures.md | 713 ++++++++++++++++++ static/img/turbosign/api/bulk-api.jpg | Bin 0 -> 71315 bytes .../api/bulk/cancel-batch/nodejs/express.js | 37 + .../api/bulk/cancel-batch/nodejs/fastify.js | 53 ++ .../api/bulk/cancel-batch/python/fastapi.py | 71 ++ .../api/bulk/cancel-batch/python/flask.py | 58 ++ .../api/bulk/ingest/nodejs/express.js | 121 +++ .../api/bulk/ingest/nodejs/fastify.js | 136 ++++ .../api/bulk/ingest/python/fastapi.py | 156 ++++ .../turbosign/api/bulk/ingest/python/flask.py | 144 ++++ .../api/bulk/list-batches/nodejs/express.js | 53 ++ .../api/bulk/list-batches/nodejs/fastify.js | 57 ++ .../api/bulk/list-batches/python/fastapi.py | 88 +++ .../api/bulk/list-batches/python/flask.py | 79 ++ .../api/bulk/list-jobs/nodejs/express.js | 64 ++ .../api/bulk/list-jobs/nodejs/fastify.js | 67 ++ .../api/bulk/list-jobs/python/fastapi.py | 111 +++ .../api/bulk/list-jobs/python/flask.py | 93 +++ 18 files changed, 2101 insertions(+) create mode 100644 docs/TurboSign/API Bulk Signatures.md create mode 100644 static/img/turbosign/api/bulk-api.jpg create mode 100644 static/scripts/turbosign/api/bulk/cancel-batch/nodejs/express.js create mode 100644 static/scripts/turbosign/api/bulk/cancel-batch/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/bulk/cancel-batch/python/fastapi.py create mode 100644 static/scripts/turbosign/api/bulk/cancel-batch/python/flask.py create mode 100644 static/scripts/turbosign/api/bulk/ingest/nodejs/express.js create mode 100644 static/scripts/turbosign/api/bulk/ingest/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/bulk/ingest/python/fastapi.py create mode 100644 static/scripts/turbosign/api/bulk/ingest/python/flask.py create mode 100644 static/scripts/turbosign/api/bulk/list-batches/nodejs/express.js create mode 100644 static/scripts/turbosign/api/bulk/list-batches/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/bulk/list-batches/python/fastapi.py create mode 100644 static/scripts/turbosign/api/bulk/list-batches/python/flask.py create mode 100644 static/scripts/turbosign/api/bulk/list-jobs/nodejs/express.js create mode 100644 static/scripts/turbosign/api/bulk/list-jobs/nodejs/fastify.js create mode 100644 static/scripts/turbosign/api/bulk/list-jobs/python/fastapi.py create mode 100644 static/scripts/turbosign/api/bulk/list-jobs/python/flask.py diff --git a/docs/TurboSign/API Bulk Signatures.md b/docs/TurboSign/API Bulk Signatures.md new file mode 100644 index 0000000..f6f044a --- /dev/null +++ b/docs/TurboSign/API Bulk Signatures.md @@ -0,0 +1,713 @@ +--- +title: TurboSign Bulk API Integration +sidebar_position: 4 +description: Send documents for signature at scale using TurboSign Bulk API. Process hundreds or thousands of signature requests in batches with comprehensive tracking and management capabilities. +keywords: + - turbosign bulk api + - bulk signature api + - batch signature sending + - mass document signing + - bulk e-signature + - batch processing api + - bulk upload api + - signature automation + - batch document workflow + - turbodocx bulk api + - bulk signature integration + - api batch processing + - mass signature collection + - enterprise signature api + - bulk sending api + - batch management api + - signature tracking api + - bulk document processing + - scalable signature api + - high-volume signatures +--- + +import ScriptLoader from '@site/src/components/ScriptLoader'; + +# TurboSign Bulk API Integration + +Send documents for electronic signature at scale. The TurboSign Bulk API allows you to process hundreds or thousands of signature requests in organized batches, with comprehensive tracking and management capabilities. + +![TurboSign Single-Step Workflow](/img/turbosign/api/bulk-api.jpg) + +## Overview + +The Bulk API is designed for high-volume signature collection scenarios. Instead of sending documents one at a time, you can create batches that process multiple signature requests efficiently. + +### What is Bulk Sending? + +Bulk sending allows you to send the **same document to multiple recipients** in a single batch operation. Each recipient gets their own personalized copy with customized signature fields and recipient information. + +**Common Use Cases:** + +- **Employment Contracts**: Send offer letters or contracts to multiple new hires +- **Client Agreements**: Distribute service agreements to hundreds of clients +- **Policy Updates**: Send updated terms or policies requiring acknowledgment +- **Event Registrations**: Collect signatures from event participants +- **Real Estate**: Send disclosure forms to multiple buyers or sellers +- **Insurance**: Distribute policy documents requiring signatures + +### Key Benefits + +- **Efficiency**: Create one batch instead of hundreds of individual requests +- **Cost Effective**: Batch processing optimizes credit usage +- **Tracking**: Monitor progress across all jobs in a batch +- **Management**: Cancel, retry, or review jobs as needed +- **Scalability**: Handle thousands of signature requests seamlessly +- **Organization**: Group related requests together logically + +## Key Concepts + +Understanding these core concepts will help you effectively use the Bulk API. + +### Batches + +A **batch** is a container for multiple document send operations. All documents in a batch share the same source file (PDF, template, or deliverable) but can have different recipients and field configurations. + +### Jobs + +A **job** represents a single document send operation within a batch. Each job corresponds to one set of recipients receiving the shared document. + +### Source Types + +The Bulk API supports four source types for your documents: + +| Source Type | Description | When to Use | +| --------------- | ------------------------------ | ---------------------------------- | +| `file` | Upload PDF directly | One-time documents, custom content | +| `deliverableId` | Reference existing deliverable | Documents generated from templates | +| `templateId` | Use template directly | Reusable document structures | +| `fileLink` | URL to document | Documents hosted externally | + +### Batch-Level vs Document-Level Configuration + +**Batch-Level:** + +- Source file (all jobs use same document) +- Default metadata (documentName, documentDescription) +- Default sender information + +**Document-Level:** + +- Recipients (unique per job) +- Signature fields (customized per job) +- Optional metadata overrides per job + +## Prerequisites + +Before using the Bulk API, ensure you have: + +- **API Access Token**: Bearer token for authentication +- **Organization ID**: Your organization identifier +- **Bulk Sending Enabled**: Feature must be enabled for your organization +- **Sufficient Credits**: Credits will be reserved when creating batches + +### Getting Your Credentials + +1. **Login to TurboDocx**: Visit [https://www.turbodocx.com](https://www.turbodocx.com) +2. **Navigate to Settings**: Access your organization settings +3. **API Keys Section**: Generate or retrieve your API access token +4. **Organization ID**: Copy your organization ID from the settings + +![TurboSign API Key](/img/turbosign/api/api-key.png) +![TurboSign Organization ID](/img/turbosign/api/org-id.png) + +## Authentication + +All TurboSign Bulk API requests require authentication using a Bearer token: + +```http +Authorization: Bearer YOUR_API_TOKEN +``` + +Additional required headers for all requests: + +```http +x-rapiddocx-org-id: YOUR_ORGANIZATION_ID +User-Agent: TurboDocx API Client +``` + +:::tip Authentication +These authentication headers are identical to the single-step API. If you're already using the single-step API, you can use the same credentials for bulk operations. +::: + +## API Endpoints + +The Bulk API provides four endpoints for complete batch management: + +| Endpoint | Method | Purpose | +| --------------------------------------- | ------ | --------------------------------- | +| `/turbosign/bulk/ingest` | POST | Create and ingest a new batch | +| `/turbosign/bulk/batches` | GET | List all batches for organization | +| `/turbosign/bulk/batch/:batchId/jobs` | GET | List jobs within a specific batch | +| `/turbosign/bulk/batch/:batchId/cancel` | POST | Cancel a batch and pending jobs | + +--- + +## Endpoint 1: Ingest Bulk Batch + +Create a new batch and ingest multiple document send operations. This is the primary endpoint for initiating bulk sending. + +### Endpoint + +```http +POST https://api.turbodocx.com/turbosign/bulk/ingest +``` + +### Headers + +```http +Content-Type: multipart/form-data +Authorization: Bearer YOUR_API_TOKEN +x-rapiddocx-org-id: YOUR_ORGANIZATION_ID +User-Agent: TurboDocx API Client +``` + +### Request Body (multipart/form-data) + +⚠️ **Important**: `documents` must be sent as a JSON string in form-data + +| Field | Type | Required | Description | +| ------------------- | ----------------- | ------------- | ----------------------------------------------------------------- | +| sourceType | String | **Yes** | Source type: `file`, `deliverableId`, `templateId`, or `fileLink` | +| file | File | Conditional\* | PDF file upload (required when sourceType is `file`) | +| sourceValue | String (UUID/URL) | Conditional\* | UUID or URL (required for deliverableId/templateId/fileLink) | +| batchName | String | **Yes** | Name for this batch (max 255 chars) | +| documentName | String | No | Default document name for all jobs (max 255 chars) | +| documentDescription | String | No | Default description for all jobs (max 1000 chars) | +| senderName | String | No | Name of sender (max 255 chars) | +| senderEmail | String (email) | No | Email of sender | +| documents | String (JSON) | **Yes** | JSON string array of document objects | + +\* **File Source**: Must provide exactly ONE of: + +- `file` (upload) when sourceType is `file` +- `sourceValue` (UUID) when sourceType is `deliverableId` or `templateId` +- `sourceValue` (URL) when sourceType is `fileLink` + +### Documents Array Format + +The `documents` parameter must be a JSON string containing an array of document objects: + +```json +[ + { + "recipients": [ + { + "name": "John Smith", + "email": "john.smith@company.com", + "signingOrder": 1 + } + ], + "fields": [ + { + "recipientEmail": "john.smith@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": true + }, + { + "recipientEmail": "john.smith@company.com", + "type": "date", + "page": 1, + "x": 100, + "y": 300, + "width": 150, + "height": 30, + "required": true + } + ], + "documentName": "Employment Contract - John Smith", + "documentDescription": "Please review and sign your employment contract" + }, + { + "recipients": [ + { + "name": "Jane Doe", + "email": "jane.doe@company.com", + "signingOrder": 1 + } + ], + "fields": [ + { + "recipientEmail": "jane.doe@company.com", + "type": "signature", + "page": 1, + "x": 100, + "y": 200, + "width": 200, + "height": 80, + "required": true + } + ], + "documentName": "Employment Contract - Jane Doe" + } +] +``` + +**Document Object Properties:** + +| Property | Type | Required | Description | +| ------------------- | ------ | -------- | ----------------------------------------------------------- | +| recipients | Array | **Yes** | Array of recipient objects (same format as single-step API) | +| fields | Array | **Yes** | Array of field objects (same format as single-step API) | +| documentName | String | No | Override batch-level document name for this job | +| documentDescription | String | No | Override batch-level description for this job | + +:::tip Field Types +All field types from the single-step API are supported: `signature`, `initial`, `date`, `full_name`, `first_name`, `last_name`, `title`, `company`, `email`, `text`, `checkbox`. See the [single-step API documentation](/docs/TurboSign/API%20Signatures) for details. +::: + +### Response (Success) + +```json +{ + "success": true, + "batchId": "550e8400-e29b-41d4-a716-446655440000", + "batchName": "Q4 Employment Contracts", + "totalJobs": 50, + "status": "pending", + "message": "Batch created successfully with 50 jobs. Processing will begin shortly." +} +``` + +### Response (Error) + +```json +{ + "error": "Validation failed for 3 documents", + "code": "BulkValidationFailed", + "data": { + "invalidDocuments": [ + { + "index": 0, + "errors": ["recipients[0].email is required"] + }, + { + "index": 5, + "errors": ["fields[0].recipientEmail does not match any recipient"] + } + ] + } +} +``` + +### Code Example + + + +--- + +## Endpoint 2: List All Batches + +Retrieve all batches for your organization with pagination and filtering capabilities. + +### Endpoint + +```http +GET https://api.turbodocx.com/turbosign/bulk/batches +``` + +### Headers + +```http +Authorization: Bearer YOUR_API_TOKEN +x-rapiddocx-org-id: YOUR_ORGANIZATION_ID +User-Agent: TurboDocx API Client +``` + +### Query Parameters + +| Parameter | Type | Required | Description | +| --------- | ------------------ | -------- | ----------------------------------------------------------------------------- | +| limit | Number | No | Number of batches to return (default: 20, max: 100) | +| offset | Number | No | Number of batches to skip (default: 0) | +| query | String | No | Search query to filter batches by name | +| status | String or String[] | No | Filter by status: `pending`, `processing`, `completed`, `failed`, `cancelled` | +| startDate | String (ISO 8601) | No | Filter batches created on or after this date | +| endDate | String (ISO 8601) | No | Filter batches created on or before this date | + +### Response + +```json +{ + "data": { + "batches": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Q4 Employment Contracts", + "status": "completed", + "totalJobs": 50, + "succeededJobs": 48, + "failedJobs": 2, + "pendingJobs": 0, + "createdOn": "2025-01-15T10:30:00Z", + "updatedOn": "2025-01-15T14:25:00Z", + "metadata": {} + }, + { + "id": "660e8400-e29b-41d4-a716-446655440001", + "name": "Client Agreements - January 2025", + "status": "processing", + "totalJobs": 100, + "succeededJobs": 75, + "failedJobs": 5, + "pendingJobs": 20, + "createdOn": "2025-01-16T09:00:00Z", + "updatedOn": "2025-01-16T12:30:00Z", + "metadata": {} + } + ], + "totalRecords": 2 + } +} +``` + +### Code Example + + + +--- + +## Endpoint 3: List Jobs in Batch + +Retrieve all jobs within a specific batch with detailed status information. + +### Endpoint + +```http +GET https://api.turbodocx.com/turbosign/bulk/batch/:batchId/jobs +``` + +### Headers + +```http +Authorization: Bearer YOUR_API_TOKEN +x-rapiddocx-org-id: YOUR_ORGANIZATION_ID +User-Agent: TurboDocx API Client +``` + +### Path Parameters + +| Parameter | Type | Required | Description | +| --------- | ------------- | -------- | --------------- | +| batchId | String (UUID) | **Yes** | ID of the batch | + +### Query Parameters + +| Parameter | Type | Required | Description | +| --------- | ------------------ | -------- | ------------------------------------------------ | +| limit | Number | No | Number of jobs to return (default: 20, max: 100) | +| offset | Number | No | Number of jobs to skip (default: 0) | +| status | String or String[] | No | Filter by job status | +| query | String | No | Search query to filter jobs | + +### Response + +```json +{ + "data": { + "batchId": "550e8400-e29b-41d4-a716-446655440000", + "batchName": "Q4 Employment Contracts", + "batchStatus": "completed", + "jobs": [ + { + "id": "770e8400-e29b-41d4-a716-446655440000", + "batchId": "550e8400-e29b-41d4-a716-446655440000", + "documentId": "880e8400-e29b-41d4-a716-446655440000", + "documentName": "Employment Contract - John Smith", + "status": "SUCCEEDED", + "recipientEmails": ["john.smith@company.com"], + "attempts": 0, + "errorCode": null, + "errorMessage": null, + "createdOn": "2025-01-15T10:30:00Z", + "updatedOn": "2025-01-15T11:45:00Z", + "lastAttemptedAt": "2025-01-15T11:45:00Z" + }, + { + "id": "770e8400-e29b-41d4-a716-446655440001", + "batchId": "550e8400-e29b-41d4-a716-446655440000", + "documentId": null, + "documentName": "Employment Contract - Jane Doe", + "status": "FAILED", + "recipientEmails": ["jane.doe@company.com"], + "attempts": 1, + "errorCode": "INVALID_EMAIL", + "errorMessage": "Invalid recipient email address", + "createdOn": "2025-01-15T10:30:00Z", + "updatedOn": "2025-01-15T10:31:00Z", + "lastAttemptedAt": "2025-01-15T10:31:00Z" + } + ], + "totalJobs": 50, + "totalRecords": 50, + "succeededJobs": 48, + "failedJobs": 2, + "pendingJobs": 0 + } +} +``` + +### Code Example + + + +--- + +## Endpoint 4: Cancel Batch + +Cancel a batch and stop all pending jobs. Already completed jobs are not affected. + +### Endpoint + +```http +POST https://api.turbodocx.com/turbosign/bulk/batch/:batchId/cancel +``` + +### Headers + +```http +Authorization: Bearer YOUR_API_TOKEN +x-rapiddocx-org-id: YOUR_ORGANIZATION_ID +User-Agent: TurboDocx API Client +``` + +### Path Parameters + +| Parameter | Type | Required | Description | +| --------- | ------------- | -------- | ------------------------- | +| batchId | String (UUID) | **Yes** | ID of the batch to cancel | + +### Response + +```json +{ + "success": true, + "message": "Batch cancelled. 20 job(s) stopped, 20 credit(s) refunded.", + "batchId": "550e8400-e29b-41d4-a716-446655440000", + "cancelledJobs": 20, + "succeededJobs": 30, + "refundedCredits": 20, + "status": "cancelled" +} +``` + +### Code Example + + + +:::warning Cancellation Notice + +- Only pending and processing jobs will be cancelled +- Already succeeded jobs are not affected +- Credits are refunded only for cancelled jobs +- Cancellation cannot be undone + ::: + +--- + +## Best Practices + +### Batch Size + +**Recommended batch sizes:** + +- Small batches (10-50 jobs): Faster processing, easier to monitor +- Medium batches (50-200 jobs): Balanced efficiency +- Large batches (200-1000 jobs): Maximum efficiency for large-scale operations + +:::tip Batch Size Strategy +For first-time use, start with smaller batches (10-20 jobs) to validate your configuration. Once confident, scale up to larger batches. +::: + +### Error Handling + +**Always check for validation errors:** + +```javascript +if (response.data.code === "BulkValidationFailed") { + const errors = response.data.data.invalidDocuments; + errors.forEach((err) => { + console.log(`Document ${err.index}: ${err.errors.join(", ")}`); + }); +} +``` + +**Monitor job failures:** + +- Regularly check job status +- Review error messages for failed jobs +- Fix issues and retry with corrected data + +### Credit Management + +**Credits are reserved when batch is created:** + +- 1 credit per recipient per job +- Credits are consumed as jobs succeed +- Credits are refunded for failed or cancelled jobs + +**Example**: Batch with 50 jobs, 1 recipient each = 50 credits reserved + +### Performance Tips + +1. **Use templateId or deliverableId**: Faster than file uploads +2. **Batch similar documents**: Group documents with similar configurations +3. **Validate data before submission**: Prevent validation errors +4. **Use pagination**: When listing large result sets +5. **Implement webhooks**: For real-time status updates (see [Webhooks documentation](/docs/TurboSign/Webhooks)) + +### Data Validation + +**Before creating a batch, validate:** + +- All email addresses are valid +- Field positioning doesn't overlap +- RecipientEmail in fields matches actual recipients +- Required fields are present + +--- + +## Error Codes + +Common error codes you may encounter: + +| Code | Description | Solution | +| ---------------------- | ------------------------------------------- | --------------------------------------- | +| `FileUploadRequired` | No file provided when sourceType is 'file' | Include file in form-data | +| `SourceValueRequired` | No sourceValue for deliverableId/templateId | Provide valid UUID | +| `InvalidSourceValue` | sourceValue provided with file upload | Remove sourceValue when using file | +| `InvalidDocumentsJSON` | Documents JSON is malformed | Validate JSON structure | +| `BulkValidationFailed` | One or more documents failed validation | Check data.invalidDocuments for details | +| `DeliverableNotFound` | deliverableId doesn't exist | Verify deliverable exists | +| `TemplateNotFound` | templateId doesn't exist | Verify template exists | +| `BatchNotFound` | batchId doesn't exist | Check batch ID | +| `BatchNotCancellable` | Batch already completed | Cannot cancel completed batches | +| `InsufficientCredits` | Not enough credits for batch | Add credits to organization | + +--- + +## Limits and Quotas + +### Batch Limits + +| Limit | Value | +| -------------------------- | -------------- | +| Maximum jobs per batch | 1,000 | +| Maximum document size | 25 MB | +| Maximum recipients per job | 50 | +| Maximum fields per job | 100 | +| Maximum batch name length | 255 characters | + +### Rate Limits + +| Endpoint | Rate Limit | +| ------------ | ------------------ | +| Ingest batch | 10 requests/minute | +| List batches | 60 requests/minute | +| List jobs | 60 requests/minute | +| Cancel batch | 10 requests/minute | + +:::warning Rate Limiting +If you exceed rate limits, you'll receive a `429 Too Many Requests` response. Implement exponential backoff in your retry logic. +::: + +### Credit Consumption + +- **1 credit per recipient**: Each recipient in each job consumes 1 credit +- **Reserved on creation**: Credits are reserved when batch is created +- **Consumed on success**: Credits are consumed when job succeeds +- **Refunded on failure/cancellation**: Credits are refunded for failed or cancelled jobs + +**Example Calculations:** + +- 100 jobs × 1 recipient = 100 credits +- 50 jobs × 2 recipients = 100 credits +- 200 jobs × 1 recipient = 200 credits + +--- + +## Troubleshooting + +### Batch Stuck in "Processing" + +**Possible causes:** + +- High system load (normal during peak times) +- Large batch size +- Complex document processing + +**Solutions:** + +- Wait for processing to complete (usually < 30 minutes) +- Check job status to see progress +- Contact support if stuck for > 1 hour + +### Jobs Failing with "Invalid Email" + +**Cause**: Email validation failed + +**Solutions:** + +- Verify email format +- Check for typos +- Ensure no special characters in local part + +### Credits Not Refunded + +**Cause**: Jobs succeeded before cancellation + +**Solution**: Credits are only refunded for cancelled jobs. Already succeeded jobs consume credits. + +### "DeliverableNotFound" Error + +**Possible causes:** + +- Wrong organization ID +- Deliverable deleted +- Incorrect deliverable ID + +**Solutions:** + +- Verify deliverable exists in your organization +- Check organization ID matches +- Generate new deliverable if needed + +--- + +## Next Steps + +Now that you understand the Bulk API, you might want to: + +- **Set up webhooks**: Get real-time notifications for batch and job status changes. See [Webhooks documentation](/docs/TurboSign/Webhooks) +- **Explore single-step API**: For individual document sending. See [Single-Step API documentation](/docs/TurboSign/API%20Signatures) +- **Review field types**: Learn about all available signature field types +- **Integrate with your app**: Build bulk sending into your application workflow + +--- + +_Have questions? Contact our support team at support@turbodocx.com_ diff --git a/static/img/turbosign/api/bulk-api.jpg b/static/img/turbosign/api/bulk-api.jpg new file mode 100644 index 0000000000000000000000000000000000000000..870cabc77fac325a2b56f936af8e87941061e295 GIT binary patch literal 71315 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

WXm1@z*)d=9F%6`s77>k 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 YPRRsaFu2iVu8X7rWE8@KoPa<7*+$Dr1ONa4 literal 0 HcmV?d00001 diff --git a/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/express.js b/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/express.js new file mode 100644 index 0000000..d4edf5e --- /dev/null +++ b/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/express.js @@ -0,0 +1,37 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; +const BATCH_ID = "YOUR_BATCH_ID"; // Replace with actual batch ID to cancel + +// Send POST request to cancel batch +const response = await fetch(`${BASE_URL}/turbosign/bulk/batch/${BATCH_ID}/cancel`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + 'Content-Type': 'application/json' + } +}); + +const result = await response.json(); + +if (result.success) { + console.log('✅ Batch cancelled successfully'); + console.log('Batch ID:', result.batchId); + console.log('Status:', result.status); + console.log('\n📊 Cancellation Summary:'); + console.log('Cancelled Jobs:', result.cancelledJobs); + console.log('Succeeded Jobs (already completed):', result.succeededJobs); + console.log('Refunded Credits:', result.refundedCredits); + console.log('\n💰 Credits have been refunded to your account for cancelled jobs'); + console.log('✅ Jobs that already succeeded remain completed'); +} else { + console.error('❌ Error:', result.error || result.message); + if (result.code) { + console.error('Error Code:', result.code); + } +} diff --git a/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/fastify.js b/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/fastify.js new file mode 100644 index 0000000..5cc197f --- /dev/null +++ b/static/scripts/turbosign/api/bulk/cancel-batch/nodejs/fastify.js @@ -0,0 +1,53 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; +const BATCH_ID = "YOUR_BATCH_ID"; // Replace with actual batch ID to cancel + +// Fastify route handler example +async function cancelBatch(request, reply) { + try { + // Send POST request to cancel batch + const response = await fetch(`${BASE_URL}/turbosign/bulk/batch/${BATCH_ID}/cancel`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + return reply.send({ + success: true, + batchId: result.batchId, + status: result.status, + message: result.message, + cancelledJobs: result.cancelledJobs, + succeededJobs: result.succeededJobs, + refundedCredits: result.refundedCredits + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || result.message, + code: result.code + }); + } + } catch (error) { + console.error('Error cancelling batch:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { cancelBatch }; diff --git a/static/scripts/turbosign/api/bulk/cancel-batch/python/fastapi.py b/static/scripts/turbosign/api/bulk/cancel-batch/python/fastapi.py new file mode 100644 index 0000000..5bd16db --- /dev/null +++ b/static/scripts/turbosign/api/bulk/cancel-batch/python/fastapi.py @@ -0,0 +1,71 @@ +import json +import requests +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" +BATCH_ID = "YOUR_BATCH_ID" # Replace with actual batch ID to cancel + +app = FastAPI() + +class CancelBatchResponse(BaseModel): + success: bool + batchId: str + status: str + message: str + cancelledJobs: int + succeededJobs: int + refundedCredits: int + +@app.post('/cancel-batch', response_model=CancelBatchResponse) +async def cancel_batch(): + try: + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + 'Content-Type': 'application/json' + } + + # Send POST request to cancel batch + response = requests.post( + f'{BASE_URL}/turbosign/bulk/batch/{BATCH_ID}/cancel', + headers=headers + ) + + result = response.json() + + if result.get('success'): + return CancelBatchResponse( + success=True, + batchId=result['batchId'], + status=result['status'], + message=result['message'], + cancelledJobs=result['cancelledJobs'], + succeededJobs=result['succeededJobs'], + refundedCredits=result['refundedCredits'] + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to cancel batch'), + 'code': result.get('code') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error cancelling batch: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/bulk/cancel-batch/python/flask.py b/static/scripts/turbosign/api/bulk/cancel-batch/python/flask.py new file mode 100644 index 0000000..72300de --- /dev/null +++ b/static/scripts/turbosign/api/bulk/cancel-batch/python/flask.py @@ -0,0 +1,58 @@ +import json +import requests +from flask import Flask, jsonify + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" +BATCH_ID = "YOUR_BATCH_ID" # Replace with actual batch ID to cancel + +app = Flask(__name__) + +@app.route('/cancel-batch', methods=['POST']) +def cancel_batch(): + try: + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + 'Content-Type': 'application/json' + } + + # Send POST request to cancel batch + response = requests.post( + f'{BASE_URL}/turbosign/bulk/batch/{BATCH_ID}/cancel', + headers=headers + ) + + result = response.json() + + if result.get('success'): + return jsonify({ + 'success': True, + 'batchId': result['batchId'], + 'status': result['status'], + 'message': result['message'], + 'cancelledJobs': result['cancelledJobs'], + 'succeededJobs': result['succeededJobs'], + 'refundedCredits': result['refundedCredits'] + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to cancel batch'), + 'code': result.get('code') + }), 400 + + except Exception as error: + print(f'Error cancelling batch: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/scripts/turbosign/api/bulk/ingest/nodejs/express.js b/static/scripts/turbosign/api/bulk/ingest/nodejs/express.js new file mode 100644 index 0000000..68e1598 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/ingest/nodejs/express.js @@ -0,0 +1,121 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Prepare documents array - each document represents one signing job +const documents = [ + { + recipients: [ + { + name: 'John Smith', + email: 'john.smith@company.com', + signingOrder: 1 + } + ], + fields: [ + { + recipientEmail: 'john.smith@company.com', + type: 'signature', + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: 'john.smith@company.com', + type: 'date', + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + } + ], + documentName: 'Employment Contract - John Smith', + documentDescription: 'Please review and sign your employment contract' + }, + { + recipients: [ + { + name: 'Jane Doe', + email: 'jane.doe@company.com', + signingOrder: 1 + } + ], + fields: [ + { + recipientEmail: 'jane.doe@company.com', + type: 'signature', + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: 'jane.doe@company.com', + type: 'date', + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + } + ], + documentName: 'Employment Contract - Jane Doe', + documentDescription: 'Please review and sign your employment contract' + } +]; + +// Create form data +const formData = new FormData(); +formData.append('sourceType', 'file'); +formData.append('file', fs.createReadStream('./contract_template.pdf')); +formData.append('batchName', 'Q4 Employment Contracts'); +formData.append('documentName', 'Employment Contract'); +formData.append('documentDescription', 'Please review and sign your employment contract'); +formData.append('senderName', 'HR Department'); +formData.append('senderEmail', 'hr@company.com'); +formData.append('documents', JSON.stringify(documents)); + +// Send request to bulk ingest endpoint +const response = await fetch(`${BASE_URL}/turbosign/bulk/ingest`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData +}); + +const result = await response.json(); + +if (result.success) { + console.log('✅ Bulk batch created successfully'); + console.log('Batch ID:', result.batchId); + console.log('Batch Name:', result.batchName); + console.log('Total Jobs:', result.totalJobs); + console.log('Status:', result.status); + console.log('\n📧 Documents will be sent to recipients asynchronously'); + console.log('💡 Tip: Use the batch ID to monitor progress and list jobs'); +} else { + console.error('❌ Error:', result.error || result.message); + if (result.code) { + console.error('Error Code:', result.code); + } + if (result.data?.invalidDocuments) { + console.error('Invalid Documents:', JSON.stringify(result.data.invalidDocuments, null, 2)); + } +} diff --git a/static/scripts/turbosign/api/bulk/ingest/nodejs/fastify.js b/static/scripts/turbosign/api/bulk/ingest/nodejs/fastify.js new file mode 100644 index 0000000..b2c063f --- /dev/null +++ b/static/scripts/turbosign/api/bulk/ingest/nodejs/fastify.js @@ -0,0 +1,136 @@ +const FormData = require('form-data'); +const fs = require('fs'); +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Fastify route handler example +async function bulkIngest(request, reply) { + try { + // Prepare documents array - each document represents one signing job + const documents = [ + { + recipients: [ + { + name: 'John Smith', + email: 'john.smith@company.com', + signingOrder: 1 + } + ], + fields: [ + { + recipientEmail: 'john.smith@company.com', + type: 'signature', + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: 'john.smith@company.com', + type: 'date', + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + } + ], + documentName: 'Employment Contract - John Smith', + documentDescription: 'Please review and sign your employment contract' + }, + { + recipients: [ + { + name: 'Jane Doe', + email: 'jane.doe@company.com', + signingOrder: 1 + } + ], + fields: [ + { + recipientEmail: 'jane.doe@company.com', + type: 'signature', + page: 1, + x: 100, + y: 200, + width: 200, + height: 80, + required: true + }, + { + recipientEmail: 'jane.doe@company.com', + type: 'date', + page: 1, + x: 100, + y: 300, + width: 150, + height: 30, + required: true + } + ], + documentName: 'Employment Contract - Jane Doe', + documentDescription: 'Please review and sign your employment contract' + } + ]; + + // Create form data + const formData = new FormData(); + formData.append('sourceType', 'file'); + formData.append('file', fs.createReadStream('./contract_template.pdf')); + formData.append('batchName', 'Q4 Employment Contracts'); + formData.append('documentName', 'Employment Contract'); + formData.append('documentDescription', 'Please review and sign your employment contract'); + formData.append('senderName', 'HR Department'); + formData.append('senderEmail', 'hr@company.com'); + formData.append('documents', JSON.stringify(documents)); + + // Send request to bulk ingest endpoint + const response = await fetch(`${BASE_URL}/turbosign/bulk/ingest`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client', + ...formData.getHeaders() + }, + body: formData + }); + + const result = await response.json(); + + if (result.success) { + return reply.send({ + success: true, + batchId: result.batchId, + batchName: result.batchName, + totalJobs: result.totalJobs, + status: result.status, + message: 'Bulk batch created successfully' + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || 'Failed to create bulk batch', + code: result.code, + data: result.data + }); + } + } catch (error) { + console.error('Error creating bulk batch:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { bulkIngest }; diff --git a/static/scripts/turbosign/api/bulk/ingest/python/fastapi.py b/static/scripts/turbosign/api/bulk/ingest/python/fastapi.py new file mode 100644 index 0000000..18e14f2 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/ingest/python/fastapi.py @@ -0,0 +1,156 @@ +import json +import requests +from fastapi import FastAPI, HTTPException, UploadFile, File +from pydantic import BaseModel + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = FastAPI() + +class BulkIngestResponse(BaseModel): + success: bool + batchId: str + batchName: str + totalJobs: int + status: str + message: str + +@app.post('/bulk-ingest', response_model=BulkIngestResponse) +async def bulk_ingest(file: UploadFile = File(...)): + try: + # Prepare documents array - each document represents one signing job + documents = [ + { + 'recipients': [ + { + 'name': 'John Smith', + 'email': 'john.smith@company.com', + 'signingOrder': 1 + } + ], + 'fields': [ + { + 'recipientEmail': 'john.smith@company.com', + 'type': 'signature', + 'page': 1, + 'x': 100, + 'y': 200, + 'width': 200, + 'height': 80, + 'required': True + }, + { + 'recipientEmail': 'john.smith@company.com', + 'type': 'date', + 'page': 1, + 'x': 100, + 'y': 300, + 'width': 150, + 'height': 30, + 'required': True + } + ], + 'documentName': 'Employment Contract - John Smith', + 'documentDescription': 'Please review and sign your employment contract' + }, + { + 'recipients': [ + { + 'name': 'Jane Doe', + 'email': 'jane.doe@company.com', + 'signingOrder': 1 + } + ], + 'fields': [ + { + 'recipientEmail': 'jane.doe@company.com', + 'type': 'signature', + 'page': 1, + 'x': 100, + 'y': 200, + 'width': 200, + 'height': 80, + 'required': True + }, + { + 'recipientEmail': 'jane.doe@company.com', + 'type': 'date', + 'page': 1, + 'x': 100, + 'y': 300, + 'width': 150, + 'height': 30, + 'required': True + } + ], + 'documentName': 'Employment Contract - Jane Doe', + 'documentDescription': 'Please review and sign your employment contract' + } + ] + + # Prepare file + files = { + 'file': (file.filename, file.file, file.content_type) + } + + # Prepare form data + data = { + 'sourceType': 'file', + 'batchName': 'Q4 Employment Contracts', + 'documentName': 'Employment Contract', + 'documentDescription': 'Please review and sign your employment contract', + 'senderName': 'HR Department', + 'senderEmail': 'hr@company.com', + 'documents': json.dumps(documents) + } + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/bulk/ingest', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return BulkIngestResponse( + success=True, + batchId=result['batchId'], + batchName=result['batchName'], + totalJobs=result['totalJobs'], + status=result['status'], + message=result['message'] + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to create bulk batch'), + 'code': result.get('code'), + 'data': result.get('data') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error creating bulk batch: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/bulk/ingest/python/flask.py b/static/scripts/turbosign/api/bulk/ingest/python/flask.py new file mode 100644 index 0000000..15c506f --- /dev/null +++ b/static/scripts/turbosign/api/bulk/ingest/python/flask.py @@ -0,0 +1,144 @@ +import json +import requests +from flask import Flask, jsonify + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = Flask(__name__) + +@app.route('/bulk-ingest', methods=['POST']) +def bulk_ingest(): + try: + # Prepare documents array - each document represents one signing job + documents = [ + { + 'recipients': [ + { + 'name': 'John Smith', + 'email': 'john.smith@company.com', + 'signingOrder': 1 + } + ], + 'fields': [ + { + 'recipientEmail': 'john.smith@company.com', + 'type': 'signature', + 'page': 1, + 'x': 100, + 'y': 200, + 'width': 200, + 'height': 80, + 'required': True + }, + { + 'recipientEmail': 'john.smith@company.com', + 'type': 'date', + 'page': 1, + 'x': 100, + 'y': 300, + 'width': 150, + 'height': 30, + 'required': True + } + ], + 'documentName': 'Employment Contract - John Smith', + 'documentDescription': 'Please review and sign your employment contract' + }, + { + 'recipients': [ + { + 'name': 'Jane Doe', + 'email': 'jane.doe@company.com', + 'signingOrder': 1 + } + ], + 'fields': [ + { + 'recipientEmail': 'jane.doe@company.com', + 'type': 'signature', + 'page': 1, + 'x': 100, + 'y': 200, + 'width': 200, + 'height': 80, + 'required': True + }, + { + 'recipientEmail': 'jane.doe@company.com', + 'type': 'date', + 'page': 1, + 'x': 100, + 'y': 300, + 'width': 150, + 'height': 30, + 'required': True + } + ], + 'documentName': 'Employment Contract - Jane Doe', + 'documentDescription': 'Please review and sign your employment contract' + } + ] + + # Prepare file + files = { + 'file': ('contract_template.pdf', open('./contract_template.pdf', 'rb'), 'application/pdf') + } + + # Prepare form data + data = { + 'sourceType': 'file', + 'batchName': 'Q4 Employment Contracts', + 'documentName': 'Employment Contract', + 'documentDescription': 'Please review and sign your employment contract', + 'senderName': 'HR Department', + 'senderEmail': 'hr@company.com', + 'documents': json.dumps(documents) + } + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send request + response = requests.post( + f'{BASE_URL}/turbosign/bulk/ingest', + headers=headers, + data=data, + files=files + ) + + result = response.json() + + if result.get('success'): + return jsonify({ + 'success': True, + 'batchId': result['batchId'], + 'batchName': result['batchName'], + 'totalJobs': result['totalJobs'], + 'status': result['status'], + 'message': result['message'] + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to create bulk batch'), + 'code': result.get('code'), + 'data': result.get('data') + }), 400 + + except Exception as error: + print(f'Error creating bulk batch: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/scripts/turbosign/api/bulk/list-batches/nodejs/express.js b/static/scripts/turbosign/api/bulk/list-batches/nodejs/express.js new file mode 100644 index 0000000..eb0c760 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-batches/nodejs/express.js @@ -0,0 +1,53 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Prepare query parameters +const queryParams = new URLSearchParams({ + limit: '20', + offset: '0', + status: 'pending,processing,completed' // Can be single value or comma-separated + // startDate: '2024-01-01', // Optional: Filter by start date + // endDate: '2024-12-31', // Optional: Filter by end date + // query: 'Q4' // Optional: Search batches by name +}); + +// Send GET request to list batches endpoint +const response = await fetch(`${BASE_URL}/turbosign/bulk/batches?${queryParams}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } +}); + +const result = await response.json(); + +if (result.data) { + console.log('✅ Successfully retrieved batches'); + console.log('Total Records:', result.data.totalRecords); + console.log('Batches in this page:', result.data.batches.length); + console.log('\n📦 Batches:'); + + result.data.batches.forEach((batch, index) => { + console.log(`\n${index + 1}. ${batch.batchName}`); + console.log(' Batch ID:', batch.id); + console.log(' Status:', batch.status); + console.log(' Total Jobs:', batch.totalJobs); + console.log(' Succeeded:', batch.succeededJobs); + console.log(' Failed:', batch.failedJobs); + console.log(' Pending:', batch.pendingJobs); + console.log(' Created:', new Date(batch.createdOn).toLocaleString()); + }); + + console.log('\n💡 Tip: Use limit and offset parameters for pagination'); +} else { + console.error('❌ Error:', result.error || 'Failed to retrieve batches'); + if (result.code) { + console.error('Error Code:', result.code); + } +} diff --git a/static/scripts/turbosign/api/bulk/list-batches/nodejs/fastify.js b/static/scripts/turbosign/api/bulk/list-batches/nodejs/fastify.js new file mode 100644 index 0000000..518afb1 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-batches/nodejs/fastify.js @@ -0,0 +1,57 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; + +// Fastify route handler example +async function listBatches(request, reply) { + try { + // Prepare query parameters + const queryParams = new URLSearchParams({ + limit: '20', + offset: '0', + status: 'pending,processing,completed' // Can be single value or comma-separated + // startDate: '2024-01-01', // Optional: Filter by start date + // endDate: '2024-12-31', // Optional: Filter by end date + // query: 'Q4' // Optional: Search batches by name + }); + + // Send GET request to list batches endpoint + const response = await fetch(`${BASE_URL}/turbosign/bulk/batches?${queryParams}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + }); + + const result = await response.json(); + + if (result.data) { + return reply.send({ + success: true, + totalRecords: result.data.totalRecords, + batches: result.data.batches + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || 'Failed to retrieve batches', + code: result.code + }); + } + } catch (error) { + console.error('Error retrieving batches:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { listBatches }; diff --git a/static/scripts/turbosign/api/bulk/list-batches/python/fastapi.py b/static/scripts/turbosign/api/bulk/list-batches/python/fastapi.py new file mode 100644 index 0000000..5f068fd --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-batches/python/fastapi.py @@ -0,0 +1,88 @@ +import json +import requests +from fastapi import FastAPI, HTTPException, Query +from pydantic import BaseModel +from typing import List, Optional + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = FastAPI() + +class BatchInfo(BaseModel): + id: str + batchName: str + status: str + totalJobs: int + succeededJobs: int + failedJobs: int + pendingJobs: int + createdOn: str + updatedOn: str + +class ListBatchesResponse(BaseModel): + success: bool + totalRecords: int + batches: List[BatchInfo] + +@app.get('/list-batches', response_model=ListBatchesResponse) +async def list_batches( + limit: Optional[int] = Query(20, description="Number of batches to return"), + offset: Optional[int] = Query(0, description="Number of batches to skip"), + status: Optional[str] = Query(None, description="Filter by status") +): + try: + # Prepare query parameters + params = { + 'limit': str(limit), + 'offset': str(offset) + } + + if status: + params['status'] = status + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send GET request + response = requests.get( + f'{BASE_URL}/turbosign/bulk/batches', + headers=headers, + params=params + ) + + result = response.json() + + if result.get('data'): + batches_data = result['data'] + return ListBatchesResponse( + success=True, + totalRecords=batches_data['totalRecords'], + batches=[BatchInfo(**batch) for batch in batches_data['batches']] + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to retrieve batches'), + 'code': result.get('code') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error retrieving batches: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/bulk/list-batches/python/flask.py b/static/scripts/turbosign/api/bulk/list-batches/python/flask.py new file mode 100644 index 0000000..9e0ac23 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-batches/python/flask.py @@ -0,0 +1,79 @@ +import json +import requests +from flask import Flask, jsonify, request + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" + +app = Flask(__name__) + +@app.route('/list-batches', methods=['GET']) +def list_batches(): + try: + # Prepare query parameters + params = { + 'limit': '20', + 'offset': '0', + 'status': 'pending,processing,completed' # Can be single value or comma-separated + # 'startDate': '2024-01-01', # Optional: Filter by start date + # 'endDate': '2024-12-31', # Optional: Filter by end date + # 'query': 'Q4' # Optional: Search batches by name + } + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send GET request + response = requests.get( + f'{BASE_URL}/turbosign/bulk/batches', + headers=headers, + params=params + ) + + result = response.json() + + if result.get('data'): + batches_data = result['data'] + formatted_batches = [] + + for batch in batches_data['batches']: + formatted_batches.append({ + 'id': batch['id'], + 'batchName': batch['batchName'], + 'status': batch['status'], + 'totalJobs': batch['totalJobs'], + 'succeededJobs': batch['succeededJobs'], + 'failedJobs': batch['failedJobs'], + 'pendingJobs': batch['pendingJobs'], + 'createdOn': batch['createdOn'], + 'updatedOn': batch['updatedOn'] + }) + + return jsonify({ + 'success': True, + 'totalRecords': batches_data['totalRecords'], + 'batches': formatted_batches + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to retrieve batches'), + 'code': result.get('code') + }), 400 + + except Exception as error: + print(f'Error retrieving batches: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/static/scripts/turbosign/api/bulk/list-jobs/nodejs/express.js b/static/scripts/turbosign/api/bulk/list-jobs/nodejs/express.js new file mode 100644 index 0000000..b205481 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-jobs/nodejs/express.js @@ -0,0 +1,64 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; +const BATCH_ID = "YOUR_BATCH_ID"; // Replace with actual batch ID + +// Prepare query parameters +const queryParams = new URLSearchParams({ + limit: '20', + offset: '0', + // status: 'pending,processing,succeeded,failed', // Optional: Filter by status + // query: 'john' // Optional: Search by recipient name/email +}); + +// Send GET request to list jobs for a batch +const response = await fetch(`${BASE_URL}/turbosign/bulk/batch/${BATCH_ID}/jobs?${queryParams}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } +}); + +const result = await response.json(); + +if (result.data) { + console.log('✅ Successfully retrieved jobs for batch'); + console.log('Batch ID:', result.data.batchId); + console.log('Batch Name:', result.data.batchName); + console.log('Batch Status:', result.data.batchStatus); + console.log('\n📊 Statistics:'); + console.log('Total Jobs:', result.data.totalJobs); + console.log('Total Records:', result.data.totalRecords); + console.log('Succeeded:', result.data.succeededJobs); + console.log('Failed:', result.data.failedJobs); + console.log('Pending:', result.data.pendingJobs); + + console.log('\n📋 Jobs in this page:', result.data.jobs.length); + + result.data.jobs.forEach((job, index) => { + console.log(`\n${index + 1}. Job ID: ${job.id}`); + console.log(' Document:', job.documentName); + console.log(' Document ID:', job.documentId || 'N/A'); + console.log(' Status:', job.status); + console.log(' Recipients:', job.recipientEmails.join(', ')); + console.log(' Attempts:', job.attempts); + if (job.errorMessage) { + console.log(' Error:', job.errorMessage); + console.log(' Error Code:', job.errorCode); + } + console.log(' Created:', new Date(job.createdOn).toLocaleString()); + console.log(' Updated:', new Date(job.updatedOn).toLocaleString()); + }); + + console.log('\n💡 Tip: Use status filter to find failed or pending jobs'); +} else { + console.error('❌ Error:', result.error || 'Failed to retrieve jobs'); + if (result.code) { + console.error('Error Code:', result.code); + } +} diff --git a/static/scripts/turbosign/api/bulk/list-jobs/nodejs/fastify.js b/static/scripts/turbosign/api/bulk/list-jobs/nodejs/fastify.js new file mode 100644 index 0000000..d294bd0 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-jobs/nodejs/fastify.js @@ -0,0 +1,67 @@ +const fetch = require('node-fetch'); + +// Configuration - Update these values +const API_TOKEN = "YOUR_API_TOKEN"; +const ORG_ID = "YOUR_ORGANIZATION_ID"; +const BASE_URL = "https://api.turbodocx.com"; +const BATCH_ID = "YOUR_BATCH_ID"; // Replace with actual batch ID + +// Fastify route handler example +async function listJobs(request, reply) { + try { + // Prepare query parameters + const queryParams = new URLSearchParams({ + limit: '20', + offset: '0' + // status: 'pending,processing,succeeded,failed', // Optional: Filter by status + // query: 'john' // Optional: Search by recipient name/email + }); + + // Send GET request to list jobs for a batch + const response = await fetch(`${BASE_URL}/turbosign/bulk/batch/${BATCH_ID}/jobs?${queryParams}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + }); + + const result = await response.json(); + + if (result.data) { + // Jobs include: id, batchId, documentId, documentName, status, recipientEmails, + // attempts, errorCode, errorMessage, createdOn, updatedOn, lastAttemptedAt + return reply.send({ + success: true, + batchId: result.data.batchId, + batchName: result.data.batchName, + batchStatus: result.data.batchStatus, + statistics: { + totalJobs: result.data.totalJobs, + totalRecords: result.data.totalRecords, + succeededJobs: result.data.succeededJobs, + failedJobs: result.data.failedJobs, + pendingJobs: result.data.pendingJobs + }, + jobs: result.data.jobs + }); + } else { + return reply.code(400).send({ + success: false, + error: result.error || 'Failed to retrieve jobs', + code: result.code + }); + } + } catch (error) { + console.error('Error retrieving jobs:', error); + return reply.code(500).send({ + success: false, + error: 'Internal server error', + message: error.message + }); + } +} + +// Export for use in Fastify app +module.exports = { listJobs }; diff --git a/static/scripts/turbosign/api/bulk/list-jobs/python/fastapi.py b/static/scripts/turbosign/api/bulk/list-jobs/python/fastapi.py new file mode 100644 index 0000000..84e0e7a --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-jobs/python/fastapi.py @@ -0,0 +1,111 @@ +import json +import requests +from fastapi import FastAPI, HTTPException, Path, Query +from pydantic import BaseModel +from typing import List, Optional, Any + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" +BATCH_ID = "YOUR_BATCH_ID" # Replace with actual batch ID + +app = FastAPI() + +class JobStatistics(BaseModel): + totalJobs: int + totalRecords: int + succeededJobs: int + failedJobs: int + pendingJobs: int + +class JobInfo(BaseModel): + id: str + batchId: str + documentId: Optional[str] + documentName: str + status: str + recipientEmails: List[str] + attempts: int + errorCode: Optional[str] = None + errorMessage: Optional[str] = None + createdOn: str + updatedOn: str + lastAttemptedAt: Optional[str] = None + +class ListJobsResponse(BaseModel): + success: bool + batchId: str + batchName: str + batchStatus: str + statistics: JobStatistics + jobs: List[JobInfo] + +@app.get('/list-jobs', response_model=ListJobsResponse) +async def list_jobs( + limit: Optional[int] = Query(20, description="Number of jobs to return"), + offset: Optional[int] = Query(0, description="Number of jobs to skip"), + status: Optional[str] = Query(None, description="Filter by status") +): + try: + # Prepare query parameters + params = { + 'limit': str(limit), + 'offset': str(offset) + } + + if status: + params['status'] = status + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send GET request + response = requests.get( + f'{BASE_URL}/turbosign/bulk/batch/{BATCH_ID}/jobs', + headers=headers, + params=params + ) + + result = response.json() + + if result.get('data'): + jobs_data = result['data'] + return ListJobsResponse( + success=True, + batchId=jobs_data['batchId'], + batchName=jobs_data['batchName'], + batchStatus=jobs_data['batchStatus'], + statistics=JobStatistics( + totalJobs=jobs_data['totalJobs'], + totalRecords=jobs_data['totalRecords'], + succeededJobs=jobs_data['succeededJobs'], + failedJobs=jobs_data['failedJobs'], + pendingJobs=jobs_data['pendingJobs'] + ), + jobs=jobs_data['jobs'] + ) + else: + raise HTTPException( + status_code=400, + detail={ + 'error': result.get('error', 'Failed to retrieve jobs'), + 'code': result.get('code') + } + ) + + except HTTPException: + raise + except Exception as error: + print(f'Error retrieving jobs: {error}') + raise HTTPException( + status_code=500, + detail={ + 'error': 'Internal server error', + 'message': str(error) + } + ) diff --git a/static/scripts/turbosign/api/bulk/list-jobs/python/flask.py b/static/scripts/turbosign/api/bulk/list-jobs/python/flask.py new file mode 100644 index 0000000..a581c17 --- /dev/null +++ b/static/scripts/turbosign/api/bulk/list-jobs/python/flask.py @@ -0,0 +1,93 @@ +import json +import requests +from flask import Flask, jsonify, request + +# Configuration - Update these values +API_TOKEN = "YOUR_API_TOKEN" +ORG_ID = "YOUR_ORGANIZATION_ID" +BASE_URL = "https://api.turbodocx.com" +BATCH_ID = "YOUR_BATCH_ID" # Replace with actual batch ID + +app = Flask(__name__) + +@app.route('/list-jobs', methods=['GET']) +def list_jobs(): + try: + # Prepare query parameters + params = { + 'limit': '20', + 'offset': '0' + # 'status': 'pending,processing,succeeded,failed', # Optional: Filter by status + # 'query': 'john' # Optional: Search by recipient name/email + } + + # Prepare headers + headers = { + 'Authorization': f'Bearer {API_TOKEN}', + 'x-rapiddocx-org-id': ORG_ID, + 'User-Agent': 'TurboDocx API Client' + } + + # Send GET request + response = requests.get( + f'{BASE_URL}/turbosign/bulk/batch/{BATCH_ID}/jobs', + headers=headers, + params=params + ) + + result = response.json() + + if result.get('data'): + jobs_data = result['data'] + formatted_jobs = [] + + for job in jobs_data['jobs']: + job_info = { + 'id': job['id'], + 'batchId': job['batchId'], + 'documentId': job.get('documentId'), + 'documentName': job['documentName'], + 'status': job['status'], + 'recipientEmails': job['recipientEmails'], + 'attempts': job['attempts'], + 'createdOn': job['createdOn'], + 'updatedOn': job['updatedOn'], + 'lastAttemptedAt': job.get('lastAttemptedAt') + } + if job.get('errorMessage'): + job_info['errorMessage'] = job['errorMessage'] + job_info['errorCode'] = job['errorCode'] + + formatted_jobs.append(job_info) + + return jsonify({ + 'success': True, + 'batchId': jobs_data['batchId'], + 'batchName': jobs_data['batchName'], + 'batchStatus': jobs_data['batchStatus'], + 'statistics': { + 'totalJobs': jobs_data['totalJobs'], + 'totalRecords': jobs_data['totalRecords'], + 'succeededJobs': jobs_data['succeededJobs'], + 'failedJobs': jobs_data['failedJobs'], + 'pendingJobs': jobs_data['pendingJobs'] + }, + 'jobs': formatted_jobs + }), 200 + else: + return jsonify({ + 'success': False, + 'error': result.get('error', 'Failed to retrieve jobs'), + 'code': result.get('code') + }), 400 + + except Exception as error: + print(f'Error retrieving jobs: {error}') + return jsonify({ + 'success': False, + 'error': 'Internal server error', + 'message': str(error) + }), 500 + +if __name__ == '__main__': + app.run(debug=True)