-            
+            
               
                 <%= t('submission_example_payload') %>
               
               <% if @webhook_url.url.present? && @webhook_url.events.include?('form.completed') %>
-                <%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
+                <%= button_to button_title(title: 'Test Webhook', disabled_with: t('sending'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhook_resend_path(@webhook_url), class: 'btn btn-neutral btn-outline btn-sm', method: :post %>
               <% end %>
             
           
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index e9220590..12b896aa 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -511,13 +511,17 @@ en: &en
   webhook_secret_has_been_saved: Webhook Secret has been saved.
   webhook_url_has_been_saved: Webhook URL has been saved.
   webhook_request_has_been_sent: Webhook request has been sent.
+  webhook_url_has_been_updated: Webhook URL has been updated.
+  webhook_url_has_been_deleted: Webhook URL has been deleted.
+  unable_to_resend_webhook_request: Unable to resend webhook request.
+  new_webhook: New Webhook
+  delete_webhook: Delete webhook
   count_submissions_have_been_created: '%{count} submissions have been created.'
   gmail_has_been_connected: Gmail has been connected
   microsoft_account_has_been_connected: Microsoft Account has been connected
   sms_length_cant_be_longer_than_120_bytes: SMS length can't be longer than 120 bytes
   connected_successfully: Connected successfully.
   user_nameid_not_found: 'User %{nameid} not found.'
-  webhook_request_has_been_sent: Webhook request has been sent.
   sso_settings_have_been_updated: SSO settings have been updated.
   sms_has_been_sent: SMS has been sent.
   account_has_been_created: Account has been created.
@@ -1156,13 +1160,17 @@ es: &es
   webhook_secret_has_been_saved: El secreto del Webhook ha sido guardado.
   webhook_url_has_been_saved: La URL del Webhook ha sido guardada.
   webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
+  webhook_url_has_been_updated: La URL del Webhook ha sido actualizada.
+  webhook_url_has_been_deleted: La URL del Webhook ha sido eliminada.
+  unable_to_resend_webhook_request: No se pudo reenviar la solicitud del webhook.
+  new_webhook: Nuevo Webhook
+  delete_webhook: Eliminar webhook
   count_submissions_have_been_created: '%{count} envíos han sido creados.'
   gmail_has_been_connected: Gmail ha sido conectado.
   microsoft_account_has_been_connected: La cuenta de Microsoft ha sido conectada.
   sms_length_cant_be_longer_than_120_bytes: La longitud del SMS no puede ser mayor a 120 bytes.
   connected_successfully: Conectado con éxito.
   user_nameid_not_found: 'Usuario %{nameid} no encontrado.'
-  webhook_request_has_been_sent: La solicitud del Webhook ha sido enviada.
   sso_settings_have_been_updated: La configuración de SSO ha sido actualizada.
   sms_has_been_sent: El SMS ha sido enviado.
   account_has_been_created: La cuenta ha sido creada.
@@ -1801,13 +1809,17 @@ it: &it
   webhook_secret_has_been_saved: Il segreto del Webhook è stato salvato.
   webhook_url_has_been_saved: "L'URL del Webhook è stato salvato."
   webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
+  webhook_url_has_been_updated: "L'URL del Webhook è stata aggiornata."
+  webhook_url_has_been_deleted: "L'URL del Webhook è stata eliminata."
+  unable_to_resend_webhook_request: Impossibile reinviare la richiesta del webhook.
+  new_webhook: Nuovo Webhook
+  delete_webhook: Elimina webhook
   count_submissions_have_been_created: '%{count} invii sono stati creati.'
   gmail_has_been_connected: Gmail è stato connesso.
   microsoft_account_has_been_connected: "L'account Microsoft è stato connesso."
   sms_length_cant_be_longer_than_120_bytes: "La lunghezza dell'SMS non può superare i 120 byte."
   connected_successfully: Collegamento avvenuto con successo.
   user_nameid_not_found: 'Utente %{nameid} non trovato.'
-  webhook_request_has_been_sent: La richiesta del Webhook è stata inviata.
   sso_settings_have_been_updated: Le impostazioni SSO sono state aggiornate.
   sms_has_been_sent: "L'SMS è stato inviato."
   account_has_been_created: "L'account è stato creato."
@@ -2447,13 +2459,17 @@ fr: &fr
   webhook_secret_has_been_saved: Le secret du Webhook a été enregistré.
   webhook_url_has_been_saved: "L'URL du Webhook a été enregistrée."
   webhook_request_has_been_sent: La demande du Webhook a été envoyée.
+  webhook_url_has_been_updated: "L'URL du Webhook a été mise à jour."
+  webhook_url_has_been_deleted: "L'URL du Webhook a été supprimée."
+  unable_to_resend_webhook_request: Impossible de renvoyer la requête du webhook.
+  new_webhook: Nouveau Webhook
+  delete_webhook: Supprimer le webhook
   count_submissions_have_been_created: '%{count} soumissions ont été créées.'
   gmail_has_been_connected: Gmail a été connecté.
   microsoft_account_has_been_connected: Le compte Microsoft a été connecté.
   sms_length_cant_be_longer_than_120_bytes: La longueur du SMS ne peut pas dépasser 120 octets.
   connected_successfully: Connecté avec succès.
   user_nameid_not_found: 'Utilisateur %{nameid} introuvable.'
-  webhook_request_has_been_sent: La demande du Webhook a été envoyée.
   sso_settings_have_been_updated: Les paramètres de SSO ont été mis à jour.
   sms_has_been_sent: Le SMS a été envoyé.
   account_has_been_created: Le compte a été créé.
@@ -3092,13 +3108,17 @@ pt: &pt
   webhook_secret_has_been_saved: O segredo do Webhook foi salvo.
   webhook_url_has_been_saved: A URL do Webhook foi salva.
   webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
+  webhook_url_has_been_updated: URL do Webhook foi atualizada.
+  webhook_url_has_been_deleted: URL do Webhook foi excluída.
+  unable_to_resend_webhook_request: Não foi possível reenviar a solicitação do webhook.
+  new_webhook: Novo Webhook
+  delete_webhook: Excluir webhook
   count_submissions_have_been_created: '%{count} submissões foram criadas.'
   gmail_has_been_connected: O Gmail foi conectado
   microsoft_account_has_been_connected: A conta da Microsoft foi conectada
   sms_length_cant_be_longer_than_120_bytes: O comprimento do SMS não pode ultrapassar 120 bytes
   connected_successfully: Conectado com sucesso.
   user_nameid_not_found: 'Usuário %{nameid} não encontrado.'
-  webhook_request_has_been_sent: A solicitação do Webhook foi enviada.
   sso_settings_have_been_updated: As configurações de SSO foram atualizadas.
   sms_has_been_sent: O SMS foi enviado.
   account_has_been_created: A conta foi criada.
@@ -3699,8 +3719,8 @@ de: &de
   api_key: API-Schlüssel
   logo: Logo
   back: Zurück
-  add_secret: Geheimnis hinzufügen
-  edit_secret: Geheimnis bearbeiten
+  add_secret: Geheimnis hinzuf
+  edit_secret: Geheimnis bearb
   submission_example_payload: Beispiel-Payload für Einreichung
   there_are_no_signatures: Es gibt keine Unterschriften
   signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
@@ -3737,13 +3757,17 @@ de: &de
   webhook_secret_has_been_saved: Das Webhook-Geheimnis wurde gespeichert.
   webhook_url_has_been_saved: Die Webhook-URL wurde gespeichert.
   webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
+  webhook_url_has_been_updated: Webhook-URL wurde aktualisiert.
+  webhook_url_has_been_deleted: Webhook-URL wurde gelöscht.
+  unable_to_resend_webhook_request: Webhook-Anfrage konnte nicht erneut gesendet werden.
+  new_webhook: Neuer Webhook
+  delete_webhook: Webhook löschen
   count_submissions_have_been_created: '%{count} Einreichungen wurden erstellt.'
   gmail_has_been_connected: Gmail wurde verbunden.
   microsoft_account_has_been_connected: Microsoft-Konto wurde verbunden.
   sms_length_cant_be_longer_than_120_bytes: Die SMS-Länge darf 120 Bytes nicht überschreiten.
   connected_successfully: Erfolgreich verbunden.
   user_nameid_not_found: 'Benutzer %{nameid} nicht gefunden.'
-  webhook_request_has_been_sent: Die Webhook-Anfrage wurde gesendet.
   sso_settings_have_been_updated: Die SSO-Einstellungen wurden aktualisiert.
   sms_has_been_sent: Die SMS wurde gesendet.
   account_has_been_created: Das Konto wurde erstellt.
diff --git a/config/routes.rb b/config/routes.rb
index a2d5af4b..140503ff 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -168,7 +168,9 @@ Rails.application.routes.draw do
                                   defaults: { status: :integration }
     resource :personalization, only: %i[show create], controller: 'personalization_settings'
     resources :api, only: %i[index create], controller: 'api_settings'
-    resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
+    resources :webhooks, only: %i[index show new create update destroy], controller: 'webhook_settings' do
+      post :resend
+    end
     resource :account, only: %i[show update destroy]
     resources :profile, only: %i[index] do
       collection do
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index d42fa2f4..d552111d 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -68,7 +68,7 @@ module Submissions
                                name: item['name'])
         end
 
-      return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
+      return ApplicationRecord.no_touching { result_attachments.map { |e| e.tap(&:save!) } } if image_pdfs.size < 2
 
       images_pdf =
         image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
@@ -87,7 +87,9 @@ module Submissions
           name: template.name
         )
 
-      (result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
+      ApplicationRecord.no_touching do
+        (result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
+      end
     end
 
     def generate_pdfs(submitter)
@@ -177,6 +179,8 @@ module Submissions
           font_size   = preferences_font_size
           font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
 
+          fill_color = field.dig('preferences', 'color').presence
+
           font = pdf.fonts.add(field.dig('preferences', 'font').presence || FONT_NAME)
 
           value = submitter.values[field['uuid']]
@@ -378,6 +382,7 @@ module Submissions
               next if char.blank?
 
               text = HexaPDF::Layout::TextFragment.create(char, font:,
+                                                                fill_color:,
                                                                 font_size:)
 
               line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -385,6 +390,7 @@ module Submissions
               if preferences_font_size.blank? && line_height > (area['h'] * height)
                 text = HexaPDF::Layout::TextFragment.create(char,
                                                             font:,
+                                                            fill_color:,
                                                             font_size: (font_size / 1.4).to_i)
 
                 line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -393,6 +399,7 @@ module Submissions
               if preferences_font_size.blank? && line_height > (area['h'] * height)
                 text = HexaPDF::Layout::TextFragment.create(char,
                                                             font:,
+                                                            fill_color:,
                                                             font_size: (font_size / 1.9).to_i)
 
                 line_height = layouter.fit([text], cell_width, height).lines.first.height
@@ -412,6 +419,7 @@ module Submissions
             value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
 
             text = HexaPDF::Layout::TextFragment.create(value, font:,
+                                                               fill_color:,
                                                                font_size:)
 
             lines = layouter.fit([text], area['w'] * width, height).lines
@@ -420,6 +428,7 @@ module Submissions
             if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
               text = HexaPDF::Layout::TextFragment.create(value,
                                                           font:,
+                                                          fill_color:,
                                                           font_size: (font_size / 1.4).to_i)
 
               lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
@@ -430,6 +439,7 @@ module Submissions
             if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
               text = HexaPDF::Layout::TextFragment.create(value,
                                                           font:,
+                                                          fill_color:,
                                                           font_size: (font_size / 1.9).to_i)
 
               lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
diff --git a/lib/templates/find_acro_fields.rb b/lib/templates/find_acro_fields.rb
index b6ae50ce..ac2d5c45 100644
--- a/lib/templates/find_acro_fields.rb
+++ b/lib/templates/find_acro_fields.rb
@@ -6,6 +6,20 @@ module Templates
 
     FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
     SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
+    SELECT_PLACEHOLDER_REGEXP = /\b(
+      Select      |
+      Choose      |
+      Wählen      |
+      Auswählen   |
+      Sélectionner|
+      Choisir     |
+      Seleccionar |
+      Elegir      |
+      Seleziona   |
+      Scegliere   |
+      Selecionar  |
+      Escolher
+    )\b/ix
 
     module_function
 
@@ -143,8 +157,8 @@ module Templates
         {
           **attrs,
           type: 'select',
-          options: build_options(field[:Opt]),
-          default_value: field.field_value
+          options: build_options(field[:Opt], 'select'),
+          default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value
         }
       elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
         {
@@ -178,11 +192,14 @@ module Templates
     def build_options(values, type = nil)
       is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
 
-      values.map do |option|
+      values.filter_map do |option|
         is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
 
+        option = option[1] if option.is_a?(Array) && option.size == 2
         option = option.encode('utf-8', invalid: :replace, undef: :replace, replace: '') if option.is_a?(String)
 
+        next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
+
         {
           uuid: SecureRandom.uuid,
           value: is_option_number || is_skip_single_value ? '' : option
diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb
index b840bd9a..d7f3d36f 100644
--- a/lib/templates/process_document.rb
+++ b/lib/templates/process_document.rb
@@ -122,6 +122,15 @@ module Templates
 
       io = StringIO.new
 
+      pdf.acro_form.each_field do |field|
+        next if field.field_type != :Ch ||
+                field[:Opt].blank? ||
+                %i[combo_box editable_combo_box].exclude?(field.concrete_field_type) ||
+                !field.field_value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
+
+        field[:V] = ''
+      end
+
       pdf.acro_form.create_appearances(force: true) if pdf.acro_form[:NeedAppearances]
       pdf.acro_form.flatten
 
diff --git a/lib/webhook_urls.rb b/lib/webhook_urls.rb
index 482e66a7..e57c802b 100644
--- a/lib/webhook_urls.rb
+++ b/lib/webhook_urls.rb
@@ -10,7 +10,7 @@ module WebhookUrls
 
     event_arel = events.map { |event| Arel::Table.new(:webhook_urls)[:events].matches("%\"#{event}\"%") }.reduce(:or)
 
-    if Docuseal.multitenant?
+    if Docuseal.multitenant? || account_id == 1
       rel.where(event_arel)
     else
       linked_account_rel =
diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb
index ceec4c00..c4a09da8 100644
--- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb
@@ -26,11 +26,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.completed',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormCompletedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.completed',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb
index 88a88906..c53a1999 100644
--- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb
@@ -26,11 +26,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.declined',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormDeclinedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.declined',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb
index 863ce826..929a2187 100644
--- a/spec/jobs/send_form_started_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb
@@ -26,11 +26,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.started',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormStartedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.started',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
index 495e6239..c3c7c216 100644
--- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb
@@ -26,11 +26,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.viewed',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -43,11 +43,11 @@ RSpec.describe SendFormViewedWebhookRequestJob do
       described_class.new.perform('submitter_id' => submitter.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'form.viewed',
-          'timestamp' => Time.current,
-          'data' => Submitters::SerializeForWebhook.call(submitter.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submitters::SerializeForWebhook.call(submitter.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
index 468430f7..05947f33 100644
--- a/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_archived_webhook_request_job_spec.rb
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.archived',
-          'timestamp' => Time.current,
-          'data' => submission.reload.as_json(only: %i[id archived_at])
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionArchivedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.archived',
-          'timestamp' => Time.current,
-          'data' => submission.reload.as_json(only: %i[id archived_at])
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(submission.reload.as_json(only: %i[id archived_at]).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
index 1101b12a..2cef72cc 100644
--- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.completed',
-          'timestamp' => Time.current,
-          'data' => Submissions::SerializeForApi.call(submission.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCompletedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.completed',
-          'timestamp' => Time.current,
-          'data' => Submissions::SerializeForApi.call(submission.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb
index e4004dc7..1b2e02fe 100644
--- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb
+++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb
@@ -23,11 +23,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.created',
-          'timestamp' => Time.current,
-          'data' => Submissions::SerializeForApi.call(submission.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -40,11 +40,11 @@ RSpec.describe SendSubmissionCreatedWebhookRequestJob do
       described_class.new.perform('submission_id' => submission.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'submission.created',
-          'timestamp' => Time.current,
-          'data' => Submissions::SerializeForApi.call(submission.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Submissions::SerializeForApi.call(submission.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb
index 532a1bea..14df0b3e 100644
--- a/spec/jobs/send_template_created_webhook_request_job_spec.rb
+++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'template.created',
-          'timestamp' => Time.current,
-          'data' => Templates::SerializeForApi.call(template.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateCreatedWebhookRequestJob do
       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'template.created',
-          'timestamp' => Time.current,
-          'data' => Templates::SerializeForApi.call(template.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb
index 1bef3eb2..4451b071 100644
--- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb
+++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb
@@ -22,11 +22,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'template.updated',
-          'timestamp' => Time.current,
-          'data' => Templates::SerializeForApi.call(template.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook'
@@ -39,11 +39,11 @@ RSpec.describe SendTemplateUpdatedWebhookRequestJob do
       described_class.new.perform('template_id' => template.id, 'webhook_url_id' => webhook_url.id)
 
       expect(WebMock).to have_requested(:post, webhook_url.url).with(
-        body: replace_timestamps({
+        body: {
           'event_type' => 'template.updated',
-          'timestamp' => Time.current,
-          'data' => Templates::SerializeForApi.call(template.reload)
-        }.deep_stringify_keys),
+          'timestamp' => /.*/,
+          'data' => JSON.parse(Templates::SerializeForApi.call(template.reload).to_json)
+        },
         headers: {
           'Content-Type' => 'application/json',
           'User-Agent' => 'DocuSeal.com Webhook',
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 94f071be..86a15a9f 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -67,19 +67,3 @@ RSpec.configure do |config|
     Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline
   end
 end
-
-def replace_timestamps(data, replace = /.*/)
-  timestamp_fields = %w[created_at updated_at completed_at sent_at opened_at timestamp]
-
-  data.each do |key, value|
-    if timestamp_fields.include?(key) && (value.is_a?(String) || value.is_a?(Time))
-      data[key] = replace
-    elsif value.is_a?(Hash)
-      replace_timestamps(value)
-    elsif value.is_a?(Array)
-      value.each { |item| replace_timestamps(item) if item.is_a?(Hash) }
-    end
-  end
-
-  data
-end
diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb
index 859b654d..0e36ef94 100644
--- a/spec/system/template_spec.rb
+++ b/spec/system/template_spec.rb
@@ -44,7 +44,9 @@ RSpec.describe 'Template' do
 
     it 'archives a template' do
       expect do
-        click_button 'Archive'
+        accept_confirm('Are you sure?') do
+          click_button 'Archive'
+        end
       end.to change { Template.active.count }.by(-1)
 
       expect(page).to have_content('Template has been archived')
diff --git a/spec/system/webhook_settings_spec.rb b/spec/system/webhook_settings_spec.rb
index a12b98f8..fbbcaea2 100644
--- a/spec/system/webhook_settings_spec.rb
+++ b/spec/system/webhook_settings_spec.rb
@@ -10,22 +10,58 @@ RSpec.describe 'Webhook Settings' do
     sign_in(user)
   end
 
-  it 'shows webhook settings page' do
+  it 'shows webhook settings page with empty form when there are no webhooks' do
+    visit settings_webhooks_path
+
+    expect(page).to have_content('Webhook')
+    expect(page).to have_content('Webhook URL')
+    expect(page).to have_field('webhook_url[url]', type: 'url')
+    expect(page).to have_button('Save')
+
+    WebhookUrl::EVENTS.each do |event|
+      expect(page).to have_field(event, type: 'checkbox')
+    end
+  end
+
+  it 'shows list of webhooks when there are more than one' do
+    webhook_urls = create_list(:webhook_url, 2, account:)
+
     visit settings_webhooks_path
 
     expect(page).to have_content('Webhooks')
-    expect(page).to have_field('Webhook URL')
+    expect(page).to have_link('New Webhook')
+
+    webhook_urls.each do |webhook_url|
+      expect(page).to have_content(webhook_url.url)
+
+      within("a[href='#{settings_webhook_path(webhook_url)}']") do
+        webhook_url.events.each do |event|
+          expect(page).to have_content(event)
+        end
+      end
+    end
+  end
+
+  it 'shows webhook settings page with pre-filled form when there is one webhook' do
+    webhook_url = create(:webhook_url, account:)
+
+    visit settings_webhooks_path
+
+    expect(page).to have_content('Webhook')
+    expect(page).to have_field('webhook_url[url]', type: 'url', with: webhook_url.url)
     expect(page).to have_button('Save')
+    expect(page).to have_button('Delete')
+    expect(page).to have_link('Add Secret')
 
     WebhookUrl::EVENTS.each do |event|
-      expect(page).to have_field(event, type: 'checkbox', disabled: true)
+      expect(page).to have_field(event, type: 'checkbox', checked: webhook_url.events.include?(event))
     end
   end
 
   it 'creates the webhook' do
     visit settings_webhooks_path
 
-    fill_in 'Webhook URL', with: 'https://example.com/webhook'
+    fill_in 'webhook_url[url]', with: 'https://example.com/webhook'
 
     expect do
       click_button 'Save'
@@ -34,6 +70,8 @@ RSpec.describe 'Webhook Settings' do
     webhook_url = account.webhook_urls.first
 
     expect(webhook_url.url).to eq('https://example.com/webhook')
+    expect(page).to have_content('Webhook URL has been saved.')
+    expect(page.current_path).to eq(settings_webhooks_path)
   end
 
   it 'updates the webhook' do
@@ -41,12 +79,14 @@ RSpec.describe 'Webhook Settings' do
 
     visit settings_webhooks_path
 
-    fill_in 'Webhook URL', with: 'https://example.org/webhook'
+    fill_in 'webhook_url[url]', with: 'https://example.org/webhook'
     click_button 'Save'
 
     webhook_url.reload
 
     expect(webhook_url.url).to eq('https://example.org/webhook')
+    expect(page).to have_content('Webhook URL has been updated.')
+    expect(page.current_path).to eq(settings_webhooks_path)
   end
 
   it 'deletes the webhook' do
@@ -54,11 +94,14 @@ RSpec.describe 'Webhook Settings' do
 
     visit settings_webhooks_path
 
-    fill_in 'Webhook URL', with: ''
-
     expect do
-      click_button 'Save'
+      accept_confirm('Are you sure?') do
+        click_button 'Delete'
+      end
     end.to change(WebhookUrl, :count).by(-1)
+
+    expect(page).to have_content('Webhook URL has been deleted.')
+    expect(page.current_path).to eq(settings_webhooks_path)
   end
 
   it 'updates the webhook events' do
@@ -94,6 +137,9 @@ RSpec.describe 'Webhook Settings' do
 
       expect(webhook_url.secret).to eq({ 'X-Signature' => 'secret-value' })
     end
+
+    expect(page).to have_link('Edit Secret')
+    expect(page).to have_content('Webhook Secret has been saved.')
   end
 
   it 'removes a secret from the webhook' do
@@ -113,5 +159,31 @@ RSpec.describe 'Webhook Settings' do
 
       expect(webhook_url.secret).to eq({})
     end
+
+    expect(page).to have_link('Add Secret')
+    expect(page).to have_content('Webhook Secret has been saved.')
+  end
+
+  context 'when testing the webhook' do
+    let!(:webhook_url) { create(:webhook_url, account:) }
+    let!(:template) { create(:template, account:, author: user) }
+    let!(:submission) { create(:submission, template:, created_by_user: user) }
+    let!(:submitter) do
+      create(:submitter, submission:, uuid: template.submitters.first['uuid'], completed_at: Time.current)
+    end
+
+    it 'sends the webhook request' do
+      visit settings_webhooks_path
+
+      expect do
+        click_button 'Test Webhook'
+      end.to change(SendFormCompletedWebhookRequestJob.jobs, :size).by(1)
+
+      args = SendFormCompletedWebhookRequestJob.jobs.last['args'].first
+
+      expect(args['webhook_url_id']).to eq(webhook_url.id)
+      expect(args['submitter_id']).to eq(submitter.id)
+      expect(page).to have_content('Webhook request has been sent.')
+    end
   end
 end