Skip to main content
Send server-side conversion events to Plainrouter at https://plainrouter.com/api/v1/events using a Signals workspace secret. A new event returns 202; check its delivery trace separately to confirm Meta acceptance. SDKs and command-line clients use the same Signals REST API. This is Plainrouter’s API, with Plainrouter request fields and authentication. It is not a direct Meta Graph API endpoint. To choose between managed and direct Meta integrations, read the Meta Conversions API overview. For one fetchable comparison of REST, SDK, CLI, MCP, and management credentials, see Authentication and clients. New to the API? Start with the API introduction for the base URL, a synthetic first request, and endpoint navigation. This guide covers production conversion handling and recovery.

Before you begin

You need an active Signals workspace, its server-side secret, and a recorded consent decision for the conversion. To deliver the event to Meta, connect the intended dataset first. Start with the identity-free ingestion diagnostic if you only need to check authentication. Use the sandbox for synthetic payload validation or a controlled destination test to check Meta delivery. The conversion request below can create a real event and destination delivery.

Choose an interface

The signed REST contract is published at version 0.5.0. TypeScript SDK and npm CLI 0.5.1, Go SDK v0.5.2, Python package 0.5.2, and Ruby SDK 0.1.1 target that signed contract. The Python package also includes the PyPI CLI distribution. See Client releases for package links and the CLI’s command boundary.

Authenticate

For authenticated Signals operations, send the Signals workspace secret issued during provisioning:
The sandbox operations have separate authentication rules. Keep this secret on your server or in your CLI’s secure credential store. Never put it in browser code, source control, prompts, or logs. The Conversion API does not accept an MCP OAuth token or a workspace token. Workspace execution tokens authorize scoped agent tools. OAuth management credentials are limited to management discovery and cannot authenticate MCP tool calls. Neither authenticates the Signals API.

Submit an event

Send the request only after a real purchase and a recorded full advertising consent grant. Replace order-123, the source URL, and the value with that purchase’s data. The consent fields below describe that prerequisite; they do not obtain consent. Use a stable, non-personal event_id for each logical purchase and reuse it across retries. Money values use decimal strings. This immediate-event example omits event_time, so Plainrouter uses receipt time. For delayed or queued events, supply the original Unix timestamp or ISO-8601 time from your order record and preserve it on retries. Do not copy a sample timestamp or change an old purchase to the current time.
A new event returns HTTP 202 and a Location header for its event resource. A retry with the same event ID returns HTTP 200 with duplicate: true; it does not create another conversion. A duplicate response is not a replay of failed destination delivery. You can supply Idempotency-Key instead of a body event_id. If you supply both, their values must match. The submit-event reference defines the request and responses.
For browser consent and visitor joins, see Track events and consent. Every POST /events request must include consent_basis. Use consent only with granted downstream advertising state. legitimate_interest is restricted to allowlisted lifecycle events and does not create an attributed advertising join. A new server event authenticated with a Signals workspace secret returns HTTP 202 with a warnings array. The array is empty when no warning applies. If you supply visitor_id or user_data without a parseable consent capture time, the acknowledgement includes consent_captured_at_invalid:
The event was accepted; the warning does not mean the request failed. A 202 alone does not prove an identity join or Meta delivery. Inspect the acknowledgement before treating the event as fully linked.
  1. Read the consent snapshot captured by your application or CMP. Supply its actual decision time in consent.captured_at; do not invent consent or replace a missing historical decision with the current time.
  2. Use a timestamp with seconds and a timezone, such as 2026-09-14T10:30:00Z or 2026-09-14T13:30:00+03:00. An optional fractional second may contain one to six digits. Date-only values, timestamps without a timezone, and impossible calendar dates are invalid.
  3. Correct future submissions and inspect the original event’s delivery trace. Reusing an accepted event ID returns the original duplicate acknowledgement; it does not update that event’s consent. Do not create another purchase ID just to remove a warning.
Duplicate HTTP 200 acknowledgements keep their existing response shape and do not include this new array. Browser/publishable-key and sandbox acknowledgements are unchanged. An empty array is not proof of fresh consent: a parseable but stale capture time can still fail the existing identity freshness checks. The warning does not broaden consent, identity, or destination eligibility. For a server implementation that retains the original consent timestamp, use the Plainrouter SDK purchase recipe. The published OpenAPI contract defines the acknowledgement fields; older generated SDK models may not expose newly added fields.

How do I confirm Meta accepted the event?

Use GET /events/{event} for the event ID you submitted. For the example above:
The response includes the event and its deliveries. For IDs containing characters that need URL encoding, use the returned Location URL or the SDK’s getEvent operation. Keep credentials in the authorization header. Meta acceptance does not establish attribution or improved advertising performance. An empty delivery list does not establish Meta acceptance either. Check the connected destination and the event’s eligibility. See delivery statuses for the complete meanings and GET event for the response schema.

Read a failed delivery trace

The following is an illustrative excerpt from GET /events/order-123; other response fields are omitted. event.id identifies the conversion. The numeric delivery id identifies one destination attempt record and is the value used for replay.
Here, ingestion succeeded but Meta authentication failed. Reconnect the intended Meta assets, then consider replay of this delivery. Repeating POST /events with order-123 only returns the existing event; it does not repair or replay the delivery.

How do I replay a failed delivery?

Use POST /deliveries/replay after correcting the destination problem. Replay sends the existing retained payload again; it does not edit the purchase or create a new event. Before replay, inspect the event trace and confirm:
  • You are using the Signals workspace secret for the event and destination.
  • The destination is active, and the event is eligible for attributed advertising delivery.
  • The delivery is in a failed:* or expired state. Pending, accepted, and skipped deliveries are excluded.
  • The retained payload is available and the original event time remains within the configured replay window, which cannot exceed seven days. A recent event can still have an unavailable payload.
Replace 1042 with the delivery ID from your trace. This request can contact Meta. Always provide the specific delivery_ids you intend to replay: omitting the field or sending an empty array selects a broader set of eligible workspace deliveries.
HTTP 202 returns counts, for example:
These counts are not a per-ID rejection report. All-zero counts can mean the selected ID is outside the workspace or does not meet eligibility requirements. Inspect the trace and delivery status instead of repeatedly replaying it. For failed:permanent, read the explanation before replaying: sending the same invalid payload again will not correct it. Do not change an old purchase’s timestamp or generate a new event ID to bypass duplicate detection or expiry. See the replay operation reference for optional filters and the complete schema.

Verify Signal ingestion

The verification operation proves that the authenticated Signal can write to the event ledger without sending identity or destination data:
A new verification returns 202; a repeat returns 200 with duplicate: true. The request has no body. It confirms ledger receipt but does not count as real arrival evidence or complete onboarding. See Test Signal ingestion for the MCP alternative.

Available operations

SDKs and CLI clients derive their method and command names from these semantic operation IDs: Resource IDs remain scoped to the authenticated workspace. A client cannot use its Signals secret to read another workspace’s events or destinations. Cursor listing accepts per_page up to 100. Omit cursor on the first request, then pass the returned next_cursor unchanged to fetch the next page. TypeScript exposes listEventsByCursor; Python exposes it through plainrouter.generated.api.operations.list_events_by_cursor. The CLI has no cursor-listing command.

Read workspace identifiers

Signals configuration now belongs directly to the workspace. API responses use the numeric workspace_id as the canonical tenant identifier. Existing response shapes also include signal_tracker_id as a deprecated decimal-string alias of the same workspace ID. The currently published 0.5.x SDKs were generated before workspace_id became canonical, so their models may expose only signal_tracker_id. You can continue reading that compatibility field until a regenerated client release adds workspace_id. Do not treat the two fields as different tenants. The signed contract marks user_data_hashed as a deprecated compatibility field whose value is always null. Delivery identity is never returned. Use delivery status and explanation to diagnose the outcome, rather than expecting identity hashes in the trace.

Delete retained user data

DELETE /user-data accepts either a verified raw identifier or a legacy SHA-256 identifier_hash, together with identifier_type. Send the identifier only in the JSON request body. Plainrouter rejects identifiers in the query string so they do not enter URLs, access logs, browser history, caches, or referrer data. Deletion is idempotent and scoped to the authenticated workspace. The response reports whether the request was a duplicate and how many retained event and session records were updated.

Use the signed OpenAPI contract

Download or import the canonical contract from:
The document uses OpenAPI 3.1.0, publishes API version 0.5.0, and includes x-signed: true. Plainrouter serves the reviewed bytes with an ETag so automated tooling can detect a new contract without downloading an unchanged document. Generate clients only from a signed document. Pin the generated SDK or CLI version in production and review contract changes before updating it.

Client releases

The TypeScript package @plainrouter/sdk@0.5.1, Python package plainrouter==0.5.2, Ruby gem plainrouter-sdk 0.1.1, and Go module github.com/plainrouter/sdk-go@v0.5.2 target the signed Conversion API contract 0.5.0. All four expose ingestion verification and the required event consent fields. The TypeScript, Python, and Ruby patch releases publish canonical documentation metadata; Go v0.5.2 improves canonical documentation and package examples without changing the API contract. Install the CLI from PyPI with pipx install plainrouter==0.5.2, from npm with npm install --global @plainrouter/cli@0.5.1, or from the official Homebrew tap with brew install plainrouter/tap/plainrouter. The PyPI and npm distributions expose the same nine terminal commands and consent-aware event shape. Neither adds a command for verifySignalIngestion; use an SDK, direct REST, or MCP for that operation.

Handle errors

Validation responses include a message and field-level errors. Treat 401 as a stopped credential and avoid retrying it indefinitely. For missing or duplicate conversions after a successful API request, follow Meta delivery troubleshooting.