Documentation
¶
Overview ¶
Package easydocforms is the official Go SDK for the EasyDocForms Partner API.
EasyDocForms turns a blank PDF intake form into a hosted, mobile-friendly fillable form — and returns the completed, pixel-exact PDF. The API wraps the same pipeline EasyDocForms uses in production: import a blank PDF, poll (or receive a webhook) until the template is ready, mint a hosted fill link, hand it to a patient, then retrieve structured answers and the completed PDF.
The SDK has no dependencies outside the standard library.
Quickstart ¶
client := easydocforms.NewClient(os.Getenv("EASYDOCFORMS_API_KEY"))
pong, err := client.Ping(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("authenticated as org %s (key %q, scopes %v)\n", pong.OrgID, pong.KeyName, pong.Scopes)
The full loop ¶
ctx := context.Background()
// 1. Import a blank PDF (async — returns immediately).
created, err := client.CreateImport(ctx, easydocforms.CreateImportParams{
PDFURL: "https://example.com/new-patient-intake.pdf",
Filename: "new-patient-intake.pdf",
BlankFormAttestation: true, // you attest the PDF is a blank template, no PHI
})
// 2. Wait for processing (typically 1–10 minutes).
imp, err := client.WaitForImport(ctx, created.ImportID, 5*time.Second)
// 3. Mint a hosted fill link and hand it to the patient.
link, err := client.CreateFillLink(ctx, easydocforms.CreateFillLinkParams{
TemplateID: imp.TemplateID,
ExternalRef: "visit-8675309", // your correlation id — MUST NOT contain PHI
})
// 4. On the submission.created webhook (or by other means), fetch results.
sub, err := client.GetSubmission(ctx, submissionID)
pdf, err := client.DownloadSubmissionPDF(ctx, submissionID)
Webhooks ¶
Deliveries are signed with an X-EDF-Signature header. Verify them with WebhookVerifier:
verifier := easydocforms.NewWebhookVerifier(os.Getenv("EASYDOCFORMS_WEBHOOK_SECRET"))
http.HandleFunc("/webhooks/easydocforms", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := verifier.ParseEvent(body, r.Header.Get(easydocforms.WebhookSignatureHeader))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
switch event.Event {
case string(easydocforms.EventSubmissionCreated):
var data easydocforms.SubmissionCreatedData
_ = event.DecodeData(&data)
// fetch answers with your API key via data.SubmissionID
}
w.WriteHeader(http.StatusOK)
})
PHI boundary ¶
Imports are blank forms only: every import requires BlankFormAttestation, asserting the uploaded PDF contains no patient-identifiable information. ExternalRef values must never contain PHI. Webhook payloads are PHI-minimized by design — they carry ids and retrieve URLs, never patient answers; answers are only available over the authenticated API.
API reference: https://easydocforms.com/docs/api
Index ¶
- Constants
- Variables
- func IsNotFound(err error) bool
- func IsPDFPending(err error) bool
- func SignWebhookPayload(secret string, t time.Time, body []byte) string
- type Client
- func (c *Client) CreateFillLink(ctx context.Context, params CreateFillLinkParams) (*FillLink, error)
- func (c *Client) CreateImport(ctx context.Context, params CreateImportParams) (*CreateImportResult, error)
- func (c *Client) CreateWebhook(ctx context.Context, params CreateWebhookParams) (*CreateWebhookResult, error)
- func (c *Client) DeleteWebhook(ctx context.Context, webhookID string) error
- func (c *Client) DownloadSubmissionPDF(ctx context.Context, submissionID string) ([]byte, error)
- func (c *Client) GetImport(ctx context.Context, importID string) (*Import, error)
- func (c *Client) GetSubmission(ctx context.Context, submissionID string) (*Submission, error)
- func (c *Client) GetSubmissionPDFLink(ctx context.Context, submissionID string) (*SubmissionPDFLink, error)
- func (c *Client) ListTemplates(ctx context.Context) ([]Template, error)
- func (c *Client) ListWebhooks(ctx context.Context) ([]Webhook, error)
- func (c *Client) Ping(ctx context.Context) (*Ping, error)
- func (c *Client) TestWebhook(ctx context.Context, webhookID string) (*WebhookTestResult, error)
- func (c *Client) WaitForImport(ctx context.Context, importID string, pollInterval time.Duration) (*Import, error)
- type CreateFillLinkParams
- type CreateImportParams
- type CreateImportResult
- type CreateWebhookParams
- type CreateWebhookResult
- type Error
- type Event
- type EventType
- type FillLink
- type Import
- type ImportCompletedData
- type ImportFailedData
- type ImportStatus
- type Option
- type Ping
- type Scope
- type Submission
- type SubmissionCreatedData
- type SubmissionPDFLink
- type SubmissionPDFReadyData
- type Template
- type TestData
- type Webhook
- type WebhookTestResult
- type WebhookVerifier
- type WebhookVerifierOption
Constants ¶
const ( // ErrCodePartnerAPINotEnabled: the organization is not enrolled in the // Partner API. ErrCodePartnerAPINotEnabled = "PARTNER_API_NOT_ENABLED" // ErrCodeScopeRequired: the key authenticated but lacks the scope this // route requires. ErrCodeScopeRequired = "SCOPE_REQUIRED" )
Machine-readable error codes returned on authorization failures.
const DefaultBaseURL = "https://form.easydocforms.com/api/v1"
DefaultBaseURL is the production Partner API endpoint.
const DefaultWebhookTolerance = 5 * time.Minute
DefaultWebhookTolerance is how far a delivery's timestamp may drift from the verifier's clock before it is rejected as a possible replay.
const Version = "0.1.0"
Version is the SDK version, reported in the User-Agent header.
const WebhookSignatureHeader = "X-EDF-Signature"
WebhookSignatureHeader is the HTTP header carrying a delivery's signature.
Variables ¶
var ( // ErrMalformedSignature: the header is not "t=<unix>,v1=<hex>". ErrMalformedSignature = errors.New("easydocforms: malformed webhook signature header") // ErrSignatureMismatch: no v1 signature matches the payload. ErrSignatureMismatch = errors.New("easydocforms: webhook signature mismatch") // ErrSignatureExpired: the signature is valid but its timestamp is outside // the replay tolerance. ErrSignatureExpired = errors.New("easydocforms: webhook timestamp outside tolerance") )
Webhook verification failures. All three wrap into the errors returned by WebhookVerifier.Verify; match with errors.Is.
Functions ¶
func IsNotFound ¶
IsNotFound reports whether err is an API 404 — no such resource in your organization.
func IsPDFPending ¶
IsPDFPending reports whether err is the pdf-link 409: the frozen completed PDF is not available (yet). Fall back to Client.DownloadSubmissionPDF, which renders on demand.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a client for the EasyDocForms Partner API. It is safe for concurrent use. Construct it with NewClient.
func NewClient ¶
NewClient returns a Client authenticated with an edfk_live_* API key. Keys are created in the EasyDocForms app under Settings → Integrations → Partner API, and are shown exactly once.
func (*Client) CreateFillLink ¶
func (c *Client) CreateFillLink(ctx context.Context, params CreateFillLinkParams) (*FillLink, error)
CreateFillLink mints a hosted URL where a patient fills the form (scope fill_links:write). Hand the returned URL to the patient; no EasyDocForms account is needed on their side.
func (*Client) CreateImport ¶
func (c *Client) CreateImport(ctx context.Context, params CreateImportParams) (*CreateImportResult, error)
CreateImport stages a blank PDF and queues processing (scope imports:write). Processing typically takes 1–10 minutes; poll with Client.GetImport or Client.WaitForImport, or subscribe to the import.completed / import.failed webhooks.
func (*Client) CreateWebhook ¶
func (c *Client) CreateWebhook(ctx context.Context, params CreateWebhookParams) (*CreateWebhookResult, error)
CreateWebhook registers a delivery URL and mints its signing secret (scope webhooks:manage). The secret is returned exactly once. At most 10 active webhooks per organization.
func (*Client) DeleteWebhook ¶
DeleteWebhook deactivates a subscription (scope webhooks:manage). The record is retained for audit.
func (*Client) DownloadSubmissionPDF ¶
DownloadSubmissionPDF returns the completed, pixel-exact PDF bytes (scope submissions:read). It serves the artifact frozen at submission when available, falling back to an on-demand render — it works even while the submission's CompletedPDFStatus is "pending" (the fetch is just slower).
func (*Client) GetSubmission ¶
GetSubmission fetches a patient submission: structured answers plus correlation back to the fill link that produced it (scope submissions:read).
func (*Client) GetSubmissionPDFLink ¶
func (c *Client) GetSubmissionPDFLink(ctx context.Context, submissionID string) (*SubmissionPDFLink, error)
GetSubmissionPDFLink returns a time-limited signed URL (valid ~10 minutes) that downloads the completed PDF without any Authorization header (scope submissions:read) — a handoff link safe to pass onward without embedding your API key. Every call is audit-logged.
Only the artifact frozen at submission can be signed: while the submission's CompletedPDFStatus is "pending" this returns an error for which IsPDFPending is true — fall back to Client.DownloadSubmissionPDF.
func (*Client) ListTemplates ¶
ListTemplates returns the organization's active PDF templates, newest first (scope templates:read).
func (*Client) ListWebhooks ¶
ListWebhooks returns the organization's active subscriptions, newest first, without secrets (scope webhooks:manage).
func (*Client) Ping ¶
Ping proves the key authenticates, names the organization it is bound to, and echoes its scopes. It requires no scope — every valid key can call it.
func (*Client) TestWebhook ¶
TestWebhook synchronously delivers one signed test event to the subscription's URL (scope webhooks:manage), so you can verify your receiver and signature check end to end.
func (*Client) WaitForImport ¶
func (c *Client) WaitForImport(ctx context.Context, importID string, pollInterval time.Duration) (*Import, error)
WaitForImport polls GetImport until the job reaches a terminal status. pollInterval <= 0 defaults to 5 seconds.
A terminal Import is returned with a nil error even when its Status is ImportFailed — check Status and ErrorMessage. A non-nil error means the wait itself failed (context cancelled, transport or API error while polling).
type CreateFillLinkParams ¶
type CreateFillLinkParams struct {
TemplateID string `json:"template_id"`
// ExpiresInDays is how many days until the link stops accepting
// responses. 0 = no expiry.
ExpiresInDays int `json:"expires_in_days,omitempty"`
// MaxResponses caps how many submissions the link accepts. 0 = unlimited.
MaxResponses int `json:"max_responses,omitempty"`
// ExternalRef is an opaque correlation id (≤256 chars) echoed on
// submissions and webhook events. It MUST NOT contain PHI.
ExternalRef string `json:"external_ref,omitempty"`
}
CreateFillLinkParams is the request for Client.CreateFillLink.
type CreateImportParams ¶
type CreateImportParams struct {
// PDFBase64 is the blank PDF, standard base64.
PDFBase64 string `json:"pdf_base64,omitempty"`
// PDFURL is a public HTTPS URL of the blank PDF. Private/internal
// addresses are rejected.
PDFURL string `json:"pdf_url,omitempty"`
// Filename is the original filename, e.g. "new-patient-intake.pdf".
Filename string `json:"filename"`
// Title optionally sets the display title of the resulting template.
Title string `json:"title,omitempty"`
// BlankFormAttestation must be true: you attest this PDF is a blank form
// template containing no patient-identifiable information.
BlankFormAttestation bool `json:"blank_form_attestation"`
}
CreateImportParams is the request for Client.CreateImport. Supply the document as PDFBase64 OR a publicly reachable HTTPS PDFURL — exactly one. Maximum PDF size is 10 MB.
type CreateImportResult ¶
CreateImportResult acknowledges a queued import.
type CreateWebhookParams ¶
type CreateWebhookParams struct {
// URL is the delivery endpoint. Public HTTPS only — private/internal
// addresses are rejected.
URL string `json:"url"`
// Events filters which events this subscription receives. Empty or
// omitted = all events. EventTest is not valid here.
Events []EventType `json:"events,omitempty"`
}
CreateWebhookParams is the request for Client.CreateWebhook.
type CreateWebhookResult ¶
type CreateWebhookResult struct {
Webhook Webhook `json:"webhook"`
// Secret is the whsec_* signing secret, shown ONLY in this response.
// Store it; it verifies the X-EDF-Signature header on every delivery
// (see WebhookVerifier).
Secret string `json:"secret"`
}
CreateWebhookResult is the subscription plus its one-time signing secret.
type Error ¶
type Error struct {
// StatusCode is the HTTP status of the response.
StatusCode int
// Message is the human-readable error from the API.
Message string
// Code is the machine-readable code on authorization failures
// (ErrCodePartnerAPINotEnabled, ErrCodeScopeRequired); empty otherwise.
Code string
// Hint, when present, says what to do instead (e.g. the pdf-link 409
// points at the /pdf streaming endpoint).
Hint string
// CompletedPDFStatus is set on the pdf-link 409 response ("pending").
CompletedPDFStatus string
}
Error is an API-level failure: the request reached EasyDocForms and was rejected. Transport failures are returned as ordinary wrapped errors, not *Error. Use errors.As to inspect:
var apiErr *easydocforms.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { ... }
type Event ¶
type Event struct {
// Event is the event type, e.g. "submission.created". Compare against the
// EventType constants.
Event string `json:"event"`
Timestamp time.Time `json:"timestamp"`
OrgID string `json:"org_id"`
Data json.RawMessage `json:"data"`
}
Event is a verified webhook delivery envelope. Data holds the event-specific payload; decode it with Event.DecodeData after switching on Event.
func (*Event) DecodeData ¶
DecodeData unmarshals the event-specific payload into v — typically one of ImportCompletedData, ImportFailedData, SubmissionCreatedData, SubmissionPDFReadyData, TestData.
type EventType ¶
type EventType string
EventType identifies a webhook event. The four non-test events are valid in a subscription's event filter; EventTest arrives only from the test endpoint.
const ( EventImportCompleted EventType = "import.completed" EventImportFailed EventType = "import.failed" EventSubmissionCreated EventType = "submission.created" EventSubmissionPDFReady EventType = "submission.pdf_ready" // EventTest is the synthetic event sent by TestWebhook. It is not valid in // subscription event filters. EventTest EventType = "test" )
type FillLink ¶
type FillLink struct {
FillLinkID string `json:"fill_link_id"`
TemplateID string `json:"template_id"`
ShortCode string `json:"short_code"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
ExternalRef string `json:"external_ref,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
MaxResponses int `json:"max_responses,omitempty"`
}
FillLink is a hosted URL where a patient fills the form — no EasyDocForms account needed on their side. Links always serve the template's latest version, so re-importing an updated PDF propagates to live links.
type Import ¶
type Import struct {
ImportID string `json:"import_id"`
Status ImportStatus `json:"status"`
Filename string `json:"filename"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TemplateID string `json:"template_id,omitempty"`
PageCount int `json:"page_count,omitempty"`
FieldCount int `json:"field_count,omitempty"`
// Detector names the detection pipeline that produced the template
// (e.g. "acroform", "born-digital", "azure").
Detector string `json:"detector,omitempty"`
// ReviewRequired is true when the pipeline recommends a human double-check
// the template in the EasyDocForms editor before sending it to patients.
// Imports never fail for quality reasons — the template is always created.
ReviewRequired bool `json:"review_required,omitempty"`
ReviewReasons []string `json:"review_reasons,omitempty"`
Warnings []string `json:"warnings,omitempty"`
ErrorMessage string `json:"error,omitempty"`
}
Import is an import job. Result fields (TemplateID, counts, review flags) are populated only when Status is ImportSucceeded; ErrorMessage only when Status is ImportFailed.
type ImportCompletedData ¶
type ImportCompletedData struct {
ImportID string `json:"import_id"`
TemplateID string `json:"template_id"`
PageCount int `json:"page_count"`
FieldCount int `json:"field_count"`
ReviewRequired bool `json:"review_required"`
}
ImportCompletedData is the payload of an import.completed event.
type ImportFailedData ¶
ImportFailedData is the payload of an import.failed event.
type ImportStatus ¶
type ImportStatus string
ImportStatus is the lifecycle state of an import job.
const ( ImportQueued ImportStatus = "queued" ImportProcessing ImportStatus = "processing" ImportSucceeded ImportStatus = "succeeded" ImportFailed ImportStatus = "failed" )
type Option ¶
type Option func(*Client)
Option configures a Client.
func WithBaseURL ¶
WithBaseURL overrides the API base URL (default DefaultBaseURL).
func WithHTTPClient ¶
WithHTTPClient supplies a custom *http.Client — for proxies, instrumentation, or a different timeout. The default client times out after 60 seconds.
func WithUserAgent ¶
WithUserAgent replaces the User-Agent header sent with every request.
type Ping ¶
type Ping struct {
OrgID string `json:"org_id"`
KeyName string `json:"key_name"`
Scopes []Scope `json:"scopes"`
}
Ping is the authenticated key's identity, from Client.Ping.
type Scope ¶
type Scope string
Scope is a permission carried by an API key. A request without the required scope fails with 403 SCOPE_REQUIRED.
type Submission ¶
type Submission struct {
SubmissionID string `json:"submission_id"`
SubmittedAt time.Time `json:"submitted_at"`
// Answers maps the template's field ids to submitted values. Signatures
// and drawings are rendered into the completed PDF, not included here.
Answers map[string]any `json:"answers"`
TemplateID string `json:"template_id"`
// CompletedPDFStatus is "ready" (the frozen completed PDF exists and
// pdf-link can sign it) or "pending" (fetch via DownloadSubmissionPDF,
// which renders on demand; never "lost").
CompletedPDFStatus string `json:"completed_pdf_status"`
FillLinkID string `json:"fill_link_id,omitempty"`
ExternalRef string `json:"external_ref,omitempty"`
}
Submission is a patient's completed form: structured answers plus correlation back to the fill link that produced it.
type SubmissionCreatedData ¶
type SubmissionCreatedData struct {
SubmissionID string `json:"submission_id"`
TemplateID string `json:"template_id"`
FillLinkID string `json:"fill_link_id"`
ExternalRef string `json:"external_ref"`
RetrieveURL string `json:"retrieve_url"`
}
SubmissionCreatedData is the payload of a submission.created event. PHI-minimized: no answers — fetch them from RetrieveURL with your API key.
type SubmissionPDFLink ¶
SubmissionPDFLink is a time-limited signed URL (valid ~10 minutes) that downloads the completed PDF without any Authorization header — safe to pass to a browser, an EMR, or an AI agent's user without embedding your API key. Treat the URL as a bearer credential for this one document.
type SubmissionPDFReadyData ¶
type SubmissionPDFReadyData struct {
SubmissionID string `json:"submission_id"`
TemplateID string `json:"template_id"`
ExternalRef string `json:"external_ref"`
PDFURL string `json:"pdf_url"`
}
SubmissionPDFReadyData is the payload of a submission.pdf_ready event.
type Template ¶
type Template struct {
TemplateID string `json:"template_id"`
Title string `json:"title"`
SourceFilename string `json:"source_filename"`
PageCount int `json:"page_count"`
FieldCount int `json:"field_count"`
Detector string `json:"detector"`
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Template is an active PDF template — created via this API or in the EasyDocForms app alike.
type TestData ¶
type TestData struct {
WebhookID string `json:"webhook_id"`
}
TestData is the payload of the synthetic test event.
type Webhook ¶
type Webhook struct {
WebhookID string `json:"webhook_id"`
OrganizationID string `json:"organizationId"`
URL string `json:"url"`
// Events is the subscription's filter. Empty = subscribed to all events.
Events []EventType `json:"events"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Webhook is a delivery subscription, listed without its signing secret.
type WebhookTestResult ¶
type WebhookTestResult struct {
Delivered bool `json:"delivered"`
// StatusCode is your endpoint's HTTP status, when it was reachable.
StatusCode int `json:"status_code,omitempty"`
// Error is the transport error, when it was not.
Error string `json:"error,omitempty"`
}
WebhookTestResult is the outcome of a test delivery. A delivery failure is reported here, not as an HTTP error.
type WebhookVerifier ¶
type WebhookVerifier struct {
// contains filtered or unexported fields
}
WebhookVerifier authenticates deliveries against a subscription's whsec_* secret (returned once by Client.CreateWebhook). It is safe for concurrent use.
func NewWebhookVerifier ¶
func NewWebhookVerifier(secret string, opts ...WebhookVerifierOption) *WebhookVerifier
NewWebhookVerifier returns a verifier for one subscription's secret.
func (*WebhookVerifier) ParseEvent ¶
func (v *WebhookVerifier) ParseEvent(body []byte, header string) (*Event, error)
ParseEvent verifies a delivery and unmarshals its envelope. Use it as the single entry point of a webhook receiver.
func (*WebhookVerifier) Verify ¶
func (v *WebhookVerifier) Verify(body []byte, header string) error
Verify authenticates a delivery: body must be the RAW request body bytes (before any JSON parsing), header the X-EDF-Signature value. It recomputes the HMAC, compares in constant time, and rejects timestamps outside the tolerance. A nil return means the delivery is authentic.
type WebhookVerifierOption ¶
type WebhookVerifierOption func(*WebhookVerifier)
WebhookVerifierOption configures a WebhookVerifier.
func WithTolerance ¶
func WithTolerance(d time.Duration) WebhookVerifierOption
WithTolerance overrides the replay tolerance (default DefaultWebhookTolerance).