Agreement generation and signature submission endpoints
GET /api/v1/agreements/generate?application_id=APP-2025-001234 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"request_id": "req_cm09_20251123_153000",
"status": "success",
"agreement": {
"id": "AGR-2025-001234",
"application_id": "APP-2025-001234",
"pdf_url": "/api/v1/agreements/AGR-2025-001234/download",
"preview_html": "This Business Loan Agreement...",
"terms": {
"principal": 150000,
"apr": 4.5,
"term_months": 36,
"monthly_payment": 4440,
"total_repayment": 159840
},
"generated_at": "2025-11-23T15:30:00Z"
},
"processing_time_ms": 847
}
POST /api/v1/signatures/submit Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Content-Type: application/json { "application_id": "APP-2025-001234", "agreement_id": "AGR-2025-001234", "signature_data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEU...", "legal_confirmations": { "terms_understood": true, "information_accurate": true, "legally_binding": true, "credit_monitoring_consent": true }, "signer_details": { "name": "John Smith", "signing_date": "2025-11-23", "ip_address": "86.134.45.123", "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)" } }
{
"request_id": "req_cm09_20251123_153130",
"status": "success",
"signature": {
"id": "SIG-2025-001234",
"application_id": "APP-2025-001234",
"agreement_id": "AGR-2025-001234",
"docusign_envelope_id": "a7f3c5d9-e8b2-4a1c-9f6d-3e4b5a7c8d9e",
"signed_at": "2025-11-23T15:31:30Z",
"signed_pdf_url": "/api/v1/agreements/AGR-2025-001234/signed"
},
"next_step": {
"screen": "CM10",
"action": "view_confirmation",
"message": "Application submitted successfully!"
},
"processing_time_ms": 1847
}
Agreement generation and signature validation
async function generateAgreement(applicationId) { // 1. Get application data const app = await Application.findById(applicationId); // 2. Load legal template const template = await loadTemplate('business_loan_agreement.html'); // 3. Merge data into template const mergedHtml = template .replace('{{principal}}', formatCurrency(app.chosen_amount)) .replace('{{apr}}', app.apr) .replace('{{term_months}}', app.chosen_term_months) .replace('{{monthly_payment}}', formatCurrency(app.monthly_payment)) .replace('{{borrower_name}}', app.company_name) .replace('{{company_number}}', app.company_number) .replace('{{loan_purpose}}', app.specific_use); // 4. Generate PDF const pdf = await htmlToPdf(mergedHtml); // 5. Upload to S3 const agreementId = generateAgreementId(); const s3Key = `agreements/${applicationId}/${agreementId}.pdf`; await s3.upload(s3Key, pdf); // 6. Save metadata to database await Agreement.create({ id: agreementId, application_id: applicationId, s3_key: s3Key, status: 'unsigned' }); return { agreementId, s3Key, html: mergedHtml }; }
function validateSignature(signatureData, confirmations) { // Check all confirmations are true if (!confirmations.terms_understood || !confirmations.information_accurate || !confirmations.legally_binding || !confirmations.credit_monitoring_consent) { throw new ValidationError("All confirmations required"); } // Check signature is not blank const image = decodeBase64Image(signatureData); const inkPixels = countNonWhitePixels(image); if (inkPixels < 100) { throw new ValidationError("Signature appears blank"); } // Check signature size if (signatureData.length > 500000) { throw new ValidationError("Signature image too large"); } return true; }
Embedded Mode: Customer signs within AINA app (no DocuSign account needed)
Branding: White-label experience with AINA branding
Audit Trail: Captures IP, timestamp, geolocation, authentication method
Certificate: Final page shows complete signing history and validation
DocuSign API integration and event processing
async function createDocuSignEnvelope(agreementId, signerDetails) { // 1. Get PDF from S3 const pdfBytes = await s3.download(agreementS3Key); // 2. Create envelope definition const envelopeDefinition = { emailSubject: "Sign Your AINA Loan Agreement", documents: [{ documentBase64: pdfBytes.toString('base64'), name: "Loan Agreement", fileExtension: "pdf", documentId: "1" }], recipients: { signers: [{ email: signerDetails.email, name: signerDetails.name, recipientId: "1", routingOrder: "1", tabs: { signHereTabs: [{ documentId: "1", pageNumber: "7", // Last page xPosition: "100", yPosition: "200" }] } }] }, status: "sent" }; // 3. Create envelope via DocuSign API const envelope = await docusign.createEnvelope(envelopeDefinition); // 4. Generate embedded signing URL const viewRequest = { returnUrl: "https://aina.app/cm10-confirmation", authenticationMethod: "none", email: signerDetails.email, userName: signerDetails.name }; const view = await docusign.createRecipientView(envelope.envelopeId, viewRequest); return { envelopeId: envelope.envelopeId, signingUrl: view.url }; }
async function handleDocuSignWebhook(event) { // Verify webhook signature const isValid = verifyWebhookSignature(event); if (!isValid) throw new SecurityError("Invalid webhook"); if (event.event === "envelope-completed") { const envelopeId = event.data.envelopeId; // Download signed PDF from DocuSign const signedPdf = await docusign.downloadDocument(envelopeId); // Upload to S3 const s3Key = `agreements/${applicationId}/${agreementId}_signed.pdf`; await s3.upload(s3Key, signedPdf); // Update database await Agreement.update(agreementId, { status: 'signed', signed_pdf_s3_key: s3Key, docusign_envelope_id: envelopeId, signed_at: new Date() }); // Update application status await Application.update(applicationId, { status: 'signed' }); // Send notifications await sendSignedConfirmation(applicationId); } }
DocuSign e-signature platform
Embedded Signing: Sign within AINA app (no DocuSign account needed)
Templates: Pre-configured signature placement on loan agreements
Webhooks: Real-time notifications when envelope completed
Certificate of Completion: Legal audit trail appended to signed PDF
Mobile Responsive: Optimized signing experience on mobile devices
Agreement and signature metadata storage
| Column | Type | Description | Example |
|---|---|---|---|
id |
VARCHAR(50) | Primary key | AGR-2025-001234 |
application_id |
VARCHAR(50) | Foreign key | APP-2025-001234 |
unsigned_pdf_s3_key |
VARCHAR(500) | S3 path to unsigned PDF | agreements/APP.../AGR....pdf |
signed_pdf_s3_key |
VARCHAR(500) | S3 path to signed PDF | agreements/...signed.pdf |
status |
VARCHAR(20) | unsigned/signed | signed |
docusign_envelope_id |
VARCHAR(100) | DocuSign envelope UUID | a7f3c5d9-e8b2-4a1c... |
generated_at |
TIMESTAMP | When PDF generated | 2025-11-23 15:30:00 |
signed_at |
TIMESTAMP | When customer signed | 2025-11-23 15:31:30 |
| Column | Type | Description | Example |
|---|---|---|---|
id |
VARCHAR(50) | Primary key | SIG-2025-001234 |
agreement_id |
VARCHAR(50) | Foreign key | AGR-2025-001234 |
signature_image_s3_key |
VARCHAR(500) | Canvas signature PNG | signatures/.../SIG....png |
signer_name |
VARCHAR(200) | Full legal name | John Smith |
signer_ip_address |
VARCHAR(45) | IP at signing time | 86.134.45.123 |
signer_user_agent |
TEXT | Browser/device info | Mozilla/5.0 (iPhone...) |
signed_at |
TIMESTAMP(3) | Exact timestamp (ms) | 2025-11-23 15:31:30.847 |
legal_confirmations |
JSONB | 4 checkbox states | {"terms_understood": true...} |
CREATE TABLE agreements ( id VARCHAR(50) PRIMARY KEY, application_id VARCHAR(50) NOT NULL REFERENCES applications(id), unsigned_pdf_s3_key VARCHAR(500) NOT NULL, signed_pdf_s3_key VARCHAR(500), status VARCHAR(20) DEFAULT 'unsigned', docusign_envelope_id VARCHAR(100), generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, signed_at TIMESTAMP ); CREATE TABLE signatures ( id VARCHAR(50) PRIMARY KEY, agreement_id VARCHAR(50) NOT NULL REFERENCES agreements(id), signature_image_s3_key VARCHAR(500) NOT NULL, signer_name VARCHAR(200) NOT NULL, signer_ip_address VARCHAR(45) NOT NULL, signer_user_agent TEXT, signed_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, legal_confirmations JSONB NOT NULL ); CREATE INDEX idx_agreements_app ON agreements(application_id); CREATE INDEX idx_signatures_agreement ON signatures(agreement_id);
INSERT INTO signatures ( id, agreement_id, signature_image_s3_key, signer_name, signer_ip_address, signer_user_agent, legal_confirmations ) VALUES ( 'SIG-2025-001234', 'AGR-2025-001234', 'signatures/APP-2025-001234/SIG-2025-001234.png', 'John Smith', '86.134.45.123', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)', '{"terms_understood": true, "information_accurate": true, "legally_binding": true, "credit_monitoring_consent": true}'::jsonb );
UPDATE applications SET current_status = 'signed', signed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = 'APP-2025-001234'; UPDATE agreements SET status = 'signed', signed_pdf_s3_key = 'agreements/.../AGR-2025-001234_signed.pdf', docusign_envelope_id = 'a7f3c5d9-e8b2-4a1c-9f6d-3e4b5a7c8d9e', signed_at = CURRENT_TIMESTAMP WHERE id = 'AGR-2025-001234';
{
"event_type": "agreement_signed",
"application_id": "APP-2025-001234",
"agreement_id": "AGR-2025-001234",
"signature_id": "SIG-2025-001234",
"customer_id": "CUST-001234",
"timestamp": "2025-11-23T15:31:30.847Z",
"signer_details": {
"name": "John Smith",
"ip_address": "86.134.45.123",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)"
},
"legal_confirmations": {
"terms_understood": true,
"information_accurate": true,
"legally_binding": true,
"credit_monitoring_consent": true
},
"docusign": {
"envelope_id": "a7f3c5d9-e8b2-4a1c-9f6d-3e4b5a7c8d9e",
"certificate_url": "https://docusign.net/certificate/..."
}
}
Customer submits without drawing anything on canvas
One or more legal confirmations not checked
DocuSign service unavailable or rate limit exceeded
Agreement ID doesn't exist or was deleted
Customer tries to sign agreement that's already signed
Error generating PDF from template
Network error or S3 service unavailable
JWT token expired during signing process
Agreement display with canvas-based signature capture
1. Loan Amount: Principal amount and currency
2. Interest Rate: APR and calculation method
3. Repayment Terms: Monthly payment, term, total repayment
4. Purpose: Approved use of funds
5. Security: Personal guarantee and asset charge details
6. Early Repayment: Terms for early settlement (no penalty)
7. Default: Consequences of non-payment
const canvas = document.getElementById('signatureCanvas'); const ctx = canvas.getContext('2d'); let isDrawing = false; let lastX = 0; let lastY = 0; canvas.addEventListener('mousedown', (e) => { isDrawing = true; [lastX, lastY] = [e.offsetX, e.offsetY]; }); canvas.addEventListener('mousemove', (e) => { if (!isDrawing) return; ctx.strokeStyle = '#000000'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); [lastX, lastY] = [e.offsetX, e.offsetY]; }); canvas.addEventListener('mouseup', () => { isDrawing = false; });