# Integration quick start (/docs/server-to-server/integration-quick-start)

## What this covers [#what-this-covers]

The shortest path to a successful test transaction: create a client, create a purchase, submit card data, complete 3D Secure, confirm the result. Six requests.

Once this works, the *Integration guide* covers every flow in depth — preauthorisation, delayed capture, external MPI, network tokens, merchant-initiated charges and the full webhook event set.

## Before you start [#before-you-start]

| You need | Where from |
|---|---|
| **Bearer token** | Developers section of your dashboard |
| **S2S token** | Issued by Precium to qualifying merchants. **Not on the dashboard** |
| **Brand ID** | Your dashboard. May require approval before live use |
| **An HTTPS endpoint** | Yours, for the 3D Secure return and for webhooks |

Use your **test** keys throughout. Test and production share one base URL and are separated only by the key you send, so there is no URL to get wrong — and no URL-level protection if you send the wrong key.

```text
https://gate.reviopay.com/api/v1/
```


## Step 0 — Check your credentials [step] [#step-0--check-your-credentials-step]

Confirm the key works before building anything:

```bash
curl -X GET https://gate.reviopay.com/api/v1/clients/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY"
```

An empty result set is success. A `401` means the key is wrong or malformed; check for a stray newline or a missing `Bearer` prefix.

## Step 1 — Create a client [step] [#step-1--create-a-client-step]

A client is the customer record that purchases attach to. Only `email` is required, and it must be unique.

```bash
curl -X POST https://gate.reviopay.com/api/v1/clients/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@example.com",
    "full_name": "Test Buyer",
    "phone": "+27821234567"
  }'
```

Keep the returned `id` — that is your `client_id`.

You can skip this step entirely by passing an inline `client` object when creating the purchase. Precium will find or create the client from the email. Creating the client explicitly is clearer when you already hold customer records.

## Step 2 — Create a purchase [step] [#step-2--create-a-purchase-step]

Three fields are required: `client_id` (or an inline `client`), `purchase`, and `brand_id`. **All amounts are integers in cents** — 149900 is R1,499.00.

```bash
curl -X POST https://gate.reviopay.com/api/v1/purchases/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "00000000-0000-4000-8000-000000000001",
    "brand_id":  "00000000-0000-4000-8000-000000000002",
    "purchase": {
      "currency": "ZAR",
      "language": "en",
      "products": [
        { "name": "Order 1042", "price": 149900 }
      ]
    },
    "payment_method_whitelist": ["visa", "mastercard"],
    "reference": "ORDER-1042",
    "force_recurring": false,
    "skip_capture": false,
    "success_redirect": "https://www.example.com/success",
    "failure_redirect": "https://www.example.com/failure",
    "cancel_redirect":  "https://www.example.com/cancel"
  }'
```

From the response, keep:

| Field | Use |
|---|---|
| `id` | The purchase id. Every later call uses it |
| `direct_post_url` | Where you submit card data in step 3 |
| `checkout_url` | Precium-hosted page, if you would rather not handle card data at all |
| `status` | `created` at this point |

<Callout type="warning">
**If `direct_post_url` is null**, it is because `success_redirect` and `failure_redirect` were not both supplied. Both are required for the direct post flow.
</Callout>

### Two flags to be deliberate about [step] [#two-flags-to-be-deliberate-about-step]

| Flag | Effect |
|---|---|
| `skip_capture: false` | Authorise and capture in one step. This is what you want for a simple sale |
| `skip_capture: true` | Authorise only. Funds are held and you must later `/capture/` or `/release/`. With a purchase total of 0 this becomes preauthorisation — card verification with no financial transaction |
| `force_recurring: true` | Store the card. The purchase id then becomes the token for future merchant-initiated charges |

## Step 3 — Submit the card data [step] [#step-3--submit-the-card-data-step]

Post the card fields to the `direct_post_url` from step 2, with `?s2s=true`. This request uses your **S2S token**, not the bearer token.

```bash
curl -X POST "$DIRECT_POST_URL?s2s=true" \
  -H "Authorization: Bearer $PRECIUM_S2S_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cardholder_name": "Test Buyer",
    "card_number": "4111111111111111",
    "expires": "12/29",
    "cvc": "123",
    "remember_card": "off",
    "remote_ip": "196.0.0.1",
    "user_agent": "Mozilla/5.0",
    "accept_header": "text/html",
    "language": "en-US",
    "java_enabled": false,
    "javascript_enabled": true,
    "color_depth": 24,
    "utc_offset": 0,
    "screen_width": 1920,
    "screen_height": 1080
  }'
```

### Field rules [step] [#field-rules-step]

| Field | Format |
|---|---|
| `cardholder_name` | Latin letters, plus space, apostrophe, dot and dash. Max 30 characters |
| `card_number` | Digits only, no spaces. Max 19 |
| `expires` | `MM/YY` exactly — two digits, slash, two digits |
| `cvc` | 3 or 4 digits |
| `remember_card` | `on` to store the card, `off` otherwise |

<Callout type="warning">
**Validate these yourself before submitting.** A format error is treated as a payment failure, not as a validation response, so a malformed expiry date looks like a decline.
</Callout>

The browser fields from `remote_ip` down are the 3D Secure device fingerprint. Send them for customer-initiated payments; they materially affect whether the issuer challenges the cardholder.

## Step 4 — Complete 3D Secure [step] [#step-4--complete-3d-secure-step]

If the card is enrolled, the response carries 3D Secure parameters. Post the customer's browser to the issuer, then return them to the callback.

```html
<form id='tds' method='POST' action='{{URL}}'>
  <input type='hidden' name='MD'      value='{{md}}'>
  <input type='hidden' name='PaReq'   value='{{PaReq}}'>
  <input type='hidden' name='TermUrl' value='{{callback_url}}'>
  <noscript><button type='submit'>Continue to your bank</button></noscript>
</form>
<script>document.getElementById('tds').submit();</script>
```

<Callout type="info">
Although `MD`, `PaReq` and `PaRes` are 3DS1 parameter names, **3D Secure 2 is fully supported** — Precium performs the 3DS2 verification and challenge behind this simpler redirect contract. You do not implement 3DS2 yourself.
</Callout>

If the card is not enrolled, authorisation completes synchronously and there is nothing to redirect.

## Step 5 — Confirm the outcome [step] [#step-5--confirm-the-outcome-step]

Read the purchase back:

```bash
curl -X GET https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY"
```

| Status | Meaning |
|---|---|
| `paid` | Complete. Fulfil the order |
| `hold` | Authorised only. Capture or release it |
| `preauthorized` | Card verified, no money moved |
| `pending_charge` | In flight. Wait for the webhook |
| `error` | Failed. Read `transaction_data.attempts[]`, newest first, for the error code |

<Callout type="important">
Do not treat the browser redirect as confirmation. A customer who closes the tab still completes a payment, and you will never see the redirect. **Webhooks are the authoritative source.**
</Callout>

## Step 6 — Receive the result by webhook [step] [#step-6--receive-the-result-by-webhook-step]

Register an endpoint once:

```bash
curl -X POST https://gate.reviopay.com/api/v1/webhooks/ \
  -H "Authorization: Bearer $PRECIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Order events",
    "callback": "https://www.example.com/webhooks/precium",
    "events": ["purchase.paid", "purchase.payment_failure"]
  }'
```

Three things your handler must do:

1. **Verify the signature.** Every delivery carries an `X-Signature` header — a base64-encoded RSA PKCS#1 v1.5 signature of the SHA-256 digest of the raw request body. Get the key from `public_key` on the webhook object. Verify the *raw bytes*, before parsing.
2. **Tolerate duplicates.** The same event can arrive twice even after you return 200, if the confirmation is lost in transit.
3. **Return 2xx quickly** and process asynchronously. Failures are retried up to 8 further times, and nothing is attempted more than 36 hours after the event.


## Test cards [#test-cards]

Card numbers for the sandbox are listed in *Test scenarios*, alongside the expected outcome for each. Use `123` as the CVC for a successful verification.

The examples above use `4111111111111111`, the standard Visa test number. Confirm the current set against *Test scenarios* before building an automated test suite.

## When something fails [#when-something-fails]

| Symptom | Most likely cause |
|---|---|
| `401 Unauthorized` | Wrong key, or the standard key used where the S2S key is required |
| `400` on purchase creation | Missing `brand_id`, or `currency` and `products` placed at the top level instead of inside `purchase` |
| `direct_post_url` is null | `success_redirect` and `failure_redirect` not both supplied |
| `409 Conflict` | The purchase has already been paid. A purchase may be paid only once |
| Looks like a decline, but the card is fine | A card field failed format validation — check `expires` is `MM/YY` |

Full error codes, retry guidance and issuer response codes are in *Error mapping* and *Issuer response codes*.

## Next [#next]

- *Integration guide* — every flow in detail, including preauthorisation, delayed capture, external MPI, network tokens and merchant-initiated charges
- *Test scenarios* — the full test matrix to work through before going live
- *Code examples* — the same flow in Python, Node.js, PHP, Ruby, Java, C# and Go
