Hillwinds API
API Reference

Writing

Manage reusable Copywriter data and asynchronously generate email or LinkedIn copy.

Scopes, key type, and credits
Reads require writing:read, resource mutations require writing:write, and generation requires writing:generate. Live keys are required. An accepted job reserves the configured writing-email or writing-linkedin credit rate, but only a completed job is charged. Failed, errored, and timed-out jobs release the reservation and settle to zero charged credits; the other endpoints on this page are free.

Personal Play Temperature

GET/v1/writing/temperature0 credits
POST/v1/writing/temperature0 credits
PATCH/v1/writing/temperature0 credits

Read or change the authenticated user's Personal Play temperature. Lower values keep generated writing closer to the saved play; higher values allow more variation. The setting applies to both personalization and contextualization when writing.play_source is personal. Template plays continue to use their configured prompt temperature.

ParameterTypeDescription
temperaturerequirednumber (0–2) | nullThe user's override. Send null to clear it and resume using the global value.

GET requires writing:read. POST and PATCH have identical setter behavior and require writing:write. Root keys include both scopes; a restricted child key must be granted the relevant scope. The request never accepts a user ID, so it cannot change another user's setting. All keys linked to the same user share the same override.

bash
curl -X POST "https://api.hillwinds.ai/v1/writing/temperature" \
  -H "Authorization: Bearer $HILLWINDS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"temperature": 0.3}'
json
{
  "ok": true,
  "meta": {"credits_charged": 0},
  "data": {
    "global_temperature": 0.1,
    "personal_play_temperature_override": 0.3,
    "effective_temperature": 0.3,
    "temperature_source": "user"
  }
}

To restore the global value, send {"temperature": null} with POST or PATCH. The response then reports personal_play_temperature_override: null and temperature_source: "global".

Plays, CTAs, and Subject Lines

Each collection supports list, create, detail, partial update, and soft delete:

text
GET|POST          /v1/writing-plays
GET|PATCH|DELETE /v1/writing-plays/{id}

GET|POST          /v1/writing-ctas
GET|PATCH|DELETE /v1/writing-ctas/{id}

GET|POST          /v1/writing-subjects
GET|PATCH|DELETE /v1/writing-subjects/{id}

Lists accept page, page_size, and search. Plays use { name, subject?, body, source?: manual|draft }; CTAs use { name, body }; subjects use { name, text }. Play creation also accepts a plays array of up to 100 items.

Every single-resource create returns data as one object. Only a play request using { plays: [...] } returns an array, with meta.bulk: true.

Discover Writing Options

GET/v1/writing/options0 credits
ParameterTypeDescription
identity_idstringInclude choices derived from a saved marketing identity.

Returns saved plays, CTAs, and subjects; supported channels and modes; and identity-derived ICP, pain point, case study, value proposition, and content angle choices. Placeholder rows, incomplete case studies, duplicate IDs, and testimonial quotes without a customer name are excluded from selectable identity content.

Identity choices require identity_id
When identity_id is omitted, personal plays, CTAs, and subjects are still returned, while identity-derived lists are empty. The response includes a warnings entry with code IDENTITY_ID_REQUIRED and the affected fields so the empty lists are never ambiguous.

Start a Writing Job

POST/v1/writing-jobsConfigured by channel
ParameterTypeDescription
source_moderequiredstringsaved, inline, or hybrid.
channelrequiredstringemail or linkedin.
linkedin_typestringmessage or connection_request.
identity_idstringRequired for saved and hybrid modes.
identityobjectCaller-supplied identity data.
senderobjectCaller-supplied sender context.
targetobjectRecipient and target-company context.
writingobjectWriting preferences and inline content.
sender.full_namestringCanonical sender name; required when sender is supplied.
sender.titlestringSender role or title used in the generated signature.
sender.company_namestringSender organization used in the generated signature.
sender.signaturestringOptional multiline signature preserved in completed email output.
writing.play_sourcestringtemplate or personal.
writing.personal_play_idstringCanonical saved personal-play selector.
writing.value_prop_idstringSpecific owned value-proposition ID, auto, or none.
writing.case_study_idstringSpecific owned case-study ID, auto, or none.
writing.content_angle_idstringSpecific owned content-angle ID, auto, or none.
writing.value_proposition_selectionobjectSelection object; mutually exclusive with writing.value_prop_id.
writing.case_study_selectionobjectSelection object; mutually exclusive with writing.case_study_id.
writing.content_angle_selectionobjectSelection object; mutually exclusive with writing.content_angle_id.
writing.cta_idstringCanonical saved CTA selector.
writing.instructionsstringOptional generation guidance, limited to 4,000 characters. Invalid or oversized values are rejected before credits are reserved.
dry_runbooleanResolve the same selected context as a real job without generating prose, creating a job, recording usage, reserving credits, or charging credits.
include_ai_costsbooleanInternal-token diagnostics only; external keys receive 403.
force_failurebooleanInternal-token test access only. Forces a terminal failure so reservation release and zero-charge accounting can be verified; external keys receive 403.
json
{
  "source_mode": "inline",
  "channel": "email",
  "identity": {
    "companyName": "Acme Bennies",
    "valueProps": [{"id": "vp-renewals", "feature": "Renewal cleanup"}],
    "caseStudies": [{"id": "case-renewals", "result": "Saved 15 hours"}],
    "contentAngles": [{"id": "angle-ops", "headline": "Cleaner renewal handoffs"}]
  },
  "sender": {
    "full_name": "Dana Whitfield",
    "title": "Head of Sales",
    "company_name": "Acme Bennies",
    "signature": "Dana Whitfield\nHead of Sales\nAcme Bennies"
  },
  "target": {"first_name": "Jordan", "company_name": "Example Benefits"},
  "writing": {
    "value_prop_id": "vp-renewals",
    "case_study_id": "none",
    "content_angle_id": "angle-ops",
    "content_mode": "contextualization"
  }
}
Field names are case-sensitive
The top-level request, sender, target, and writing objects reject unknown fields. Use the exact snake_case names documented here. Incorrect casing such as sourceMode, ctaId, or fullName returns 400 VALIDATION_ERROR. The nested identity object intentionally uses the casing returned by GET /v1/identities/schema.
Always send Idempotency-Key
Retrying the same Idempotency-Key returns the original job without charging credits or executing generation twice. Rejected requests are validated before a job or execution record is created and before credits are charged.
Dry runs use real context resolution
Set dry_run: true to run the same automatic selection and personal-play preference resolution used by a real job. The response returns resolved_context, job_created: false, and zero charged credits, but no prose is generated and no writing job is persisted.
Context values must be objects
When present, identity, sender, target, and writing must each be a JSON object. Inline defaults are used only when a value is omitted; a malformed supplied value returns 400 VALIDATION_ERROR.

A rejected request uses the standard error envelope and always reports zero credits:

json
{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One or more request fields are not supported.",
    "details": [
      {"field": "writing.ctaId", "issue": "unknown_field"}
    ],
    "request_id": "req_..."
  },
  "meta": {"credits_charged": 0}
}

An accepted request returns 202 with the queued job, the held amount in meta.credits_reserved, and meta.credits_charged: 0. The configured defaults are 5 reserved credits for email and 3 for LinkedIn. A repeated Idempotency-Key returns the existing job with 200, meta.idempotency_reused: true, and zero additional credits.

Source and Selection Modes

  • saved uses identity_id and optional saved play, CTA, and subject IDs.
  • inline uses supplied identity, sender, target, and writing data without saving it. An explicitly supplied sender.full_name, sender.title, sender.company_name, and sender.signature are used for the generated context and signature. identity.brandVoice accepts either an object or a descriptive string.
  • hybrid applies supplied overrides to a saved identity; arrays replace rather than merge.

A structured brand voice may include avoid: string[]. Listed terms, phrases, and punctuation styles are passed as hard generation constraints and checked against raw model output; em dash and em dashes also prohibit the Unicode em dash character.

Resolution always starts with the saved identity and then applies inline overrides, so inline scalar and object values win. Nested objects are merged recursively; arrays are replaced in full rather than appended or deduplicated. The same resolution rules apply to email, LinkedIn connection requests, and LinkedIn messages.

A valid explicit sender is never replaced by the account owner. When sender is omitted, the account default is the saved email signature, then the authenticated user's full name, then the local part of their email address. Completed output exposes the applied sender with source: "explicit" or source: "account_default" under output.resolved_context.sender. The deprecated sender.name and sender.company compatibility aliases remain accepted, but new clients should use the canonical fields above.

CTA precedence is consistent in inline and hybrid modes: a non-empty writing.cta string or object wins first, then an explicit inline or saved cta_id, then the saved or template fallback. For example, an inline CTA asking for a 15-minute comparison is passed to generation unchanged in intent and is not replaced by a saved video CTA.

The canonical direct selectors are writing.personal_play_id, writing.value_prop_id, writing.case_study_id, writing.content_angle_id, and writing.cta_id. A personal play also requires writing.play_source: "personal". Value proposition, case study, and content angle selectors accept a specific owned identity item ID, auto, or none: a specific ID forces that item, auto leaves the category available for generation, and none removes it from resolved context.

The equivalent selector objects accept { selection: auto|specific|none, selected_ids: [...] }. A direct selector and its equivalent selection object cannot be combined. Invalid, missing, or cross-organization references return the same non-disclosing invalid_reference validation result before job creation or billing. A specific saved or inline subject is returned unchanged; otherwise subject instructions guide generation.

List and Poll Jobs

GET/v1/writing-jobs0 credits
GET/v1/writing-jobs/{job_id}0 credits
ParameterTypeDescription
wait_msinteger (0–5000)Optional bounded long-poll duration. Waits for progress or terminal-state changes and works without last_event_count.
last_event_countintegerOptional last observed event count. When supplied, return when the count changes or wait_ms expires.

List by optional status or channel, then poll the detail route. Completed jobs include output, resolved identity selections, and normalized target context. Completed history is retained for 30 days by default across API and worker restarts. Channel filters also include older records whose channel is derived from persisted output.

Poll until status is completed or failed; a 202 response alone is not completion. List and detail responses expose the same persisted output and resolved context. Accepted jobs reserve credits while queued or running. Only a completed job finalizes the charge; a failed, errored, or timed-out job releases the reservation and reports billing.credits_charged: 0. This is charge-on-success, not a refund: the compatibility field billing.credits_refunded is always zero because no charge was finalized.

AI cost diagnostics are internal-only
Internal test access can request include_ai_costs=true. Results include estimated USD cost, pricing version, input/cached/output tokens, and per-model calls. Prompts, inline input, and generated copy are excluded from usage logs.

Writing Usage Reports

GET/v1/usage/writing0 credits
GET/v1/usage/ai-costs0 credits

Writing usage requires usage:read, accepts from and to, and groups outcomes and credits by day, channel, and operation. AI-cost reporting is internal-token only. HTTP requests and background execution records use the standard API usage-log format for later reporting. Create rows retain their email or LinkedIn channel and configured charge; worker execution rows use the separate execute operation and charge zero, preventing double counting.

Need a key?

Keys are issued by our team, not a signup form. Book a 25-minute walkthrough and you'll leave with sandbox and live credentials.