Phoenix Nest Booking → E-Signature Bridge
WordPress plugin bridging MotoPress bookings to pre-filled, e-signed rental agreements (WP E-Signature): auto-prefill, confirmation redirect, and tamper-safe value baking. Live on phoenixnest.properties.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/codebyshoaib/phoenix-esign-bridge/archive/refs/heads/main.zipA single‑file WordPress plugin that turns a MotoPress vehicle booking into a pre‑filled, legally e‑signed Motor Vehicle Rental Agreement via WP E‑Signature — with the renter's data captured once and baked into the signed PDF so the executed copy is tamper‑correct, not a blank template.
Live in production: phoenixnest.properties — Phoenix Nest Properties LLC (Destin, FL vehicle rentals). A customer books a vehicle, is redirected into a pre‑filled agreement, signs once, and receives a completed PDF; the owner counter‑signature is applied automatically.
Why this exists
Off‑the‑shelf, MotoPress and WP E‑Signature don't talk to each other. A rental business needs the booking a customer just made to flow straight into a specific, pre‑filled, legally‑binding agreement — no re‑typing, no blank fields, no manual document creation per booking. This plugin is the integration layer that makes that a single, hands‑off flow.
It solves three problems that a naïve "just add a shortcode" approach gets wrong:
- Timing — MotoPress writes customer data after the initial save, and submits over
admin-ajax, so you can't read the data or steer the browser at submit time. - Prefill transport — WP E‑Signature's
[esigget]merge fields read the original request viafilter_input(), so values must ride the signing URL as real query parameters. - Persistence — merge‑field values render on screen but do not persist into the signed copy by default; the multi‑step signing flow re‑renders the document without them, leaving blanks in the executed PDF.
Architecture
sequenceDiagram
actor R as Renter
participant MP as MotoPress Booking
participant BR as Bridge Plugin
participant ES as WP E-Signature
R->>MP: Submit booking (dates, address, DL, insurance)
MP->>BR: save_post (pri 20) — flag booking "pending e-sign" (15-min transient)
MP-->>R: Redirect to booking-confirmation view
R->>BR: template_redirect intercepts the confirmation
Note over BR: Gather booking meta →<br/>build the pn_* prefill map
BR-->>R: 302 → signing page with ?pn_*=… prefill
R->>ES: Review pre-filled agreement, draw/type signature
ES->>BR: esig_document_clone_render_content (per-signer clone created)
Note over BR: Bake pn_* values into the clone<br/>as static text — BEFORE signature capture
ES->>ES: Capture signature on the baked clone (tamper-integrity holds)
ES-->>R: Store signed PDF + auto owner counter-sign → /booking-confirmed/
The three hooks
| Hook | Function | Responsibility |
|---|---|---|
save_post (pri 20) |
pn_esign_mark_pending |
Flags a front‑end booking as pending e‑sign. Correctly distinguishes a real booking (admin-ajax) from a genuine wp‑admin edit. |
template_redirect |
pn_esign_confirmation_redirect |
Intercepts MotoPress's post‑booking return, gathers all booking meta into the pn_* prefill map, and redirects the renter to the signing page. Matches both MotoPress return‑URL shapes and resolves the booking from payment_id (v1.2.2 — see below). The pending‑flag gate prevents booking‑ID enumeration from leaking renter PII. |
esig_document_clone_render_content |
pn_esign_store_doc_id |
v1.1.0 — records which signed document belongs to which booking (_pn_esign_doc_id), the link nothing stored before. Only writes when the request carries a matching HMAC over the booking id, because that id can arrive via an attacker‑controlled referer. |
esig_document_clone_render_content |
pn_esign_bake_clone |
The persistence fix: when WP E‑Signature clones the master document for a signer, replaces every [esigget pn_*] merge field with the real value as static text, before the signature is captured — so the signed content is exactly what was signed. |
esig-document-index-docs + esig_documents_loading |
pn_esign_widen_document_list / _counts |
v1.3.0 — lets a second WordPress administrator see the documents in E‑Signature → My Documents, which the plugin otherwise scopes to a single stored super admin. Read‑only widening; changes no ownership and no notification recipient. |
v1.3.0 — the agreement "disappeared" from the second admin's document list (2026‑08‑05)
Once the rental agreement was owned by the client's own account, E‑Signature → My Documents went
empty for the developer/second administrator account. Nothing was deleted: WP E‑Signature scopes that
screen to one stored super admin (option esig_superadmin_user) and shows every other administrator
only rows whose esign_documents.user_id equals their own id.
The two obvious fixes are both wrong here:
- Move the super‑admin slot.
is_esig_super_admin()is a bare$wp_user_id == $admin_user_idwith no filter, andUser::checkEsigAdmin()gates plugin access on it — so moving the slot locks the other admin out of the plugin entirely. - Reassign document ownership. The auto‑added owner counter‑signature and the completion
notification both follow
esign_documents.user_id. Reassigning ownership re‑points the counter‑signature at the wrong person and mails the wrong administrator — that is the bug this release is careful not to reintroduce.
So the bridge widens only the two things the screen actually reads, for a user who already holds
manage_options, and nothing else:
- The row set, through the plugin's own
esig-document-index-docsfilter, by re‑runningfetchAllOnStatus( $status, true )— its unscoped super‑admin branch. - The tab counts, by priming the object‑cache key
getDocumentsTotal()checks first. Only a non‑zero count is ever written: if a tab keys ondocument_typerather thandocument_status, our count is0and writing it would hide a number the plugin computes correctly.
A search result is left alone — fetchAllonSearch() is already unscoped. This grants no capability a
WordPress administrator does not already have, and touches neither esig_superadmin_user, document
ownership, nor any signature row.
v1.2.2 — the redirect never actually fired (2026‑08‑04)
Walking the live order flow end to end showed the renter was never reaching the agreement: every booking landed on the confirmation page and stopped. Three independent faults, each on its own enough to break the redirect — so fixing any two still looks broken. Verify all three before concluding.
booking_idis never sent. MotoPress Booking Calendar 1.3.3 puts onlypayment_id+tokenon the post‑booking URL. The old code read$_GET['booking_id']and bailed immediately, every time. The booking is now resolved from the payment post.- There are two different return‑URL shapes. With no Reservation Received Page configured,
MotoPress redirects to
home_url()with?mpbc_action=booking-confirmation; once that page is configured it redirects to the page withoutmpbc_action. Matching only the first shape means configuring the page silently re‑breaks the redirect. Now keyed onmpbc_actionor (payment_id+token) — widening the match is safe because the short‑lived pending transient, not the query string, is the real gate. - The meta key is not the metabox field name. The payment metabox field is
mpbc_booking_id, but the stored meta key is the underscore‑protected_mpbc_booking_id(the same split asmpbc_customer_*elsewhere in this plugin). Reading only the unprefixed key returns'', so$booking_idstayed0and the redirect bailed with no error anywhere. Both keys are now read, thenwp_get_post_parent_id()as a last resort.
Two host‑side notes that are not code but will waste your afternoon:
Reservation Received Pagemust contain the Booking Calendar block. MotoPress registers no separate confirmation block —mpbc/booking-calendaris what renders the confirmation view — so pointing the setting at an ordinary page yields a blank page, and leaving it unset sends the renter to the site homepage.- The confirmation view is single‑use. Its token is consumed on first render; re‑loading the same URL shows a bare calendar. That is not a regression.
Verified live: booking → /motor-vehicle-rental-agreement/ with every pn_* value pre‑filled and
rendered in the document body.
v1.1.0 — linking a booking to its signed document
Nothing recorded which signed document belonged to which booking, so anything downstream could only guess an agreement from name + date. v1.1.0 closes that:
pn_bid+pn_sigride the signing URL alongside the existing prefill, so they survive the same referer fallback.- At clone time,
pn_esign_store_doc_id()validates and writes_pn_esign_doc_id. - Only the document id is stored, never the checksum. The
did=hash in the PDF URL issha1( $doc_id . $bakedContent )and changes if the document is ever re-saved, so it is resolved at render time viadocument_checksum_by_id().
Two decisions worth knowing before touching it:
- The guard is an HMAC this plugin mints itself. A
pn_bidarriving throughHTTP_REFERERis attacker-controlled, so without a signature a forged referer could stamp_pn_esign_doc_idonto any post on the site.wp_salt()means one cannot be forged. - First clone wins.
copyDocument()runs once per signer, so a counter-signature or a re-send would otherwise overwrite the renter's document id.
Installation
# From wp-content/plugins/
git clone https://github.com/codebyshoaib/phoenix-esign-bridge.git
Or download the ZIP and upload via Plugins → Add New → Upload Plugin, then Activate.
Requirements: WordPress with MotoPress Booking Calendar, WP E‑Signature (core + Signer Input Fields / Stand Alone Documents add‑ons), PHP 7.4+.
Configuration
The plugin is convention‑driven and needs no settings page. To wire it to your document:
- In WP E‑Signature, build a Stand Alone document and drop
[esigget pn_key]merge fields into the body (e.g.[esigget pn_renter_name],[esigget pn_total_due]). - Set the document's post‑sign redirect to your confirmation page (e.g.
/booking-confirmed/). - Point the signing page path via the
pn_esign_page_pathfilter if it differs from the default:
add_filter( 'pn_esign_page_path', fn() => '/motor-vehicle-rental-agreement/' );
Any pn_* merge key you add to the document is covered automatically by the bake step. The prefill map (name, address, driver's licence, pickup/return, total due, additional drivers, …) is assembled in pn_esign_build_prefill().
Engineering notes
- Bake at clone time, never after signing. Rewriting content after
esig_signature_savedwould break WP E‑Signature's tamper checksum. The clone filter fires on the raw decrypted content before checksum + encrypt + store — the only correct place to inject values. - PII stays off persistent storage. Prefill rides a single, one‑time confirmation→signing redirect gated by a short‑lived transient; there's no booking‑ID endpoint that echoes renter data.
- Packaged as its own plugin, not appended to another. Isolation means a host or theme update can't silently wipe the integration, and a fatal in this code disables only the feature — it never white‑screens the site.
- Watch the PHP function‑hoisting trap. A top‑level
if ( function_exists('…') ) return;"safety guard" is a footgun here: PHP hoists top‑level function definitions at compile time, so such a guard is always true and the plugin returns before registering any hooks — functions exist, but nothing is wired. Verify integrations by confirming the hook is actually attached (or with an end‑to‑end signed‑PDF check), not merely "no fatal error."
Tests
php tests/selftest.php # → OK (31 assertions)
No framework, no WordPress: it stubs the WP functions the plugin touches and asserts the two things that fail silently — that the hooks are actually attached (see the hoisting note above, which cost this plugin a production bug), and that the doc-id guard rejects an unsigned id, a wrong signature, a correctly-signed id that is not a booking, and a post that does not exist. Nothing local replaces the end-to-end signing check.
Part of a four-plugin system
Each plugin owns one job and one set of meta keys, so a bug in a reporting screen can never take down the booking flow.
| Plugin | Job | |
|---|---|---|
| 1 | Custom booking fields (site-specific, not published) | Injects and saves renter fields on the booking form → _phoenix_* |
| 2 | phoenix-esign-bridge (this repo) | Booking → pre-filled agreement → signed PDF, and records which document belongs to which booking |
| 3 | phoenix-vehicle-condition | Pickup + return condition photos at handover → _pn_vc_* |
| 4 | phoenix-customer-records | Read-only: groups every booking into customers and shows all of the above in one place |
flowchart LR
F["1 · booking fields<br/><code>_phoenix_*</code>"] --> E["2 · e-sign bridge<br/><code>_pn_esign_doc_id</code>"]
E --> V["3 · condition photos<br/><code>_pn_vc_*</code> + attachments"]
F --> R["4 · customer records<br/>reads only"]
E --> R
V --> R
_pn_esign_doc_id is the seam between 2 and 4: plugin 4's agreement link only works for documents
signed after v1.1.0 went live, because a link that was never recorded cannot be backfilled.
Compatibility
Validated against WP E‑Signature 2.1.x and MotoPress Booking Calendar. The bake step depends on the esig_document_clone_render_content filter; after any major WP E‑Signature update, run one test signing and confirm the resulting PDF contains real field values.
Author
Shoaib Ud Din — Full‑stack engineer LinkedIn · GitHub
License
GPL‑2.0‑or‑later — consistent with the WordPress plugin ecosystem.