API · VERSION 1

Data Receipt API

Read your requests and their data from R, a script, or anything else that speaks HTTP. Create a token under Settings > API tokens and send it as a bearer header.

Base URL https://datareceipt.io/api/v1 OpenAPI document (JSON) R package

Getting started

  1. 01

    Create a token

    Under Settings > API tokens, name a token and copy it. It is shown once. Tokens look like 12|dr_… and never expire unless you give them an expiry.

  2. 02

    Check it works

    Send it as a bearer header to /user. A 200 with your name and email means the token is good.

    curl
    export DATARECEIPT_API_KEY="12|dr_..."
    
    curl https://datareceipt.io/api/v1/user \
      -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
      -H "Accept: application/json"
  3. 03

    Get the data

    List your requests to find an id (it is also the number in a request's URL on the site), then ask for every row of it in one paginated call.

    curl
    curl https://datareceipt.io/api/v1/requests/12/data \
      -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
      -H "Accept: application/json"

Authentication

Every endpoint needs a personal access token in the Authorization: Bearer header. A token sees exactly what its owner sees: your requests, their submissions, and their data. Another account's ids are 404s, not 403s, so a token cannot tell whether an id exists.

Tokens are read-only for now. Revoke one at any time under Settings > API tokens; requests with a revoked or expired token get a 401.

Each account may make 120 requests a minute. Beyond that the API answers 429 until the minute is up.

Pagination

Lists come in pages. Requests and submissions are paged by number: ?page=2&per_page=50, up to 100 per page, with links and meta beside the data array saying where you are.

A request's data is paged by cursor, and the page size counts submissions rather than rows, since rows live inside submissions. Follow links.next until it is null. Every page carries the request's columns and the page's submissions in meta, so one call is enough to build a typed table with sender columns.

Errors

Errors are JSON with a message. Send Accept: application/json so an error never comes back as a redirect to the login page.

Status Meaning
401No token, or one that has been revoked or has expired.
404Nothing with that id belongs to the token's owner.
429More than 120 requests in a minute. Wait for the minute to pass.
Example
{
    "message": "Unauthenticated."
}

From R

The datareceipt package wraps every endpoint and returns tidy, typed tibbles: text as character, numbers as double, whole numbers as integer, dates as Date, yes/no as logical. Set the key once and it is found in every session.

R
# install.packages("pak")
pak::pak("dgkeyes/datareceipt")

library(datareceipt)
datareceipt_api_key("12|dr_...", install = TRUE)

list_requests()
site_data <- get_data(12)

Endpoints

All paths are relative to https://datareceipt.io/api/v1. Every one is a GET and answers JSON.

GET /user

Who the token belongs to

The cheapest way for a client to check that a token works.

Request

curl
curl https://datareceipt.io/api/v1/user \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
datareceipt_whoami()

Response · 200

Field Type Description
id integer
name string
email string
Example response
{
    "id": 7,
    "name": "Ada Lovelace",
    "email": "ada@example.org"
}

Also answers 401 (see Errors).

GET /requests

List your requests

Newest first, with each request's column spec and how many submissions and rows it has received.

Parameters

per_page
query
integer Requests per page, 1 to 100. Default 50.
page
query
integer Page number. Default 1.

Request

curl
curl https://datareceipt.io/api/v1/requests \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
list_requests()

Response · 200

Paginated set of DataRequestResource

Field Type Description
data array of objects
└ id integer
└ title string
└ description string or null
└ is_open boolean Whether senders can still submit.
└ closed_at string (date-time) or null
└ submission_count integer Accepted submissions so far.
└ row_count integer Rows across all submissions.
└ columns array of objects The spec: one entry per column, in order, with its type and rules.
└ name string
└ type string One of text, numeric, integer, date, boolean
└ require_non_empty boolean
└ min string or null
└ max string or null
└ allowed_values array of strings or null
└ on_break string One of block, flag
└ share_url string The link senders use.
└ created_at string (date-time) or null
└ updated_at string (date-time) or null
links object
└ first string or null
└ last string or null
└ prev string or null
└ next string or null
meta object
└ current_page integer
└ from integer or null
└ last_page integer
└ links array of objects Generated paginator links.
└ url string or null
└ label string
└ active boolean
└ path string or null Base path for paginator generated URLs.
└ per_page integer Number of items shown per page.
└ to integer or null Number of the last item in the slice.
└ total integer Total number of items being paginated.
Example response
{
    "data": [
        {
            "id": 12,
            "title": "Q2 site data",
            "description": "Monthly enrollment and completion by site.",
            "is_open": true,
            "closed_at": null,
            "submission_count": 3,
            "row_count": 18,
            "columns": [
                {
                    "name": "site_id",
                    "type": "text",
                    "require_non_empty": true,
                    "min": null,
                    "max": null,
                    "allowed_values": null,
                    "on_break": "block"
                }
            ],
            "share_url": "https://datareceipt.io/api/v1/r/k4Ld3vQ9pXm2Rt7YwBn8Hc1Zs5Fj6Ga0Ue4Io2Pq",
            "created_at": "2026-08-01T15:02:11Z",
            "updated_at": "2026-08-14T22:20:49Z"
        }
    ],
    "links": {
        "first": "https://datareceipt.io/api/v1/requests?page=1",
        "last": "https://datareceipt.io/api/v1/requests?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "1",
                "active": true
            }
        ],
        "path": "https://datareceipt.io/api/v1/requests",
        "per_page": 50,
        "to": 3,
        "total": 3
    }
}

Also answers 401 (see Errors).

GET /requests/{id}

Get one request and its spec

Parameters

id
in path · required
integer The request id, as shown in its URL on the site.

Request

curl
curl https://datareceipt.io/api/v1/requests/12 \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
get_request(12)

# Or just the spec, as a tibble
get_columns(12)

Response · 200

DataRequestResource

Field Type Description
data object
└ id integer
└ title string
└ description string or null
└ is_open boolean Whether senders can still submit.
└ closed_at string (date-time) or null
└ submission_count integer Accepted submissions so far.
└ row_count integer Rows across all submissions.
└ columns array of objects The spec: one entry per column, in order, with its type and rules.
└ name string
└ type string One of text, numeric, integer, date, boolean
└ require_non_empty boolean
└ min string or null
└ max string or null
└ allowed_values array of strings or null
└ on_break string One of block, flag
└ share_url string The link senders use.
└ created_at string (date-time) or null
└ updated_at string (date-time) or null
Example response
{
    "data": {
        "id": 12,
        "title": "Q2 site data",
        "description": "Monthly enrollment and completion by site.",
        "is_open": true,
        "closed_at": null,
        "submission_count": 3,
        "row_count": 18,
        "columns": [
            {
                "name": "site_id",
                "type": "text",
                "require_non_empty": true,
                "min": null,
                "max": null,
                "allowed_values": null,
                "on_break": "block"
            }
        ],
        "share_url": "https://datareceipt.io/api/v1/r/k4Ld3vQ9pXm2Rt7YwBn8Hc1Zs5Fj6Ga0Ue4Io2Pq",
        "created_at": "2026-08-01T15:02:11Z",
        "updated_at": "2026-08-14T22:20:49Z"
    }
}

Also answers 401, 404 (see Errors).

GET /requests/{id}/data

Get every row of a request

All accepted rows across all submissions, oldest submission first. Each row carries the id of the submission it came from and its position in that submission; values is an object keyed by column name in the request's column order, with null for an empty cell.

Pages are counted in submissions, not rows, and the cursor is a submission id. Follow links.next (or pass meta.next_cursor as cursor) until it is null. meta.columns is the request's spec and meta.submissions describes the submissions on this page, so one paginated call is enough to build a typed table with sender columns.

Cells that broke a rule on a column set to accept and flag keep the sender's text as a string in values, and each row's flags lists them (empty for a clean row).

Parameters

id
in path · required
integer The request id.
per_page
query
integer Submissions per page, 1 to 100. Default 20.
cursor
query
string Opaque cursor from a previous page's meta.next_cursor.

Request

curl
curl https://datareceipt.io/api/v1/requests/12/data \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
get_data(12)

# A request's URL works too
get_data("https://datareceipt.io/requests/12")

Response · 200

Field Type Description
data array of objects
└ submission_id integer
└ row integer
└ values object
└ flags array of objects
└ column string
└ value string or null
└ message string
links object
└ next string or null
└ prev string or null
meta object
└ request_id integer
└ columns array of objects
└ name string
└ type string One of text, numeric, integer, date, boolean
└ require_non_empty boolean
└ min string or null
└ max string or null
└ allowed_values array of strings or null
└ on_break string One of block, flag
└ submissions array of objects
└ id integer
└ request_id integer
└ sender_name string
└ sender_email string
└ source string How the sender got the data in: xlsx or csv. Submissions from August 2026 may also read paste, typed, or form, from sender methods that no longer exist.
└ row_count integer
└ flag_count integer Cells that broke a rule on a column set to accept and flag, rather than block.
└ original_filename string or null
└ submitted_at string (date-time) or null
└ revised_at string (date-time) or null When the request's owner last edited this submission's data, or null if it is as received.
└ per_page integer
└ next_cursor string or null
└ prev_cursor string or null
└ path string
Example response
{
    "data": [
        {
            "submission_id": 41,
            "row": 1,
            "values": {
                "site_id": "S-001",
                "month": "2026-04-01",
                "enrollment": 48,
                "completion_rate": 0.81
            },
            "flags": []
        }
    ],
    "links": {
        "next": "https://datareceipt.io/api/v1/requests/12/data?cursor=eyJpZCI6NDEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
        "prev": null
    },
    "meta": {
        "request_id": 12,
        "columns": [
            {
                "name": "site_id",
                "type": "text",
                "require_non_empty": true,
                "min": null,
                "max": null,
                "allowed_values": null,
                "on_break": "block"
            }
        ],
        "submissions": [
            {
                "id": 41,
                "request_id": 12,
                "sender_name": "Maria Lopez",
                "sender_email": "maria@northsidehealth.org",
                "source": "xlsx",
                "row_count": 6,
                "flag_count": 0,
                "original_filename": "northside_q2.xlsx",
                "submitted_at": "2026-08-14T22:20:49Z",
                "revised_at": null
            }
        ],
        "per_page": 20,
        "next_cursor": "eyJpZCI6NDEsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
        "prev_cursor": null,
        "path": "https://datareceipt.io/api/v1/requests/12/data"
    }
}

Also answers 401, 404 (see Errors).

GET /requests/{id}/submissions

List a request's submissions

Oldest first, without their rows: who sent what, when, and how many rows.

Parameters

id
in path · required
integer The request id.
per_page
query
integer Submissions per page, 1 to 100. Default 50.
page
query
integer Page number. Default 1.

Request

curl
curl https://datareceipt.io/api/v1/requests/12/submissions \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
list_submissions(12)

Response · 200

Paginated set of SubmissionResource

Field Type Description
data array of objects
└ id integer
└ request_id integer
└ sender_name string
└ sender_email string
└ source string How the sender got the data in: xlsx or csv. Submissions from August 2026 may also read paste, typed, or form, from sender methods that no longer exist.
└ row_count integer
└ flag_count integer Cells that broke a rule on a column set to accept and flag, rather than block.
└ original_filename string or null
└ submitted_at string (date-time) or null
└ revised_at string (date-time) or null When the request's owner last edited this submission's data, or null if it is as received.
links object
└ first string or null
└ last string or null
└ prev string or null
└ next string or null
meta object
└ current_page integer
└ from integer or null
└ last_page integer
└ links array of objects Generated paginator links.
└ url string or null
└ label string
└ active boolean
└ path string or null Base path for paginator generated URLs.
└ per_page integer Number of items shown per page.
└ to integer or null Number of the last item in the slice.
└ total integer Total number of items being paginated.
Example response
{
    "data": [
        {
            "id": 41,
            "request_id": 12,
            "sender_name": "Maria Lopez",
            "sender_email": "maria@northsidehealth.org",
            "source": "xlsx",
            "row_count": 6,
            "flag_count": 0,
            "original_filename": "northside_q2.xlsx",
            "submitted_at": "2026-08-14T22:20:49Z",
            "revised_at": null
        }
    ],
    "links": {
        "first": "https://datareceipt.io/api/v1/requests?page=1",
        "last": "https://datareceipt.io/api/v1/requests?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "1",
                "active": true
            }
        ],
        "path": "https://datareceipt.io/api/v1/requests",
        "per_page": 50,
        "to": 3,
        "total": 3
    }
}

Also answers 401, 404 (see Errors).

GET /submissions/{id}

Get one submission with its rows

Each row is an object keyed by column name, in the request's column order, with null for an empty cell. Values are typed: text as strings, numbers as numbers, dates as YYYY-MM-DD strings, yes/no as booleans. The exception is a flagged cell (a value that broke a rule on a column set to accept and flag): it holds the sender's text as a string, and flags says which cells those are and what rule each broke.

Parameters

id
in path · required
integer The submission id.

Request

curl
curl https://datareceipt.io/api/v1/submissions/41 \
  -H "Authorization: Bearer $DATARECEIPT_API_KEY" \
  -H "Accept: application/json"
R
get_submission(41)

Response · 200

SubmissionWithRowsResource

Field Type Description
data object
└ id integer
└ request_id integer
└ sender_name string
└ sender_email string
└ source string How the sender got the data in: xlsx or csv. Submissions from August 2026 may also read paste, typed, or form, from sender methods that no longer exist.
└ row_count integer
└ flag_count integer Cells that broke a rule on a column set to accept and flag, rather than block.
└ original_filename string or null
└ submitted_at string (date-time) or null
└ revised_at string (date-time) or null When the request's owner last edited this submission's data, or null if it is as received.
└ columns array of objects The request's spec, so the rows can be typed without another call.
└ name string
└ type string One of text, numeric, integer, date, boolean
└ require_non_empty boolean
└ min string or null
└ max string or null
└ allowed_values array of strings or null
└ on_break string One of block, flag
└ rows array of objects One object per row, keyed by column name in spec order, with null for an empty cell. A flagged cell holds the sender's text as a string, whatever the column's type.
└ flags array of objects Cells that broke a rule but were accepted: the 1-based row, the column name, the sender's text, and what rule it broke.
└ row integer
└ column string
└ value string or null
└ message string
Example response
{
    "data": {
        "id": 41,
        "request_id": 12,
        "sender_name": "Maria Lopez",
        "sender_email": "maria@northsidehealth.org",
        "source": "xlsx",
        "row_count": 6,
        "flag_count": 0,
        "original_filename": "northside_q2.xlsx",
        "submitted_at": "2026-08-14T22:20:49Z",
        "revised_at": null,
        "columns": [
            {
                "name": "site_id",
                "type": "text",
                "require_non_empty": true,
                "min": null,
                "max": null,
                "allowed_values": null,
                "on_break": "block"
            }
        ],
        "rows": [
            {}
        ],
        "flags": [
            {
                "row": 1,
                "column": "string",
                "value": "string",
                "message": "string"
            }
        ]
    }
}

Also answers 401, 404 (see Errors).