log in
consulting hosting industries the daily tools about contact

HelloSign API: Embedded e-signatures that don't feel like a redirect

Most e-sign integrations punt users to a third-party URL and hope they come back. HelloSign's embedded flow keeps them in your app — and it actually works.

Every e-signature integration I've built before HelloSign followed the same depressing pattern: generate a document, redirect the user to DocuSign or Adobe Sign, pray they complete it, wait for a webhook, reconcile the state. Drop-off rates are brutal. Users get confused. Support tickets pile up. One healthcare client was losing nearly 30% of patients at the "go sign this" step — they'd click the email link days later, the session was cold, and they'd just bail.

HelloSign — now rebranded Dropbox Sign, though the API endpoints and SDKs still largely say HelloSign — has an embedded signing mode that fixes this. You render the signing experience inside an iframe in your own app. No redirect. No abandoned email link. The user never leaves. I've shipped this for medical intake forms, real estate disclosure packets, and SaaS onboarding agreements, and completion rates are night-and-day better.

Here's how it actually works, what will bite you, and when I'd reach for something else.

What the embedded flow actually does

Normal HelloSign flow: you create a signature request, HelloSign emails the signer a link, signer clicks it, signs on hellosign.com, you get a webhook. Fine for B2B contracts where the signer is some external party you'll never see again.

Embedded flow: you create a signature request, immediately request an embedded sign URL for that request, render it in an iframe using HelloSign's JavaScript client, and the signer completes the whole thing without leaving your page. The JS client fires events (sign, cancel, error) so you can react in real time — show a confirmation, unlock the next step, whatever.

The embedded URL is short-lived (around 30 minutes by default), which is actually correct behavior. You generate it on-demand when the user is about to sign, not days in advance.

A working Laravel integration

HelloSign has an official PHP SDK. Install it:

composer require hellosign/hellosign-php-sdk

I'll show you a realistic controller pattern — creating a signature request against a stored template, then returning an embedded sign URL to a Vue/Livewire frontend.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use HelloSign\Client as HelloSignClient;
use HelloSign\EmbeddedSignatureRequest;
use HelloSign\SignatureRequestSigner;
use HelloSign\Template;
use HelloSign\TemplateSignatureRequest;

class SignatureController extends Controller
{
    private HelloSignClient $client;

    public function __construct()
    {
        $this->client = new HelloSignClient(config('services.hellosign.api_key'));
    }

    /**
     * Create a signature request from a template and return
     * an embedded sign URL for the current user.
     */
    public function createEmbeddedRequest(Request $request)
    {
        $request->validate([
            'document_type' => 'required|string',
        ]);

        $user = $request->user();
        $templateId = config('services.hellosign.templates.' . $request->document_type);

        abort_if(! $templateId, 404, 'Unknown document type');

        $signerRole = 'Client'; // must match the role name in your HelloSign template

        $signer = new SignatureRequestSigner();
        $signer->setName($user->full_name);
        $signer->setEmailAddress($user->email);

        $signatureRequest = new TemplateSignatureRequest();
        $signatureRequest->setTemplateId($templateId);
        $signatureRequest->setSubject('Please sign: ' . $request->document_type);
        $signatureRequest->setSigner($signerRole, $signer);
        $signatureRequest->setClientId(config('services.hellosign.client_id'));

        // Custom fields if your template has merge tags
        $signatureRequest->setCustomFieldValue('PatientName', $user->full_name);
        $signatureRequest->setCustomFieldValue('ClinicName', 'Northwest Regional');

        // This is the key call — creates the request AND makes it embeddable
        $response = $this->client->createEmbeddedSignatureRequestWithTemplate(
            $signatureRequest
        );

        $signatureRequestId = $response->getId();
        $signerId = $response->getSignatures()[0]->getId();

        // Store locally so you can track status via webhook later
        $user->signatureRequests()->create([
            'hellosign_request_id' => $signatureRequestId,
            'document_type'        => $request->document_type,
            'status'               => 'pending',
        ]);

        // Now get the short-lived embedded URL
        $embeddedResponse = $this->client->getEmbeddedSignUrl($signerId);

        return response()->json([
            'sign_url' => $embeddedResponse->getSignUrl(),
        ]);
    }
}

On the frontend, you pull in the HelloSign JS client and open it against that URL:

<script src="https://cdn.hellosign.com/public/js/embedded/v2.9.0/embedded.development.js"></script>
import HelloSign from 'hellosign-embedded';

const client = new HelloSign();

async function openSigningModal(documentType) {
    const { data } = await axios.post('/signature/create', { document_type: documentType });

    client.open(data.sign_url, {
        clientId: import.meta.env.VITE_HELLOSIGN_CLIENT_ID,
        skipDomainVerification: import.meta.env.DEV, // only in local dev
    });

    client.on('sign', (data) => {
        // User completed signing
        window.dispatchEvent(new CustomEvent('document-signed', { detail: data }));
        client.close();
    });

    client.on('cancel', () => {
        console.log('User cancelled signing');
    });

    client.on('error', (data) => {
        console.error('HelloSign error', data);
    });
}

The sign event fires client-side immediately. Don't use that as your source of truth for application state — use the webhook. But you can absolutely use it to improve the UX: close the modal, show a "Thanks, you're all set" message, unlock the next step in an onboarding flow.

The gotchas that will bite you

Domain verification is mandatory in production. You register allowed domains in your HelloSign app settings. If your iframe is hosted on a domain not on that list, signing silently fails. In local dev, set skipDomainVerification: true in the JS client options. Do not ship that flag to production — I've seen it happen. Put it behind an environment check.

The client_id is not the same as your API key. Your API key is a server-side secret. The client ID is a separate identifier associated with your HelloSign app, safe to expose in frontend code. Took me longer than I'd like to admit to sort that out the first time.

Template field names are case-sensitive and whitespace-sensitive. If your template has a merge field called Patient Name (with a space) and you call setCustomFieldValue('PatientName', ...), it silently skips it. You won't get an error. The PDF just goes out with an empty field. Always double-check field names in the template editor.

Webhooks fire asynchronously and sometimes out of order. You might get a signature_request_all_signed event before a signature_request_signed event for the last signer. Build your webhook handler idempotently and don't rely on ordering.

The embedded sign URL expires fast. Don't generate it at page load and cache it. Generate it on demand, right before the user opens the modal. If your app has any kind of multi-step flow where 30+ minutes might pass, regenerate it when the user returns to the sign step.

Test mode documents are watermarked. HelloSign stamps "TEST DOCUMENT" across everything when you use a test API key. This is correct behavior — just warn your clients during UAT so they don't panic.

When I'd reach for this

Anytime the signer is already logged into your application and the signing step is part of a larger flow — patient intake, lease agreements, contractor onboarding, SaaS terms acceptance — the embedded flow is the right call. You're not interrupting the experience. The user stays in context. Completion rates are dramatically better.

I'd also use it when you need to pre-fill document fields from application data. HelloSign's template merge fields are clean and reliable, and doing that server-side means you're not asking the user to re-enter information you already have.

When I'd skip it: if the signer is an external party who isn't logged into your system — say, a customer receiving a contract via email after a sales call — the standard email-based flow is fine. There's no session to embed into. HelloSign's email delivery and hosted signing page are perfectly solid for that use case.

I'd also think twice if you're a small team and budget is a concern. HelloSign isn't cheap at scale. For high-volume scenarios — thousands of documents a month — run the math against DocuSign's API plans or even a self-hosted solution like DocSeal. For low-to-medium volume on a professional app, HelloSign's pricing is reasonable and the API quality justifies it.

One more thing: the Dropbox rebrand has created some confusion in documentation. You'll find tutorials, Stack Overflow answers, and SDK READMEs that mix hellosign and dropbox sign terminology. The v3 REST API is now branded Dropbox Sign, but the PHP SDK I showed above is still the hellosign-php-sdk package on Packagist and it works fine. Just be aware when you're reading docs that you may be looking at two slightly different eras of the same product.

Bottom line

HelloSign's embedded signing is one of the cleaner API integrations I've shipped — the concept is sound, the JS client is reliable, and the webhook delivery has been solid across several years and multiple clients. The gotchas are real but survivable. If you're building an app where signing is a step in a user journey rather than a standalone event, do yourself a favor and keep users in your app.

Need help shipping something like this? Get in touch.