# GET /certificate/{identifier}/download

Download the certificate of authenticity of an item as a PDF, in your brand's colors. Public endpoint, no API key.

Source: https://docs.sealtrust.io/en/reference/get-certificate-download/

---

You get back a PDF file ready to print or to attach to a message: the
certificate of authenticity of an item, in your brand's colors, in French or in
English.

The full URL is
`https://api.sealtrust.io/v1/certificate/{identifier}/download`. The same
endpoint exists without the `/v1` prefix, and the `/v1` form is the recommended
one for a new integration.

On success, the response is the document itself, of type `application/pdf`,
served as an attachment. Write the body of the response into a file. Do not try
to read it as text.

The server builds the document on every call and stores it nowhere. Two
successive calls can therefore give two different files if the state of the
certificate changed in between.

> [!ATTENTION] Anyone who knows the identifier gets the document
> This endpoint asks for no API key and no session. The identifier you put in
> the URL is the only thing that protects the file. Treat a certificate number
> as data you hand out only to the people you want to give the certificate to.
> The document also prints the name of the issuing brand and up to four of the
> custom fields attached to the certificate: put nothing in them that you would
> not want made public.

## Authorization

None, public endpoint. It expects neither an API key, nor a session cookie, nor
an `Authorization` header. A server-to-server call is accepted.

## Rate limit

60 calls per 60 second window, counted per calling IP address. Every URL that
starts with `/certificate` shares this counter, and the form
`/v1/certificate/{identifier}/download` counts in the same counter as the form
without the prefix.

Under normal conditions, every response carries the headers
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, the last
one giving the reset time in seconds since January 1, 1970. Treat these three
headers as optional: read them when they are there, do not make your
integration depend on their presence. Going over returns 429 with, in addition,
`Retry-After`, in seconds.

This endpoint consumes no quota of your plan.

## Path and query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | yes | What designates the item or the certificate. Four forms are accepted, see the detail below. The server strips surrounding whitespace. |
| `lang` | `string` | no | Language of the document: `fr` or `en`. Default value `en`. |

### The four forms of the identifier

The server tries the certificate number first. If that leads nowhere, it looks
at the shape of the value: `0x` followed by 64 hexadecimal characters is
treated as an item hash, any other value as a token identifier. A hash is
therefore never tried as a token identifier, nor the other way around. As a
last resort, the server tries the printed serial number.

| Form | Example | Detail |
| --- | --- | --- |
| Certificate number | `ST-CERT-000000000000` | Exact comparison, case matters. This is the number your console shows on the certificate and that `GET /certificate/{identifier}` returns. |
| Hash of the item | `0x0000000000000000000000000000000000000000000000000000000000000000` | `0x` followed by 64 hexadecimal characters. Case does not matter. |
| Token identifier | `11111111111111111111111111111111111111111111111111111111111111111111111111111` | The number carried by the token on the chain, in base 10, as is. This number is 77 to 78 digits long. Read it as text, never as an integer of your language. |
| Printed serial number | `00000000ABCD` | The 12 characters printed on the label of the item. Case does not matter, and the server brings back to their canonical form the characters that are confused when read: `I` and `L` count as `1`, `O` counts as `0`. You can therefore copy a number by hand without worrying about those three letters. |

The last three forms designate an item. The server then looks for the
certificate of that item: the certificate currently valid if there is one,
otherwise the most recent one whatever its state.

When it resolves those three forms, the server sets aside items that were
destroyed and items withdrawn from the catalog, and answers 404. The
certificate number does not go through the item: it finds the certificate row
directly, and the document downloads even when the item was destroyed or
withdrawn from the catalog.

### The language of the document

`fr` and `en` are the two values provided for, in lowercase. A value that
starts with neither `fr` nor `en` produces a document in English.

> [!ATTENTION] Send the language in lowercase
> `FR` in uppercase gives a document whose labels are in French, but the dates
> and the text of the watermark seal stay in English. Send `fr`, or `fr-FR`,
> which both give a fully French document.

## Request body

None. This is a `GET` request, everything goes through the URL.

## Example request

:::onglets
```bash title="curl"
curl -sS -D - \
  -o certificat.pdf \
  "https://api.sealtrust.io/v1/certificate/ST-CERT-000000000000/download?lang=fr"
```
```typescript
import { writeFile } from "node:fs/promises";

const response = await fetch(
  "https://api.sealtrust.io/v1/certificate/ST-CERT-000000000000/download?lang=fr",
);

if (!response.ok) {
  throw new Error(`${response.status} ${await response.text()}`);
}

console.log(response.headers.get("Content-Type"));
console.log(response.headers.get("Content-Disposition"));
console.log(response.headers.get("X-RateLimit-Remaining"));

await writeFile("certificat.pdf", Buffer.from(await response.arrayBuffer()));
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/certificate/ST-CERT-000000000000/download",
    params={"lang": "fr"},
    timeout=60,
)
response.raise_for_status()

print(response.headers["Content-Type"])
print(response.headers["Content-Disposition"])
print(response.headers["X-RateLimit-Remaining"])

with open("certificat.pdf", "wb") as fichier:
    fichier.write(response.content)
```
:::

> [!INFO] The TypeScript tab calls the API directly, and that is deliberate
> Everywhere else on this site, the TypeScript tab uses the `@sealtrust-io/sdk`
> package. Here, the SDK does not expose this endpoint: it covers batch
> minting, batch tracking, the history of an item, batch verification, the
> integrity check of metadata and the subscriptions to notifications.
> Downloading a certificate is therefore called over plain HTTP, as above. It
> is not an editorial oversight.

## Example response

HTTP code 200. The body is the PDF file.

```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="certificate-ST-CERT-000000000000.pdf"
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1755000060
```

The file name offered is always `certificate-` followed by the certificate
number, then `.pdf`. That number can differ from the identifier you sent: if
you queried the item by its serial number, the file name carries the number of
the certificate found.

### What the document contains

One A4 page. Here is what the server draws on it, from top to bottom, then the
border that surrounds the whole page.

| Element | Detail |
| --- | --- |
| Title banner | "Certificat d'authenticité" or "Certificate of Authenticity", followed by "Émis par" and the name of your brand. The banner is painted in your secondary color. |
| Logo | The logo of your brand. The server fetches it only if its address is in `https`, if it answers 200 in less than 4 seconds with an `image/...` type, and without a redirect. An address in `http`, a redirect, a longer delay or an address that points to a private network leave the document without a logo, the rest is unchanged. The logo fetched is also placed at the center of the QR code. |
| Status badge | `ACTIF`, `RÉVOQUÉ` or `EXPIRÉ`, at the top right. The server recomputes the state at render time: a certificate recorded as active whose expiry date has passed prints as `EXPIRÉ`. |
| Details | Product, certificate number, issue date, then the issuing brand and the expiry date when they are filled in, then at most four of the custom fields attached to the certificate. The label printed for a custom field is the name of the field with the underscores replaced by spaces and each word capitalized: `numero_lot` becomes `Numero Lot`. The value is printed as is, converted to text. |
| Blockchain proof | The name of the network, always present, `Base` in production. Then the address of the contract in shortened form and the token identifier, each one only when the item carries one. |
| Verification QR code | Points to the public page of the certificate on the SealTrust site. The address is also written out in full under the box. |
| Watermark seal | Two circles, a check mark and the words `AUTHENTIQUE` and `VÉRIFIÉ BLOCKCHAIN`, drawn transparently in your primary color, at the middle of the bottom of the page. The server always draws it. |
| Footer | "Propulsé par SealTrust, authenticité vérifiée par blockchain" or "Powered by SealTrust, Blockchain-verified authenticity". If your plan includes white label, this mention is not printed and the line stays empty. The server always draws the colored banner that carries this text. |
| Border | A rounded edging in your primary color, all around the page. The server always draws it. |

The server cuts a value that is too long for its line and ends it with a
continuation character. That is the case of the token identifier, which is 77
to 78 digits long. Do not copy a long value from the document, read it from the
API.

The document takes up the primary color and the secondary color of your brand.
If your brand has filled in no color, the document uses `#6386F1` as the
primary and `#0f172a` as the secondary.

The logo and the two colors are the three settings that change the look of the
document, and you set them yourself in the administration, tab `Settings`, then
`Brand`. They apply to the certificate as of the next download. Every paid plan
opens this screen. The free trial keeps it closed.

> [!ATTENTION] A 200 does not mean "valid certificate"
> A revoked certificate and an expired certificate download normally, with a
> 200. It is the badge printed on the document that carries the state. If your
> processing needs the state in a usable form, read it first from
> `GET /certificate/{identifier}`, which returns JSON.

## Errors

The errors, for their part, are JSON. A document starts with `%PDF`. A body
that starts with `{` signals a refusal. Check the HTTP code before writing the
file.

| Code | Condition | What to do |
| --- | --- | --- |
| 404 | No certificate carries this number, and no item in the catalog matches this identifier. Message `Product not found`. An item destroyed, replaced by a new mint or withdrawn from the catalog answers the same thing. | Check the value sent in the URL. If the item was destroyed or withdrawn from the catalog, this code is final for this identifier. The certificate number, for its part, keeps working. |
| 404 | The item exists, no certificate was ever issued for it. Message `No certificate found`. | Issue a certificate for this item from your console, then call again. |
| 429 | More than 60 calls in 60 seconds from the same IP address, across all `/certificate` URLs. Message `Rate limit exceeded: 60 requests per 60s`. | Wait the number of seconds given by `Retry-After`. Spread your calls out instead of sending them in bursts. |
| 500 | Unexpected server error. Fixed body `{"detail": "Internal Server Error"}`. | Try again. When the `X-Request-Id` header is present, it identifies the call: pass it on to us if the error repeats. |

This endpoint has no other refusal code. The `lang` parameter is never
rejected, and the identifier is accepted whatever its shape, even if that means
finding nothing.

> [!INFO] The order of the checks
> The checks run in this order: rate limit, lookup by certificate number, then
> lookup of the item by hash or by token identifier depending on the shape of
> the value, then by serial number, then lookup of the certificate of that
> item, then building of the document. The server reads `lang` at the last
> moment, and an unexpected value gives English.

## See also

- [`GET /certificate/{identifier}`](/en/reference/get-certificate/),
  read the certificate of authenticity of an item.
- [`GET /resolve/{identifier}`](/en/reference/get-resolve/),
  read in one call everything a product page displays.
- [Basics](/en/notions/),
  tell model, batch and item apart before ordering a single label.
