Integration guide

HurrySign is a website and an API. Keep API keys on your server. The browser only ever sees a short-lived signing URL.

Machine-readable spec: /api/v1/openapi.json

Partner accounts and sub-organizations

A firm that only sends its own agreements uses one organization. A product such as a claims CRM or a mitigation platform turns on “partner” in settings, then calls POST /api/v1/sub-organizations. Each customer can be a child organization billed to the partner (billingMode: parent) or billed on its own. Act as a child by sending X-HurrySign-Organization: <child id> with the partner key, or store the API key returned when the child is created. X-SignPact-Organization is still accepted.

Create and send — Node

const base = "https://your-host.example";
const headers = { Authorization: "Bearer " + process.env.HURRYSIGN_API_KEY, "Content-Type": "application/json" };

const created = await fetch(base + "/api/v1/envelopes", {
  method: "POST",
  headers,
  body: JSON.stringify({
    subject: "Service agreement",
    signingOrder: "sequential",
    recipients: [{ name: "Ada Lovelace", email: "ada@example.com", role: "signer", routingOrder: 1 }],
  }),
}).then((res) => res.json());

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("agreement.pdf")], { type: "application/pdf" }), "agreement.pdf");
const uploaded = await fetch(base + "/api/v1/envelopes/" + created.envelope.id + "/documents", {
  method: "POST",
  headers: { Authorization: headers.Authorization },
  body: form,
}).then((res) => res.json());

await fetch(base + "/api/v1/envelopes/" + created.envelope.id + "/fields", {
  method: "POST",
  headers,
  body: JSON.stringify({
    documentId: uploaded.document.id,
    recipientId: created.envelope.recipients[0].id,
    type: "signature",
    anchor: "{{sig_1}}",
  }),
});

await fetch(base + "/api/v1/envelopes/" + created.envelope.id + "/send", {
  method: "POST",
  headers,
  body: JSON.stringify({ confirmExclusions: true }),
});

Create and send — PHP

<?php
$base = "https://your-host.example";
$key = getenv("HURRYSIGN_API_KEY");

function hurrysign_json($method, $url, $key, $body = null) {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
  ]);
  $raw = curl_exec($ch);
  curl_close($ch);
  return json_decode($raw, true);
}

$created = hurrysign_json("POST", "$base/api/v1/envelopes", $key, [
  "subject" => "Service agreement",
  "recipients" => [["name" => "Ada Lovelace", "email" => "ada@example.com", "role" => "signer"]],
]);
$id = $created["envelope"]["id"];

$ch = curl_init("$base/api/v1/envelopes/$id/documents");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
  CURLOPT_POSTFIELDS => ["file" => new CURLFile("agreement.pdf", "application/pdf", "agreement.pdf")],
]);
$uploaded = json_decode(curl_exec($ch), true);
curl_close($ch);

hurrysign_json("POST", "$base/api/v1/envelopes/$id/fields", $key, [
  "documentId" => $uploaded["document"]["id"],
  "recipientId" => $created["envelope"]["recipients"][0]["id"],
  "type" => "signature",
  "page" => 1, "x" => 0.12, "y" => 0.72, "width" => 0.36, "height" => 0.07,
]);
hurrysign_json("POST", "$base/api/v1/envelopes/$id/send", $key, ["confirmExclusions" => true]);

Embed signing

<script src="https://your-host.example/embed.js"></script>
<div id="signing"></div>
<script>
  HurrySign.embed({
    url: SIGNING_URL,
    container: "#signing",
    height: "720px",
    onEvent: function (event) { console.log(event.event, event.envelopeId); }
  });
</script>

// The signing URL comes from your server:
// POST /api/v1/envelopes/:id/recipients/:recipientId/signing-url
// { "returnUrl": "https://app.example.com/claims/42", "expiresInSeconds": 900 }

Verify a webhook — Node

const crypto = require("crypto");

function verifyHurrySignWebhook(secret, rawBody, header) {
  const parts = Object.fromEntries(String(header).split(",").map((part) => {
    const [key, ...rest] = part.split("=");
    return [key, rest.join("=")];
  }));
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!parts.v1 || age > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Verify a webhook — PHP

<?php
function hurrysign_verify($secret, $rawBody, $header) {
  $parts = [];
  foreach (explode(",", $header) as $piece) {
    [$k, $v] = array_pad(explode("=", $piece, 2), 2, "");
    $parts[$k] = $v;
  }
  if (empty($parts["v1"]) || abs(time() - intval($parts["t"])) > 300) return false;
  $expected = hash_hmac("sha256", $parts["t"] . "." . $rawBody, $secret);
  return hash_equals($expected, $parts["v1"]);
}