Skip to main content

Overview

GetSigned fires webhooks when significant events happen on your envelopes. Your backend receives an HTTP POST with a JSON payload. You control what happens next — GetSigned doesn’t know or care.

Configure a webhook endpoint

curl -X POST https://api.getsigned.ca/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/getsigned",
    "events": ["envelope.completed", "envelope.declined", "envelope.voided", "envelope.expired"],
    "secret": "your-webhook-signing-secret"
  }'
secret is used to sign the payload (HMAC-SHA256). Store it in your secrets manager.

Event types

EventWhen it fires
envelope.completedAll signers have signed. Sealed PDF is ready.
envelope.declinedA signer declined to sign.
envelope.voidedYou voided the envelope.
envelope.expiredThe signing deadline passed.
signer.viewedA signer opened their signing link.
signer.signedAn individual signer completed signing.
signer.otp_failedA signer failed OTP verification (3 failed attempts).

Payload format

{
  "event": "envelope.completed",
  "id": "evt_01HX...",
  "createdAt": "2026-06-19T14:32:00Z",
  "data": {
    "envelopeId": "env_01HX...",
    "tenantId": "acme-corp",
    "subject": "Service Agreement",
    "completedAt": "2026-06-19T14:32:00Z",
    "documentUrl": "https://api.getsigned.ca/v1/envelopes/env_01HX.../document",
    "signers": [
      {
        "id": "sgn_01HX...",
        "name": "Jane Smith",
        "email": "jane@example.com",
        "signedAt": "2026-06-19T14:31:45Z"
      }
    ]
  }
}

Verifying the webhook signature

Every webhook request includes a X-GetSigned-Signature header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook secret. Always verify this signature before processing the payload. This prevents attackers from spoofing events.
import hmac
import hashlib

def verify_signature(payload_bytes: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", header)
Use a constant-time comparison (hmac.compare_digest, timingSafeEqual, FixedTimeEquals). A naive == comparison leaks timing information that can be exploited.

Responding to webhooks

Your endpoint must return 2xx within 10 seconds. If it times out or returns a non-2xx status, GetSigned will retry. Do not do heavy work inside the webhook handler. Acknowledge receipt immediately and process asynchronously:
@app.post("/webhooks/getsigned")
async def handle_webhook(request: Request):
    body = await request.body()
    sig = request.headers.get("X-GetSigned-Signature")
    if not verify_signature(body, sig, WEBHOOK_SECRET):
        raise HTTPException(status_code=401)

    payload = json.loads(body)
    await queue.enqueue("process_getsigned_event", payload)  # async queue

    return {"received": True}  # 200 immediately

Retry schedule

If your endpoint doesn’t return 2xx within 10 seconds, GetSigned retries with exponential backoff:
AttemptDelay
1Immediate
21 minute
310 minutes
41 hour
56 hours
After 5 failed attempts, the delivery is marked as failed. You can view failed deliveries and trigger a manual resend in the Console under Webhooks → Deliveries.

Idempotency

Webhooks may be delivered more than once (network retries can cause duplicates even after a successful delivery). Use the id field (evt_01HX...) to deduplicate:
if await redis.exists(f"webhook:{payload['id']}"):
    return {"received": True}  # already processed

await redis.setex(f"webhook:{payload['id']}", 86400, "1")
await process(payload)

Testing webhooks locally

Use ngrok or Cloudflare Tunnel to expose your local server:
ngrok http 3000
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000

# Register your ngrok URL as the webhook endpoint
curl -X POST https://api.getsigned.ca/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"url": "https://abc123.ngrok.io/webhooks/getsigned", "events": ["envelope.completed"]}'