Your publishing webhook timed out, but the article may already be public. The missing response leaves you with an uncertain outcome: did creation fail, or did only the reply get lost? Safe retries need the same durable article identity on every attempt, atomic duplicate prevention at the publishing destination, and a saved public URL that the handler can return again.
Design your receiver for at-least-once delivery: the same request to create an article may arrive more than once. RFC 9110 explains that POST is not inherently safe to retry automatically. Repeating it safely requires idempotent semantics—the same intended effect each time—or a way to establish that the first request was never applied. A timeout alone cannot tell you whether it was applied.
Choose an identity that survives retries
We recommend identifying each article with a durable key scoped to its target site or blog. An adapter could use the pair (publishing_namespace, alias), with the namespace identifying the destination. Both values must stay unchanged across retries and process restarts. A fresh alias on each attempt makes each retry a distinct create, even though the caller is trying to publish the same article.
Our webhook creation contract requires repeated creation with the same alias to avoid creating a duplicate article. Creation requests use POST with JSON, and the handler must save every supplied language. The pair (publishing_namespace, alias) is one way to enforce that requirement in your database. It is not a pair of additional Blogent request fields.
Our V2 webhook uses one endpoint for POST creation, PUT updates, and GET reads. The article’s public URL serves as its identifier, and a successful POST returns the real public URL after saving. Store that URL against your durable create key: it is the result of creation, not an incoming idempotency key. Our CMS connection guide covers endpoint setup and the broader integration flow.
Let the database resolve concurrent creates
When your adapter controls the transactional article store, enforce the durable key with a unique constraint. A lookup can retrieve an existing result, but finding no article does not reserve the key. Two workers can both find nothing and start creating. PostgreSQL’s uniqueness checks prevent competing inserts from committing identical keys. When a conflicting insert is still uncommitted, PostgreSQL waits for its outcome. If that insert commits and retains the key, the competing insertion encounters a uniqueness violation.
Keep the durable key, article data, every supplied language, and final public URL in one transaction. When all publication data can share that transaction, the handler can use this sequence:
- Begin a transaction and insert the durable article key under its unique constraint.
- Save the article and every supplied language, then record the final public URL against the key.
- Commit before returning success.
- If the insert fails with a uniqueness conflict, roll back the failed transaction. In a fresh transaction, read the committed result for the same key and return its stored URL instead of publishing again.
Suppose worker A inserts the key while worker B tries to insert the same key. If A commits, B reuses A’s result. If A rolls back without external side effects, B can proceed with creation. The unique constraint resolves this race at the database level, where an application-level “already exists” check cannot.
PostgreSQL’s ON CONFLICT clause offers another way to handle insertion conflicts. Choose behavior that preserves the existing article and retrieves its result. A repeated POST should not silently update the article’s content. Our V2 contract uses PUT for updates.
Recover when the success response is lost
Once the transaction commits, the article exists even if the caller never receives the response. In the transactional-store design, a lost response and concurrent retries can unfold like this:
Example of using the shortcode function through Blogent
- Initial request: the handler starts creation using the stable article key.
- Commit: the handler commits the unique key, all supplied languages, article data, and public URL together.
- Response loss: the success response never reaches the caller, which observes a timeout.
- Concurrent retries: two requests arrive using the same key. Both resolve to the existing committed article rather than starting another publication.
- Recovery: each successful retry returns the same stored public URL.
The saved key and result must survive a process restart. If the original transaction rolled back before commit and produced no external side effects, a retry can create the article. If it committed, the retry returns the saved result—even when the original handler process is gone.
When publication happens in a separate CMS
A separate CMS introduces a gap that your local transaction cannot close. The CMS could publish the article, then your adapter could crash before saving the returned URL. Your local database would show no completed result, even though the public article exists.
For this architecture, we recommend enforcing duplicate prevention atomically at the destination, using the same durable identity. If the destination’s response leaves the outcome uncertain, look up that identity there before issuing another create, then save any recovered URL locally. The lookup helps recover a completed publication. To prevent two workers from creating at once, the destination also needs a mechanism that makes repeated or concurrent creates resolve to one article.
If the destination lacks safe deduplication or enough access to recover an existing result, stop automatic recreation while the outcome is unknown. A unique key in your local database cannot prevent duplicates across that remote publication step.
Return the saved result
Our webhook responses must be JSON with Content-Type application/json. Return success only after every supplied language has been saved. Whether the handler has just completed creation or recovered a successful result, return the real public address in the url field. This example shows the response format:
{"url":"https://site.example/blog/article"}On failure, our contract requires HTTP 500 with a JSON error description explaining the specific cause. After a 500 response or a timeout, check the durable operation state before deciding to create again. Recovery depends on whether publication committed and where the saved result can be found.
Test the failure boundaries
Before enabling automatic retries, test what happens when publication succeeds but the response or local result is lost. Include these checks:
- Lose the response after commit. Retry with the same key and verify that the handler returns the stored public URL without creating another article.
- Send simultaneous creates with the same key. Verify that only one logical article exists, every supplied language is saved, and successful attempts return the same URL.
- Force a rollback before commit without external side effects. Verify that a later retry can create the article once.
- Restart the handler after a successful commit. Repeat the request and verify that recovery uses durable storage rather than process memory.
- For a separate CMS, crash after the destination creates the article but before the adapter saves the result locally. Verify that destination deduplication and reconciliation recover the existing article without publishing another one.
