PHP SDK
fobo/datamaker-sdk reads a .dmf, verifies the signature + FOBO attestation,
sealed-box encrypts a submission, and posts it. PHP 8.0+ with ext-sodium,
ext-zip, ext-json, ext-curl.
Install
composer require fobo/datamaker-sdkSubmit
use DataMaker\Sdk\Client;
$dmf = file_get_contents('contact.dmf');
$result = Client::submit([ 'dmf' => $dmf, // verifies signature + form hash 'values' => ['email' => 'ada@example.com', 'full_name' => 'Ada'],]);// => ['submissionId' => '...', 'editToken' => '...', 'formId' => 'contact_form']Or read the form once and reuse it: Client::submit(['form' => $form, 'values' => ...]).
Building the values array
values is keyed by each field’s name (its storage key — not the
label). Inspect the form to see the names + kinds you need to fill:
use DataMaker\Sdk\DmfReader;
$form = DmfReader::read($dmf);foreach ($form->fields as $f) { echo $f['key'], ' · ', $f['kind'], $f['required'] ? ' (required)' : '', "\n";}// email · email (required)// full_name · text// age · number// subscribed · boolean// plan · choice// interests · multi-choiceMap your data to those names, using the value type each kind expects:
$values = [ 'email' => 'ada@example.com', // email → string 'full_name' => 'Ada Lovelace', // text → string 'age' => 37, // number → number (37, not "37") 'subscribed' => true, // boolean → bool 'plan' => 'pro', // choice → one of the choice values 'interests' => ['news', 'beta'], // multi-choice → string[] 'signup_date' => '2026-05-29', // date → "YYYY-MM-DD" 'budget' => 1999.99, // money → number];
Client::submit(['form' => $form, 'values' => $values]);The full value type for every kind is in the field kinds reference.
Trust: who receives the submission
DmfReader::read independently verifies the FOBO attestation against the
baked-in root key. Use it to show the verified publisher before submitting:
$form = DmfReader::read($dmf);
if ($form->isFoboVerified()) { $a = $form->foboVerification; echo "Verified by FOBO as {$a['email']}"; // + $a['company'] when admin-verified}if (!$form->acceptsSubmissions()) { // share-only bundle: no sealed-box recipient, submissions can't route back}use DataMaker\Sdk\ValidationError;
try { Client::submit(['form' => $form, 'values' => $values]);} catch (ValidationError $e) { foreach ($e->issues as $i) { fwrite(STDERR, "{$i['field']}: {$i['message']}\n"); }}API
DmfReader::read($dmfBytes, $verify = true)→FormDescriptor(formId,name,schemaVersion,submitPolicy,recipientUserId,recipientPublicKey,signer,fields,verified,foboVerification,isFoboVerified(),acceptsSubmissions()). ThrowsDmfErroron tampering.Client::submit(['form' => …|'dmf' => …, 'values' => …, ...opts])→['submissionId', 'editToken', 'formId']. Options:apiBaseUrl,submitterId,validate(defaulttrue),allowUnknown,verify.Client::buildSubmission($form, $values, $opts)→ seal without sending (['submissionId', 'envelope', 'payload', 'values']).Client::postSubmission($envelope, $opts)→ post a pre-built envelope.Validator::validateValues($fields, $input, $allowUnknown)→['values', 'issues'].
Errors all extend DataMaker\Sdk\DataMakerError and carry a stable string in
->errorCode (PHP’s Exception::getCode() is an int, so the machine-readable
code lives on ->errorCode): DMF_INVALID, VALIDATION_FAILED (with
->issues), SUBMISSION_REJECTED (with ->status, ->body), NO_RECIPIENT
(share-only bundle — can’t receive submissions), NO_FORM (submit called with
neither a form nor .dmf bytes).
Scope
Sender-side only — read a .dmf and submit. It does not decrypt or receive
submissions; that’s the form owner’s app, which guards the X25519 private half.
The WordPress plugin uses this SDK to verify the bundles it renders.