Data Flow Diagram
This document provides a comprehensive overview of how data flows through the Ledly platform. It is intended for security reviewers, compliance teams, and technical architects evaluating Ledly for their higher education institution.
Ledly processes student lead data for higher education enrollment management. All data flows are encrypted in transit using TLS 1.3 and at rest using AES-256.
Overview
Ledly is a lead management platform designed for higher education. Data enters the system through multiple ingestion channels, is processed and enriched, then synchronized to CRM systems and delivered via webhooks.
High-Level Architecture
LEDLY PLATFORM
+------------------------------------------+
| |
INGESTION | +-------------+ +-----------+ | DELIVERY
+---------+ | | | | | | +---------+
| Web |----->|---->| Ledly |--->| PostgreSQL| |---->| HubSpot |
| Forms | | | API | | Database | | +---------+
+---------+ | | | | | |
| +------+------+ +-----+-----+ | +---------+
+---------+ | | | |---->|Dynamics |
| Landing |----->|------------+ | | | 365 |
| Pages | | | | | +---------+
+---------+ | v | |
| +-------------+ | | +---------+
+---------+ | | Webhook |<---------+ |---->| Custom |
| API |----->| | Service | | | Webhook |
| Clients | | +-------------+ | +---------+
+---------+ | |
+------------------------------------------+Data Categories
| Category | Description | Sensitivity |
|---|---|---|
| Lead PII | Names, emails, phone numbers, addresses | High |
| Education Data | Program interests, start dates, campus preferences | Medium |
| Marketing Data | UTM parameters, referral sources, campaign IDs | Low |
| System Data | Timestamps, IDs, sync status, API logs | Low |
Lead Ingestion Flow
Leads enter Ledly through three primary channels. All ingestion endpoints require authentication and validate data before storage.
Ingestion Channels
+------------------+ HTTPS/TLS 1.3 +------------------+
| WEB FORMS |----------------------->| |
| (JavaScript SDK) | POST /api/leads/inbound | |
+------------------+ + API Key | |
| |
+------------------+ HTTPS/TLS 1.3 | LEDLY API |
| LANDING PAGES |----------------------->| |
| (Form Submit) | POST /api/leads/inbound | - Validates |
+------------------+ + API Key | - Deduplicates |
| - Enriches |
+------------------+ HTTPS/TLS 1.3 | - Routes |
| API CLIENTS |----------------------->| |
| (Server-to-Server)| POST /api/leads/inbound | |
+------------------+ + API Key +--------+---------+
|
| Validated Lead
v
+------------------+
| PostgreSQL |
| Database |
| |
| - leads table |
| - Encrypted PII |
| - org_id scope |
+------------------+Ingestion Process
- Authentication: API key validated against
api_keystable - Rate Limiting: Request checked against rate limits (100 req/10s default)
- Validation: Schema validation, email format, required fields
- Deduplication: Email-based duplicate check within organization
- Enrichment: Optional field enrichment and normalization
- Storage: Lead persisted to PostgreSQL with
organization_idscope - Event Emission:
lead.createdevent triggers downstream processing
Data Transformation
| Input Field | Validation | Transformation | Storage |
|---|---|---|---|
email | RFC 5322 format | Lowercase, trim | leads.email |
phone | E.164 format | Normalize to E.164 | leads.phone |
first_name | 1-100 chars | Trim whitespace | leads.first_name |
last_name | 1-100 chars | Trim whitespace | leads.last_name |
utm_* | 1-500 chars | None | leads.utm_data (JSONB) |
All ingestion requests must include a valid API key in the Authorization header. Requests without authentication receive a 401 response.
CRM Synchronization Flow
Ledly synchronizes leads bidirectionally with CRM systems. Each CRM integration uses OAuth 2.0 for secure authorization.
HubSpot Integration
LEDLY HUBSPOT
+------------------+ +------------------+
| | OAuth 2.0 Authorization | |
| CRM Settings |<---------------------------->| Developer App |
| | (Initial Setup) | |
+--------+---------+ +------------------+
|
| Stores: access_token, refresh_token
v
+------------------+ +------------------+
| | POST /crm/v3/objects/ | |
| Lead Created |----------------------------->| Contacts |
| or Updated | contacts (HTTPS) | |
| | + Bearer Token | |
+--------+---------+ +--------+---------+
| |
| |
v v
+------------------+ +------------------+
| | POST /api/hubspot/ | |
| Update Lead |<-----------------------------| Webhook |
| Status | webhook (HTTPS) | Event |
| | + Signature | |
+------------------+ +------------------+OAuth Scopes Required:
crm.objects.contacts.readcrm.objects.contacts.writecrm.schemas.contacts.read
Token Storage:
| Data | Storage | Encryption |
|---|---|---|
| Access Token | crm_connections.access_token | AES-256 |
| Refresh Token | crm_connections.refresh_token | AES-256 |
| Client Secret | crm_connections.client_secret | AES-256 |
Microsoft Dynamics 365 Integration
LEDLY DYNAMICS 365
+------------------+ +------------------+
| | Azure AD OAuth 2.0 | |
| CRM Settings |<---------------------------->| Azure AD App |
| | (Initial Setup) | Registration |
+--------+---------+ +------------------+
|
| Stores: access_token, refresh_token, tenant_id
v
+------------------+ +------------------+
| | POST /api/data/v9.2/ | |
| Lead Created |----------------------------->| leads |
| or Updated | leads (HTTPS/OData) | (Entity) |
| | + Bearer Token | |
+--------+---------+ +--------+---------+
| |
| |
v v
+------------------+ +------------------+
| | POST /api/dynamics/ | Power Automate |
| Update Lead |<-----------------------------| Flow |
| Status | webhook (HTTPS) | |
| | + Webhook Secret | |
+------------------+ +------------------+Azure AD Permissions:
Dynamics CRM>user_impersonation(Delegated)
Authentication Flow:
- User initiates OAuth in Ledly settings
- Redirect to Azure AD authorization endpoint
- User consents to permissions
- Authorization code exchanged for tokens
- Tokens stored encrypted in database
- Access token auto-refreshed before expiration (1 hour)
CRM Sync Status
Each lead tracks its CRM synchronization state:
| Status | Description |
|---|---|
pending | Queued for sync |
synced | Successfully pushed to CRM |
failed | Sync failed, will retry |
not_connected | No CRM integration configured |
Webhook Delivery Flow
Ledly delivers real-time notifications to customer endpoints when lead events occur.
Webhook Architecture
+------------------+ Event Bus +------------------+
| | | |
| Lead Event |--------------------->| Webhook |
| (Created, | lead.created | Service |
| Updated, etc.) | lead.updated | |
| | lead.status_changed| |
+------------------+ +--------+---------+
|
For each subscribed endpoint:
|
+----------+---------------+----------------+
| | | |
v v v v
+--------+--+ +-----+------+ +------+-----+ +--------+---+
| | | | | | | |
| Customer | | Customer | | Zapier | | Slack |
| API #1 | | API #2 | | Webhook | | Webhook |
| | | | | | | |
+-----------+ +------------+ +------------+ +------------+
HTTPS HTTPS HTTPS HTTPS
+ Signature + Signature + Signature + SignatureWebhook Request Structure
Every webhook request includes security headers for verification:
| Header | Description |
|---|---|
X-Ledly-Signature | HMAC-SHA256 signature of payload |
X-Ledly-Timestamp | Unix timestamp of request |
X-Ledly-Webhook-Id | Unique webhook configuration ID |
X-Ledly-Delivery-Id | Unique delivery attempt ID |
Signature Verification
Signature Generation:
+------------------+
| |
| Payload (JSON) |
| |
+--------+---------+
|
v
+------------------+ +------------------+
| | | |
| timestamp. |---->| HMAC-SHA256 |
| payload | | (webhook_secret)|
| | | |
+------------------+ +--------+---------+
|
v
+------------------+
| |
| X-Ledly- |
| Signature |
| |
+------------------+Verification Steps (Recipient):
- Extract
X-Ledly-Timestampheader - Reject if timestamp > 5 minutes old (replay protection)
- Concatenate:
{timestamp}.{raw_body} - Compute HMAC-SHA256 with webhook secret
- Compare signatures using constant-time comparison
Retry Logic
Failed webhook deliveries are automatically retried with exponential backoff:
Attempt 1 (immediate)
|
| Failed (non-2xx or timeout)
v
Attempt 2 (+1 minute)
|
| Failed
v
Attempt 3 (+5 minutes)
|
| Failed
v
Attempt 4 (+30 minutes)
|
| Failed
v
Attempt 5 (+2 hours)
|
| Failed
v
Attempt 6 (+24 hours)
|
| Failed
v
MARKED AS FAILED (manual retry available)| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | 2.5 hours |
| 6 | 24 hours | 26.5 hours |
Webhook deliveries timeout after 30 seconds. For long-running processes, acknowledge the webhook immediately and process asynchronously.
Data Storage
All persistent data is stored in PostgreSQL with strict organization-level isolation.
Database Architecture
+------------------------------------------------------------------+
| PostgreSQL Database |
| (AWS RDS / Encrypted) |
+------------------------------------------------------------------+
| |
| +-------------------+ +-------------------+ |
| | organizations | | users | |
| |-------------------| |-------------------| |
| | id (PK) |<---| organization_id | |
| | name | | email | |
| | settings (JSONB) | | password_hash | |
| | created_at | | role | |
| +-------------------+ +-------------------+ |
| | |
| | 1:N |
| v |
| +-------------------+ +-------------------+ |
| | leads | | api_keys | |
| |-------------------| |-------------------| |
| | id (PK) | | id (PK) | |
| | organization_id |--->| organization_id | |
| | email | | key_hash | |
| | first_name | | permissions | |
| | last_name | | ip_restrictions | |
| | phone | | last_used_at | |
| | utm_data (JSONB) | +-------------------+ |
| | custom_fields | |
| | crm_sync_status | +-------------------+ |
| | created_at | | crm_connections | |
| +-------------------+ |-------------------| |
| | id (PK) | |
| | organization_id | |
| +-------------------+ | provider | |
| | webhook_configs | | access_token (E) | |
| |-------------------| | refresh_token (E) | |
| | id (PK) | | client_secret (E) | |
| | organization_id | | expires_at | |
| | url | +-------------------+ |
| | secret_hash | (E) = Encrypted |
| | events | |
| | active | +-------------------+ |
| +-------------------+ | webhook_deliveries| |
| |-------------------| |
| | id (PK) | |
| | webhook_id | |
| | lead_id | |
| | status | |
| | attempts | |
| | response_code | |
| +-------------------+ |
+------------------------------------------------------------------+Data Stored by Table
| Table | Data Stored | Retention |
|---|---|---|
organizations | Account details, billing info, settings | Account lifetime |
users | User profiles, hashed passwords, roles | Account lifetime |
leads | Lead PII, UTM data, custom fields, status | Configurable (default: indefinite) |
api_keys | Hashed API keys, permissions, usage stats | Until revoked |
crm_connections | OAuth tokens (encrypted), provider config | Until disconnected |
webhook_configs | Endpoint URLs, event subscriptions, secrets | Until deleted |
webhook_deliveries | Delivery attempts, responses, timestamps | 30 days |
audit_logs | User actions, API calls, security events | 90 days |
Data Isolation
All queries are scoped by organization_id to ensure strict tenant isolation:
-- Example: All lead queries include organization_id
SELECT * FROM leads
WHERE organization_id = :current_org_id
AND email = :email;
-- Row-Level Security enforced at database level
ALTER TABLE leads ENABLE ROW LEVEL SECURITY;
CREATE POLICY leads_org_isolation ON leads
USING (organization_id = current_setting('app.organization_id')::uuid);Cross-organization data access is not possible through the API. Row-level security is enforced at the database level as an additional safeguard.
Authentication Flow
Ledly uses JWT tokens for user authentication and API keys for vendor/system authentication.
User Authentication (JWT)
+------------------+ +------------------+
| | POST /api/auth/login | |
| User |----------------------------->| Ledly API |
| (Browser) | email + password | |
| | +--------+---------+
+--------+---------+ |
^ |
| v
| +------------------+
| | |
| | Verify password |
| | (bcrypt hash) |
| | |
| +--------+---------+
| |
| 200 OK |
| { token: "eyJ...", expires_at: "..." } |
|<------------------------------------------------+
|
|
| GET /api/leads
| Authorization: Bearer eyJ...
+------------------------------------------------>|
|
+--------+---------+
| |
| Verify JWT |
| - Signature |
| - Expiration |
| - Claims |
| |
+--------+---------+
|
|<------------------------------------------------+
| 200 OK { leads: [...] }JWT Token Structure:
| Claim | Description |
|---|---|
sub | User ID |
org | Organization ID |
role | User role (admin, member, viewer) |
exp | Expiration timestamp (24 hours) |
iat | Issued at timestamp |
Token Security:
- Signed with HS256 using server-side secret
- 24-hour expiration
- Refresh via
/api/auth/refreshendpoint - Invalidated on logout or password change
API Key Authentication
+------------------+ +------------------+
| | POST /api/leads/inbound | |
| Vendor Server |----------------------------->| Ledly API |
| | Authorization: Bearer vk_live_... | |
| | +--------+---------+
+------------------+ |
v
+------------------+
| |
| Hash API Key |
| Lookup in DB |
| Check: |
| - Active |
| - IP allowed |
| - Rate limit |
| |
+--------+---------+
|
+--------+---------+
| |
| Process Request |
| (scoped to |
| organization) |
| |
+------------------+API Key Security:
- Keys hashed with SHA-256 before storage
- Optional IP allowlist restrictions
- Per-key rate limiting
- Separate test (
vk_test_) and production (vk_live_) keys
Data Types Reference
Complete reference of data types flowing through Ledly:
| Data Type | Source | Storage Location | Destinations | Encryption |
|---|---|---|---|---|
| Lead PII | Forms, API, Imports | leads table | CRM, Webhooks | At rest (AES-256) |
| Ingestion | leads.email | CRM, Webhooks | At rest | |
| Phone | Ingestion | leads.phone | CRM, Webhooks | At rest |
| Name | Ingestion | leads.first_name, leads.last_name | CRM, Webhooks | At rest |
| Address | Ingestion | leads.address_* columns | CRM, Webhooks | At rest |
| UTM Data | Forms, Query Params | leads.utm_data (JSONB) | CRM, Webhooks | At rest |
| utm_source | Query string | JSONB field | CRM custom field | At rest |
| utm_medium | Query string | JSONB field | CRM custom field | At rest |
| utm_campaign | Query string | JSONB field | CRM custom field | At rest |
| Education Data | Forms, Custom Fields | leads.custom_fields | CRM, Webhooks | At rest |
| Program interest | Form field | JSONB field | CRM custom field | At rest |
| Start date | Form field | JSONB field | CRM custom field | At rest |
| Campus | Form field | JSONB field | CRM custom field | At rest |
| CRM Credentials | OAuth flow | crm_connections | CRM API only | AES-256 encrypted |
| Access tokens | OAuth | Encrypted column | Bearer auth | AES-256 |
| Refresh tokens | OAuth | Encrypted column | Token refresh | AES-256 |
| Client secrets | Config | Encrypted column | OAuth flow | AES-256 |
| Webhook Secrets | Configuration | webhook_configs | HMAC signing | SHA-256 hashed |
| User Credentials | Registration | users | Auth only | bcrypt hashed |
| API Keys | Generation | api_keys | Never transmitted | SHA-256 hashed |
External Integrations
Summary of all external services Ledly integrates with:
CRM Integrations
| Integration | Protocol | Authentication | Data Sent |
|---|---|---|---|
| HubSpot | HTTPS REST API | OAuth 2.0 | Leads as Contacts |
| Microsoft Dynamics 365 | HTTPS OData 4.0 | Azure AD OAuth 2.0 | Leads |
| Salesforce | HTTPS REST API | OAuth 2.0 | Leads |
Email Services
| Integration | Protocol | Authentication | Data Sent |
|---|---|---|---|
| Resend | HTTPS REST API | API Key | Transactional emails (password reset, notifications) |
Customer Webhooks
| Integration | Protocol | Authentication | Data Sent |
|---|---|---|---|
| Custom Endpoints | HTTPS POST | HMAC-SHA256 signature | Lead events (JSON payload) |
| Zapier | HTTPS POST | Signature | Lead events |
| Make (Integromat) | HTTPS POST | Signature | Lead events |
Infrastructure
| Service | Purpose | Data Processed |
|---|---|---|
| AWS RDS | PostgreSQL database | All persistent data |
| AWS S3 | File storage | Exports, imports |
| Vercel | Application hosting | Request/response data |
All external integrations use HTTPS with TLS 1.2 or higher. No sensitive data is sent to analytics or third-party tracking services.
Data Retention
Ledly retains data according to the following policies:
Default Retention Periods
| Data Type | Retention Period | Deletion Method |
|---|---|---|
| Lead data | Until deleted by customer | Hard delete |
| Webhook delivery logs | 30 days | Automatic purge |
| API request logs | 90 days | Automatic purge |
| Audit logs | 90 days | Automatic purge |
| User sessions | 24 hours (JWT expiry) | Automatic expiry |
| Soft-deleted leads | 30 days | Permanent deletion |
Data Deletion Procedures
Lead Deletion:
- Lead marked as
deleted_at(soft delete) - Lead excluded from all queries and exports
- After 30 days, permanently removed from database
- CRM record updated or deleted (configurable)
Account Deletion:
- All leads hard deleted
- All API keys revoked
- CRM connections disconnected
- Webhook endpoints removed
- User accounts deleted
- Organization record anonymized and retained for billing history
Right to Erasure (GDPR/CCPA):
- Submit deletion request via Settings or [email protected]
- Verification of request ownership
- 72-hour processing window
- Confirmation email sent upon completion
- Data deleted from all backups within 30 days
Backup Retention
| Backup Type | Retention | Encryption |
|---|---|---|
| Daily snapshots | 7 days | AES-256 |
| Weekly snapshots | 4 weeks | AES-256 |
| Monthly snapshots | 12 months | AES-256 |
Data deleted from the primary database may persist in backups for up to 30 days. For GDPR erasure requests, backup data is purged within this window.
Security Controls Summary
| Layer | Control | Implementation |
|---|---|---|
| Transport | Encryption in transit | TLS 1.3 (minimum TLS 1.2) |
| Storage | Encryption at rest | AES-256 (AWS RDS) |
| Application | Authentication | JWT tokens, API keys |
| Application | Authorization | Role-based access control |
| Database | Tenant isolation | Row-level security policies |
| API | Rate limiting | 100 requests/10 seconds default |
| Webhooks | Request integrity | HMAC-SHA256 signatures |
| Webhooks | Replay protection | 5-minute timestamp validation |
| Credentials | Password storage | bcrypt with salt |
| Credentials | API key storage | SHA-256 hash |
| Credentials | OAuth tokens | AES-256 encryption |
Related Documentation
| Resource | Link |
|---|---|
| API Authentication | /api-reference/authentication |
| Webhook Configuration | /integrations/webhooks |
| HubSpot Integration | /integrations/hubspot |
| Dynamics 365 Integration | /integrations/dynamics |
Questions?
For security inquiries or to request a SOC 2 Type II report, contact:
- Security Team: [email protected]
- General Support: [email protected]