Skip to content

Implement the validation URL

Before you start — you are implementing, not calling

The validation URL is an endpoint you create on your own server. Appmax calls that endpoint during installation (POST /app/client/generate) — you do not call anything from Appmax here.

If you are trying to find out which Appmax endpoint to invoke in order to "validate the URL", you are on the wrong track. What actually happens is:

  • Appmax sends a POST to your URL with a known payload.
  • You respond with HTTP 200 and a JSON body containing external_id (a UUID v4 generated by you).

To test the endpoint once implemented, use the interactive tool in Validate installation URL.

This guide gives you a ready-to-run handler in Go, Node.js and PHP. Copy it, adjust the route and register the public URL in the application panel.

Contract

Appmax makes a single server-to-server call against the validation URL registered in the panel, as part of processing POST /app/client/generate. Your URL must meet the contract below exactly.

Request — what Appmax sends you

ItemValue
MethodPOST
Content-Typeapplication/json
BodyJSON with the fields below — only app_id is guaranteed; the rest are optional
json
{
  "app_id": 123,
  "client_id": "MERCHANT_CLIENT_ID",
  "client_secret": "MERCHANT_CLIENT_SECRET",
  "client_key": "EXTERNAL_KEY",
  "external_key": "EXTERNAL_KEY"
}
FieldTypeRequiredDescription
app_idintegerYesThe application's App Numerical ID (numeric ID, e.g. 123) — not the UUID. The only field always present.
client_idstringNoClient ID generated for the merchant in this installation. May not be sent.
client_secretstringNoClient Secret generated for the merchant in this installation. May not be sent.
client_keystringNoSame value as external_key (kept for backwards compatibility). May not be sent.
external_keystringNoKey provided by the merchant when the installation was created (store_id, merchant_id, etc.). May not be sent.

WARNING

Your handler must only validate the presence of app_id — the remaining fields are optional and their absence must not be treated as an error. Remember that app_id arrives as the Numerical ID (numeric), not as a UUID.

Response — what you must return

ItemValue
HTTP status200 (exactly — 201, 204 and 2xx in general do not count)
Content-Typeapplication/json
BodyJSON with external_id (required) and alias (optional)
json
{
  "external_id": "37bb0791-ee0b-457d-860c-186e32978bcd",
  "alias": "My Store"
}
FieldTypeRequiredDescription
external_idstring (UUID v1-v5)YesA UUID generated by you, unique per installation. Appmax stores this value and later returns it as the external-id header in front-end calls through the CDN.
aliasstringNoDisplay name of the store in Appmax. If omitted, Appmax uses the default name.

Failures that abort the installation

If your URL responds with a status other than 200, without parseable JSON, or without an external_id in a valid UUID format, the POST /app/client/generate step returns 500 and no merchant credentials are issued. Details in Installation — health check.

Implementation

The examples below are minimal handlers that fulfil the contract. In production, before responding 200, persist the triple external_keyclient_id/client_secretexternal_id in your database (see the Persistence section).

Minimal dependencies — just net/http from the stdlib plus github.com/google/uuid to generate a UUID v4.

bash
go mod init my-app
go get github.com/google/uuid
go
package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/google/uuid"
)

type request struct {
	AppID        int64  `json:"app_id"`
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret"`
	ClientKey    string `json:"client_key"`
	ExternalKey  string `json:"external_key"`
}

type response struct {
	ExternalID string `json:"external_id"`
	Alias      string `json:"alias,omitempty"`
}

func validationHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", "POST")
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req request
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

	if req.AppID == 0 {
		http.Error(w, "invalid payload", http.StatusBadRequest)
		return
	}

	// In production: persist req.ClientID, req.ClientSecret and externalID
	// linked to req.ExternalKey in your database before responding.
	externalID := uuid.New().String()

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_ = json.NewEncoder(w).Encode(response{ExternalID: externalID})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/appmax/validate", validationHandler)

	log.Println("listening on :8080 — POST /appmax/validate")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

Minimal dependencies — Express plus crypto.randomUUID from the stdlib.

bash
npm init -y
npm install express
js
import express from 'express'
import crypto from 'node:crypto'

const app = express()
app.use(express.json())

app.post('/appmax/validate', (req, res) => {
  const { app_id, client_id, client_secret, client_key, external_key } = req.body ?? {}

  if (!app_id) {
    return res.status(400).json({ error: 'invalid payload' })
  }

  // In production: persist client_id, client_secret and externalId
  // linked to external_key in your database before responding.
  const externalId = crypto.randomUUID()

  res.status(200).json({ external_id: externalId })
})

app.listen(3000, () => {
  console.log('listening on :3000 — POST /appmax/validate')
})

No framework — json_decode straight from php://input and a UUID v4 generated with random_bytes.

php
<?php
declare(strict_types=1);

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Allow: POST');
    exit;
}

$raw = file_get_contents('php://input');
$body = json_decode($raw, true);

if (!is_array($body) || empty($body['app_id'])) {
    http_response_code(400);
    exit;
}

function uuidV4(): string {
    $b = random_bytes(16);
    $b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
    $b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}

// In production: persist client_id, client_secret and externalId
// linked to external_key in your database before responding.
$externalId = uuidV4();

header('Content-Type: application/json; charset=utf-8');
http_response_code(200);
echo json_encode(['external_id' => $externalId], JSON_THROW_ON_ERROR);

Persistence

The external_id is not disposable. It comes back as the external-id header in every front-end call through the CDN (tokenization, Apple Pay). Generate a new UUID on every health check request — Appmax rejects repeated values: if the external_id it receives already exists in the database, it is discarded and automatically replaced by the installation's client_id. Persist it the moment you generate it and, if the health check runs again for the same store, always keep the latest value returned and discard the previous one.

The minimum persistence structure is:

ColumnSourceUse
external_keyreceived in the payloadLinks the row to the platform's merchant.
client_idreceived in the payloadCredential used later in server-to-server calls.
client_secretreceived in the payloadCredential used later in server-to-server calls.
external_idgenerated by youReturned in the response and reused as the external-id header on the front end.
aliasoptionalDisplay name.

For a complete service example (callback + health check + Postgres) that wires this end to end, see Automate credential creation. For the external_id lifecycle after installation, see external-id.

Register the URL in the panel

Once your handler is live at a public URL, open View Application → Develop and fill the Validation URL field with the full URL (including the endpoint path, e.g. https://onboarding.myapp.com/appmax/validate). Details of the other panel fields in Application identifiers and URLs.

Before submitting for approval or starting a real installation, validate your handler in Validate installation URL — the tool makes two calls with different external_key values and shows both UUIDs side by side, to make sure you are not returning a hardcoded value.

Common limitations

  • localhost does not work — Appmax cannot reach private networks. In development, use ngrok, beeceptor or similar and register the generated public URL.
  • HTTPS is mandatory in production — in production Appmax only calls https:// URLs. In sandbox the validation tool accepts HTTP, but avoid that setup.
  • Redirects are not followed — the health check does not follow 301/302. If your URL redirects (e.g. trailing slash, canonical subdomain), respond at the final destination and register that URL in the panel.
  • Short timeout — respond in under 5 seconds. Heavy logic (third-party synchronization, welcome emails) must go to an asynchronous queue after the 200.
  • The HTTP status must be exactly 200201, 202 and 204 fail the installation.
  • external_id must be unique per installation — do not return a hardcoded UUID. The validation tool detects this and marks it as a failure.

See also