Webhooks¶
Webhooks are HTTP requests esigna sends to your app when something happens in a signing process. They are not instant and may arrive out of order, and the same event can arrive more than once.
Portal API
Webhooks are part of the Portal API.
To receive events:
- Set up an endpoint that checks signatures and answers pings (below).
- Create a subscription for the events you want.
- Activate it. Until then it receives nothing.
Your endpoint¶
esigna only calls endpoints that:
- use
https://and a public address: nolocalhost, private network or link-local addresses, and no user name or password in the URL. The address is checked again on every connection, and redirects are not followed; - answer within 15 seconds;
- answer a correctly signed request with a
2xxstatus (the body is ignored); - answer a request whose signature doesn't verify with
401 Unauthorized.
Headers¶
Each request carries:
| Header | Content |
|---|---|
webhook-id |
The event's ID. Retries of the same event keep the same ID. |
webhook-timestamp |
When the request was sent, in Unix seconds. |
webhook-signature |
One or more signatures, separated by spaces. |
webhook-subscription-id |
The ID of the subscription the request is for. |
Checking the signature¶
Check the signature on every request, before anything else. If no signature verifies, answer 401 Unauthorized.
The signed content is the webhook-id, the webhook-timestamp and the raw request body, joined with periods:
Use the body exactly as received. webhook-signature lists signatures as scheme,base64-signature, separated by spaces:
| Scheme | Algorithm | Key |
|---|---|---|
v1 |
HMAC-SHA256 | Your subscription's secret: the 64 hexadecimal characters returned once when you created the subscription, decoded to its 32 bytes. |
v1a |
ed25519 | esigna's public key (see Public keys). |
Accept the request if any one signature verifies. The list may grow, for example during a key change, so ignore schemes you don't know.
Also refuse requests whose webhook-timestamp is more than about five minutes from your clock, so a captured request can't be replayed later.
Public keys¶
esigna's ed25519 public keys, in OpenSSH format:
| Environment | Key |
|---|---|
| Production | https://docs.esigna.is/keys/webhooks-production.pub |
| Sandbox | https://docs.esigna.is/keys/webhooks-sandbox.pub |
Duplicates¶
esigna may send the same event more than once, for example when your endpoint handled a request but its answer didn't reach esigna in time. Store the webhook-id of each event you have handled and skip repeats.
Check the signature before looking at webhook-id. Activation and the security checks send requests that must be refused, and a handler that skips "repeats" before checking the signature would answer them with 2xx.
Pings and security checks¶
esigna sends a webhook.ping to every active subscription about once an hour, whether or not it subscribes to webhook.ping. Answer it with 2xx.
At random times, every one to five hours, esigna also sends a security check: a ping signed so that it can't verify, which your endpoint must answer with 401.
- If your endpoint doesn't answer, or answers with an error, esigna tries again every 5 minutes. The account's admins get an email about it, at most one a day.
- After three days in which every ping failed, the subscription is switched off and the admins get an email.
- If your endpoint answers a security check with
2xx, it accepted a request it could not have verified: the subscription is switched off at once and the admins get an email. An outage during a security check doesn't count; the check just waits until your endpoint answers.
A switched-off subscription receives nothing until you activate it again.
Managing subscriptions¶
Scopes required
portal/read to list and get subscriptions; portal/write for everything else.
Creating a subscription¶
POST /api/v1/portal/webhooks
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | Your endpoint, at most 512 characters. See Your endpoint. |
eventTypes |
array | Yes | The events to receive. |
Example response: 201 Created¶
The Location header holds the subscription's address, /api/v1/portal/webhooks/{id}.
Warning
secret is returned only here, once. Store it securely: it can't be read again.
A 400 names the problem: a URL that isn't public https://, or an event type that isn't available.
Activating a subscription¶
POST /api/v1/portal/webhooks/{id}/activate
New subscriptions are inactive. Activation sends your endpoint two pings, each with its own webhook-id:
- one correctly signed, which must get a
2xx; - one that can't verify, which must get
401 Unauthorized.
If both pass, the subscription is active and the response is 200 with the subscription. If either fails, or your endpoint can't be reached, the response is 422 Unprocessable Entity and says which check failed:
The two pings can take up to 30 seconds. If the subscription's URL is changed while they run, the response is 409 Conflict and the subscription stays inactive: activate it again, so the new URL gets checked.
curl https://api.esigna.is/api/v1/portal/webhooks/$ID/activate \
--request POST \
--header "Authorization: Bearer $TOKEN"
Sending a test event¶
POST /api/v1/portal/webhooks/{id}/test
Sends one correctly signed webhook.ping. Returns 204 No Content if your endpoint answered 2xx, and 502 Bad Gateway if it answered anything else, timed out or couldn't be reached.
Listing and getting subscriptions¶
GET /api/v1/portal/webhooks lists your subscriptions; GET /api/v1/portal/webhooks/{id} returns one, or 404. Neither returns the secret.
Updating a subscription¶
PUT /api/v1/portal/webhooks/{id}
Send only the fields you want to change.
| Field | Type | Description |
|---|---|---|
url |
string | A new endpoint. Changing the URL makes the subscription inactive until you activate it again. |
eventTypes |
array | A new list of events. |
isActive |
bool | false switches the subscription off. true is refused with 422: use activate. |
Returns 200 with the subscription, 400 for a URL or event type that isn't allowed, or 404.
Deleting a subscription¶
DELETE /api/v1/portal/webhooks/{id} deletes the subscription for good. Returns 204 No Content, or 404.
Delivery and retries¶
Events come from the signing processes your account created through the API; documents sent from the esigna web app send none. esigna sends each event to the subscriptions that include its type and are active when it is first sent, normally within seconds of it happening: a subscription created or activated later doesn't get earlier events.
If your endpoint doesn't answer 2xx within 15 seconds, the event is sent again, to that subscription only:
| Attempt | When |
|---|---|
| 1 | As soon as the event happens |
| 2 | 10 seconds after attempt 1 failed |
| 3 | 1 minute after attempt 2 failed |
| 4 | 5 minutes after attempt 3 failed |
| 5 | 30 minutes after attempt 4 failed |
| 6 | 2 hours after attempt 5 failed |
| 7 | 6 hours after attempt 6 failed |
| 8 | 24 hours after attempt 7 failed; after that the event is dropped |
So an event is retried for about a day and a half. If your endpoint was down for longer, read the signing process's status to catch up.
Events¶
Every event body has the same shape: a type, a timestamp and a data object.
| Event | When |
|---|---|
participant.signed |
A participant signed the document. |
participant.declined |
A participant declined to sign. |
process.completed |
Every signer has signed. |
webhook.ping |
Liveness check. Sent to every active subscription; you don't subscribe to it. |
kennitala and phone are empty strings when esigna doesn't know them.
participant.signed¶
| Field | Type | Description |
|---|---|---|
data.signingProcessId |
string (UUID) | The signing process. |
data.participant.name |
string | The signer's name, as their electronic ID gives it. |
data.participant.email |
string | The email address the invitation went to. |
data.participant.kennitala |
string | The signer's kennitala, as their electronic ID gives it. |
data.participant.phone |
string | The phone number given when the process was created. |
data.participant.signature |
string (Base64) | The signer's signature value. |
data.participant.signedAt |
string (ISO 8601, with offset) | When they signed. |
participant.declined¶
| Field | Type | Description |
|---|---|---|
data.signingProcessId |
string (UUID) | The signing process. |
data.participant.name |
string | The participant's name. |
data.participant.email |
string | The email address the invitation went to. |
data.participant.kennitala |
string | The participant's kennitala, if known. |
data.participant.phone |
string | The phone number given when the process was created. |
data.participant.declineReason |
string | The reason the participant gave. |
data.participant.declinedAt |
string (ISO 8601, with offset) | When they declined. |
process.completed¶
Sent when every signer has signed. The event doesn't carry the document: download the signed document with the signingProcessId.
| Field | Type | Description |
|---|---|---|
data.signingProcessId |
string (UUID) | The signing process. |
data.participants |
array | The signers (viewers are not listed), each with name, email, kennitala, phone, signature and signedAt as in participant.signed. |