The Script Meridian globe mark Script Meridian A community of Frontier
and Radio users
Protocols

The MetaWeblog API, Explained

How XML-RPC gave weblogs titles, links and a publishing contract that outlived them.

Cover illustration for The MetaWeblog API, Explained

In 2002 a weblog post was, as far as the publishing API was concerned, a string. You handed the server some text and it appeared. That was the Blogger API, and its minimalism was deliberate — but the tools had grown up around it, and a string was no longer enough.

The MetaWeblog API was the answer: a small XML-RPC interface that gave a post a title, a link, a category and a date, without asking anyone to abandon what already worked. It is worth understanding today for two reasons. Systems still speak it. And it is a genuinely good case study in extending a protocol you do not control.

The problem, as it was stated at the time

The original API had a very simple idea of what a post was: basically a string. Meanwhile the tools matured, and a deeper interface was needed — which became immediately evident as soon as titles and links shipped in Radio. The existing API was too narrow to carry the new data, and the people building tools said, in effect: let us have more.

Note what the request was not. Nobody proposed replacing the transport, inventing an authentication scheme, or designing a general content model. The ask was narrow: let a post carry the fields a post visibly already had.

Why this is a good design story

The extension kept the transport (XML-RPC), kept the authentication (username and password in the call), and kept the method-naming convention. It changed exactly one thing: the shape of the value that represents a post. New clients gained fields; old clients kept working. That is the whole trick, and it is rarer than it should be.

XML-RPC in ninety seconds

XML-RPC is a remote procedure call encoded as an XML document, sent as an HTTP POST. There is a method name, an ordered list of parameters, and each parameter has a type: string, int, boolean, double, dateTime.iso8601, base64, array and struct. That is the entire type system, and its smallness is the point: it maps cleanly onto the native types of almost any language, including UserTalk, where a struct is simply a table.

XML-RPC — a newPost call on the wire
POST /RPC2 HTTP/1.1
Host: www.scriptmeridian.org
Content-Type: text/xml
Content-Length: 612

<?xml version="1.0"?>
<methodCall>
  <methodName>metaWeblog.newPost</methodName>
  <params>
    <param><value><string>weblog-1</string></value></param>
    <param><value><string>editor</string></value></param>
    <param><value><string>••••••••</string></value></param>
    <param><value><struct>
      <member><name>title</name>
        <value><string>TLS 0.3.1 released</string></value></member>
      <member><name>description</name>
        <value><string>A bug-fix release for the secure server.</string></value></member>
      <member><name>link</name>
        <value><string>https://www.scriptmeridian.org/resources.html#tls</string></value></member>
      <member><name>dateCreated</name>
        <value><dateTime.iso8601>20031209T09:00:00</dateTime.iso8601></value></member>
      <member><name>categories</name>
        <value><array><data>
          <value><string>releases</string></value>
          <value><string>security</string></value>
        </data></array></value></member>
    </struct></value></param>
    <param><value><boolean>1</boolean></value></param>
  </params>
</methodCall>

The response is equally plain: a single value, the identifier of the post that was created. Errors come back as a fault with a numeric code and a string, and the distinction between a fault and a transport failure matters when you write the client.

The method set

The core of the interface. Implementations commonly add blogger.* methods alongside for backward compatibility, which is exactly what the design intended.
MethodParametersReturnsNotes
metaWeblog.newPostblogid, user, password, struct, publish post id (string)The publish flag distinguishes a draft from a live post.
metaWeblog.editPostpostid, user, password, struct, publish booleanReplaces the post wholesale; there is no partial update.
metaWeblog.getPostpostid, user, password structRound-trips the same shape newPost accepts.
metaWeblog.getRecentPostsblogid, user, password, count array of structsNewest first. Count is advisory; servers may cap it.
metaWeblog.getCategoriesblogid, user, password array of structsEach with a description and an HTML URL.
metaWeblog.newMediaObjectblogid, user, password, struct struct with urlBase64 bytes in, a public URL out. The awkward one.

The post struct

The fields that carry the design: title, description (the body, named for its RSS ancestry), link, dateCreated, categories. The naming is borrowed straight from RSS, deliberately, so that a post and a feed item describe the same thing in the same words. Extra members are permitted and ignored by servers that do not know them — which is what made incremental adoption possible at all.

Implementing the server side

In Frontier this is unusually direct, because an XML-RPC struct and a UserTalk table are the same idea and the runtime has done the decoding before your handler is called.

UserTalk — a newPost handler with real validation
on newPost (blogid, username, password, struct, publish = true) {
	«Authenticate first; never leak whether the blog exists to an unauthorised caller.
	if not member.authenticate (username, password) {
		scriptError ("Authentication failed")};
	if not member.canPostTo (username, blogid) {
		scriptError ("Not permitted to post to " + blogid)};

	«Validate before writing anything.
	if not defined (struct.description) {
		scriptError ("description is required")};
	if sizeOf (struct.description) > 200000 {
		scriptError ("description exceeds the 200 KB limit")};

	local (adrPost = weblog.newPostNode (blogid));
	adrPost^.title = string (struct.title, "");
	adrPost^.body = struct.description;
	adrPost^.link = string (struct.link, "");
	adrPost^.created = date (struct.dateCreated, clock.now ());
	adrPost^.categories = struct.categories ?? {};
	adrPost^.draft = not publish;
	adrPost^.author = username;

	if publish {
		weblog.render (adrPost);
		weblog.rebuildFeeds (blogid)};

	return (string (nameOf (adrPost^)))}

Three mistakes we still find in live implementations

Returning a fault for an empty result. No recent posts is a successful call that returns an empty array. Clients treat a fault as an outage.
Trusting dateCreated blindly. Clients send local time, sometimes without a zone. Normalise to UTC on arrival or your archive ordering slowly rots.
Rebuilding the whole site inside the call. The client is holding an HTTP connection open. Queue the rebuild and return.

Implementing the client side

A client has three jobs: encode, transport, and distinguish the three failure modes — transport failure, fault response, and a success whose contents you did not expect. Conflating them is what produces the classic “it posted twice” bug, because a timeout is retried when the server actually succeeded.

UserTalk — a client that retries safely
on publishPost (endpoint, blogid, user, pw, postStruct) {
	local (attempt = 0, maxAttempts = 3, result, idem);
	«An idempotency key lets the server recognise a retry of the same post.
	idem = string.hashMD5 (blogid + user + postStruct.title +
		string (postStruct.dateCreated));
	postStruct.idempotencyKey = idem;

	while attempt < maxAttempts {
		attempt++;
		try {
			result = xml.rpc (endpoint, "metaWeblog.newPost",
				{blogid, user, pw, postStruct, true});
			return (result)}
		else {
			«A fault is the server's considered answer: do not retry it.
			if string.patternMatch ("fault", tryError) > 0 {
				scriptError ("Server rejected the post: " + tryError)};
			«A transport failure may be retried, with backoff.
			if attempt >= maxAttempts {
				scriptError ("Giving up after " + attempt + " attempts: " + tryError)};
			thread.sleepFor (attempt * 2)}};
	return (nil)}

What it got right, and what it got wrong

Right

  • It extended rather than replaced. Old clients kept working on day one.
  • It borrowed RSS vocabulary. One mental model for feeds and for posting.
  • It stayed small. Six methods can be implemented in an afternoon and tested by hand with a text editor and curl.
  • It was written down publicly while it was being argued about, which is why multiple independent implementations interoperated.

Wrong, or at least aged badly

  • Credentials on every call. Reasonable in 2002; indefensible now. Modern deployments front it with a token and treat the password fields as vestigial.
  • Base64 media uploads. A 4 MB image becomes a 5.5 MB XML document held in memory at both ends.
  • No partial updates. editPost replaces everything, so two editors working at once silently overwrite each other.
  • Underspecified dates. The zone question was never settled, and every implementation guessed slightly differently.

Why it still matters in 2026

Partly because it is still deployed: desktop editors, static-site publishers, migration tools and a surprising number of internal newsroom systems speak it, and an endpoint that answers metaWeblog.getRecentPosts is often the cheapest way to extract twenty years of content from a system whose export function was never finished.

And partly because the design lesson keeps applying. When you need to extend an interface other people depend on, the MetaWeblog approach — same transport, same conventions, richer value, unknown fields ignored — is still the one that causes the least damage. It is how we approach every migration in the publishing lineage, and it is why our migration work so often starts by standing an XML-RPC endpoint back up rather than writing a database scraper.

Take this away

  • A post gained title, link, date and categories — the transport never changed
  • XML-RPC structs map directly onto UserTalk tables
  • Distinguish faults from transport failures, or you will double-post
  • Never rebuild a site inside the call; queue it and return
  • Still the cheapest extraction route out of a twenty-year-old weblog