# Create Reference

Create Reference through this project environment’s SafeHaven account. Uses snake_case fields and NGN minor units. Requires payments:write.

## Endpoint

POST https://api.veyqo.org/api/v1/safehaven/ussd-payment

Server secret key required. Scope: payments:write. The key determines the project and environment; no project-selection header is accepted.

> For each new operation, generate and persist one OPERATION_IDEMPOTENCY_KEY. Retry the unchanged request with that same key. Do not generate a new key on every retry.

## Example request

Examples use illustrative IDs and provider URLs. Replace path variables with IDs returned by your own requests. Never paste a live secret into client code or a public tool.

### cURL

```bash
curl --request POST "https://api.veyqo.org/api/v1/safehaven/ussd-payment" \
  --header "Authorization: Bearer $VEYQO_SECRET_KEY" \
  --header "Accept: application/json" \
  --header "Idempotency-Key: $OPERATION_IDEMPOTENCY_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "amount": 50000,
  "merchant_name": "example",
  "ussd_bank_code": "090286"
}'
```

### Node.js

```javascript
// Server-side Node.js 18+. Save operation.idempotencyKey before a write.
const response = await fetch(`https://api.veyqo.org/api/v1/safehaven/ussd-payment`, {
  method: 'POST',
  headers: {
  "Authorization": `Bearer ${process.env.VEYQO_SECRET_KEY}`,
  "Accept": "application/json",
  "Idempotency-Key": operation.idempotencyKey,
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "amount": 50000,
  "merchant_name": "example",
  "ussd_bank_code": "090286"
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
  console.error(response.status, result.error?.code, response.headers.get('X-Request-Id'));
  throw new Error(result.error?.message ?? 'Veyqo request failed');
}
console.log(result.data);
```

### PHP

```php
<?php
// Server-side PHP with the cURL extension. Persist $operationIdempotencyKey.
$curl = curl_init('https://api.veyqo.org/api/v1/safehaven/ussd-payment');
curl_setopt_array($curl, [
  CURLOPT_CUSTOMREQUEST => 'POST',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer '.getenv('VEYQO_SECRET_KEY'),
    'Accept: application/json',
    'Idempotency-Key: '.$operationIdempotencyKey,
    'Content-Type: application/json'
  ],
  CURLOPT_POSTFIELDS => <<<'JSON'
{
  "amount": 50000,
  "merchant_name": "example",
  "ussd_bank_code": "090286"
}
JSON,
]);
$body = curl_exec($curl);
if ($body === false) throw new RuntimeException(curl_error($curl));
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400 || !($result['success'] ?? false)) {
  throw new RuntimeException($result['error']['code'] ?? 'Veyqo request failed');
}
print_r($result['data']);
```

### Python

```python
# pip install requests; run on your server.
import os, requests
from urllib.parse import quote

response = requests.request('POST', f'https://api.veyqo.org/api/v1/safehaven/ussd-payment',
    headers={
  "Authorization": 'Bearer ' + os.environ['VEYQO_SECRET_KEY'],
  "Accept": "application/json",
  "Idempotency-Key": operation_idempotency_key,
  "Content-Type": "application/json"
},
    json={
  "amount": 50000,
  "merchant_name": "example",
  "ussd_bank_code": "090286"
},
    timeout=30)
result = response.json()
if not response.ok or not result.get('success'):
    raise RuntimeError(f"{response.status_code} {result.get('error')} request_id={response.headers.get('X-Request-Id')}")
print(result['data'])
```

## Request body

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| amount | integer | Required | Amount in NGN minor units (kobo). Veyqo converts it to naira for SafeHaven. Minimum 1. |
| merchant_name | string | Required | See the JSON example. |
| ussd_bank_code | string | Required | See the JSON example. |
| settlement_account | object | Optional | See the JSON example. |
| settlement_account.bank_code | string | Optional | 6 digits bank code. Must be `090286` |
| settlement_account.account_number | string | Optional | 10 digits NUBAN account number |

## Successful response · 200

HTTP success means the request was accepted. A pending payment or refund is not completed merely because the response is 200 or 201.

```json
{
  "success": true,
  "data": {
    "_id": "000000000000000000000001",
    "client": {
      "search_paid": false,
      "sub_account_creation": false,
      "long_term_virtual_account": false,
      "override_sub_account_limit": false,
      "allow_debit_account_info": false,
      "allow_debit_account_name": false,
      "indemnity_signed": false,
      "_id": "000000000000000000000001",
      "type": "example",
      "kyc_level": "example",
      "full_name": "example",
      "gender": "example",
      "date_of_birth": [],
      "email_address": "customer@example.com",
      "phone_number": "+2348000000000",
      "phone_number2": "+2348000000000",
      "bvn": "example",
      "nin": "example",
      "tin": "example",
      "contact_address": {
        "street": "example",
        "city": "example",
        "state": "example",
        "country": "example"
      },
      "identification_details": {
        "status": "Created",
        "type": "example",
        "number": "example"
      },
      "notification_settings": {
        "login_notification": false,
        "newsletter": false,
        "product_notification": false,
        "vas_notification": false,
        "webhook_notifcation": {
          "url": "https://example.com",
          "enabled": false
        }
      },
      "sub_account_prefix": "example",
      "is_blocked": false,
      "status": "Created",
      "referees": [],
      "is_deleted": false,
      "created_at": "example",
      "updated_at": "example",
      "identity_status": [],
      "__v": 0,
      "cba_client_id": "000000000000000000000001",
      "contract": "example",
      "virtual_account_limit": {
        "daily_limit": 0,
        "single_transaction_limit": 0,
        "allow_other_prefix": "example"
      },
      "documents": {
        "passport_url": "https://example.com",
        "id_card_url": "https://example.com",
        "signature_url": "https://example.com",
        "certificate_url": "https://example.com",
        "memat_url": "https://example.com",
        "board_resolution_url": "https://example.com",
        "utility_bill_url": "https://example.com"
      },
      "employment_information": [],
      "next_of_kin": []
    },
    "account": {
      "can_debit": false,
      "can_credit": false,
      "_id": "000000000000000000000001",
      "client": "example",
      "account_product": "example",
      "account_number": "0000000000",
      "account_name": "example",
      "account_type": "example",
      "currency_code": "example",
      "bvn": "example",
      "account_balance": 0,
      "book_balance": 0,
      "interest_balance": 0,
      "with_holding_tax_balance": 0,
      "status": "Created",
      "is_default": false,
      "nominal_annual_interest_rate": 0,
      "interest_compounding_period": "example",
      "interest_posting_period": "example",
      "interest_calculation_type": "example",
      "interest_calculation_days_in_year_type": "example",
      "min_required_opening_balance": 0,
      "lockin_period_frequency": 0,
      "lockin_period_frequency_type": "example",
      "allow_overdraft": false,
      "overdraft_limit": 0,
      "charge_with_holding_tax": false,
      "charge_value_added_tax": false,
      "charge_stamp_duty": false,
      "notification_settings": {
        "sms_notification": false,
        "email_notification": false,
        "email_monthly_statement": false,
        "sms_monthly_statement": false
      },
      "is_sub_account": false,
      "is_deleted": false,
      "created_at": "example",
      "updated_at": "example",
      "__v": 0,
      "cba_account_id": "000000000000000000000001"
    },
    "bank_code": "090286",
    "account_name": "example",
    "amount": 50000,
    "callback_url": "https://example.com",
    "status": "Created",
    "fees": 0,
    "settlement_account": {
      "bank_code": "090286",
      "account_number": "0000000000"
    },
    "external_reference": "order-1001",
    "is_deleted": false,
    "created_at": "example",
    "updated_at": "example",
    "__v": 0,
    "account_number": "0000000000",
    "details": {
      "reference": "order-1001",
      "amount": 50000,
      "transaction_id": "000000000000000000000001",
      "trace_id": "000000000000000000000001",
      "ussd_string": "example",
      "time_stamp": 0,
      "signature": "example"
    },
    "operation_id": "pso_01M6EB66SC61Q3RQYAN0693DGM"
  }
}
```

## Response fields

| Field in data | Type | Presence | Description |
| --- | --- | --- | --- |
| _id | string | Optional | See the JSON example. |
| client | object | Optional | See the JSON example. |
| client.search_paid | boolean | Optional | See the JSON example. |
| client.sub_account_creation | boolean | Optional | See the JSON example. |
| client.long_term_virtual_account | boolean | Optional | See the JSON example. |
| client.override_sub_account_limit | boolean | Optional | See the JSON example. |
| client.allow_debit_account_info | boolean | Optional | See the JSON example. |
| client.allow_debit_account_name | boolean | Optional | See the JSON example. |
| client.indemnity_signed | boolean | Optional | See the JSON example. |
| client._id | string | Optional | See the JSON example. |
| client.type | string | Optional | See the JSON example. |
| client.kyc_level | string | Optional | See the JSON example. |
| client.full_name | string | Optional | See the JSON example. |
| client.gender | string | Optional | See the JSON example. |
| client.date_of_birth | JSON | Optional | See the JSON example. |
| client.email_address | string | Optional | See the JSON example. |
| client.phone_number | string | Optional | See the JSON example. |
| client.phone_number2 | string | Optional | See the JSON example. |
| client.bvn | string | Optional | See the JSON example. |
| client.nin | string | Optional | See the JSON example. |
| client.tin | string | Optional | See the JSON example. |
| client.contact_address | object | Optional | See the JSON example. |
| client.contact_address.street | string | Optional | See the JSON example. |
| client.contact_address.city | string | Optional | See the JSON example. |
| client.contact_address.state | string | Optional | See the JSON example. |
| client.contact_address.country | string | Optional | See the JSON example. |
| client.identification_details | object | Optional | See the JSON example. |
| client.identification_details.status | string | Optional | See the JSON example. |
| client.identification_details.type | string | Optional | See the JSON example. |
| client.identification_details.number | string | Optional | See the JSON example. |
| client.notification_settings | object | Optional | See the JSON example. |
| client.notification_settings.login_notification | boolean | Optional | See the JSON example. |
| client.notification_settings.newsletter | boolean | Optional | See the JSON example. |
| client.notification_settings.product_notification | boolean | Optional | See the JSON example. |
| client.notification_settings.vas_notification | boolean | Optional | See the JSON example. |
| client.notification_settings.webhook_notifcation | object | Optional | See the JSON example. |
| client.sub_account_prefix | string | Optional | See the JSON example. |
| client.is_blocked | boolean | Optional | See the JSON example. |
| client.status | string | Optional | See the JSON example. |
| client.referees | array | Optional | See the JSON example. |
| client.is_deleted | boolean | Optional | See the JSON example. |
| client.created_at | string | Optional | See the JSON example. |
| client.updated_at | string | Optional | See the JSON example. |
| client.identity_status | array | Optional | See the JSON example. |
| client.__v | integer | Optional | See the JSON example. |
| client.cba_client_id | string | Optional | See the JSON example. |
| client.contract | string | Optional | See the JSON example. |
| client.virtual_account_limit | object | Optional | See the JSON example. |
| client.virtual_account_limit.daily_limit | integer | Optional | See the JSON example. |
| client.virtual_account_limit.single_transaction_limit | integer | Optional | See the JSON example. |
| client.virtual_account_limit.allow_other_prefix | string | Optional | See the JSON example. |
| client.documents | object | Optional | See the JSON example. |
| client.documents.passport_url | string | Optional | See the JSON example. |
| client.documents.id_card_url | string | Optional | See the JSON example. |
| client.documents.signature_url | string | Optional | See the JSON example. |
| client.documents.certificate_url | string | Optional | See the JSON example. |
| client.documents.memat_url | string | Optional | See the JSON example. |
| client.documents.board_resolution_url | string | Optional | See the JSON example. |
| client.documents.utility_bill_url | string | Optional | See the JSON example. |
| client.employment_information | object | Optional | See the JSON example. |
| client.next_of_kin | object | Optional | See the JSON example. |
| account | object | Optional | See the JSON example. |
| account.can_debit | boolean | Optional | See the JSON example. |
| account.can_credit | boolean | Optional | See the JSON example. |
| account._id | string | Optional | See the JSON example. |
| account.client | string | Optional | See the JSON example. |
| account.account_product | string | Optional | See the JSON example. |
| account.account_number | string | Optional | See the JSON example. |
| account.account_name | string | Optional | See the JSON example. |
| account.account_type | string | Optional | See the JSON example. |
| account.currency_code | string | Optional | See the JSON example. |
| account.bvn | string | Optional | See the JSON example. |
| account.account_balance | integer | Optional | Amount in NGN minor units (kobo). |
| account.book_balance | integer | Optional | Amount in NGN minor units (kobo). |
| account.interest_balance | integer | Optional | Amount in NGN minor units (kobo). |
| account.with_holding_tax_balance | integer | Optional | Amount in NGN minor units (kobo). |
| account.status | string | Optional | See the JSON example. |
| account.is_default | boolean | Optional | See the JSON example. |
| account.nominal_annual_interest_rate | integer | Optional | See the JSON example. |
| account.interest_compounding_period | string | Optional | See the JSON example. |
| account.interest_posting_period | string | Optional | See the JSON example. |
| account.interest_calculation_type | string | Optional | See the JSON example. |
| account.interest_calculation_days_in_year_type | string | Optional | See the JSON example. |
| account.min_required_opening_balance | integer | Optional | See the JSON example. |
| account.lockin_period_frequency | integer | Optional | See the JSON example. |
| account.lockin_period_frequency_type | string | Optional | See the JSON example. |
| account.allow_overdraft | boolean | Optional | See the JSON example. |
| account.overdraft_limit | integer | Optional | See the JSON example. |
| account.charge_with_holding_tax | boolean | Optional | See the JSON example. |
| account.charge_value_added_tax | boolean | Optional | See the JSON example. |
| account.charge_stamp_duty | boolean | Optional | See the JSON example. |
| account.notification_settings | object | Optional | See the JSON example. |
| account.notification_settings.sms_notification | boolean | Optional | See the JSON example. |
| account.notification_settings.email_notification | boolean | Optional | See the JSON example. |
| account.notification_settings.email_monthly_statement | boolean | Optional | See the JSON example. |
| account.notification_settings.sms_monthly_statement | boolean | Optional | See the JSON example. |
| account.is_sub_account | boolean | Optional | See the JSON example. |
| account.is_deleted | boolean | Optional | See the JSON example. |
| account.created_at | string | Optional | See the JSON example. |
| account.updated_at | string | Optional | See the JSON example. |
| account.__v | integer | Optional | See the JSON example. |
| account.cba_account_id | string | Optional | See the JSON example. |
| bank_code | string | Optional | See the JSON example. |
| account_name | string | Optional | See the JSON example. |
| amount | integer | Optional | Amount in NGN minor units (kobo). |
| callback_url | string | Optional | See the JSON example. |
| status | string | Optional | See the JSON example. |
| fees | integer | Optional | Amount in NGN minor units (kobo). |
| settlement_account | object | Optional | See the JSON example. |
| settlement_account.bank_code | string | Optional | See the JSON example. |
| settlement_account.account_number | string | Optional | See the JSON example. |
| external_reference | string | Optional | See the JSON example. |
| is_deleted | boolean | Optional | See the JSON example. |
| created_at | string | Optional | See the JSON example. |
| updated_at | string | Optional | See the JSON example. |
| __v | integer | Optional | See the JSON example. |
| account_number | string | Optional | See the JSON example. |
| details | object | Optional | See the JSON example. |
| details.reference | string | Optional | See the JSON example. |
| details.amount | integer | Optional | Amount in NGN minor units (kobo). |
| details.transaction_id | string | Optional | See the JSON example. |
| details.trace_id | string | Optional | See the JSON example. |
| details.ussd_string | string | Optional | See the JSON example. |
| details.time_stamp | integer | Optional | See the JSON example. |
| details.signature | string | Optional | See the JSON example. |
| operation_id | string | Optional | Veyqo pso_ operation ID. Save it to reconcile retries and match provider_service.updated events. |
| merchant_reference | string | Optional | Your external_reference or payment_reference, if supplied. SafeHaven receives a globally unique Veyqo reference for supported writes. |

## Errors and recovery

| Code | HTTP | Recovery | Retry policy |
| --- | --- | --- | --- |
| VALIDATION_FAILED | 422 | Read error.details when present, correct the named fields and retry only after correction. Validation messages may describe state/capability errors without field details. | No automatic retry. |
| INVALID_API_KEY | 401 | Use a current kc_test_ or kc_live_ secret key on /api/v1. Use a pk_test_ or pk_live_ publishable key only on browser checkout. | After fixing credentials. |
| INSUFFICIENT_PERMISSION | 403 | Grant the required scope or use a suitably scoped key. Dashboard organization roles and staff permissions are separate. | After permission changes. |
| RESOURCE_NOT_FOUND | 404 | Check the ID, project and environment. Do not guess another project’s IDs. An inactive payment link is also not-found. | No blind retry. |
| PROVIDER_NOT_CONNECTED | 422 | Connect a provider, choose the project default, or explicitly pass provider. An account shared with another project still needs its own assignment here. | After configuration changes. |
| PROVIDER_AUTHENTICATION_FAILED | 502 | The merchant must check/rotate the provider credentials in the dashboard. Do not expose those secrets to your application users. | After credential correction; reconcile existing payment first. |
| PROVIDER_REQUEST_FAILED | 422 | Read the safe message, inspect the payment’s failure details and correct the request or provider account. Never repeatedly retry a business rejection. | Only after resolving the cause. |
| PROVIDER_UNAVAILABLE | 503 | Check the existing payment and verify its state before starting another attempt. Retry safe reads with backoff; preserve operation identity for retried writes. | Conditional: reconcile unknown outcomes first. |
| RATE_LIMIT_EXCEEDED | 429 | Honor Retry-After, use bounded exponential backoff with jitter, and reduce concurrent requests. Read X-RateLimit headers. | Yes, after Retry-After; preserve write idempotency. |
| IDEMPOTENCY_KEY_REUSED | 409 | Reuse the exact original request to recover that operation, or use a new key only for a genuinely different operation. | Do not retry a changed body with that key. |
| IDEMPOTENCY_IN_PROGRESS | 409 | Wait briefly, retry with the same key and unchanged request, then reconcile by reference if the result remains uncertain. | Back off with jitter; preserve key/body. |

- [Complete error catalogue](https://veyqo.org/docs/errors): Every Veyqo error code, including which surface emits it and how to recover.
