AKD Pay Docs
Bahasa IndonesiaID Dashboard
LIVE This documentation applies to both sandbox and live.

Get started

Quickstart

From sign-up to your first webhook in the sandbox, in about 15 minutes.

This guide uses the sandbox: there is no real money, and payments can be paid by simulation. You need a terminal with curl and, for the webhook step, a server with a public HTTPS endpoint.

1. Sign up for a sandbox account#

Open the sandbox sign-up page, fill in your business name, contact name, work email, phone, and business type, then enter the 6-digit code sent to your email. Sandbox sign-up is automatic, with no approval to wait for: the account, the project "Proyek utama", QRIS and Virtual Account, payment simulation, and one API key are created at once, and you are signed in as the owner.

The success page shows your merchant_id, project_id, and sandbox API key. The API key is shown only once: copy it now and keep it on your server. Afterwards, sign in to the sandbox Merchant Dashboard (https://sandbox-merchant.algoritmakitadigital.id) with your email and a 6-digit code.

2. Get your sandbox credentials#

You need three values:

Value Example Where
Sandbox API key akd_sk_… the sign-up success page, shown once; afterwards the dashboard shows only its prefix. Lost it? Ask the AKD team for a new key.
project_id 8f1c2d3e-… the sign-up success page, or Merchant Dashboard → Pengembang (Developer) → environment and IDs
merchant_id UUID the sign-up success page, or Merchant Dashboard → Pengembang (for correspondence with the AKD team)

Keep the API key in a secret manager or server environment variable, never in a mobile app, frontend, or code repository. For the next steps:

export AKD_BASE_URL="https://sandbox-api.algoritmakitadigital.id"
export AKD_API_KEY="akd_sk_..."        # your sandbox API key
export AKD_PROJECT_ID="..."            # your sandbox project_id

Check that the key and project are correct. A 200 answer lists the payment methods enabled for your project:

curl -sS "$AKD_BASE_URL/v1/integration-context?project_id=$AKD_PROJECT_ID" \
  -H "Authorization: Bearer $AKD_API_KEY"

3. Create a QRIS payment#

Idempotency-Key is required; use one unique key per attempt to create a payment for an order.

cURL

curl -sS -X POST "$AKD_BASE_URL/v1/payments" \
  -H "Authorization: Bearer $AKD_API_KEY" \
  -H "Idempotency-Key: ORDER-1001-attempt-1" \
  -H "Content-Type: application/json" \
  --data @- <<JSON
{
  "project_id": "$AKD_PROJECT_ID",
  "external_order_id": "ORDER-1001",
  "amount": 15000,
  "currency": "IDR",
  "payment_method": "QRIS"
}
JSON

Node.js

// create-payment.mjs (Node.js 18+): node create-payment.mjs
const res = await fetch(`${process.env.AKD_BASE_URL}/v1/payments`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.AKD_API_KEY}`,
    'Idempotency-Key': 'ORDER-1001-attempt-1',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    project_id: process.env.AKD_PROJECT_ID,
    external_order_id: 'ORDER-1001',
    amount: 15000,
    currency: 'IDR',
    payment_method: 'QRIS',
  }),
});
const payment = await res.json();
if (!res.ok) throw new Error(`${res.status} ${payment.error}`);
console.log(payment.payment_id, payment.status, payment.checkout.qr_string);

PHP

<?php
// create-payment.php: php create-payment.php
$body = json_encode([
    'project_id' => getenv('AKD_PROJECT_ID'),
    'external_order_id' => 'ORDER-1001',
    'amount' => 15000,
    'currency' => 'IDR',
    'payment_method' => 'QRIS',
]);
$ch = curl_init(getenv('AKD_BASE_URL') . '/v1/payments');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('AKD_API_KEY'),
        'Idempotency-Key: ORDER-1001-attempt-1',
        'Content-Type: application/json',
    ],
]);
$raw = curl_exec($ch);
if ($raw === false) {
    throw new RuntimeException(curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$payment = json_decode($raw, true);
if ($status !== 200) {
    throw new RuntimeException($status . ' ' . ($payment['error'] ?? $raw));
}
echo $payment['payment_id'], ' ', $payment['status'], ' ', $payment['checkout']['qr_string'], PHP_EOL;

Go

// main.go: go run main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
)

func main() {
	body, err := json.Marshal(map[string]any{
		"project_id":        os.Getenv("AKD_PROJECT_ID"),
		"external_order_id": "ORDER-1001",
		"amount":            15000,
		"currency":          "IDR",
		"payment_method":    "QRIS",
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost, os.Getenv("AKD_BASE_URL")+"/v1/payments", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AKD_API_KEY"))
	req.Header.Set("Idempotency-Key", "ORDER-1001-attempt-1")
	req.Header.Set("Content-Type", "application/json")

	res, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	var payment struct {
		PaymentID string `json:"payment_id"`
		Status    string `json:"status"`
		Error     string `json:"error"`
		Checkout  struct {
			QRString string `json:"qr_string"`
		} `json:"checkout"`
	}
	if err := json.NewDecoder(res.Body).Decode(&payment); err != nil {
		log.Fatal(err)
	}
	if res.StatusCode != http.StatusOK {
		log.Fatalf("%d %s", res.StatusCode, payment.Error)
	}
	fmt.Println(payment.PaymentID, payment.Status, payment.Checkout.QRString)
}

A 200 response contains payment_id, status: "PENDING", expires_at (currently 30 minutes), and checkout.qr_string. Your app renders this string as a QR code. Keep the payment_id:

export AKD_PAYMENT_ID="..."   # payment_id from the response

Fields and errors: Creating a payment.

4. Simulate the payment#

In the sandbox, request a simulated payment for a PENDING payment (no body):

curl -sS -X POST "$AKD_BASE_URL/v1/payments/$AKD_PAYMENT_ID/simulate" \
  -H "Authorization: Bearer $AKD_API_KEY"

The answer is 202 {"status":"simulation_requested"}. A few seconds later the payment becomes PAID:

curl -sS "$AKD_BASE_URL/v1/payments/$AKD_PAYMENT_ID" \
  -H "Authorization: Bearer $AKD_API_KEY"

5. Receive the payment.paid webhook#

When the payment is paid, AKD sends a JSON POST to your HTTPS webhook endpoint with the headers X-AKD-Signature-V2 and X-AKD-Event-ID.

  1. Ask the AKD team to register your sandbox webhook endpoint (see change requests). The signing secret (whsec_…) is shown only once; store it on your server.
  2. Add signature verification from the Node.js, PHP, or Go example: read the raw body, reject timestamps more than 5 minutes off, compare the HMAC in constant time.
  3. Deduplicate on X-AKD-Event-ID, mark the order paid exactly once, and answer 2xx within 10 seconds.
  4. Create a new payment and simulate it again; check that the webhook arrives and passes verification.

6. Check the Merchant Dashboard#

In the sandbox Merchant Dashboard:

  • Transaksi (Transactions): payment ORDER-1001 is paid, with its status timeline.
  • Pengembang → Riwayat webhook (Developer → webhook history): the payment.paid delivery is DELIVERED, with your server's response code. Failed deliveries are retried automatically; those that end DEAD can be resent from here.

Next steps#

  • Complete the testing checklist: idempotency, errors, payment.expired, Virtual Account.
  • Read the onboarding flow through go-live. When your integration is ready, the owner applies for a live account from the Ajukan Live (Apply for live) menu of the sandbox Merchant Dashboard.
  • Read error codes and rate limits before production.