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. Review the event schema before sending, or use 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. SDK package versions are independent of that contract version. See Client releases for the published versions, package links and the CLI’s command boundary. The Python package also includes the PyPI CLI distribution.

Authenticate

For authenticated Signals operations, send the Signals workspace secret issued during provisioning:
The unauthenticated sandbox is retired; a former sandbox key cannot authenticate production requests. 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 can create an eligible observed join only under the configured global market and effective global policy, with applicable opt-out checks. Strict-EU, denied, opted-out and portfolio-only data do not gain attributed delivery by changing that field. See policy behavior.

How do shared order IDs reconcile purchases?

Keep event_id stable for retries of one event. Use value_data.order_id as the same non-personal order key when a browser, backend and verified Stripe source describe the same purchase. Cross-source purchase reconciliation uses that explicit shared key in the workspace; it does not guess from equal amounts or nearby timestamps. Keep each source’s event ID and original evidence. Multiple source records can remain inspectable while the purchase and destination handling select the eligible canonical observation. A bounded wait for another observation can leave a delivery queued; do not create another purchase or reset timestamps to force progress. skipped:duplicate records a suppressed duplicate destination delivery, not a new Meta acceptance. For Stripe, set metadata.order_id on the Checkout Session. Without that metadata, its Session ID is a separate fallback key; a different browser order ID will not automatically match it. See Stripe setup and order association.

Why is optional identity data absent?

Use a recognized ISO 3166-1 alpha-2 country code in user_data.country and a valid phone number with the required country context. An unusable optional country or phone can be omitted from identity matching while an otherwise eligible event is retained. This does not turn missing geography into a consent grant or relax the workspace’s policy. Check the event and delivery trace instead of assuming every supplied identifier was accepted. 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 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 far back can I read events and deliveries?

Event lists, cursor pages, event details, and lineage use the workspace’s plan window: 30 days on Free or 12 calendar months on paid plans, reduced by an explicit shorter workspace setting. A missing event can be outside that read window; it does not prove the original ingestion failed. Event records can outlast their original payloads and delivery records. The event read window does not extend replay eligibility or promise that every historical delivery trace remains available. The event-list responses label their delivery aggregate with metrics.acceptance_rate_window, such as 90_days, rather than all_time. Read this label: the aggregate is distinct from the paginated events and also respects the event-read boundary. A missing acceptance rate is not zero. See reporting windows.

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. The reconciliation report returns buckets as a JSON object keyed by reason. When there are no buckets, its value is {}, not []; an empty object is not evidence that every conversion reached Meta. Read the report’s counts, window and delivery states together.

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. Current TypeScript and Python 0.10.1 SDKs expose workspace_id in the regenerated response models. Older generated clients can continue reading signal_tracker_id where that compatibility field is exposed. Do not treat the two fields as different tenants. Check your installed SDK version before choosing a response field. 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.6.0, Python package plainrouter==0.6.0, Ruby gem plainrouter-sdk 0.2.0, and Go module github.com/plainrouter/sdk-go@v0.6.0 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.6.0 improves canonical documentation and package examples without changing the API contract. Install the CLI from PyPI with pipx install plainrouter==0.6.0, from npm with npm install --global @plainrouter/cli@0.6.0, 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.