Docs

Docs / Integrations / WordPress plugin

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.

  1. 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.
  2. You sign in to SignSealer and approve (OAuth with PKCE).
  3. 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.
ScopeUsed for
signing:readThe business, its forms and templates, whether it can text, and whether somebody signed before an order or form goes through.
signing:writeRecording consent to be texted.
signing:workflowsStarting a workflow for an order, a submission or a booking.
signing:webhooksRegistering 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.

AttributeIn the blockWhat it is
workflowFormRequired. The form's public address, such as kayak-waiver, not its id. SignSealer → Templates lists the shortcode for each.
labelButton textSign now unless set.
businessNot offeredAnother 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

WhenSent to SignSealer
Every callThe access token, and SignSealer-WordPress/{version}; {site address} as the user agent.
A signing startsThe 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 tickedThe 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 answersA 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 triggerRequests 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

HookArgumentsRuns
signsealer_event (action)$event, $payloadAfter each verified webhook delivery.
signsealer_document_completed (action)$document, with its answersOnce a document completes, from WP-Cron just after the delivery (1.17.0).
signsealer_woocommerce_participant$participant, $order, $itemBefore 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, $valuesBefore a submission's agreement starts. $plugin: gf, wpf, ff, cf7, frm, nf, srfm, fmn, mtf, evf, jfb.
signsealer_booking_participant$participant, $plugin, $booking_idBefore 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$ipThe address that limit counts: REMOTE_ADDR, or empty from WP-CLI or cron.
signsealer_admin_post, signsealer_admin_after_connection (actions)NoneOn 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.

KeyHolds
answerOne 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.
answersEach answer with its question, who gave it and who it is about.
answers_text, flagged_text, flaggedEvery answer, one line each; the ones that want a person to read them; how many.
document_id, workflow_id, title, external_refWhich 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:

AnswerWhy
503 not configuredThe site holds no signing secret.
400 unsignedA SignSealer-Event-Id, -Timestamp or -Signature header is missing.
400 staleThe timestamp is over five minutes from the site's clock. A server clock that far out fails every delivery.
401 bad signatureNo v1= value is the HMAC-SHA256 of {timestamp}.{body}.
200, "repeat": trueThe same body within the hour: a retry, or a replay you asked for. Ignored.
400 not jsonThe body is not JSON.
200The 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 saysWhat 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 madeSignSealer could not read /wp-json/signsealer/v1/site; 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

YouWhat happens
DeactivateThe 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 SettingsThe connection, the webhook's secret and the cached lists are forgotten, and deliveries refused. Every setting stays.
DeleteEvery 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