---
description: "Send checkout data to your own endpoint, inspect the response, and replay the request before enabling a sequence."
---
# Custom API

Use **Send Webhook** in a Plandalf sequence to send a JSON request to your server. You choose the destination, method, payload and headers. Your server performs the next step, such as recording an order or provisioning access.

> [!NOTE]
> **Verification status**
> On 2 October 2026, the local editor sent a synthetic request to a local receiver. A second request returned the same unique record. A deliberate HTTP 503 exposed a false success message; a local repair now shows the failure and clears the previous test result. Recovery to HTTP 200 was replayed. This repair has not been deployed. The sequence remains paused. A new paid checkout, automatic delivery, authenticated endpoint, promo enrolment and affiliate sale have not been replayed for this integration.

## Choose your path

| Product | Start here | What to verify |
| --- | --- | --- |
| Checkouts | [Create the checkout](#prepare-your-checkout), then add the action below | Paid invoice and matching destination record |
| Automations | [Configure Send Webhook](#configure-send-webhook) | Request, HTTP status, response and duplicate handling |
| Timers / Promos | [Connect a campaign](#connect-a-promo) | Stable participant, deadline and checkout price |
| Affiliates | [Choose the sale source](#track-affiliate-sales) | Attributed sale, commission, duplicate and refund |

## Before you start

-   A Plandalf organization and a test checkout.
-   A server endpoint you control. For a hosted Plandalf account, use a reachable HTTPS endpoint. `127.0.0.1` in these screenshots works only because both the local Plandalf app and receiver ran on the same computer.
-   A sandbox receiver that records the result without sending messages, granting real access or fulfilling real orders.
-   Access to the receiver’s records and the payment provider’s test records.

Keep a receiver credential in your team’s approved password manager. Record only its vault reference in a replay recipe. Use a dedicated sandbox credential with the permissions your receiver needs. Never put a private API key in website code, a guide, a screenshot or a test report.

The action’s **Headers** setting can send credentials to your destination. Its configuration and test details can display those values; treat them as sensitive. This guide’s local receiver uses synthetic data and no credential. Safe masked credential entry and storage are not verified here.

## Prepare your checkout

Follow [Create your first checkout](https://plandalf.com/docs/product-guides/checkouts/create) to configure a product and price. Confirm the payment connection is in test mode before paying.

Open the checkout editor, choose **Automation**, then **New**. The local editor created a paused sequence with a **Checkout completed** trigger linked to that checkout. Keep it off while preparing the destination.

Expand the trigger and check its checkout selection. Test the trigger and inspect the selected sample before using its fields. A trigger test may use an existing invoice or generated sample data; it does not collect a payment.

## Configure Send Webhook

1.  Choose **Add an action**.
2.  Choose the **Plandalf** app and **Send Webhook**, then **Create Action**.
3.  Open **2\. Config** and give the action a recognizable name.
4.  Set **Webhook URL** to your sandbox receiver and **HTTP Method** to `POST`.
5.  Enter the JSON your destination expects in **Payload**. Add headers only when your destination requires them.
6.  Choose **Save & Continue**.

<figure data-docs-annotated-screenshot="" style="margin:1.5rem 0"><div style="position:relative;line-height:0"><img src="/images/integrations/custom-api/webhook-config-desktop.jpg" alt="The actual Send Webhook configuration with a local URL, POST method, synthetic JSON and empty headers." width="1440" height="1000" loading="lazy" style="display:block;width:100%;height:auto;margin:0;border-radius:0"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1000" aria-hidden="true" focusable="false" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none"><g><line x1="1130" y1="205" x2="652" y2="227" stroke="white" stroke-width="12"></line><line x1="1130" y1="205" x2="652" y2="227" stroke="#b5264d" stroke-width="6"></line><polygon points="652,227 668.1765426999351,234.89426793801536 667.3830207378885,217.65319985354927" fill="#b5264d"></polygon><circle cx="1130" cy="205" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="1130" y="215" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">1</text></g><g><line x1="1040" y1="432" x2="576" y2="453" stroke="white" stroke-width="12"></line><line x1="1040" y1="432" x2="576" y2="453" stroke="#b5264d" stroke-width="6"></line><polygon points="576,453 592.1704997180958,460.90663891088144 591.3901655155871,443.66496891259413" fill="#b5264d"></polygon><circle cx="1040" cy="432" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="1040" y="442" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">2</text></g><g><line x1="1115" y1="775" x2="713" y2="618" stroke="white" stroke-width="12"></line><line x1="1115" y1="775" x2="713" y2="618" stroke="#b5264d" stroke-width="6"></line><polygon points="713,618 724.5747766342132,631.7849390955517 730.8535047816249,615.7081956862556" fill="#b5264d"></polygon><circle cx="1115" cy="775" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="1115" y="785" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">3</text></g></svg></div><figcaption style="line-height:1.6;margin-top:1rem"><p>Local setup using a fixed sample record. Replace the local URL with your own reachable sandbox endpoint.</p><ol style="list-style-type:decimal;padding-left:1.5rem"><li>Set the receiver URL. The local address shown is not a hosted-account destination.</li><li>Start with synthetic JSON, then verify the mapped trigger fields separately.</li><li>Add only the headers your receiver requires. Keep their values out of screenshots.</li></ol></figcaption></figure>

### Start with a fixed sample

This is the exact synthetic payload used in the local run:

```json
{
  "event": "checkout.completed",
  "invoice_ulid": "inv_example",
  "example": true
}
```

The receiver accepted it, recorded the event and invoice identifier, and returned:

```json
{"accepted":true,"duplicate":false,"uniqueRecords":1}
```

For real checkout data, replace the fixed invoice identifier using the tested trigger’s variable picker. The current Checkout completed source exposes `invoice_ulid`, `invoice_number`, `customer_email`, `customer_name`, `total`, `currency`, `offer_id` and `line_items`, among other fields. It does not send all those fields automatically: include only what your endpoint needs.

For example, this mapping is source-checked but still needs a paid-event replay in your account:

```json
{
  "event": "checkout.completed",
  "invoice_ulid": "{{trigger.invoice_ulid}}",
  "invoice_number": "{{trigger.invoice_number}}"
}
```

Have the receiver reject an empty identifier. Use the paid invoice identifier, event type and organization scope as a durable deduplication key. Enforce uniqueness in storage before creating an order or granting access.

## Test the destination

Open **3\. Test**, choose **Run Test**, then inspect **Action Output** and the destination record.

<figure data-docs-annotated-screenshot="" style="margin:1.5rem 0"><div style="position:relative;line-height:0"><img src="/images/integrations/custom-api/webhook-accepted-desktop.jpg" alt="Webhook test output showing HTTP 200 and the receiver's accepted response with one unique record." width="1440" height="1000" loading="lazy" style="display:block;width:100%;height:auto;margin:0;border-radius:0"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1000" aria-hidden="true" focusable="false" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none"><g><line x1="995" y1="420" x2="454" y2="499" stroke="white" stroke-width="12"></line><line x1="995" y1="420" x2="454" y2="499" stroke="#b5264d" stroke-width="6"></line><polygon points="454,499 470.87764323105813,505.2566092226634 468.38378467951327,488.1784133190461" fill="#b5264d"></polygon><circle cx="995" cy="420" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="995" y="430" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">1</text></g><g><line x1="995" y1="593" x2="752" y2="515" stroke="white" stroke-width="12"></line><line x1="995" y1="593" x2="752" y2="515" stroke="#b5264d" stroke-width="6"></line><polygon points="752,515 764.4031655157494,528.0445960147842 769.6781079832052,511.6111214046333" fill="#b5264d"></polygon><circle cx="995" cy="593" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="995" y="603" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">2</text></g></svg></div><figcaption style="line-height:1.6;margin-top:1rem"><p>The receiver acknowledged the synthetic request. This proves an action test reached the endpoint; it does not prove a paid checkout fired the sequence.</p><ol style="list-style-type:decimal;padding-left:1.5rem"><li>Check status_code: this receiver returned HTTP 200.</li><li>Read the destination response and reconcile it with the destination record.</li></ol></figcaption></figure>

Run the same test again. The local receiver returned `duplicate: true` and `uniqueRecords: 1`. That behavior belongs to the receiver; your own endpoint must implement its own persistent deduplication.

### Check failures explicitly

> [!WARNING]
> **Verify failure handling in your running version**
> The original local run incorrectly showed success for HTTP 503. A local repair now rejects non-2xx responses, clears old successful test results and requires a new successful test before finishing. The repair is not deployed. Check the actual response and destination record in your running version. Automatic workflow retries remain unverified.

<figure data-docs-annotated-screenshot="" style="margin:1.5rem 0"><div style="position:relative;line-height:0"><img src="/images/integrations/custom-api/webhook-failure-corrected-desktop.jpg" alt="The repaired local editor shows HTTP 503 as a failed test, marks the action Not tested and requires a successful test before finishing." width="1440" height="1000" loading="lazy" style="display:block;width:100%;height:auto;margin:0;border-radius:0"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 1000" aria-hidden="true" focusable="false" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none"><g><line x1="580" y1="200" x2="1079" y2="264" stroke="white" stroke-width="12"></line><line x1="580" y1="200" x2="1079" y2="264" stroke="#b5264d" stroke-width="6"></line><polygon points="1079,264 1064.4296741156968,253.4309128291416 1062.234039137278,270.55000430150045" fill="#b5264d"></polygon><circle cx="580" cy="200" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="580" y="210" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">1</text></g><g><line x1="960" y1="730" x2="720" y2="833" stroke="white" stroke-width="12"></line><line x1="960" y1="730" x2="720" y2="833" stroke="#b5264d" stroke-width="6"></line><polygon points="720,833 737.9195083434607,834.7003589999289 731.1127544401924,818.8399615554199" fill="#b5264d"></polygon><circle cx="960" cy="730" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="960" y="740" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">2</text></g><g><line x1="580" y1="956" x2="1038" y2="920" stroke="white" stroke-width="12"></line><line x1="580" y1="956" x2="1038" y2="920" stroke="#b5264d" stroke-width="6"></line><polygon points="1038,920 1021.5758591214128,912.6347032374575 1022.928315291217,929.8409511755236" fill="#b5264d"></polygon><circle cx="580" cy="956" r="27" fill="#b5264d" stroke="white" stroke-width="4"></circle><text x="580" y="966" text-anchor="middle" fill="white" font-size="29" font-weight="700" font-family="system-ui,sans-serif">3</text></g></svg></div><figcaption style="line-height:1.6;margin-top:1rem"><p>Local repair: the intentional HTTP 503 now fails the test. After reload, the action remains Not tested. Restoring the receiver and testing again returned HTTP 200.</p><ol style="list-style-type:decimal;padding-left:1.5rem"><li>A failed attempt removes the previous Tested badge.</li><li>HTTP 503 is a failure. Restore the receiver, then run a new test.</li><li>Finish stays unavailable until a new test succeeds.</li></ol></figcaption></figure>

The current workflow source records a failed step and continues to later steps. Do not rely on a failed webhook to stop later fulfilment actions. Verify the event log and recovery procedure with an isolated sandbox run before enabling the sequence.

## Verify a paid checkout

After the fixed sample works, use the tested trigger fields and your authenticated sandbox destination. Enable only this test sequence, complete a new test payment, and check all of the following:

1.  The payment provider shows the successful test payment.
2.  The Plandalf invoice is paid and its amount and currency match.
3.  The sequence’s **Events** view shows this checkout’s run.
4.  Your receiver has a matching invoice identifier and the intended result.
5.  Replaying the same event creates no second order or entitlement.
6.  A declined checkout grants no access and creates no paid-order record.
7.  A failed receiver request is visible and has a tested recovery procedure.

Do not infer payment or access from the browser’s thank-you page alone. Keep production fulfilment disabled until this replay passes.

## Use signed payment events when needed

**Send Webhook** does not automatically add a Plandalf signature. Configurable headers are separate from signature verification.

Plandalf also has a webhook endpoint API for signed events. The implementation registers endpoints through `POST /api/v1/webhook_endpoints`, returns a signing secret at creation, and sends `Plandalf-Signature` and `Plandalf-Event-Id` headers. Its delivery worker records responses and schedules retries. This is a separate delivery path with its own payload contract.

That endpoint requires a public HTTPS URL. Keep its signing secret on your server, verify the signature against the raw request body and reject stale timestamps before processing. Deduplicate by event ID. Use the [MemberPress technical reference](https://plandalf.com/docs/product-guides/platforms/memberpress-reference) for an existing client example of this protocol. The signed API path has not been replayed in this Custom API guide.

## Connect a promo

Create the campaign using [Create a promo](https://plandalf.com/docs/product-guides/promos/create-promo), then connect the checkout and countdown using [Custom HTML](https://plandalf.com/docs/product-guides/platforms/custom-html). Verify the price before and after a tier changes.

If your backend supplies the enrolment event, use a Plandalf **Webhook Trigger** followed by **Enroll in Promo**. Copy that trigger’s actual catch URL and map your stable participant identifier in the action. Keep the catch URL private and send the same identifier when the same person returns. An arbitrary API call does not automatically bind a participant to a checkout.

This inbound enrolment path still needs its own replay: first enrolment, repeated enrolment, returning participant, deadline, and matching checkout price. The outbound action test above does not prove these outcomes.

## Track affiliate sales

Choose the source of the paid sale before configuring reporting:

-   **Plandalf checkout:** follow [Affiliates with Plandalf checkout](https://plandalf.com/docs/product-guides/affiliates/plandalf-checkout). Verify the referral reaches the checkout and the paid invoice creates the expected attributed sale.
-   **Your own checkout:** capture the referral context, then report the confirmed payment from your server using the [Affiliate sales API](https://plandalf.com/docs/product-guides/affiliates/sales-api). Follow its separate refund and duplicate rules.

Use an approved vault reference for your server’s API key. Keep test and live records separate. Test the initial sale, duplicate, refund and renewal where applicable. Sending a generic webhook to your server does not itself create an affiliate commission.

## Next steps

-   [Custom API integration overview](https://plandalf.com/platforms/custom-api)
-   [Custom HTML checkout and countdown setup](https://plandalf.com/docs/product-guides/platforms/custom-html)
-   [Affiliate sales API](https://plandalf.com/docs/product-guides/affiliates/sales-api)

Source: https://plandalf.com/docs/product-guides/platforms/custom-api
