Docs/Shop Payments

Shop Payments

You manage your own product catalog. You just need Astapa to handle the Midtrans checkout. Pass an amount and an order ID โ€” we create the session, handle the webhook, and redirect the buyer back to you.

When should I use this?

Use shop payments when...Use items when...
Your product catalog lives in your own databaseYou want Astapa to manage the catalog
Each order has a unique amount (e.g. cart total)Items have a fixed price (with optional override)
You already have order IDs from your own systemYou want Astapa to generate order IDs
Karejo uses this
Shop payments power Karejo's store checkout. Each order has a unique amount and order ID from Karejo's own database โ€” Astapa just handles the Midtrans layer.

How it works

1
Your backend calls POST /api/platform/shop-payment
Pass your client credentials, the order amount, your own order ID, and buyer details.
2
You get back a checkout URL
Redirect the buyer to invoice_url. Midtrans handles the payment UI.
3
Midtrans notifies us
We verify the signature and record the payment.
4
We notify you
If you have a project-level webhook URL, we POST item.purchased to it.
5
Buyer is redirected
After payment, Midtrans sends the buyer to astapa.com/purchase/callback, which shows a status page and redirects back to your store if you passed metadata.store_slug.

Create a shop payment

Server-side only. Never call this from the browser โ€” your client_secret is in the request body.

create-shop-payment.tstypescript
const res = await fetch("https://astapa.com/api/platform/shop-payment", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    client_id: process.env.ASTAPA_CLIENT_ID,
    client_secret: process.env.ASTAPA_CLIENT_SECRET,
    amount: 75000,                          // IDR integer, required
    order_id: "your-internal-order-uuid",   // your order ID, must be unique
    buyer_email: "buyer@example.com",       // required
    buyer_name: "John Doe",                 // optional
    item_name: "Nasi Goreng Spesial",       // optional โ€” shown in checkout
    metadata: {                             // optional โ€” passed to webhook + callback
      store_id: "store-uuid",
      store_name: "Warung Bu Sari",
      store_slug: "warung-bu-sari",         // enables auto-redirect after payment
      product_id: "product-uuid",
    },
  }),
});

const { invoice_url, order_id, purchase_id } = await res.json();

// Redirect the buyer
return redirect(invoice_url);

Check payment status

Public endpoint โ€” no auth needed. Use this to poll for status or verify before fulfilling an order.

verify-payment.tstypescript
const res = await fetch(
  `https://astapa.com/api/platform/shop-payment/verify?order_id=${orderId}`
);
const { status, store_name, redirect_url } = await res.json();

// status: "pending" | "settlement" | "cancel" | "expire" | "refund"
// redirect_url: set if metadata.store_slug was provided

The payment callback page

After Midtrans payment, the buyer lands on astapa.com/purchase/callback?order_id=.... This page:

Shows a success, pending, or error state in Bahasa Indonesia
Polls every 3 seconds for up to 60 seconds if status is still pending
Auto-redirects back to your store if you passed metadata.store_slug
Pass store_slug for a seamless return flow
If you include metadata.store_slug, the callback page builds a redirect URL back to your store automatically. The buyer sees a success message, then gets sent home.

Webhook on settlement

Shop payments fire the same item.purchased event as item purchases. The payload includes your metadata.

webhook-payload.jsonjson
{
  "event": "item.purchased",
  "order_id": "your-internal-order-uuid",
  "item_id": "__direct__",
  "item_name": "Nasi Goreng Spesial",
  "buyer_email": "buyer@example.com",
  "buyer_name": "John Doe",
  "amount": 75000,
  "currency": "IDR",
  "paid_at": "2026-05-06T10: 00: 00Z",
  "metadata": {
    "store_id": "store-uuid",
    "store_name": "Warung Bu Sari",
    "store_slug": "warung-bu-sari",
    "product_id": "product-uuid"
  }
}
item_id will be __direct__
Shop payments use a sentinel item internally. The item_id in the webhook will always be __direct__ โ€” use your metadata.product_id to identify what was purchased.

Testing in sandbox

Use your sandbox client_id and client_secret. Midtrans sandbox is selected automatically.

MethodTest credentials
Credit card4811 1111 1111 1114 ยท any future expiry ยท CVV 123
GoPay / QRISAuto-approve in Midtrans sandbox dashboard

Related

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