> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.crisscross.money/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server.

# Create a rate lock

POST https://api.crisscross.money/v1/rate-locks
Content-Type: application/json

Locks the current reference rate for a currency pair so it can be reused across
multiple merchant transactions until lock expiry or market-drift cancellation.

The rate, markup, and validity window are fixed when the lock is created and stay
constant for its lifetime, even if pricing changes afterwards.

Pricing is mid-market reference + markup; the mid, the markup, and the resulting
all-in rate are all returned.

You can hold up to two active locks per currency pair, so you can pre-fetch a
replacement (typically about an hour before the current lock expires) and overlap it
with the lock it succeeds. While both are active, keep pricing existing browsing
sessions off the current lock and send new sessions to the new one. If fewer than two
locks are active a new one is minted (`201`); if two are already active the freshest
existing lock is returned instead (`200`).

Idempotency: **none**. Each mint creates a distinct lock, subject to the two-lock cap.


Reference: https://docs.crisscross.money/api-reference/collect/rate-locks/create-rate-lock

## Authentication

- `Authorization` header (bearer token, required) — Token obtained via the login flow.

## Request

### Body (application/json)

This endpoint expects a RateLockRequest.

- `base` (string, required) — Base currency of the pair (ISO 4217) — the currency you price in, e.g. `USD`.
- `quote` (string, required) — Quote currency of the pair (ISO 4217) — the currency you collect in, e.g. `KES`.

## Response

### 200

An existing active rate lock, returned because two active locks already exist for this currency pair (the maximum of two). The freshest of the two is returned.

- `rateLockId` (string, required)
- `status` (enum, required) — - `active` — usable. - `expired` — past `expiresAt`. Terminal. - `cancelled` — invalidated before expiry; see `cancellationReason`. Terminal. `expired` and `cancelled` are permanent — a lock never returns to `active`. A lock at or past `expiresAt` is invalid even if `status` still reads `active`.
  - Allowed values: `active`, `expired`, `cancelled`
- `base` (string, required) — Base currency (ISO 4217).
- `quote` (string, required) — Quote currency (ISO 4217).
- `midRate` (string, required) — Reference mid-market rate the lock was priced from. `1 base = <midRate> quote`.
- `markupBps` (integer, required) — The markup applied over the mid, in basis points (100 bps = 1%).
- `allInRate` (string, required) — The rate you transact at: mid plus markup. This is the rate carried onto every transaction that uses the lock. `1 base = <allInRate> quote`.
- `createdAt` (datetime, required)
- `expiresAt` (datetime, required) — End of the lock's validity window. The lock is invalid from this time even if `status` still reads `active`.
- `cancellationReason` (enum, optional) — Why the lock was cancelled. Present only when `status` is `cancelled`. `market_drift` — the rate moved beyond the allowed tolerance. `requested` — you cancelled the lock yourself via the cancel endpoint.
  - Allowed values: `market_drift`, `requested`

### 201

A newly minted active rate lock (a lock slot was available).

- `rateLockId` (string, required)
- `status` (enum, required) — - `active` — usable. - `expired` — past `expiresAt`. Terminal. - `cancelled` — invalidated before expiry; see `cancellationReason`. Terminal. `expired` and `cancelled` are permanent — a lock never returns to `active`. A lock at or past `expiresAt` is invalid even if `status` still reads `active`.
  - Allowed values: `active`, `expired`, `cancelled`
- `base` (string, required) — Base currency (ISO 4217).
- `quote` (string, required) — Quote currency (ISO 4217).
- `midRate` (string, required) — Reference mid-market rate the lock was priced from. `1 base = <midRate> quote`.
- `markupBps` (integer, required) — The markup applied over the mid, in basis points (100 bps = 1%).
- `allInRate` (string, required) — The rate you transact at: mid plus markup. This is the rate carried onto every transaction that uses the lock. `1 base = <allInRate> quote`.
- `createdAt` (datetime, required)
- `expiresAt` (datetime, required) — End of the lock's validity window. The lock is invalid from this time even if `status` still reads `active`.
- `cancellationReason` (enum, optional) — Why the lock was cancelled. Present only when `status` is `cancelled`. `market_drift` — the rate moved beyond the allowed tolerance. `requested` — you cancelled the lock yourself via the cancel endpoint.
  - Allowed values: `market_drift`, `requested`

## Errors

### 400 Bad Request Error

Bad request — validation or malformed request.

- `message` (string, required) — Human-readable description of what went wrong. For logs and display — do not parse.
- `error` (string, required) — Error category, typically the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Not Found", "Unprocessable Entity", "Service Unavailable").
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 401 Unauthorized Error

Unauthorized — missing or invalid token.

- `message` (string, required) — Human-readable description of what went wrong. For logs and display — do not parse.
- `error` (string, required) — Error category, typically the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Not Found", "Unprocessable Entity", "Service Unavailable").
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 422 Unprocessable Entity Error

The currency pair is not one we offer.

- `message` (string, required) — Human-readable description of what went wrong. For logs and display — do not parse.
- `error` (string, required) — Error category, typically the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Not Found", "Unprocessable Entity", "Service Unavailable").
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

### 503 Service Unavailable Error

Rates for this pair are temporarily unavailable — no live price right now. Retry later, or fall back to a per-session quote.

- `message` (string, required) — Human-readable description of what went wrong. For logs and display — do not parse.
- `error` (string, required) — Error category, typically the HTTP reason phrase (e.g. "Bad Request", "Unauthorized", "Not Found", "Unprocessable Entity", "Service Unavailable").
- `statusCode` (integer, required) — HTTP status code, mirroring the response header. The response header is the source of truth.

## Examples

### createRateLock_example

**Request**

```json
{
  "base": "USD",
  "quote": "KES"
}
```

**Response**

```json
{
  "rateLockId": "rlk_550e8400-e29b-41d4-a716-446655440000",
  "status": "active",
  "base": "USD",
  "quote": "KES",
  "midRate": "130.00",
  "markupBps": 200,
  "allInRate": "132.60",
  "createdAt": "2026-06-24T10:30:00Z",
  "expiresAt": "2026-06-24T22:30:00Z"
}
```

**SDK Code**

```python createRateLock_example
import requests

url = "https://api.crisscross.money/v1/rate-locks"

payload = {
    "base": "USD",
    "quote": "KES"
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript createRateLock_example
const url = 'https://api.crisscross.money/v1/rate-locks';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"base":"USD","quote":"KES"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go createRateLock_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.crisscross.money/v1/rate-locks"

	payload := strings.NewReader("{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby createRateLock_example
require 'uri'
require 'net/http'

url = URI("https://api.crisscross.money/v1/rate-locks")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}"

response = http.request(request)
puts response.read_body
```

```java createRateLock_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.crisscross.money/v1/rate-locks")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}")
  .asString();
```

```php createRateLock_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.crisscross.money/v1/rate-locks', [
  'body' => '{
  "base": "USD",
  "quote": "KES"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp createRateLock_example
using RestSharp;

var client = new RestClient("https://api.crisscross.money/v1/rate-locks");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift createRateLock_example
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "base": "USD",
  "quote": "KES"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.crisscross.money/v1/rate-locks")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Example 2

**Request**

```json
{
  "base": "USD",
  "quote": "KES"
}
```

**Response**

```json
{
  "rateLockId": "rlk_550e8400-e29b-41d4-a716-446655440000",
  "status": "active",
  "base": "USD",
  "quote": "KES",
  "midRate": "130.00",
  "markupBps": 200,
  "allInRate": "132.60",
  "createdAt": "2024-01-15T09:30:00Z",
  "expiresAt": "2024-01-15T09:30:00Z",
  "cancellationReason": "market_drift"
}
```

**SDK Code**

```python
import requests

url = "https://api.crisscross.money/v1/rate-locks"

payload = {
    "base": "USD",
    "quote": "KES"
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.crisscross.money/v1/rate-locks';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
  body: '{"base":"USD","quote":"KES"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.crisscross.money/v1/rate-locks"

	payload := strings.NewReader("{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.crisscross.money/v1/rate-locks")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.crisscross.money/v1/rate-locks")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.crisscross.money/v1/rate-locks', [
  'body' => '{
  "base": "USD",
  "quote": "KES"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.crisscross.money/v1/rate-locks");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"base\": \"USD\",\n  \"quote\": \"KES\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "base": "USD",
  "quote": "KES"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.crisscross.money/v1/rate-locks")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```