WordPress plugin
One WordPress site, connected to one SignSealer business. The site's shop, forms and bookings start the signing; the signing happens on SignSealer, and the site keeps a document's id, its status and a link. Never the evidence.
This page is what happens underneath, as of version 1.17.1. For each integration's screens, step by step, follow the WordPress guides, starting with Getting started.
Requirements
- WordPress 6.4 or later (tested up to 7.1) and PHP 7.4 or later.
- The site on https: SignSealer registers only a site it can reach over https, and posts statuses back the same way.
- A WordPress administrator (
manage_options) and a SignSealer account, which can be made while connecting.
Install it from the WordPress.org directory.
Connecting
Under SignSealer → Start here, press Connect to SignSealer.
- The plugin makes a one-time token, sends it to SignSealer to register the
site (
POST /oauth/register), and shows only its SHA-256 at/wp-json/signsealer/v1/site. SignSealer fetches that page over https and registers a client named WordPress site and your host only if the two match. That proves the site runs the plugin, not who runs the site, and the consent screen says Not checked by SignSealer for that reason. - You sign in to SignSealer and approve (OAuth with PKCE).
- Back on Start here, the plugin reads the business's name and address
(
GET /v1/account), registers its own webhook endpoint,/wp-json/signsealer/v1/webhook, and keeps the secret SignSealer shows once. Start here says Connected to and the business's name. If either step fails, the plugin's screens try again, at most once a minute.
| Scope | Used for |
|---|---|
signing:read | The business, its forms and templates, whether it can text, and whether somebody signed before an order or form goes through. |
signing:write | Recording consent to be texted. |
signing:workflows | Starting a workflow for an order, a submission or a booking. |
signing:webhooks | Registering its endpoint, or a new secret for one already there. |
A scope the person approving does not hold is left out; SignSealer →
Settings shows what was granted. In SignSealer the site is a row under
Integrations → Connected apps, and its
endpoint, WordPress: and your host, subscribed to "*", is
under Integrations → Webhooks.
The refresh token and the client's secret are in the
signsealer_connection option, not autoloaded; the access token is a
transient. SignSealer rotates the refresh token on each use and ends the
connection if a used one comes back, so the plugin refreshes one request at a
time. SIGNSEALER_API_BASE in wp-config.php points it at a
partner's own host.
The block and the shortcode
A button to sign a SignSealer form that has a public address. The block is
SignSealer agreement (signsealer/sign), the shortcode
[signsealer_sign]; both are drawn by the same code when the page is
served.
| Attribute | In the block | What it is |
|---|---|---|
workflow | Form | Required. The form's public address, such as kayak-waiver, not its id. SignSealer → Templates lists the shortcode for each. |
label | Button text | Sign now unless set. |
business | Not offered | Another business's address, to link its form. The connected business's unless set. |
[signsealer_sign workflow="kayak-waiver" label="Sign the waiver"]
It is a link to /f/{business}/{form} on SignSealer, built without an
API call. The visitor types their name and email there, and nothing is recorded
in WordPress. Missing a workflow or a business address, it shows a
visitor nothing and anyone who can edit posts a note. To style it yourself, choose Your
theme's buttons under SignSealer → Settings; it carries
the class signsealer-button.
Orders, form submissions and bookings each start a workflow instead
(POST /v1/workflows/{id}/start); the guides cover their screens.
Before payment, SignSealer's signing page is framed on a page of your site,
which connecting the site allows (Signing inside your
product).
What it sends and keeps
| When | Sent to SignSealer |
|---|---|
| Every call | The access token, and SignSealer-WordPress/ as the user agent. |
| A signing starts | The person's email, name and phone if known; the blanks the agreement asks, from the order, the form's fields, the booking or your answers; an external_ref such as wc:{order}:{line item}. |
| A box to be texted is ticked | The number, the words beside the box as shown, and which form or order on which site (Texting and consent). |
| A document completes, and something here listens for its answers | A request for the document (GET /v1/documents/{id}), whose answers are handed on and not kept. If SignSealer cannot be reached it is tried again, five tries in about fifteen minutes. |
| An Uncanny Automator recipe uses SignSealer's trigger | Requests for your forms and, for the form chosen, the questions it asks, while the recipe is built and when it runs. |
It keeps, in options named signsealer_…, the connection, the
webhook's secret, each integration's settings, and each document it started:
its newest status and a map back to its order, entry or booking. An order keeps
each line's document id, title, signing link and status
(_signsealer_documents); a Gravity Forms, WPForms or Fluent Forms
entry keeps its document. The wording of the questions an Automator recipe's
form asks is kept in one transient, signsealer_questions: the
newest twenty published template versions, until an hour passes without
another being added.
Hooks
| Hook | Arguments | Runs |
|---|---|---|
signsealer_event (action) | $event, $payload | After each verified webhook delivery. |
signsealer_document_completed (action) | $document, with its answers | Once a document completes, from WP-Cron just after the delivery (1.17.0). |
signsealer_woocommerce_participant | $participant, $order, $item | Before an order line's agreement starts. |
signsealer_woocommerce_presign_participant | $participant, $need, $key (the cart line) | Before payment. A blank given a value here is not asked. |
signsealer_form_participant | $participant, $plugin, $form_id, $values | Before a submission's agreement starts. $plugin: gf, wpf, ff, cf7, frm, nf, srfm, fmn, mtf, evf, jfb. |
signsealer_booking_participant | $participant, $plugin, $booking_id | Before a booking's agreement starts. $plugin: latepoint, amelia, ssa, tickets, em, wpbc, fb, mphb, bookly. |
signsealer_starts_per_hour | $limit (20) | Starts one visitor's address may make an hour; 0 for no limit. |
signsealer_visitor_ip | $ip | The address that limit counts: REMOTE_ADDR, or empty from WP-CLI or cron. |
signsealer_admin_post, signsealer_admin_after_connection (actions) | None | On SignSealer's admin screens, for your own settings. |
A blank the plugin cannot fill
A participant is the workflow start's body: email,
full_name, external_ref, values (text, keyed by
blank), and phone and return_url when known. A filter can
run more than once for one start, so only return the array. With WooCommerce
Bookings, the plugin's own filter at priority 5 has already added
booking_date, booking_time, persons and
resource.
add_filter( 'signsealer_woocommerce_participant', function ( $participant, $order, $item ) {
$vessel = (string) $order->get_meta( 'vessel_name' );
if ( '' !== $vessel ) {
$participant['values']['vessel_name'] = $vessel;
}
return $participant;
}, 10, 3 );
Acting on an answer
A delivery never carries the answers to a form's questions; some are about
somebody's health, and a webhook goes wherever it is pointed. When a document
completes and something here is listening, the plugin asks SignSealer for them,
just after the delivery has been answered, and fires
signsealer_document_completed; if SignSealer cannot be reached it
tries again, five tries in about fifteen minutes. The plugin keeps none of the
answers. An Uncanny Automator recipe that runs on them is kept in Automator's
own log, with its tokens, as every trigger's run is.
| Key | Holds |
|---|---|
answer | One value per question, by its key. A tick box is Yes or No; a question several people answered (each person in a party) holds every different answer, sorted, joined with a comma; a sensitive answer is masked, as your staff see it. |
answers | Each answer with its question, who gave it and who it is about. |
answers_text, flagged_text, flagged | Every answer, one line each; the ones that want a person to read them; how many. |
document_id, workflow_id, title, external_ref | Which document, from which form, and the reference it was sent with. |
add_action( 'signsealer_document_completed', function ( $document ) {
if ( 'Yes' === ( $document['answer']['married'] ?? '' ) ) {
// "Are you married?" was answered Yes.
}
} );
signsealer_get_answers( $document_id ) asks for a document's answers
in the same shape. With Uncanny Automator, no code is needed: its
trigger SignSealer → A document from a form started anywhere is signed
takes a form or Any form, meaning any of your business's forms. With one
form chosen, it can wait for one answer (“Are you married? → Yes”) and gives each
question as a token; with Any form, it runs on whatever was answered and
gives the answers together as All answers (text). Left as
started anywhere, it hears its form however
the document was started; started on this site narrows it to the
documents this site's own forms, orders and bookings started. It works in
Automator's free edition; make one recipe per answer.
Acting on a signature
The endpoint is subscribed to "*", so the action also fires for
documents this site did not start; SignSealer_Webhook::ours( $doc_id )
says whether it did. Deliveries can arrive out of order:
SignSealer_Webhook::newest_status( $doc_id, $payload ) is the newest
status known. $payload is the body as sent
(Webhooks); escape what you print from it.
add_action( 'signsealer_event', function ( $event, $payload ) {
$doc = isset( $payload['document']['id'] ) ? (string) $payload['document']['id'] : '';
if ( 'signing.completed' !== $event || '' === $doc || ! SignSealer_Webhook::ours( $doc ) ) {
return;
}
wp_schedule_single_event( time(), 'mysite_agreement_signed', array( $doc ) );
}, 10, 2 );
A handler gets one chance. The delivery is marked as seen before the action fires. If your handler fails, or the request outlasts the ten seconds SignSealer waits, the retry is answered as a repeat and your handler is not called again. Record what you need, and schedule the rest.
The limit on visitors
Starts from the page before payment and from forms count against the
visitor's address (IPv6 by its /64); orders, bookings and staff with
edit_others_posts do not. Past the limit nothing is sent, and a form
already sent keeps its entry with a note. Behind a proxy that hides visitors'
addresses, return the one your proxy vouches for, never a header a visitor could
write. A value that is not an IP address is not counted at all.
add_filter( 'signsealer_starts_per_hour', function () {
return 50;
} );
// Your own proxy, at 10.0.0.5, sets X-Real-IP: trust it from there only.
add_filter( 'signsealer_visitor_ip', function ( $ip ) {
if ( '10.0.0.5' === $ip && isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) );
}
return $ip;
} );
How deliveries are checked
The endpoint is public, so everything rests on the check, made on the raw body in this order:
| Answer | Why |
|---|---|
503 not configured | The site holds no signing secret. |
400 unsigned | A SignSealer-Event-Id, -Timestamp or -Signature header is missing. |
400 stale | The timestamp is over five minutes from the site's clock. A server clock that far out fails every delivery. |
401 bad signature | No v1= value is the HMAC-SHA256 of {timestamp}.{body}. |
200, "repeat": true | The same body within the hour: a retry, or a replay you asked for. Ignored. |
400 not json | The body is not JSON. |
200 | The status is kept and signsealer_event fires. |
A repeat is known by a SHA-256 of the body, which the signature covers. The
status kept is the latest by occurred_at, so a late
signing.sent cannot undo signing.completed.
When something goes wrong
| It says | What happened |
|---|---|
| This site is not on https. | The site's address starts http://. Move it to https and connect again. |
| … did not show the token this attempt made | SignSealer could not read /; a firewall or security plugin blocking the REST API, say. Allow that address. |
| That connection attempt has expired. | More than thirty minutes between Connect and approving. Press Connect again. |
| Connected, but SignSealer did not answer when asked which business this is. | The business's address and the webhook are not set up yet, so the block shows nothing and no statuses arrive. Any SignSealer screen in WordPress asks again once a minute has passed: reload it then. Nothing needs forgetting. |
| Signed statuses will not arrive: … | The endpoint was not registered. If SignSealer already posts to this site, delete that endpoint under Integrations → Webhooks, forget the connection and connect again. |
| SignSealer has ended this site's connection. | The refresh token was refused: ended under Connected apps, expired, or used twice. Nothing starts, and with Before payment the checkout stops. Connect again; settings stay. |
| Too many agreements have been started from your connection in the last hour. | The visitor passed signsealer_starts_per_hour. If every visitor shares one address, set signsealer_visitor_ip. |
When SignSealer refuses a start, its own sentence goes in the order's notes, the entry's notes where the form plugin keeps them, or on SignSealer → Bookings (Errors).
Deactivating, forgetting, deleting
| You | What happens |
|---|---|
| Deactivate | The connection is kept. The webhook address goes with the plugin, so deliveries fail meanwhile, and twenty in a row pause the endpoint. |
| Forget the connection, under Settings | The connection, the webhook's secret and the cached lists are forgotten, and deliveries refused. Every setting stays. |
| Delete | Every option and transient named signsealer_ goes, on every site of a network. A product's agreement and what orders and entries carry stay. |
None of them ends the grant or removes the endpoint: do that under Integrations → Connected apps and Integrations → Webhooks. The documents and their certificates were never in WordPress.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 10 agreements a month.
Get an API key