Docs/Webhooks

Webhooks

Set one URL on your project. We POST to it every time a payment settles — subscriptions, item purchases, shop payments. One endpoint, all events.

Set your webhook URL

Go to your project's Settings tab in the dashboard and paste your URL under "Payment webhook URL". Or do it via API:

set-webhook.tstypescript
await fetch(`https://astapa.com/api/platform/projects/${projectId}`, {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  credentials: "include", // builder session cookie
  body: JSON.stringify({
    webhook_url: "https://yourapp.com/webhooks/astapa",
  }),
});
One URL covers everything
This single URL receives all payment events — subscription payments, item purchases, and shop payments. Use the X-Astapa-Event header to tell them apart.

Events

EventTrigger
payment.settledSubscription payment confirmed (plan claim already updated)
item.purchasedItem purchase or shop payment confirmed

Payload shapes

payment.settled

payment.settled.jsonjson
{
  "event": "payment.settled",
  "order_id": "proj-abc123-1a2b3c4d",
  "plan_id": "plan-uuid",
  "plan_key": "pro",
  "end_user_id": "user-uuid",
  "amount": 29000,
  "currency": "IDR",
  "paid_at": "2026-05-06T10: 00: 00Z",
  "metadata": null
}

item.purchased

item.purchased.jsonjson
{
  "event": "item.purchased",
  "order_id": "item-abc12345-1a2b3c4d",
  "item_id": "item-uuid",
  "item_name": "Premium Template",
  "buyer_email": "buyer@example.com",
  "buyer_name": "John Doe",
  "amount": 50000,
  "currency": "IDR",
  "paid_at": "2026-05-06T10: 00: 00Z",
  "metadata": { "ref": "campaign-42" }
}
X-Astapa-Event header
Every request includes X-Astapa-Event: <event_name>. Route on this instead of reading the body first.

Handle both events in one endpoint

webhook-handler.tstypescript
export async function POST(req: Request) {
  const event = req.headers.get("X-Astapa-Event");
  const body = await req.json();

  if (event === "payment.settled") {
    // Plan claim is already updated — do your side effects here
    await sendReceiptEmail(body.end_user_id, body.amount);
    await provisionResources(body.end_user_id, body.plan_key);
  }

  if (event === "item.purchased") {
    await grantAccess(body.buyer_email, body.item_id);
    await sendDownloadLink(body.buyer_email, body.metadata);
  }

  // Always return 200 — anything else triggers a retry
  return new Response("ok");
}
Return 200 or we'll retry
If your endpoint returns a non-2xx status or times out (10s limit), the delivery is marked failed. You can retry manually from the dashboard or via API.

Webhook URL priority for item purchases

For item purchases, we check in this order:

1
Item-level webhook_url — Set when creating/updating the item. Takes priority.
2
Project-level webhook_url — Set in Settings. Used as fallback if the item has no URL.
3
Nothing — No webhook fires. The purchase is still recorded.
Just use the project-level URL
Unless you need different endpoints per item, set the project-level URL and leave item webhook_url blank. Simpler to manage.

Delivery behavior

Async — Webhooks fire after we respond to Midtrans — they don't block payment processing.
10s timeout — If your endpoint doesn't respond within 10 seconds, the delivery fails.
One attempt — We try once. If it fails, webhook_status is set to failed. You retry manually.
Everything is logged — Every attempt is stored with the full payload, HTTP response status, and response body.

Retrying failed deliveries

From the dashboard: Payments → Subscriptions or Items tab → find the row with failed webhook status → click Retry.

Or via API:

Webhook delivery logs

Every attempt is logged. View them in the dashboard under Payments → Webhook Logs, or fetch them via API. Each log entry has the full payload, HTTP status, response body, and attempt number.

fetch-logs.tstypescript
const res = await fetch(
  `https://astapa.com/api/platform/projects/${projectId}/webhook-logs`,
  { credentials: "include" }
);
const { logs } = await res.json();

// Filter to a specific order
const res2 = await fetch(
  `https://astapa.com/api/platform/projects/${projectId}/webhook-logs?order_id=${orderId}`,
  { credentials: "include" }
);

Related

API Playground
Click "Try it" on any endpoint to get started.
Webhooks | Astapa