Writing a Connector
Daymark ships with seven built-in mocked destinations (Bluesky, Mastodon, Instagram, YouTube, TikTok, Threads, X) — none of them make a real network call. Core Daymark deliberately owns no social API credentials or OAuth flows (see the plugin's non-goals). A real destination is added by a separate plugin that implements one interface and registers itself on init — this page is a complete, minimal example of doing that.
The contract
A connector implements Daymark_Syndication_Connector
(includes/connectors/interface-syndication-connector.php):
interface Daymark_Syndication_Connector {
public function get_id(): string;
public function get_label(): string;
public function supports_daymark_type( string $type ): bool;
public function is_connected(): bool;
public function publish( int $post_id, array $payload ): array;
public function get_status_label(): string;
}
get_id()— a unique machine slug, e.g.'my-network'.get_label()— the human-readable name shown in the composer's destination list.supports_daymark_type( $type )— whether this destination can represent a given Mark type (note,image,video,audio,gallery,mixed). Daymark checks this both in the UI (to grey out an unsupported toggle) and again server-side before ever callingpublish().is_connected()—trueonly once real credentials are configured. A connector that returnsfalsehere is never offered as a publish destination and never receives type-based auto-defaults (see "Destination visibility" in CLAUDE.md) — it can still be selected explicitly via the REST API, in which casepublish()should fail gracefully rather than assume a live session.publish( $post_id, $payload )— must never throw. Always return a result array, even on failure.get_status_label()— a short UI string, e.g.'Connected'or'Not connected'.
The publish() payload
$payload is the same Mark context Daymark's own daymark_published action
receives — build against these keys, not undocumented internals:
array(
'post_id' => 123,
'primary_type' => 'image', // note|image|video|audio|gallery|mixed
'media_ids' => array( 45, 46 ), // attachment IDs, in display order
'caption' => 'A day at the coast.',
'syndication_targets' => array( 'my-network' ), // every selected destination this publish
'default_destinations' => array( 'my-network' ), // type-based defaults that applied
'post_status' => 'publish', // or 'draft' — drafts never syndicate
'ai_assist_used' => false,
'created_from' => 'mobile',
)
Resolve the Mark's permalink and any per-image alt text from $post_id
itself (get_permalink(), get_post_meta()) rather than expecting them on
the payload — it deliberately carries only what routing/composition needs,
not the full post.
The result shape
return array(
'success' => true,
'external_id' => 'abc123',
'external_url' => 'https://example.com/posts/abc123',
'status' => 'published', // 'published' | 'mocked' | 'failed'
'message' => '',
'backflow_supported' => true, // optional — omit or false if replies can't be pulled back
);
Every attempted destination is recorded in _daymark_external_posts —
success, failure, an unsupported Mark type, or a destination that's since
been deactivated all show up there and in the Timeline's own routing
popover (see "Per-Mark routing transparency" in CLAUDE.md). Returning a
result rather than throwing is what keeps one failing connector from ever
blocking the rest of a Mark's selected destinations.
A minimal complete example
<?php
/**
* Plugin Name: Daymark Connector: Example Network
*/
add_action( 'daymark_register_connectors', function ( Daymark_Syndication_Registry $registry ) {
$registry->register_connector( new My_Example_Connector() );
} );
class My_Example_Connector implements Daymark_Syndication_Connector {
public function get_id(): string {
return 'my-network';
}
public function get_label(): string {
return __( 'My Network', 'my-network-connector' );
}
public function supports_daymark_type( string $type ): bool {
return in_array( $type, array( 'note', 'image', 'gallery' ), true );
}
public function is_connected(): bool {
return (bool) get_option( 'my_network_access_token' );
}
public function publish( int $post_id, array $payload ): array {
$token = get_option( 'my_network_access_token' );
if ( ! $token ) {
return array(
'success' => false,
'external_id' => null,
'external_url' => null,
'status' => 'failed',
'message' => __( 'My Network is not connected.', 'my-network-connector' ),
);
}
$response = wp_remote_post(
'https://api.example.com/v1/posts',
array(
'headers' => array( 'Authorization' => 'Bearer ' . $token ),
'body' => array(
'text' => $payload['caption'],
'permalink' => get_permalink( $post_id ),
),
)
);
if ( is_wp_error( $response ) ) {
return array(
'success' => false,
'external_id' => null,
'external_url' => null,
'status' => 'failed',
'message' => $response->get_error_message(),
);
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return array(
'success' => true,
'external_id' => (string) ( $body['id'] ?? '' ),
'external_url' => (string) ( $body['url'] ?? '' ),
'status' => 'published',
'message' => '',
'backflow_supported' => false,
);
}
public function get_status_label(): string {
return $this->is_connected()
? __( 'Connected', 'my-network-connector' )
: __( 'Not connected', 'my-network-connector' );
}
}
Register credentials through your own plugin's settings screen — core
Daymark stores none. Daymark_Connector_Base
(includes/connectors/class-connector-base.php) is available to extend if
a mock-only connector is all a given destination needs, but a real
connector generally implements the interface directly, as above.
Where this fits
daymark_register_connectorsfires oninit, after Daymark's own seven built-in connectors are already registered — seeDaymark_Syndication_Registryin the Hooks reference for the action itself.daymark_default_destinationslets a connector plugin (or a site's ownfunctions.php) add itself to the type-based auto-selected defaults without touching core.daymark_syndication_completefires once every selected destination for a Mark has been attempted, with the full results array — useful for logging or a follow-up action, not for changing what was published.
See the Hooks reference for the exact signature of each of these.