Writing
Manage reusable Copywriter data and asynchronously generate email or LinkedIn copy.
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
/v1/writing/temperature0 credits/v1/writing/temperature0 credits/v1/writing/temperature0 creditsRead 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.
| Parameter | Type | Description |
|---|---|---|
temperaturerequired | number (0–2) | null | The 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.
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}'{
"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:
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
/v1/writing/options0 credits| Parameter | Type | Description |
|---|---|---|
identity_id | string | Include 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_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
/v1/writing-jobsConfigured by channel| Parameter | Type | Description |
|---|---|---|
source_moderequired | string | saved, inline, or hybrid. |
channelrequired | string | email or linkedin. |
linkedin_type | string | message or connection_request. |
identity_id | string | Required for saved and hybrid modes. |
identity | object | Caller-supplied identity data. |
sender | object | Caller-supplied sender context. |
target | object | Recipient and target-company context. |
writing | object | Writing preferences and inline content. |
sender.full_name | string | Canonical sender name; required when sender is supplied. |
sender.title | string | Sender role or title used in the generated signature. |
sender.company_name | string | Sender organization used in the generated signature. |
sender.signature | string | Optional multiline signature preserved in completed email output. |
writing.play_source | string | template or personal. |
writing.personal_play_id | string | Canonical saved personal-play selector. |
writing.value_prop_id | string | Specific owned value-proposition ID, auto, or none. |
writing.case_study_id | string | Specific owned case-study ID, auto, or none. |
writing.content_angle_id | string | Specific owned content-angle ID, auto, or none. |
writing.value_proposition_selection | object | Selection object; mutually exclusive with writing.value_prop_id. |
writing.case_study_selection | object | Selection object; mutually exclusive with writing.case_study_id. |
writing.content_angle_selection | object | Selection object; mutually exclusive with writing.content_angle_id. |
writing.cta_id | string | Canonical saved CTA selector. |
writing.instructions | string | Optional generation guidance, limited to 4,000 characters. Invalid or oversized values are rejected before credits are reserved. |
dry_run | boolean | Resolve the same selected context as a real job without generating prose, creating a job, recording usage, reserving credits, or charging credits. |
include_ai_costs | boolean | Internal-token diagnostics only; external keys receive 403. |
force_failure | boolean | Internal-token test access only. Forces a terminal failure so reservation release and zero-charge accounting can be verified; external keys receive 403. |
{
"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"
}
}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.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_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.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:
{
"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
savedusesidentity_idand optional saved play, CTA, and subject IDs.inlineuses supplied identity, sender, target, and writing data without saving it. An explicitly suppliedsender.full_name,sender.title,sender.company_name, andsender.signatureare used for the generated context and signature.identity.brandVoiceaccepts either an object or a descriptive string.hybridapplies 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
/v1/writing-jobs0 credits/v1/writing-jobs/{job_id}0 credits| Parameter | Type | Description |
|---|---|---|
wait_ms | integer (0–5000) | Optional bounded long-poll duration. Waits for progress or terminal-state changes and works without last_event_count. |
last_event_count | integer | Optional 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.
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
/v1/usage/writing0 credits/v1/usage/ai-costs0 creditsWriting 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.
Keys are issued by our team, not a signup form. Book a 25-minute walkthrough and you'll leave with sandbox and live credentials.