| Internet-Draft | 2PHP | June 2026 |
| Venkateswaran & Jamwal | Expires 20 December 2026 | [Page] |
This document specifies the 2-Phase HTTP Protocol (2PHP), a backward-compatible, opt-in extension to HTTP mutation semantics that introduces a two-phase handshake at service request boundaries. 2PHP ensures that a mutation request is durably registered on both the client and the server before any processing commences, addressing a structural gap in existing HTTP REST semantics where no standard mechanism exists to confirm bilateral intent registration prior to execution.¶
2PHP operates at the HTTP protocol layer, requires no new transport mechanisms, and is composable across independently operating service boundaries. This document further defines a standardized Intent Ledger model for durable, queryable, cross-service correlation of request intent, distinct from execution telemetry provided by distributed tracing systems.¶
This document defines the protocol headers, phase state machine, idempotency and replay behavior, the three-tier Intent Ledger architecture, cross-service correlation model, and IANA registration of the associated HTTP header fields.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 20 December 2026.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
Microservices architectures built on HTTP REST APIs are widely deployed for distributed systems communication. Mutation requests—those using HTTP methods POST, PUT, PATCH, and DELETE—are treated as single atomic operations from the client’s perspective. In practice, however, no standard protocol mechanism exists to confirm that both the client and server have durably registered the intent of a request before processing begins.¶
Existing approaches—including the Saga pattern [SAGA], Transactional Outbox [OUTBOX], and application-layer idempotency keys—address failure recovery after the fact or are implemented ad hoc without a standard contract. Distributed tracing systems such as OpenTelemetry [OTEL] provide execution telemetry but cannot answer whether both parties registered intent before processing commenced.¶
This document specifies the 2-Phase HTTP Protocol (2PHP), an opt-in, backward-compatible extension to HTTP that introduces a lightweight two-phase handshake at each service boundary. 2PHP ensures bilateral durable registration of request intent before any business logic is executed. It further defines a standardized Intent Ledger model that enables cross-service correlation of request intent across distributed call trees.¶
2PHP is designed to be:¶
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, NOT RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
Current HTTP REST semantics treat a mutation request as a single atomic operation from the client’s perspective. In reality, a server may:¶
No standard HTTP mechanism exists to confirm that both the client and the server have registered the intent of a request before processing commences.¶
Modern distributed tracing systems provide visibility into what executed across services, how long operations took, and where failures occurred. However, they cannot answer:¶
| Pattern | Scope | Client Involved? | Protocol Level? | Intent Ledger? |
|---|---|---|---|---|
| Two-Phase Commit (2PC) | Database | No | No | No |
| Saga Pattern | Application | No | No | No |
| Transactional Outbox | Server-side | No | No | No |
| Idempotency Keys | Application | Partial | No | No |
| HTTP 100 Continue | Protocol | Yes | Yes | No |
| 2PHP (this document) | Protocol | Yes | Yes | Yes |
2PHP extends HTTP mutation semantics with an opt-in two-phase handshake. Each service boundary owns its own independent handshake. Cross-service traceability is the responsibility of the centralized Intent Ledger, not the protocol itself.¶
Client Server | | |-- Phase 1: POST + payload --------> | | | Persist payload + | | ledger record atomically | <-- 200 OK + Server-Corr-ID ------- | | | | (Client writes local ledger entry) | | | |-- Phase 2: POST confirm ----------> | | | PONR crossed; | | business logic executes | <-- 200 OK + Resource-ID ---------- | | |
The server MUST atomically persist the intent payload and the initial ledger record before returning the Phase 1 response. The resource is created only after Phase 2 confirmation is received and business logic has successfully executed.¶
Clients signal 2PHP intent by including the following header on any mutation request:¶
Servers that do not recognize this header MUST ignore it and process the request normally, preserving backward compatibility.¶
The client MUST include
DTT-2PHP-Client-Correlation-ID with a unique
identifier for this request. This identifier
MUST be used in all subsequent protocol
exchanges and ledger entries for this interaction.¶
The client MAY include
DTT-2PHP-Requested-TTL (in milliseconds) to
request that the server override its default TTL setting.¶
The server MUST atomically persist the
intent payload and an initial ledger record with phase
state WAITING_CONFIRM before returning the
response.¶
Response header semantics:¶
DTT-2PHP-Requested-TTL, the server
MAY accommodate by increasing the TTL.¶
WAITING_CONFIRM on Phase 1 response.¶
No DTT-2PHP-Resource-ID is returned in
Phase 1. The resource does not yet exist; it is
created only after Phase 2 confirmation is received
and business logic has successfully executed.¶
After receiving the Phase 1 response, the client
MUST write a ledger entry to its local
ledger. The client writes after the Phase 1 response
so that both client_correlation_id and
server_correlation_id are available at write
time. The client’s parent_reference_id
MUST be set to its own
client_correlation_id as the root anchor.¶
The client confirms receipt of the Phase 1 acknowledgement:¶
The client MUST send Phase 2 before
the DTT-2PHP-PONR-Deadline timestamp received
in the Phase 1 response.¶
The server crosses the PONR, executes business logic inline, and responds once processing is complete:¶
The server MUST return 200 OK
only after business logic has completed and the resource
is fully created. The DTT-2PHP-Resource-ID is
authoritative—the resource exists at the point
this response is returned.¶
If business logic fails after PONR is crossed, the
server MUST return
500 Internal Server Error. If the request is
invalid, the server MUST return the
appropriate 4xx response. In both cases the
client receives a definitive outcome in the Phase 2
response—no polling is required.¶
In asynchronous mode, the server crosses the PONR and
returns 202 Accepted immediately upon receiving
Phase 2 confirmation. Business logic executes
out-of-band on the original request channel.¶
The DTT-2PHP-Resource-ID returned in a
202 Accepted response is provisional—the
resource is not yet fully created at the point this
response is returned. The client
MUST poll the resource endpoint until
DTT-2PHP-Phase-State transitions to
COMMITTED or FAILED.¶
The phase state machine is identical to synchronous mode.
Only the timing of the COMMITTED transition
differs—in asynchronous mode it occurs out-of-band
rather than inline on the Phase 2 response channel.
202 Accepted semantics are as defined in
[RFC9110].¶
Any Phase 2 confirmation received after TTL expiry MUST be rejected with:¶
The server MAY apply an internal grace window (δ) after TTL expiry to handle race conditions at the PONR boundary. This grace window is server-internal and MUST NOT be exposed in protocol headers or communicated to the client.¶
Auto-Confirm mode is an opt-in extension to 2PHP that eliminates the explicit Phase 2 client confirmation round trip. When Auto-Confirm is enabled, the server writes the ledger record and crosses the PONR immediately after Phase 1, then executes business logic inline on the original request channel. The final result (200 OK + Resource-ID) is returned synchronously on that same channel—behaviorally identical to a standard HTTP request from the client’s perspective.¶
Concurrently, immediately after the ledger write and
before processing begins, the server dispatches an
asynchronous callback to a client-provided endpoint
carrying the server_correlation_id and ledger
state. This out-of-band delivery gives the client a
durable record of the registered intent independent of
the original response channel.¶
Auto-Confirm mode is valuable for:¶
true, the server does not wait for a
Phase 2 confirmation before crossing the PONR
and executing business logic.¶
DTT-2PHP-Auto-Confirm: true is set. If
omitted, the server proceeds in Transparent Mode
with no client-side ledger participation.¶
Client Server Client Callback | | | |-- POST /orders -------------> | | | DTT-2PHP-Enabled: true | | | DTT-2PHP-Auto-Confirm: true | | | DTT-2PHP-Callback: <url> | | | DTT-2PHP-Client-Corr-ID: C1 | | | | | | Ledger write (WAITING_CONFIRM | | -> PROCESSING, atomic) | | | | | |-- POST callback --> | | | (immediately, | | | off-thread, | | | BEFORE processing)| | | | | Server processes inline... | | | | | <-- 200 OK + Resource-ID ---- | | | DTT-2PHP-Phase-State: | | | COMMITTED | | | DTT-2PHP-Server-Corr-ID: S1 | | | | |
The key invariant: the callback
MUST be dispatched immediately after
the ledger write and MUST fire before
processing begins. This maximizes the client detection
window. If the original response is never received but
the callback was, the client knows the PONR was crossed
and MAY query the ledger by
server_correlation_id for final state.¶
| Field | Description |
|---|---|
server_correlation_id
|
Server-assigned intent identifier. |
client_correlation_id
|
Echo of the client-provided identifier. |
phase_state
|
PROCESSING at time of callback dispatch.
|
ponr_crossed
|
Always true in Auto-Confirm mode.
|
callback_timestamp
|
ISO 8601 timestamp of callback dispatch. Fired before processing begins. |
The callback creates a first-class detectable failure taxonomy:¶
| Callback Received | Original Response Received | Interpretation |
|---|---|---|
| Yes | Yes | Normal success. Reconcile by correlation IDs. |
| Yes | No |
PONR crossed; response lost in transit. Query
ledger by server_correlation_id for
final state.
|
| No | Yes | Callback delivery failed. Ledger entry exists server-side; client has no local record. |
| No | No | Network failure before PONR. Safe to retry as a new Phase 1 request. |
When DTT-2PHP-Auto-Confirm: true is set without
DTT-2PHP-Callback, the server operates in
Transparent Mode. The server records a ledger entry
using a server-generated UUID as the
client_correlation_id proxy. The client
receives a standard synchronous response with no 2PHP
headers required. The server MAY return
DTT-2PHP-Server-Correlation-ID in the response
for optional client-side ledger lookup.¶
Transparent Mode provides full server-side ledger observability with zero client behavior change. It is the recommended adoption path for brownfield services.¶
Client POST (Phase 1, DTT-2PHP-Auto-Confirm: true)
|
v
[WAITING_CONFIRM] <-- ledger write (transient, atomic)
|
v (immediate -- PONR crossed, no client confirmation)
[PROCESSING] --> (off-thread) Callback dispatched to client
| BEFORE processing begins
|
+-- Success --> [COMMITTED] <-- 200 OK + Resource-ID
| on original channel
|
+-- Failure --> [FAILED] <-- 500 / 4xx
on original channel
In Auto-Confirm mode, WAITING_CONFIRM is a
transient internal state written and immediately
superseded by PROCESSING within the same
atomic ledger operation. It is never visible to the
client on the original channel.¶
| Mode | Phase 2 Required | Callback | Latency | Client 2PHP Support | Ledger Participation |
|---|---|---|---|---|---|
| Standard 2PHP | Yes | No | 2 round trips | Yes | Both sides |
| Auto-Confirm (with callback) | No | Yes (async) | 1 round trip | Callback endpoint only | Both sides |
| Transparent Mode (no callback) | No | No | 1 round trip | None | Server-side only |
Client POST (Phase 1)
|
v
[WAITING_CONFIRM] <-- persisted in durable storage
|
+-- Client confirms within TTL
| |
| v
| [PROCESSING] <-- PONR crossed; business logic executing
| |
| +-- Success --> [COMMITTED] <-- 200 OK + Resource-ID
| |
| +-- Failure --> [FAILED] <-- 500 / 4xx
|
+-- TTL expires without confirmation
|
v
[TTL_EXPIRED] -- [ PONR, PONR+delta-x ) --> [ABANDONED]
| State | Description |
|---|---|
WAITING_CONFIRM
|
Phase 1 received and persisted. Awaiting client Phase 2 confirmation. |
PROCESSING
|
Phase 2 confirmed. PONR crossed. Business logic executing (synchronous mode). |
COMMITTED
|
Processing complete. Resource created. Terminal success state. |
FAILED
|
Processing failed after PONR. Server returned 500 or 4xx. Terminal failure state. |
TTL_EXPIRED
|
Client did not confirm within TTL. Pending cleanup. |
ABANDONED
|
Cleaned up after TTL + δ grace window. Terminal state. |
If a client replays Phase 2 before TTL expiry, the
server MUST return the same response
associated with the original
DTT-2PHP-Server-Correlation-ID. No duplicate
processing occurs.¶
Two configurable server behaviors are defined, applicable per endpoint or globally:¶
408 TTL_EXPIRED. The client
MUST re-submit Phase 1 as a new
request with a new
DTT-2PHP-Client-Correlation-ID.¶
Servers MAY expose the replay policy via the following header:¶
The Intent Ledger is a durable, queryable log of all 2PHP transactions. It is distinct from execution telemetry—it is an intent registry, not a trace of execution. Its purpose is to provide provable bilateral registration of every mutation intent across all participating services.¶
Existing distributed tracing frameworks, such as OpenTelemetry [OTEL], pass trace contexts out-of-band to log execution data after it happens. This post-mortem model suffers from ingestion lag and cannot stop data mismatches when a network drops right at a service boundary.¶
In contrast, the 2PHP Distributed Transaction Tracker (DTT) Ledger operates inline as an active safety gate. It forces both the client and server to write an atomic record of intent BEFORE data processing starts. This turns passive logging into real-time failure prevention.¶
2PHP is NOT a Distributed Transaction Coordinator (DTC). It requires zero cross-service thread blocking or lock coordination. Each service boundary handles its handshake independently. The 3-Tier Ledger architecture uses a local-first design where writes are synchronous but local, completely removing network hops from the runtime path.¶
By standardizing states like WAITING_CONFIRM and
TTL_EXPIRED across all systems, the platform
provides a uniform data layer. Infrastructure tools can
read these state transitions to automatically trigger
custom application-layer compensation scripts or
data-cleanup workflows without human intervention.¶
The Intent Ledger uses a local-first, batch-sync architecture to ensure it is never on the critical path of service processing.¶
The intent payload and ledger record
MUST both be persisted as a single unit
of work before 200 OK is returned.¶
Phase 1 received
|
v
Persist as single unit of work:
- Intent payload (durable store)
- Ledger record (Local Ledger, phase: WAITING_CONFIRM)
`|`
`v`
200 OK returned to client
|
v (async background batch job)
Local Ledger --> Central Ledger (eventually consistent)
For highly critical services, the payload and ledger SHOULD be co-located in the same store to eliminate the need for distributed transaction coordination. For less critical services, a temporary copy of the payload and ledger entry is sufficient for Phase 1 and Phase 2 lifecycle management, with the ledger entry synced to the Central Ledger via event publish, not on the critical path.¶
The Central Ledger Management Application MAY trigger an on-demand pull from any service’s local ledger. This mechanism is used during incident investigation when the latest data is needed immediately, without waiting for the next scheduled batch sync.¶
| Scenario | Ledger Used | Trigger |
|---|---|---|
| Normal operation | Local Ledger only | Per phase transition |
| Routine audit | Central Ledger | Batch sync (scheduled) |
| Incident investigation | Central Ledger | On-demand pull from Management App |
| Compliance reporting | Central Ledger | Batch sync (scheduled) |
Each ledger entry MUST include the following fields:¶
| Field | Description |
|---|---|
client_correlation_id
|
Client-assigned request identifier. |
server_correlation_id
|
Server-assigned intent identifier. |
service_ledger_id
|
GUID issued to the service at ledger registration. |
service_endpoint
|
Target service and resource path. |
actor
|
Role of the entry writer: client or
server.
|
source
|
Identity of the participant writing this entry
(e.g., client, serviceA,
serviceB1).
|
target
|
The downstream service being called. Populated
only when actor = client.
MUST be null for
actor = server entries.
|
parent_reference_id
|
The correlation ID that the immediate upstream caller provided when calling this service. Used to reconstruct the full distributed call tree. |
phase
|
Current or terminal phase state. |
phase_1_timestamp
|
When Phase 1 was received (ISO 8601). |
phase_2_timestamp
|
When Phase 2 confirmation was received, if applicable (ISO 8601). |
ttl_ms
|
Configured TTL for this request, in milliseconds. |
outcome
|
Terminal outcome: COMMITTED,
ABANDONED, or TTL_EXPIRED.
|
payload_ref
|
Reference to the persisted payload. Not the payload itself. |
sync_timestamp
|
When this entry was synced to the Central Ledger (ISO 8601). |
transaction_reference
|
Reference to transaction ID (optional). |
When a service fans out to multiple downstream services,
the Intent Ledger captures every participant’s view
of every call—both the caller
(actor = client) and the callee
(actor = server). This produces a unified,
traversable ledger that can reconstruct the full
distributed call tree without any distributed coordination
at the protocol level.¶
client_correlation_id and
server_correlation_id are available at write
time.¶
parent_reference_id MUST always
record what the immediate upstream caller provided as
its correlation ID when calling this service—
enabling upward tree traversal.¶
actor = client and
one actor = server, linked by the same
client_correlation_id +
server_correlation_id pair. This confirms
dual-side registration—the core guarantee of 2PHP.¶
target MUST be populated only
on actor = client rows. It
MUST be null on
actor = server rows.¶
Client --> Service A --> Service B1 --> Service C1
--> Service B2 --> Service D1
--> Service B3 --> Service E1
The complete ledger for this call chain contains 14
entries—two rows (one actor = client, one
actor = server) for each of the seven
service-boundary interactions. The root client entry uses
client-corr-id1 as both its
client_correlation_id and its
parent_reference_id, establishing the tree
anchor. Each subsequent tier sets
parent_reference_id to the correlation ID
received from its immediate upstream caller.¶
parent_reference_id = client-corr-id1
recursively—walks the entire fan-out tree.¶
source = serviceA AND
actor = client.¶
client_correlation_id +
server_correlation_id—one
actor = client, one actor = server.¶
phase = WAITING_CONFIRM or
phase = TTL_EXPIRED across any
source.¶
2PHP is designed to be fully backward compatible:¶
DTT-2PHP-Enabled: true
MUST experience no change in behavior.¶
Implementations SHOULD use one of the following for durable storage of intent payload and ledger records:¶
The following are recommended starting values. Implementations MAY adjust these per endpoint.¶
| Scenario | TTL | δ (server-internal) |
|---|---|---|
| Low-latency APIs | 5,000 ms | 1,000 ms |
| Standard APIs | 30,000 ms | 5,000 ms |
| Long-running operations | 120,000 ms | 10,000 ms |
δ values are server implementation defaults and MUST NOT be exposed in protocol headers.¶
Each service SHOULD register with the
Intent Ledger at onboarding and be issued a GUID
(service_ledger_id) that is included in all
ledger entries for that service. This enables
cross-service querying while maintaining service-level
isolation.¶
DTT-2PHP-Client-Correlation-ID and
DTT-2PHP-Server-Correlation-ID values
MUST be treated as opaque identifiers.
Implementations SHOULD use UUID v4
or equivalent cryptographically random values to prevent
enumeration or prediction of correlation IDs by
unauthorized parties.¶
A malicious actor in possession of both
DTT-2PHP-Client-Correlation-ID and
DTT-2PHP-Server-Correlation-ID could attempt to
submit a spoofed Phase 2 confirmation.
Implementations MUST authenticate
Phase 2 confirmation requests using the same
authentication mechanism applied to Phase 1
requests. The server MUST verify that
the authenticated identity of the Phase 2 requester
matches that of the Phase 1 requester.¶
The REUSE replay policy
(Section 8.2) introduces a window
during which a replayed Phase 2 confirmation may be
accepted. Implementations using REUSE
SHOULD apply additional controls, such as
request signing or short-lived tokens, to mitigate replay
risk within the δ grace window.¶
The intent payload persisted during Phase 1 may contain sensitive data. Implementations MUST ensure that persisted payloads are protected at rest using appropriate encryption mechanisms consistent with the sensitivity of the data.¶
The Central Ledger aggregates intent records across all services. Access to the Central Ledger MUST be restricted to authorized audit, operations, and compliance consumers. The on-demand pull mechanism (Section 9.6) MUST be protected by appropriate authentication and authorization controls.¶
All 2PHP exchanges MUST be conducted over TLS (HTTPS). Use of 2PHP over unencrypted HTTP is NOT RECOMMENDED.¶
This document requests registration of the following HTTP header fields in the "Permanent Message Header Field Names" registry defined in [RFC3864] and updated by [RFC9110]:¶
| Header Field Name | Applicable Protocol | Status | Reference |
|---|---|---|---|
DTT-2PHP-Enabled
|
HTTP | Experimental | This document |
DTT-2PHP-Client-Correlation-ID
|
HTTP | Experimental | This document |
DTT-2PHP-Auto-Confirm
|
HTTP | Experimental | This document |
DTT-2PHP-Callback
|
HTTP | Experimental | This document |
DTT-2PHP-Requested-TTL
|
HTTP | Experimental | This document |
DTT-2PHP-Server-Correlation-ID
|
HTTP | Experimental | This document |
DTT-2PHP-Phase-State
|
HTTP | Experimental | This document |
DTT-2PHP-TTL
|
HTTP | Experimental | This document |
DTT-2PHP-PONR-Deadline
|
HTTP | Experimental | This document |
DTT-2PHP-Resource-ID
|
HTTP | Experimental | This document |
DTT-2PHP-Replay-Policy
|
HTTP | Experimental | This document |
DTT-2PHP-Message
|
HTTP | Experimental | This document |
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶