# REST & other languages

> Create payments over plain HTTP from PHP, Laravel, Java / Spring Boot, Go, Python, Node.js without an SDK, or curl.

The API is plain JSON over HTTPS. You don't need an SDK. Every example below creates a **test** payment; run them on your server with:

```bash
export DEVZITY_SECRET_KEY=dz_test_sk_...
export DEVZITY_API_URL=https://api.devzity.com
```

- Headers: `Authorization: Bearer <secret key>`, `Content-Type: application/json`, and an `Idempotency-Key` (see [Idempotency](https://docs.devzity.com/v1/idempotency.md)).
- Amounts are strings: `"500.00"`.

Pick your language:

## curl

create-payment.sh:

```bash
#!/usr/bin/env bash
# Create a TEST payment with plain curl. Needs a Test secret key from
# Developers → API keys (it starts with dz_test_sk_). No real money is involved.
set -euo pipefail
: "${DEVZITY_SECRET_KEY:?Set DEVZITY_SECRET_KEY to your dz_test_sk_... key}"
: "${DEVZITY_API_URL:?Set DEVZITY_API_URL to the Devzity API base URL you were given}"

curl "$DEVZITY_API_URL/v1/payments" \
  -X POST \
  -H "Authorization: Bearer $DEVZITY_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount":"500.00"}'
```

## Node.js (no SDK)

create-payment.mjs:

```js
// Plain Node.js (18+), no dependencies: create a TEST payment with fetch.
// Run on your SERVER — the secret key must never reach a browser or an app.
import { randomUUID } from "node:crypto";

const apiUrl = process.env.DEVZITY_API_URL; // the Devzity API base URL you were given
if (!apiUrl || !process.env.DEVZITY_SECRET_KEY) throw new Error("Set DEVZITY_API_URL and DEVZITY_SECRET_KEY");

const response = await fetch(`${apiUrl}/v1/payments`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DEVZITY_SECRET_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({ amount: "500.00" }),
});

const payment = await response.json();
if (!response.ok) {
  // Every error has a code, a message and a request_id to quote to support.
  throw new Error(`${payment.error.code}: ${payment.error.message} (${payment.error.request_id})`);
}
console.log(payment.checkout_url);
```

## PHP

create-payment.php:

```php
<?php
// Plain PHP (cURL extension) — run on your SERVER. Test mode: use a dz_test_sk_ key.
$secretKey = getenv('DEVZITY_SECRET_KEY');
$apiUrl = getenv('DEVZITY_API_URL'); // the Devzity API base URL you were given
if (!$secretKey || !$apiUrl) {
    throw new RuntimeException('Set DEVZITY_SECRET_KEY and DEVZITY_API_URL');
}

$ch = curl_init("$apiUrl/v1/payments");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $secretKey",
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode(['amount' => '500.00']),
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$payment = json_decode($body, true);
if ($status >= 400) {
    // Every error has a code, a message and a request_id to quote to support.
    throw new RuntimeException("{$payment['error']['code']}: {$payment['error']['message']} ({$payment['error']['request_id']})");
}

header('Location: ' . $payment['checkout_url']); // or echo it / return it to your app
```

## Laravel

DevzityController.php:

```php
<?php
// app/Http/Controllers/DevzityController.php (Laravel 10+)
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class DevzityController extends Controller
{
    // POST /pay — creates a TEST payment and sends the customer to checkout.
    public function pay()
    {
        $response = Http::withToken(config('services.devzity.secret'))
            ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
            ->post(config('services.devzity.url') . '/v1/payments', ['amount' => '500.00'])
            ->throw();

        return redirect()->away($response->json('checkout_url'));
    }

    // POST /devzity/webhook — exclude this route from CSRF verification.
    public function webhook(Request $request)
    {
        $rawBody = $request->getContent();                          // the RAW body
        $header = (string) $request->header('Devzity-Signature');
        parse_str(str_replace(',', '&', $header), $parts);
        $timestamp = $parts['t'] ?? '';
        $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, config('services.devzity.webhook_secret'));

        abort_unless(
            ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300 && hash_equals($expected, $parts['v1'] ?? ''),
            400
        );

        $event = json_decode($rawBody, true);
        if ($event['type'] === 'payment.paid') {
            // Fulfil $event['data']['object']['id']. Deduplicate on $event['id'].
        }

        return response()->json(['received' => true]);
    }
}
```

Setup:

setup.md:

```text
# Laravel setup

`config/services.php`:

```php
'devzity' => [
    'url' => env('DEVZITY_API_URL'),
    'secret' => env('DEVZITY_SECRET_KEY'),
    'webhook_secret' => env('DEVZITY_WEBHOOK_SECRET'),
],
```

`.env` (server-side only): `DEVZITY_SECRET_KEY=dz_test_sk_...` and `DEVZITY_API_URL=<the Devzity API base URL you were given>`

`routes/web.php`:

```php
Route::post('/pay', [DevzityController::class, 'pay']);
Route::post('/devzity/webhook', [DevzityController::class, 'webhook']);
```

Exclude `devzity/webhook` from CSRF in `bootstrap/app.php`:
`$middleware->validateCsrfTokens(except: ['devzity/webhook']);`
```

## Java / Spring Boot

DevzityService.java:

```java
package com.example.devzity;

import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

/**
 * Creates Devzity payments from your Spring Boot SERVER. The secret key comes from
 * configuration (devzity.secret-key / DEVZITY_SECRET_KEY) — never hard-code it.
 */
@Service
public class DevzityService {

    private final RestClient client;

    public DevzityService(
            @Value("${devzity.api-url}") String apiUrl,
            @Value("${devzity.secret-key}") String secretKey) {
        this.client = RestClient.builder()
                .baseUrl(apiUrl)
                .defaultHeader("Authorization", "Bearer " + secretKey)
                .build();
    }

    /** Returns the checkout_url to send the customer (or your mobile app) to. */
    @SuppressWarnings("unchecked")
    public String createPayment(String amount) {
        Map<String, Object> payment = client.post()
                .uri("/v1/payments")
                .header("Idempotency-Key", UUID.randomUUID().toString())
                .body(Map.of("amount", amount))
                .retrieve()
                .body(Map.class);
        return (String) payment.get("checkout_url");
    }
}
```

application.properties:

```properties
# Supply these as environment variables (DEVZITY_API_URL, DEVZITY_SECRET_KEY, DEVZITY_WEBHOOK_SECRET) — never commit them.
devzity.api-url=${DEVZITY_API_URL}
devzity.secret-key=${DEVZITY_SECRET_KEY}
devzity.webhook-secret=${DEVZITY_WEBHOOK_SECRET:}
```

Supply `DEVZITY_API_URL`, `DEVZITY_SECRET_KEY` and `DEVZITY_WEBHOOK_SECRET` as environment variables; never commit them.

## Go

create_payment.go:

```go
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

// createPayment creates a TEST payment with plain net/http and returns its checkout URL.
// Run it on your SERVER; the secret key must never reach a browser or an app.
func createPayment(amount string) (string, error) {
	apiURL := os.Getenv("DEVZITY_API_URL") // the Devzity API base URL you were given
	if apiURL == "" || os.Getenv("DEVZITY_SECRET_KEY") == "" {
		return "", fmt.Errorf("set DEVZITY_API_URL and DEVZITY_SECRET_KEY")
	}

	body, _ := json.Marshal(map[string]string{"amount": amount})
	req, err := http.NewRequest(http.MethodPost, apiURL+"/v1/payments", bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("DEVZITY_SECRET_KEY"))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", newIdempotencyKey())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var payload struct {
		CheckoutURL string `json:"checkout_url"`
		Error       struct {
			Code      string `json:"code"`
			Message   string `json:"message"`
			RequestID string `json:"request_id"`
		} `json:"error"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return "", err
	}
	if resp.StatusCode >= 400 {
		// Every error has a code, a message and a request_id to quote to support.
		return "", fmt.Errorf("%s: %s (%s)", payload.Error.Code, payload.Error.Message, payload.Error.RequestID)
	}
	return payload.CheckoutURL, nil
}

func newIdempotencyKey() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	return fmt.Sprintf("%x", b)
}
```

## Python

create_payment.py:

```python
"""Create a TEST payment with only the Python standard library (3.8+).
Run on your SERVER; the secret key must never reach a browser or an app."""
import json
import os
import urllib.error
import urllib.request
import uuid

api_url = os.environ["DEVZITY_API_URL"]  # the Devzity API base URL you were given

request = urllib.request.Request(
    f"{api_url}/v1/payments",
    data=json.dumps({"amount": "500.00"}).encode(),
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['DEVZITY_SECRET_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
)

try:
    with urllib.request.urlopen(request) as response:
        payment = json.load(response)
except urllib.error.HTTPError as error:
    # Every error has a code, a message and a request_id to quote to support.
    detail = json.load(error)["error"]
    raise SystemExit(f"{detail['code']}: {detail['message']} ({detail['request_id']})")

print(payment["checkout_url"])
```

The response is the [Payment object](https://docs.devzity.com/v1/payments.md); send the customer to `checkout_url`.

## Webhooks

Every language needs the same steps: read the **raw** body, verify `Devzity-Signature`, deduplicate on `event.id`, respond `2xx`. Ready-to-copy verifiers for Node.js, PHP, Java, Go and Python are on the [Webhooks](https://docs.devzity.com/v1/webhooks.md) page.

## Errors

A failed request returns a JSON error with a `code`, a `message`, an optional `param`, and a `request_id`:

```json
{ "error": { "code": "invalid_amount", "message": "Enter a valid amount, e.g. 500.00.", "param": "amount", "request_id": "req_…" } }
```

See [Errors](https://docs.devzity.com/v1/errors.md) and [Troubleshooting](https://docs.devzity.com/v1/troubleshooting.md). The complete request and response reference is the [API reference](https://docs.devzity.com/v1/api-reference.md); the machine-readable source is `openapi/devzity-v1.yaml`.
