# Billing

Your invoices, the card on file, and paying an invoice now. These work
even while streaming is paused for an unpaid bill, so the bill can
always be paid.

Base URL: `https://api.cardinalstreaming.com`. Authenticate with `Authorization: Bearer <your API key>`.

## Read your billing

`GET /v1/billing`

Your last invoices, the card on file and your account credit. Works while streaming is paused for an unpaid bill.

### Examples

cURL:

```bash
curl "https://api.cardinalstreaming.com/v1/billing" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.get(
    "https://api.cardinalstreaming.com/v1/billing",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/billing', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/billing', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.cardinalstreaming.com/v1/billing', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/billing")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

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

func main() {
	req, err := http.NewRequest("GET", "https://api.cardinalstreaming.com/v1/billing", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

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

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/billing"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

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

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.cardinalstreaming.com/v1/billing");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

Billing.

| Name | Type | Required | Description |
|---|---|---|---|
| `billed_by_card` | boolean | yes | False when your organization is invoiced by agreement. |
| `invoices` | array of object | yes |  |
| `invoices[].id` | string (uuid) | yes |  |
| `invoices[].period` | string | yes | `YYYY-MM`, the month billed. |
| `invoices[].status` | string | yes |  One of `"open"`, `"paid"`, `"failed"`, `"carried"`. |
| `invoices[].usage_cents` | integer | yes |  |
| `invoices[].platform_fee_cents` | integer | yes |  |
| `invoices[].carried_in_cents` | integer | yes |  |
| `invoices[].total_cents` | integer | yes |  |
| `invoices[].credit_cents` | integer | yes |  |
| `invoices[].charged_cents` | integer | yes |  |
| `invoices[].refunded_cents` | integer | yes |  |
| `invoices[].line_items` | array of object | yes |  |
| `invoices[].line_items[].product` | string | yes |  One of `"stream"`, `"restream"`, `"captions"`, `"translation"`. |
| `invoices[].line_items[].tier` | string | yes |  |
| `invoices[].line_items[].minutes` | integer | yes |  |
| `invoices[].line_items[].cents` | integer | yes |  |
| `invoices[].attempts` | integer | yes |  |
| `invoices[].next_attempt_at` | string (date-time) or null | yes |  |
| `invoices[].last_error_class` | string or null | yes | Why the last charge failed. Treat a value not listed here as `gateway_error`. One of `"card_expired"`, `"insufficient_funds"`, `"card_invalid"`, `"card_declined"`, `"no_payment_method"`, `"gateway_timeout"`, `"gateway_error"`. |
| `invoices[].last_error_message` | string or null | yes | The same reason in plain words, with what to do about it, fit to show a person. |
| `invoices[].processor_transaction_id` | string or null | yes |  |
| `invoices[].paid_at` | string (date-time) or null | yes |  |
| `invoices[].created_at` | string (date-time) | yes |  |
| `credit_balance_cents` | integer | yes |  |
| `streaming_paused` | boolean | yes | A bill is unpaid after its seven days: no stream can be booked or started until it is paid. |
| `card` | object | no |  |
| `card.brand` | string or null | yes |  |
| `card.last4` | string or null | yes |  |
| `card.exp_month` | integer or null | yes |  |
| `card.exp_year` | integer or null | yes |  |
| `card.has_backup` | boolean | yes |  |
| `billing_email` | string or null | no |  |
| `platform_fee_cents` | integer | no |  |
| `processor_customer_id` | string or null | no | Your customer record at the payment processor, set by your first card. Tokenize any later card onto this record (pass it as `customer_id` to the card form); a card stored on another record is refused. |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 503 | `billing_unavailable` | Billing is not available on this deployment. |

## Read the bill so far

`GET /v1/billing/upcoming`

This month's invoice as it stands: usage so far priced as the invoice
will price it, the platform fee, anything carried, the credit drawn
first, the estimated charge and its date, and what is still owed on
earlier invoices.

### Examples

cURL:

```bash
curl "https://api.cardinalstreaming.com/v1/billing/upcoming" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.get(
    "https://api.cardinalstreaming.com/v1/billing/upcoming",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/billing/upcoming', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/billing/upcoming', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.cardinalstreaming.com/v1/billing/upcoming', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/billing/upcoming")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

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

func main() {
	req, err := http.NewRequest("GET", "https://api.cardinalstreaming.com/v1/billing/upcoming", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

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

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/billing/upcoming"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("GET", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

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

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("GET"), "https://api.cardinalstreaming.com/v1/billing/upcoming");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

The bill so far.

| Name | Type | Required | Description |
|---|---|---|---|
| `period` | string | yes | The current month, `YYYY-MM`. |
| `billed_by_card` | boolean | yes |  |
| `lines` | array of object | yes |  |
| `lines[].product` | string | yes |  One of `"stream"`, `"restream"`, `"captions"`, `"translation"`. |
| `lines[].tier` | string | yes |  One of `"standard_1080p"`, `"pro_4k"`. |
| `lines[].minutes` | integer | yes | Billed minutes. |
| `lines[].cents` | integer | yes | The amount in whole cents, exactly as invoiced. |
| `usage_cents` | integer | yes |  |
| `platform_fee_cents` | integer | yes |  |
| `carried_cents` | integer | yes |  |
| `credits_applied_cents` | integer | yes |  |
| `estimated_total_cents` | integer | yes |  |
| `charge_on` | string (date) | yes |  |
| `outstanding_cents` | integer | yes |  |
| `outstanding_invoices` | integer | yes |  |
| `card_last4` | string or null | yes |  |
| `streaming_paused` | boolean | yes | A bill is unpaid after its seven days: no stream can be booked or started until it is paid. |

### Errors

| Status | Code | Meaning |
|---|---|---|
| 503 | `billing_unavailable` | Billing is not available on this deployment. |

## Put a card on file

`PUT /v1/billing/card`

Sets the card streaming is billed to (or the backup card, tried when the
main one is declined). A card number never reaches this API: the console
collects it in the browser straight to the payment processor, which
returns the `payment_method_id` you send here.

### Request body

| Name | Type | Required | Description |
|---|---|---|---|
| `payment_method_id` | string | yes |  |
| `backup` | boolean | no | Set the backup card instead of the main one. |

### Examples

cURL:

```bash
curl -X PUT "https://api.cardinalstreaming.com/v1/billing/card" \
  -H "Authorization: Bearer $CARDINAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "payment_method_id": "pm_8f3c2a91d0",
  "backup": false
}'
```

Python:

```python
import os

import requests

response = requests.put(
    "https://api.cardinalstreaming.com/v1/billing/card",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    json={
        "payment_method_id": "pm_8f3c2a91d0",
        "backup": False,
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/billing/card', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "payment_method_id": "pm_8f3c2a91d0",
    "backup": false
  }),
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/billing/card', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "payment_method_id": "pm_8f3c2a91d0",
    "backup": false
  }),
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('PUT', 'https://api.cardinalstreaming.com/v1/billing/card', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'json' => [
        'payment_method_id' => 'pm_8f3c2a91d0',
        'backup' => false,
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/billing/card")
request = Net::HTTP::Put.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  "payment_method_id" => "pm_8f3c2a91d0",
  "backup" => false
})

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

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

func main() {
	req, err := http.NewRequest("PUT", "https://api.cardinalstreaming.com/v1/billing/card", strings.NewReader(`{
  "payment_method_id": "pm_8f3c2a91d0",
  "backup": false
}`))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

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

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/billing/card"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .header("Content-Type", "application/json")
            .method("PUT", HttpRequest.BodyPublishers.ofString("{\"payment_method_id\":\"pm_8f3c2a91d0\",\"backup\":false}"))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

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

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("PUT"), "https://api.cardinalstreaming.com/v1/billing/card");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));
request.Content = new StringContent("{\"payment_method_id\":\"pm_8f3c2a91d0\",\"backup\":false}", Encoding.UTF8, "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 204

The card is on file.

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `invalid_body` | The body is not JSON. |
| 400 | `invalid_request` | The payment method is missing, unknown, removed, not a card, or another customer's; or it is the organization's first card and was not stored in the last hour. |
| 503 | `billing_unavailable` | Billing is not available on this deployment. |

## Pay an invoice now

`POST /v1/billing/invoices/{invoice_id}/pay`

Charges an unpaid invoice now, after any account credit. Paying the
bill that paused streaming resumes it at once.
A declined card is `402 payment_failed` and changes nothing else.

### Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | string | yes | The invoice's `id`. |

### Examples

cURL:

```bash
curl -X POST "https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay" \
  -H "Authorization: Bearer $CARDINAL_API_KEY"
```

Python:

```python
import os

import requests

response = requests.post(
    "https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay",
    headers={
        "Authorization": f"Bearer {os.environ['CARDINAL_API_KEY']}",
    },
    timeout=30,
)
print(response.status_code, response.text)
```

JavaScript:

```js
const response = await fetch('https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data = await response.json();
console.log(response.status, data);
```

TypeScript:

```ts
const response: Response = await fetch('https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CARDINAL_API_KEY}`,
  },
});
const data: unknown = await response.json();
console.log(response.status, data);
```

PHP:

```php
<?php
require 'vendor/autoload.php'; // composer require guzzlehttp/guzzle

$client = new GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('CARDINAL_API_KEY'),
    ],
    'http_errors' => false,
]);
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
```

Ruby:

```ruby
require 'json'
require 'net/http'

uri = URI("https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('CARDINAL_API_KEY')}"

response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
puts response.code, response.body
```

Go:

```go
package main

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

func main() {
	req, err := http.NewRequest("POST", "https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CARDINAL_API_KEY"))

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

Java:

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Example {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay"))
            .header("Authorization", "Bearer " + System.getenv("CARDINAL_API_KEY"))
            .method("POST", HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

C#:

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

using var client = new HttpClient();
using var request = new HttpRequestMessage(new HttpMethod("POST"), "https://api.cardinalstreaming.com/v1/billing/invoices/0d6f2b1c-3e4a-4b8f-9c11-5a7d2e9f8b10/pay");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CARDINAL_API_KEY"));

using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
```

### Response 200

Paid, or settled by credit, or carried into next month because what remained was under $0.50.

| Name | Type | Required | Description |
|---|---|---|---|
| `status` | string | yes |  One of `"succeeded"`, `"settled_without_charge"`, `"carried"`, `"failed"`. |
| `error_class` | string | no |  |
| `message` | string | no |  |
| `suspended` | boolean | yes | Whether this attempt paused streaming. A payment never does. |
| `charged_cents` | integer | yes |  |
| `credited_cents` | integer | yes |  |

```json
{
  "status": "succeeded",
  "charged_cents": 2750,
  "credited_cents": 1500,
  "suspended": false
}
```

### Errors

| Status | Code | Meaning |
|---|---|---|
| 402 | `payment_failed` | The card was not charged; `error_class` says why and `detail` says what to do, in words fit to show a person. |
| 404 | `not_found` | No such invoice in your organization. |
| 409 | `not_payable` | The invoice is not owed. |
| 503 | `billing_unavailable` | Billing is not available on this deployment. |
