The Console API allows you to automate your newsletter-related tasks over HTTP with API key authentication. This is the same API that we internally use at the Console.
https://post.hyvor.com/api/consoleAuthorization header to Bearer <API_KEY>.GET - Retrieve a resourcePOST - Create a resource or perform an actionPUT - Update a resourceDELETE - Remove a resourceJSON (recommended) or as application/x-www-form-urlencoded.In this documentation, all objects, request params, and responses are written as Typescript interfaces in order to make type declarations concise.
The Console API endpoints are categorized based on the resource they interact with.
Jump to each category:
Endpoints:
GET /newsletter - Get newsletter dataPATCH /newsletter - Update a newsletterDELETE /newsletter - Delete a newsletterObjects:
GET /newsletter type Request = {}
type Response = NewsletterPATCH /newsletter type Request = Partial<Newsletter> // except id, created_at
type Response = NewsletterDELETE /newsletter type Request = {}
type Response = {}Endpoints:
GET /issues - Get issuesPOST /issues - Create an issueGET /issues/{id} - Get an issuePATCH /issues/{id} - Update an issueDELETE /issues/{id} - Delete an issuePOST /issues/{id}/send - Send an issueGET /issues/{id}/preview - Preview an issueGET /issues/{id}/progress - Get issue sending progressGET /issues/{id}/sends - Get issue sendsGET /issues/{id}/report - Get issue reportObjects:
GET /issues type Request = {
limit?: number; // default: 50
offset?: number; // default: 0
}
type Response = Issue[]POST /issues type Request = {}
type Response = IssueGET /issues/{id} type Request = {}
type Response = IssuePATCH /issues/{id} type Request = {
subject?: string;
lists?: number[];
content?: string;
sending_profile_id?: number;
}
type Response = IssueDELETE /issues/{id} type Request = {}
type Response = {}POST /issues/{id}/send type Request = {}
type Response = IssueRenders the HTML preview of an issue, and returns the number of subscribers it would be sendable to.
GET /issues/{id}/preview type Request = {}
type Response = {
html: string;
sendable_subscribers_count: number;
}Get the sending progress of an issue that is currently being sent.
GET /issues/{id}/progress type Request = {}
type Response = {
total: number;
sent: number;
progress: number; // percentage, 0-100
} | null // null if the issue has no sends yetGET /issues/{id}/sends type Request = {
limit?: number; // default: 50
offset?: number; // default: 0
search?: string;
type?: string;
}
type Response = Send[]Get the delivery, open, click, bounce, and complaint counts of an issue.
GET /issues/{id}/report type Request = {}
type Response = {
counts: {
total: number;
pending: number;
sent: number;
failed: number;
unsubscribed: number;
bounced: number;
complained: number;
}
}Endpoints:
POST /lists - Create a listPATCH /lists/{id} - Update a listDELETE /lists/{id} - Delete a listObjects:
POST /lists type Request = {
name: string; // max length: 255
description?: string;
}
type Response = ListPATCH /lists/{id} type Request = {
name?: string; // max length: 255
description?: string;
}
type Response = ListDELETE /lists/{id} type Request = {}
type Response = {}Endpoints:
GET /subscribers - Get subscribersGET /subscribers/email/{email} - Get a subscriber
by emailPOST /subscribers - Create or update a subscriberPOST /subscribers/{id}/resend-opt-in - Resend opt-in
confirmation emailDELETE /subscribers/{id} - Delete a subscriberPOST /subscribers/bulk - Bulk update subscribersObjects:
GET /subscribers type Request = {
limit?: number; // default: 50
offset?: number; // default: 0
// filter by status
status?: 'subscribed' | 'unsubscribed' | 'pending';
// filter by list
list_id?: number;
// search by email
search?: string;
}
type Response = Subscriber[]GET /subscribers/email/{email} type Request = {}
type Response = Subscriber // 404 if not foundPOST /subscribers type Request = {
// If a subscriber with the given email already exists, it will be updated.
// Otherwise, a new subscriber will be created.
email: string;
// Subscribe to or unsubscribe from lists based
// on the given `lists_strategy`.
// an array of list IDs or names.
lists?: (number | string)[];
// The subscriber's subscription status
// default: subscribed
status?: 'subscribed' | 'pending';
// the source of the subscriber
// default: console
source?: 'console' | 'form' | 'import';
// subscriber's IP address
subscribe_ip?: string | null;
// unix timestamp of when the subscriber opted in
// if not set, it will be set to the current time if status is 'subscribed'
subscribed_at?: number | null; // unix timestamp
// additional metadata for the subscriber
// keys must be defined in the Subscriber Metadata Definitions section (or using the API)
metadata?: Record<string, string>;
// ============ SETTINGS ===========
// change how the endpoint behaves
// how `lists` field is processed when updating an existing subscriber's list subscriptions.
// merge: merges the lists (default)
// overwrite: overwrites the lists
// remove: removes from the current lists
lists_strategy?: 'merge' | 'overwrite' | 'remove';
// if the subscriber was previously removed from a list,
// define the reason(s) for skipping the re-subscription to that list.
// see below for more info
// default: ['unsubscribe', 'bounce', 'complaint']
list_skip_resubscribe_on?: ('unsubscribe' | 'bounce' | 'complaint' | 'other')[];
// define the reason for removing the subscriber from a list
// (only when updating, see below for more info)
// default: 'unsubscribe'
list_removal_reason?: 'unsubscribe' | 'bounce' | 'complaint' | 'other';
// whether to overwrite or merge the subscriber's metadata
// when updating an existing subscriber.
// default: 'merge'
metadata_strategy?: 'merge' | 'overwrite';
// whether to send a confirmation email when adding a subscriber with 'pending' status
// or when changing an existing subscriber's status to 'pending'.
// default: false
send_pending_confirmation_email?: boolean;
}
type Response = SubscriberFor all subscribers, Hyvor Post records the lists they have previously unsubscribed from. This makes it easier to build automations around list subscriptions while respecting subscribers' preferences.
list_skip_resubscribe_on: when adding an existing subscriber to a list they were
previously removed from, this setting controls which of the removal reasons below should block the
re-add. By default, previous unsubscribes, bounces, and complaints all block a re-add; pass an
empty array to always re-add regardless of why they left.
list_removal_reason:
unsubscribe - use this reason if the subscriber is explicitly asking to be removed
from the list (e.g. they unchecked a checkbox to unsubscribe). This will record an
unsubscription, blocking future re-adds unless the re-add request's list_skip_resubscribe_on excludes unsubscribe. Hyvor Post's default
unsubscribe form uses this.bounce - recorded automatically when a send to the subscriber hard-bounces.complaint - recorded automatically when the subscriber marks a send as spam.other - use this reason if you want to remove the subscriber from the list without recording
it as one of the reasons above (does not block future re-adds by default).{
"email": "example@example.com",
"lists": ["Default"]
}{
"email": "example@example.com",
"lists": [123],
"lists_strategy": "add"
}{
"email": "example@example.com",
"lists": ["Paid Users"],
"lists_strategy": "remove",
// unsubscribe, bounce, or other
"list_removal_reason": "unsubscribe"
}{
"email": "example@example.com",
"lists": ["Default"],
"status": "pending",
"send_pending_confirmation_email": true
}{
"email": "example@example.com",
"lists": ["Default"],
"lists_strategy": "add",
// ignore unsubscription if the subscriber was removed from the list due to a bounce
// but allow re-adding if they previously unsubscribed themselves
"list_skip_resubscribe_on": ["bounce"]
}To force re-adding both previous unsubscribes and bounces, use an empty array for list_skip_resubscribe_on.
Resends the opt-in confirmation email to a pending subscriber.
POST /subscribers/{id}/resend-opt-in type Request = {}
type Response = {}DELETE /subscribers/{id} type Request = {}
type Response = {}POST /subscribers/bulk type Request = {
subscribers_ids: number[];
action: 'delete' | 'status_change' | 'metadata_update';
status?: 'subscribed' | 'unsubscribed' | 'pending'; // required if action is status_change
metadata?: Record<string, string>; // required if action is metadata_update
}
type Response = {
status: string;
message: string;
subscribers: Subscriber[];
}Subscriber metadata definitions allow you to define custom fields for subscribers. These fields can be used to store additional information about subscribers.
Endpoints:
POST /subscriber-metadata-definitions - Create a subscriber metadata definitionPATCH /subscriber-metadata-definitions/{id} - Update a subscriber metadata definitionDELETE /subscriber-metadata-definitions/{id} - Delete a subscriber metadata definitionObjects:
POST /subscriber-metadata-definitions type Request = {
key: string; // max length: 255
name: string; // max length: 255
}
type Response = SubscriberMetadataDefinitionkey can only contain lowercase letters, numbers, and underscores.key cannot be changed.PATCH /subscriber-metadata-definitions/{id} type Request = {
name: string; // max length: 255
}
type Response = SubscriberMetadataDefinitionDELETE /subscriber-metadata-definitions/{id} type Request = {}
type Response = {}Endpoints:
GET /sending-profiles - Get sending profilesPOST /sending-profiles - Create a sending profilePATCH /sending-profiles/{id} - Update a sending
profileDELETE /sending-profiles/{id} - Delete a sending
profileObjects:
GET /sending-profiles type Request = {}
type Response = SendingProfile[]POST /sending-profiles type Request = {
from_email: string;
from_name?: string | null;
reply_to_email?: string | null;
brand_name?: string | null;
brand_logo?: string | null; // a publicly accessible URL of the logo
brand_url?: string | null;
}
type Response = SendingProfilePATCH /sending-profiles/{id} type Request = {
from_email?: string;
from_name?: string | null;
reply_to_email?: string | null;
brand_name?: string | null;
brand_logo?: string | null; // a publicly accessible URL of the logo
brand_url?: string | null;
is_default?: boolean;
}
type Response = SendingProfileThe system sending profile cannot be deleted.
DELETE /sending-profiles/{id} type Request = {}
type Response = SendingProfile[] // the newsletter's remaining sending profilesEndpoints:
GET /templates - Get newsletter templatePATCH /templates - Update newsletter templatePOST /templates/render - Render newsletter template with
contentObjects:
GET /templates type Request = {}
type Response = TemplatePATCH /templates type Request = {
template?: string;
}
type Response = TemplatePOST /templates/render type Request = {
template?: string | null;
}
type Response = {
html: string;
}Admins of the organization that owns the newsletter can be added as users to collaborate on managing it.
Endpoints:
GET /users - Get userPOST /users - Create userDELETE /users - Delete userObjects:
GET /users type Request = {}
type Response = User[]POST /users The user must already be a member of the organization that owns this newsletter.
type Request = {
user_id: number; // the user's id in HYVOR (AuthInterface)
// what to do if the user is already added to the newsletter
// throw: return a 400 error (default)
// ignore: return the existing user without an error
on_duplicate?: 'throw' | 'ignore';
}
type Response = UserDELETE /users type Request = {
// one of user_id or id is required
user_id?: number; // the user's id in HYVOR (AuthInterface)
id?: number; // the user's id in this newsletter's user list
}
type Response = {}Every API key is granted one or more scopes, which limit what resources and actions it can access. Available scopes:
newsletter.read / newsletter.write / newsletter.deleteissues.read / issues.writesending_profiles.read / sending_profiles.writesubscribers.read / subscribers.writeusers.read / users.writetemplates.read / templates.writeapi_keys.read / api_keys.writemedia.writedata.read / data.write - subscriber lists, imports, and exportsEndpoints:
GET /api-keys - Get API keysPOST /api-keys - Create an API keyPATCH /api-keys/{id} - Update an API keyPOST /api-keys/{id} - Regenerate an API keyDELETE /api-keys/{id} - Delete an API keyObjects:
The raw key is not returned; only its metadata is.
GET /api-keys type Request = {}
type Response = ApiKey[]POST /api-keys type Request = {
name: string; // max length: 255
scopes: string[]; // see the list of scopes above
}
type Response = ApiKeykey property (the raw key) is only returned once, on creation. Store it securely -
it cannot be retrieved again. PATCH /api-keys/{id} type Request = {
name?: string; // max length: 255
is_enabled?: boolean;
scopes?: string[];
}
type Response = ApiKeyRegenerates the raw key of an API key. The previous key is invalidated immediately, and the new raw key is returned once.
POST /api-keys/{id} type Request = {}
type Response = ApiKeyRequests made with the deleted key will be rejected immediately.
DELETE /api-keys/{id} type Request = {}
type Response = {}Endpoints:
POST /media - Upload mediaObjects:
POST /media type Request = {
// max size: 10MB
// supported formats: jpg, jpeg, png, gif, webp
file: File;
folder: 'issue_images' | 'newsletter_images';
}
type Response = MediaEndpoints:
GET /export - Get subscriber exportsPOST /export - Create a subscriber exportObjects:
GET /export type Request = {}
type Response = SubscriberExport[]POST /export type Request = {}
type Response = SubscriberExportinterface Newsletter {
id: string;
subdomain: string;
created_at: number; // unix timestamp
name: string;
language_code: string | null;
is_rtl: boolean;
metadata: Record<string, string>;
address: string | null;
unsubscribe_text: string | null;
branding: boolean;
template_color_accent: string | null;
template_color_accent_text: string | null;
template_color_background: string | null;
template_color_background_text: string | null;
template_color_box: string | null;
template_color_box_text: string | null;
template_box_shadow: string | null;
template_box_radius: string | null;
template_box_border: string | null;
template_font_family: string | null;
template_font_size: string | null;
template_font_weight: string | null;
template_font_weight_heading: string | null;
template_font_line_height: string | null;
form_title: string | null;
form_description: string | null;
form_footer_text: string | null;
form_button_text: string | null;
form_success_message: string | null;
form_width: number | null; // null = 100%
form_custom_css: string | null;
form_color_light_text: string | null; // null = inherit
form_color_light_text_light: string | null;
form_color_light_accent: string | null;
form_color_light_accent_text: string | null;
form_color_light_input: string | null;
form_color_light_input_text: string | null;
form_light_input_box_shadow: string | null;
form_light_input_border: string | null;
form_light_border_radius: number | null;
form_color_dark_text: string | null; // null = inherit
form_color_dark_text_light: string | null;
form_color_dark_accent: string | null;
form_color_dark_accent_text: string | null;
form_color_dark_input: string | null;
form_color_dark_input_text: string | null;
form_dark_input_box_shadow: string | null;
form_dark_input_border: string | null;
form_dark_border_radius: number | null;
form_default_color_palette: 'light' | 'dark' | 'os';
form_input_border_radius: number;
}interface Issue {
id: number;
uuid: string;
created_at: number; // unix timestamp
subject: string | null;
content: string | null;
sending_profile_id: number;
status: 'draft' | 'scheduled' | 'sending' | 'sent';
lists: number[];
scheduled_at: number | null; // unix timestamp
sending_at: number | null; // unix timestamp
sent_at: number | null; // unix timestamp
total_sends: number;
from_email: string | null;
from_name: string | null;
reply_to_email: string | null;
sendable_subscribers_count: number;
}interface Send {
id: number;
created_at: number; // unix timestamp
subscriber: Subscriber | null;
email: string;
status: 'pending' | 'sent' | 'failed';
sent_at: number | null; // unix timestamp
failed_at: number | null; // unix timestamp
delivered_at: number | null; // unix timestamp
unsubscribed_at: number | null; // unix timestamp
bounced_at: number | null; // unix timestamp
hard_bounce: boolean;
complained_at: number | null; // unix timestamp
}interface List {
id: number;
created_at: number; // unix timestamp
name: string;
description: string | null;
subscribers_count: number;
}interface Subscriber {
id: number;
email: string;
source: 'console' | 'form' | 'import';
status: 'subscribed' | 'pending';
list_ids: number[];
lists: string[]; // list names
subscribe_ip: string | null;
subscribed_at: number | null; // unix timestamp
metadata: Record<string, string>;
}interface SubscriberMetadataDefinition {
id: number;
created_at: number; // unix timestamp
key: string;
name: string;
type: 'text'; // only 'text' is currently supported
}interface SendingProfile {
id: number;
created_at: number; // unix timestamp
from_email: string;
from_name: string | null;
reply_to_email: string | null;
brand_name: string | null;
brand_logo: string | null;
brand_url: string | null;
is_default: boolean;
is_system: boolean;
}interface Template {
template: string;
}interface UserMiniObject {
name: string;
email: string;
username: string | null;
picture_url: string | null;
}interface User {
id: number;
role: 'owner' | 'admin';
created_at: number; // unix timestamp
user: UserMiniObject;
}interface Media {
id: number;
created_at: number; // unix timestamp
folder: 'issue_images' | 'newsletter_images' | 'import' | 'export';
url: string;
size: number; // in bytes
extension: string;
}interface SubscriberExport {
id: number;
created_at: number; // unix timestamp
status: 'pending' | 'completed' | 'failed';
error_message: string | null;
url: string | null;
}