# Send a verification code

Source: https://jointextkit.com/docs/api/verify/send-verification
Index: https://jointextkit.com/llms.txt

`POST /v1/verify`

Generates a one-time code and sends it by text (`channel: "sms"`, the default) or by email (`channel: "email"` with `email`). Enforces expiry, attempt limits and anti-pumping controls. You do NOT need to own a phone number; TextKit sends from its own verification pool. If the pool is unavailable, a live code is sent from one of your own numbers that can already text anyone (sender status `active`, or a verified toll-free number, whose codes always name its verified business); a number still being registered is never used, and with none ready the request is refused (400) and nothing is sent. Nothing is billed here; a verification is charged only when the code is checked successfully. Supports the Idempotency-Key header, so a retried request does not text a second code. By email: the same template, limits per address and per domain, disposable addresses refused (403 `verification_blocked`), and test keys never send (the code is 111111).

Send your API key as a bearer token: `Authorization: Bearer ssms_sk_...`. Test keys run this endpoint against the [sandbox](/docs/sandbox); see [Authentication](/docs/authentication) for key modes and scopes.

## Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Makes retries safe for 24 hours: the same key and body replays the original successful response (with Idempotent-Replayed: true); a different body returns 409. Failed requests release the key. At most 255 characters. |

## Request body

JSON (`Content-Type: application/json`).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | `sms`, `email` | No | How the code goes out. `sms` needs `phone`; `email` needs `email`. Default "sms". |
| `phone` | string | No | The number to text the code to (channel `sms`). `to` works too. Example: `+14155550132`. |
| `email` | string | No | The address to email the code to (channel `email`). Disposable addresses are refused. Example: `ada@example.com`. |
| `app_name` | string | No | Your product name, shown in the message At most 24 characters. |
| `user_ip` | string | No | Optional. Your end user's IP address, as your server saw it. With it, one person requesting codes for many numbers or addresses is limited (20 an hour per IP on your account, texts and emails together); without it there is no per-IP limit, since requests come from your server. |
| `from` | string | No | Optional, channel `sms` only: send from a number you own instead of the TextKit pool |
| `customer_id` | string | No | Optional: attribute the verification to one of your customers |

## Responses

| Status | Meaning | Body |
| --- | --- | --- |
| 201 | Verification created | `Verification` |
| 401 | Missing, malformed, or revoked API key | `Error` |
| 402 | Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged | `Error` |
| 403 | Blocked by Shield, or recipient_opted_out (the number opted out, or texted STOP to the number the code comes from; they text START to get codes again). charged: false | `Error` |
| 409 | Idempotency conflict | `Error` |
| 429 | Rate limit or quota exceeded | `Error` |
| 503 | channel_unavailable: email codes cannot go out right now. Send a text instead (charged: false) | `Error` |

### 201: `Verification` fields

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Example: `ver_a1B2c3D4e5F6g7H8`. |
| `object` | `verification` |  |
| `channel` | `sms`, `email` | The channel the current code went out on |
| `phone` | string | null on an email verification Nullable. |
| `email` | string | null until a code is emailed Nullable. |
| `status` | `pending`, `approved`, `expired`, `max_attempts`, `blocked` |  |
| `attempts` | integer |  |
| `test` | boolean |  |
| `charged` | boolean |  |
| `created_at` | string (date-time) |  |
| `expires_at` | string (date-time) |  |
| `fallback_at` | string (date-time) | When the verification moved from text to email. Present only then. |
| `customer_id` | string | The customer this is attributed to. Present only when set. |

## Errors

| Status | When |
| --- | --- |
| 401 | Missing, malformed, or revoked API key |
| 402 | Billable live calls only: the prepaid credit balance cannot cover the call (insufficient_credits); nothing was done or charged |
| 403 | Blocked by Shield, or recipient_opted_out (the number opted out, or texted STOP to the number the code comes from; they text START to get codes again). charged: false |
| 409 | Idempotency conflict |
| 429 | Rate limit or quota exceeded |
| 503 | channel_unavailable: email codes cannot go out right now. Send a text instead (charged: false) |

Every error has the same JSON shape, and `request_id` matches the `X-Request-Id` response header. [Errors](/docs/errors) lists every code and what to do about it.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "What went wrong, in plain words.",
    "param": "the_field",
    "request_id": "req_a1B2c3D4e5F6g7H8"
  }
}
```

## Idempotency

Send an `Idempotency-Key` header to make retries safe. For 24 hours the same key with the same body replays the original successful response (with `Idempotent-Replayed: true`); the same key with a different body returns 409. A failed request releases its key.

## Examples

### Node.js

```javascript
import { TextKit } from 'jointextkit'; // npm install jointextkit

const sms = new TextKit(process.env.TEXTKIT_API_KEY);

const verification = await sms.verify.send({ to: '+14155550132' });

console.log(verification.id, verification.status);
```

### PHP

```php
<?php

$ch = curl_init('https://api.jointextkit.com/v1/verify');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TEXTKIT_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'phone' => '+14155550132',
        'email' => 'ada@example.com',
    ]),
]);

$data = json_decode(curl_exec($ch), true);
```

### Python

```python
import os
from jointextkit import TextKit  # pip install jointextkit

client = TextKit(os.environ["TEXTKIT_API_KEY"])

verification = client.verify.send("+14155550132")

print(verification["id"], verification["status"])
```

### Ruby

```ruby
require "net/http"
require "json"

uri = URI("https://api.jointextkit.com/v1/verify")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["TEXTKIT_API_KEY"]}"
req["Content-Type"] = "application/json"
req.body = {
  "phone": "+14155550132",
  "email": "ada@example.com"
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
```

### Go

```go
package main

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

func main() {
	body := strings.NewReader(`{
	  "phone": "+14155550132",
	  "email": "ada@example.com"
	}`)
	req, _ := http.NewRequest("POST", "https://api.jointextkit.com/v1/verify", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TEXTKIT_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	data, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status, string(data))
}
```

### Rust

```rust
// cargo add reqwest --features blocking,json
// cargo add serde_json
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("TEXTKIT_API_KEY")?;
    let res = reqwest::blocking::Client::new()
        .post("https://api.jointextkit.com/v1/verify")
        .bearer_auth(key)
        .json(&json!({
          "phone": "+14155550132",
          "email": "ada@example.com"
        }))
        .send()?
        .error_for_status()?;
    let data: serde_json::Value = res.json()?;
    println!("{data}");
    Ok(())
}
```

### Java

```java
import java.net.URI;
import java.net.http.*;

public class Main {
    public static void main(String[] args) throws Exception {
        var request = HttpRequest.newBuilder(URI.create("https://api.jointextkit.com/v1/verify"))
            .header("Authorization", "Bearer " + System.getenv("TEXTKIT_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString("""
                {
                  "phone": "+14155550132",
                  "email": "ada@example.com"
                }
                """))
            .build();

        var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

### .NET

```csharp
using System.Net.Http.Headers;
using System.Text;

var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("TEXTKIT_API_KEY"));

var body = new StringContent("""
    {
      "phone": "+14155550132",
      "email": "ada@example.com"
    }
    """, Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.jointextkit.com/v1/verify", body);
res.EnsureSuccessStatusCode();

var data = await res.Content.ReadAsStringAsync();
```

### cURL

```bash
curl -X POST "https://api.jointextkit.com/v1/verify" \
  -H "Authorization: Bearer $TEXTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2f0e-order-1042" \
  -d '{
  "phone": "+14155550132",
  "email": "ada@example.com"
}'
```

## Related

- Guide: [Verify](/docs/verify)
- [All verify endpoints](/docs/api#verify)
- [API reference](/docs/api)
