Class: Vortex::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/vortex/client.rb

Overview

Vortex API client for Ruby

Provides the same functionality as other Vortex SDKs with JWT generation, invitation management, and full API compatibility.

Constant Summary collapse

DEFAULT_BASE_URL =

Base URL for Vortex API

'https://api.vortexsoftware.com'

Instance Method Summary collapse

Constructor Details

#initialize(api_key, base_url: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • api_key (String)

    Your Vortex API key

  • base_url (String) (defaults to: nil)

    Custom base URL (optional)



20
21
22
23
24
# File 'lib/vortex/client.rb', line 20

def initialize(api_key, base_url: nil)
  @api_key = api_key
  @base_url = base_url || DEFAULT_BASE_URL
  @connection = build_connection
end

Instance Method Details

#accept_invitation(invitation_id, user) ⇒ Hash

Accept a single invitation (recommended method)

This is the recommended method for accepting invitations.

Examples:

user = { email: '[email protected]', name: 'John Doe' }
result = client.accept_invitation('inv-123', user)

Parameters:

  • invitation_id (String)

    Single invitation ID to accept

  • user (Hash)

    User hash with :email and/or :phone

Returns:

  • (Hash)

    The accepted invitation result

Raises:



268
269
270
# File 'lib/vortex/client.rb', line 268

def accept_invitation(invitation_id, user)
  accept_invitations([invitation_id], user)
end

#accept_invitations(invitation_ids, user_or_target) ⇒ Hash

Accept invitations using the new User format (preferred)

Supports three formats:

  1. User hash (preferred): { email: ‘…’, phone: ‘…’, name: ‘…’ }

  2. Target hash (deprecated): { type: ‘email’, value: ‘…’ }

  3. Array of targets (deprecated): [{ type: ‘email’, value: ‘…’ }, …]

Examples:

New format (preferred)

user = { email: '[email protected]', name: 'John Doe' }
result = client.accept_invitations(['inv-123'], user)

Legacy format (deprecated)

target = { type: 'email', value: '[email protected]' }
result = client.accept_invitations(['inv-123'], target)

Parameters:

  • invitation_ids (Array<String>)

    List of invitation IDs to accept

  • user_or_target (Hash, Array)

    User hash with :email/:phone/:name keys, OR legacy target(s)

Returns:

  • (Hash)

    The accepted invitation result

Raises:



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/vortex/client.rb', line 183

def accept_invitations(invitation_ids, user_or_target)
  # Check if it's an array of targets (legacy format with multiple targets)
  if user_or_target.is_a?(Array)
    warn '[Vortex SDK] DEPRECATED: Passing an array of targets is deprecated. ' \
         'Use the User format instead: accept_invitations(invitation_ids, { email: "[email protected]" })'

    raise VortexError, 'No targets provided' if user_or_target.empty?

    last_result = nil
    last_exception = nil

    user_or_target.each do |target|
      begin
        last_result = accept_invitations(invitation_ids, target)
      rescue => e
        last_exception = e
      end
    end

    raise last_exception if last_exception

    return last_result || {}
  end

  # Check if it's a legacy target format (has :type and :value keys)
  is_legacy_target = user_or_target.key?(:type) && user_or_target.key?(:value)

  if is_legacy_target
    warn '[Vortex SDK] DEPRECATED: Passing a target hash is deprecated. ' \
         'Use the User format instead: accept_invitations(invitation_ids, { email: "[email protected]" })'

    # Convert target to User format
    target_type = user_or_target[:type]
    target_value = user_or_target[:value]

    user = {}
    case target_type
    when 'email'
      user[:email] = target_value
    when 'phone', 'phoneNumber'
      user[:phone] = target_value
    else
      # For other types, try to use as email
      user[:email] = target_value
    end

    # Recursively call with User format
    return accept_invitations(invitation_ids, user)
  end

  # New User format
  user = user_or_target

  # Validate that either email or phone is provided
  raise VortexError, 'User must have either email or phone' if user[:email].nil? && user[:phone].nil?

  body = {
    invitationIds: invitation_ids,
    user: user.compact # Remove nil values
  }

  response = @connection.post('/api/v1/invitations/accept') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to accept invitations: #{e.message}"
end

#configure_autojoin(scope, scope_type, domains, widget_id, scope_name = nil, metadata = nil) ⇒ Hash

Configure autojoin domains for a specific scope

This endpoint syncs autojoin domains - it will add new domains, remove domains not in the provided list, and deactivate the autojoin invitation if all domains are removed (empty array).

Examples:

result = client.configure_autojoin(
  'acme-org',
  'organization',
  ['acme.com', 'acme.org'],
  'widget-123',
  'Acme Corporation'
)

Parameters:

  • scope (String)

    The scope identifier (customer’s group ID)

  • scope_type (String)

    The type of scope (e.g., “organization”, “team”)

  • domains (Array<String>)

    Array of domains to configure for autojoin

  • widget_id (String)

    The widget configuration ID

  • scope_name (String, nil) (defaults to: nil)

    Optional display name for the scope

  • metadata (Hash, nil) (defaults to: nil)

    Optional metadata to attach to the invitation

Returns:

  • (Hash)

    Response with :autojoin_domains array and :invitation

Raises:



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/vortex/client.rb', line 454

def configure_autojoin(scope, scope_type, domains, widget_id, scope_name = nil,  = nil)
  raise VortexError, 'scope is required' if scope.nil? || scope.empty?
  raise VortexError, 'scope_type is required' if scope_type.nil? || scope_type.empty?
  raise VortexError, 'widget_id is required' if widget_id.nil? || widget_id.empty?
  raise VortexError, 'domains must be an array' unless domains.is_a?(Array)

  body = {
    scope: scope,
    scopeType: scope_type,
    domains: domains,
    widgetId: widget_id
  }

  body[:scopeName] = scope_name if scope_name
  body[:metadata] =  if 

  response = @connection.post('/api/v1/invitations/autojoin') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to configure autojoin: #{e.message}"
end

#create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil, metadata = nil, unfurl_config = nil) ⇒ Hash

Create an invitation from your backend

This method allows you to create invitations programmatically using your API key, without requiring a user JWT token. Useful for server-side invitation creation, such as “People You May Know” flows or admin-initiated invitations.

Target types:

  • ‘email’: Send an email invitation

  • ‘phone’: Create a phone invitation (short link returned for you to send)

  • ‘internal’: Create an internal invitation for PYMK flows (no email sent)

Examples:

Create an email invitation with custom link preview

result = client.create_invitation(
  'widget-config-123',
  { type: 'email', value: '[email protected]' },
  { user_id: 'user-456', user_email: '[email protected]', name: 'John Doe' },
  [{ type: 'team', group_id: 'team-789', name: 'Engineering' }],
  nil,
  nil,
  nil,
  { title: 'Join the team!', description: 'You have been invited', image: 'https://example.com/og.png' }
)

Create an internal invitation (PYMK flow - no email sent)

result = client.create_invitation(
  'widget-config-123',
  { type: 'internal', value: 'internal-user-abc' },
  { user_id: 'user-456' },
  nil,
  'pymk'
)

Parameters:

  • widget_configuration_id (String)

    The widget configuration ID to use

  • target (Hash)

    The invitation target: { type: ‘email|sms|internal’, value: ‘…’ }

  • inviter (Hash)

    The inviter info: { user_id: ‘…’, user_email: ‘…’, name: ‘…’ }

  • groups (Array<Hash>, nil) (defaults to: nil)

    Optional groups: [{ type: ‘…’, group_id: ‘…’, name: ‘…’ }]

  • source (String, nil) (defaults to: nil)

    Optional source for analytics (defaults to ‘api’)

  • subtype (String, nil) (defaults to: nil)

    Optional subtype for analytics segmentation (e.g., ‘pymk’, ‘find-friends’)

  • template_variables (Hash, nil) (defaults to: nil)

    Optional template variables for email customization

  • metadata (Hash, nil) (defaults to: nil)

    Optional metadata passed through to webhooks

  • unfurl_config (Hash, nil) (defaults to: nil)

    Optional link unfurl (Open Graph) config: { title: ‘…’, description: ‘…’, image: ‘…’, type: ‘…’, site_name: ‘…’ }

Returns:

  • (Hash)

    Created invitation with :id, :short_link, :status, :created_at

Raises:



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/vortex/client.rb', line 354

def create_invitation(widget_configuration_id, target, inviter, groups = nil, source = nil, subtype = nil, template_variables = nil,  = nil, unfurl_config = nil)
  raise VortexError, 'widget_configuration_id is required' if widget_configuration_id.nil? || widget_configuration_id.empty?
  raise VortexError, 'target must have type and value' if target[:type].nil? || target[:value].nil?
  raise VortexError, 'inviter must have user_id' if inviter[:user_id].nil?

  # Build request body with camelCase keys for the API
  body = {
    widgetConfigurationId: widget_configuration_id,
    target: target,
    inviter: {
      userId: inviter[:user_id],
      userEmail: inviter[:user_email],
      userName: inviter[:user_name],
      userAvatarUrl: inviter[:user_avatar_url]
    }.compact
  }

  if groups && !groups.empty?
    body[:groups] = groups.map do |g|
      {
        type: g[:type],
        groupId: g[:group_id],
        name: g[:name]
      }
    end
  end

  body[:source] = source if source
  body[:subtype] = subtype if subtype
  body[:templateVariables] = template_variables if template_variables
  body[:metadata] =  if 
  if unfurl_config
    body[:unfurlConfig] = {
      title: unfurl_config[:title],
      description: unfurl_config[:description],
      image: unfurl_config[:image],
      type: unfurl_config[:type],
      siteName: unfurl_config[:site_name]
    }.compact
  end

  response = @connection.post('/api/v1/invitations') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to create invitation: #{e.message}"
end

#delete_invitations_by_group(group_type, group_id) ⇒ Hash

Delete invitations by group

Parameters:

  • group_type (String)

    The group type

  • group_id (String)

    The group ID

Returns:

  • (Hash)

    Success response

Raises:



292
293
294
295
296
297
# File 'lib/vortex/client.rb', line 292

def delete_invitations_by_group(group_type, group_id)
  response = @connection.delete("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to delete group invitations: #{e.message}"
end

#generate_jwt(params) ⇒ String

Generate a JWT token for a user

Examples:

Simple usage

client = Vortex::Client.new(ENV['VORTEX_API_KEY'])
jwt = client.generate_jwt({
  user: {
    id: 'user-123',
    email: '[email protected]',
    admin_scopes: ['autojoin']
  }
})

With additional attributes

jwt = client.generate_jwt({
  user: { id: 'user-123', email: '[email protected]' },
  attributes: { role: 'admin', department: 'Engineering' }
})

Parameters:

  • params (Hash)

    JWT parameters with :user (required) and optional :attributes

Returns:

  • (String)

    JWT token

Raises:

  • (VortexError)

    If API key is invalid or JWT generation fails



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/vortex/client.rb', line 47

def generate_jwt(params)
  user = params[:user]
  attributes = params[:attributes]

  # Parse API key - same format as Node.js SDK
  prefix, encoded_id, key = @api_key.split('.')

  raise VortexError, 'Invalid API key format' unless prefix && encoded_id && key
  raise VortexError, 'Invalid API key prefix' unless prefix == 'VRTX'

  # Decode the ID from base64url (same as Node.js Buffer.from(encodedId, 'base64url'))
  decoded_bytes = Base64.urlsafe_decode64(encoded_id)

  # Convert to UUID string format (same as uuidStringify in Node.js)
  id = format_uuid(decoded_bytes)

  expires = Time.now.to_i + 3600

  # Step 1: Derive signing key from API key + ID (same as Node.js)
  signing_key = OpenSSL::HMAC.digest('sha256', key, id)

  # Step 2: Build header + payload
  header = {
    iat: Time.now.to_i,
    alg: 'HS256',
    typ: 'JWT',
    kid: id
  }

  # Build payload - start with required fields
  payload = {
    userId: user[:id],
    userEmail: user[:email],
    expires: expires
  }

  # Add name if present (convert snake_case to camelCase for JWT)
  if user[:user_name]
    payload[:userName] = user[:user_name]
  end

  # Add userAvatarUrl if present (convert snake_case to camelCase for JWT)
  if user[:user_avatar_url]
    payload[:userAvatarUrl] = user[:user_avatar_url]
  end

  # Add adminScopes if present
  if user[:admin_scopes]
    payload[:adminScopes] = user[:admin_scopes]
  end

  # Add allowedEmailDomains if present (for domain-restricted invitations)
  if user[:allowed_email_domains] && !user[:allowed_email_domains].empty?
    payload[:allowedEmailDomains] = user[:allowed_email_domains]
  end

  # Add any additional properties from attributes
  if attributes && !attributes.empty?
    payload.merge!(attributes)
  end

  # Step 3: Base64URL encode (same as Node.js)
  header_b64 = base64url_encode(JSON.generate(header))
  payload_b64 = base64url_encode(JSON.generate(payload))

  # Step 4: Sign with HMAC-SHA256 (same as Node.js)
  signature = OpenSSL::HMAC.digest('sha256', signing_key, "#{header_b64}.#{payload_b64}")
  signature_b64 = base64url_encode(signature)

  "#{header_b64}.#{payload_b64}.#{signature_b64}"
rescue => e
  raise VortexError, "JWT generation failed: #{e.message}"
end

#get_autojoin_domains(scope_type, scope) ⇒ Hash

Get autojoin domains configured for a specific scope

Examples:

result = client.get_autojoin_domains('organization', 'acme-org')
result['autojoinDomains'].each do |domain|
  puts "Domain: #{domain['domain']}"
end

Parameters:

  • scope_type (String)

    The type of scope (e.g., “organization”, “team”, “project”)

  • scope (String)

    The scope identifier (customer’s group ID)

Returns:

  • (Hash)

    Response with :autojoin_domains array and :invitation

Raises:



419
420
421
422
423
424
425
426
427
428
429
# File 'lib/vortex/client.rb', line 419

def get_autojoin_domains(scope_type, scope)
  encoded_scope_type = URI.encode_www_form_component(scope_type)
  encoded_scope = URI.encode_www_form_component(scope)

  response = @connection.get("/api/v1/invitations/by-scope/#{encoded_scope_type}/#{encoded_scope}/autojoin")
  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to get autojoin domains: #{e.message}"
end

#get_invitation(invitation_id) ⇒ Hash

Get a specific invitation by ID

Parameters:

  • invitation_id (String)

    The invitation ID

Returns:

  • (Hash)

    The invitation data

Raises:



145
146
147
148
149
150
# File 'lib/vortex/client.rb', line 145

def get_invitation(invitation_id)
  response = @connection.get("/api/v1/invitations/#{invitation_id}")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to get invitation: #{e.message}"
end

#get_invitations_by_group(group_type, group_id) ⇒ Array<Hash>

Get invitations by group

Parameters:

  • group_type (String)

    The group type

  • group_id (String)

    The group ID

Returns:

  • (Array<Hash>)

    List of invitations for the group

Raises:



278
279
280
281
282
283
284
# File 'lib/vortex/client.rb', line 278

def get_invitations_by_group(group_type, group_id)
  response = @connection.get("/api/v1/invitations/by-group/#{group_type}/#{group_id}")
  result = handle_response(response)
  result['invitations'] || []
rescue => e
  raise VortexError, "Failed to get group invitations: #{e.message}"
end

#get_invitations_by_target(target_type, target_value) ⇒ Array<Hash>

Get invitations by target

Parameters:

  • target_type (String)

    Type of target (email, sms)

  • target_value (String)

    Value of target (email address, phone number)

Returns:

  • (Array<Hash>)

    List of invitations

Raises:



129
130
131
132
133
134
135
136
137
138
# File 'lib/vortex/client.rb', line 129

def get_invitations_by_target(target_type, target_value)
  response = @connection.get('/api/v1/invitations') do |req|
    req.params['targetType'] = target_type
    req.params['targetValue'] = target_value
  end

  handle_response(response)['invitations'] || []
rescue => e
  raise VortexError, "Failed to get invitations by target: #{e.message}"
end

#reinvite(invitation_id) ⇒ Hash

Reinvite a user

Parameters:

  • invitation_id (String)

    The invitation ID to reinvite

Returns:

  • (Hash)

    The reinvited invitation result

Raises:



304
305
306
307
308
309
# File 'lib/vortex/client.rb', line 304

def reinvite(invitation_id)
  response = @connection.post("/api/v1/invitations/#{invitation_id}/reinvite")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to reinvite: #{e.message}"
end

#revoke_invitation(invitation_id) ⇒ Hash

Revoke (delete) an invitation

Parameters:

  • invitation_id (String)

    The invitation ID to revoke

Returns:

  • (Hash)

    Success response

Raises:



157
158
159
160
161
162
# File 'lib/vortex/client.rb', line 157

def revoke_invitation(invitation_id)
  response = @connection.delete("/api/v1/invitations/#{invitation_id}")
  handle_response(response)
rescue => e
  raise VortexError, "Failed to revoke invitation: #{e.message}"
end

#sync_internal_invitation(creator_id, target_value, action, component_id) ⇒ Hash

Sync an internal invitation action (accept or decline)

This method notifies Vortex that an internal invitation was accepted or declined within your application, so Vortex can update the invitation status accordingly.

Examples:

result = client.sync_internal_invitation(
  'user-123',
  'user-456',
  'accepted',
  'component-uuid-789'
)
puts "Processed #{result['processed']} invitations"

Parameters:

  • creator_id (String)

    The inviter’s user ID

  • target_value (String)

    The invitee’s user ID

  • action (String)

    The action taken: “accepted” or “declined”

  • component_id (String)

    The widget component UUID

Returns:

  • (Hash)

    Response with :processed count and :invitationIds array

Raises:



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/vortex/client.rb', line 502

def sync_internal_invitation(creator_id, target_value, action, component_id)
  body = {
    creatorId: creator_id,
    targetValue: target_value,
    action: action,
    componentId: component_id
  }

  response = @connection.post('/api/v1/invitations/sync-internal-invitation') do |req|
    req.headers['Content-Type'] = 'application/json'
    req.body = JSON.generate(body)
  end

  handle_response(response)
rescue VortexError
  raise
rescue => e
  raise VortexError, "Failed to sync internal invitation: #{e.message}"
end